[
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\n\nroot = true\n\n[*]\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = true\ninsert_final_newline = true\ncharset = utf-8\nend_of_line = lf\n\n[*.yml]\nindent_size = 2\n\n[*.md]\ninsert_final_newline = false\ntrim_trailing_whitespace = false\n\n[*.bat]\nindent_style = tab\nend_of_line = crlf\n\n[LICENSE]\ninsert_final_newline = false\n\n[Makefile]\nindent_style = tab\n"
  },
  {
    "path": ".github/workflows/pytest.yml",
    "content": "name: Unit Tests\non:\n  pull_request:\n    types: [opened, synchronize, reopened, ready_for_review, unlabeled]\n    branches:\n      - develop\n  push:\n    branches:\n      - develop\n\njobs:\n  build:\n\n    name: Python ${{ matrix.python-version }} & PostgreSQL ${{ matrix.postgresql-version }}\n    env:\n      PGPASSWORD: postgres\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.8\", \"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n        postgresql-version: [\"14\", \"15\", \"16\", \"17\"]\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Set Environment Variables\n      run: |\n        echo \"py_version=$(echo ${{ matrix.python-version }} | tr -d .)\" >> $GITHUB_ENV\n        if [ \"${{ matrix.python-version }}\" == \"3.8\" ]; then\n          echo \"add_dir_str=${{ matrix.python-version }}\" >> $GITHUB_ENV\n        elif [ \"${{ matrix.python-version }}\" == \"3.9\" ]; then\n          echo \"add_dir_str=${{ matrix.python-version }}\" >> $GITHUB_ENV\n        elif [ \"${{ matrix.python-version }}\" == \"3.10\" ]; then\n          echo \"add_dir_str=cpython-310\" >> $GITHUB_ENV\n        elif [ \"${{ matrix.python-version }}\" == \"3.11\" ]; then\n          echo \"add_dir_str=cpython-311\" >> $GITHUB_ENV\n        elif [ \"${{ matrix.python-version }}\" == \"3.12\" ]; then\n          echo \"add_dir_str=cpython-312\" >> $GITHUB_ENV\n        elif [ \"${{ matrix.python-version }}\" == \"3.13\" ]; then\n          echo \"add_dir_str=cpython-313\" >> $GITHUB_ENV\n        fi\n\n    - name: Setup PostgreSQL for Linux/macOS/Windows\n      uses: ikalnytskyi/action-setup-postgres@v7\n      with:\n        # The username of the user to setup.\n        username: postgres\n        # The password of the user to setup.\n        password: postgres\n        # The database name to setup and grant permissions to created user.\n        database: postgres\n        # The server port to listen on.\n        port: 5432\n        # The PostgreSQL major version to install. Either \"14\", \"15\", \"16\" or \"17\".\n        postgres-version: ${{ matrix.postgresql-version }}\n        # When \"true\", encrypt connections using SSL (TLS).\n        ssl: false\n\n    - name: Set up TaskJuggler\n      run: |\n        sudo gem install taskjuggler\n\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v5\n      with:\n        python-version: ${{ matrix.python-version }}\n\n    - name: Update pip\n      run: |\n        sudo apt-get install -y $(grep -o ^[^#][[:alnum:]-]*.* \"packages.list\")\n        python3 -m pip install --upgrade pip\n        pip install wheel\n\n    - name: Install Python dependencies\n      run: |\n        pip install -r requirements.txt -r requirements-dev.txt\n\n    - name: Build Stalker\n      run: |\n        python3 -m build\n        ls -l dist/\n        wheel_file=$(ls dist/stalker-*.whl)\n        pip install $wheel_file\n\n    - name: Test with pytest\n      run: |\n        PYTHONPATH=src python -m pytest\n\n    - name: Archive code coverage results\n      uses: actions/upload-artifact@v4\n      with:\n        name: code-coverage-report-py${{ env.py_version }}-psql${{ matrix.postgresql-version }}\n        path: htmlcov\n        retention-days: 10\n\n  # windows:\n  #   name: Test with Python ${{ matrix.python-version }} on Windows\n  #   runs-on: windows-latest\n\n  #   strategy:\n  #     fail-fast: false\n  #     matrix:\n  #       python-version:\n  #         - \"3.8\"\n  #         - \"3.9\"\n  #         - \"3.10\"\n  #         - \"3.11\"\n\n  #   steps:\n  #   - uses: actions/checkout@v4\n\n  #   - name: Set Environment Variables\n  #     run: |\n  #       $py_version = \"${{ matrix.python-version }}\" -replace '\\.', ''\n  #       echo \"py_version=$py_version\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append\n  #       if (\"${{ matrix.python-version }}\" -eq \"3.8\") {\n  #         echo \"add_dir_str=${{ matrix.python-version }}\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append\n  #       } elseif (\"${{ matrix.python-version }}\" -eq \"3.9\") {\n  #         echo \"add_dir_str=${{ matrix.python-version }}\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append\n  #       } elseif (\"${{ matrix.python-version }}\" -eq \"3.10\") {\n  #         echo \"add_dir_str=cpython-310\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append\n  #       } elseif (\"${{ matrix.python-version }}\" -eq \"3.11\") {\n  #         echo \"add_dir_str=cpython-311\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append\n  #       } elseif (\"${{ matrix.python-version }}\" -eq \"3.12\") {\n  #         echo \"add_dir_str=cpython-312\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append\n  #       }\n\n  #   - name: Set up Python ${{ matrix.python-version }}\n  #     uses: actions/setup-python@v5\n  #     with:\n  #       python-version: ${{ matrix.python-version }}\n\n  #   - name: Update pip\n  #     run: |\n  #       python -m pip install --upgrade pip\n  #       pip install wheel\n\n  #   - name: Install Python dependencies\n  #     run: |\n  #       pip install -r requirements-tests.txt -r requirements-dev.txt\n\n  #   - name: Test with pytest\n  #     run: |\n  #       python -m pytest --verbose -n auto -W ignore --color=yes --cov=. --cov-report html\n\n  #   - name: Archive code coverage results\n  #     uses: actions/upload-artifact@v4\n  #     with:\n  #       name: code-coverage-report-${{ env.py_version }}-windows\n  #       path: htmlcov\n  #       retention-days: 10\n"
  },
  {
    "path": ".gitignore",
    "content": ".cache/*\n.coverage*\n.DS_Store\n.env\n.mypy_cache/\n.pytest_cache\n.tox/\n.venv/\n.vscode/\n*.pyc\n*.swp\n*~*\nbuild/*\ndist/\ndist/*\ndocs/build/*\ndocs/doctrees/*\ndocs/html/*\ndocs/latex/*\ndocs/source/generated/*\ndocs/source/static/design\ndocs/source/static/stalker_design*.vue\nhtmlcov\ninclude/*\nlocal\nstalker.db*\nstalker.egg-info"
  },
  {
    "path": "CHANGELOG.rst",
    "content": "===============\nStalker Changes\n===============\n\n1.0.0\n=====\n\n* `Version.take_name` has been renamed to `Version.variant_name` to follow the\n  industry standard (and then removed it completely as we now have `Variant`\n  class for this).\n* `Task.depends` renamed to `Task.depends_on`.\n* `TaskDependency.task_depends_to` renamed to `TaskDependency.task_depends_on`.\n* Modernized Stalker as a Python project. It is now fully PEP 517 compliant.\n* Stalker now supports Python versions from 3.8 to 3.13.\n* Stalker is now SQLAlchemy 2.x compliant.\n* Stalker is now fully type hinted.\n* Added GitHub actions for CI/CD practices.\n* Updated validation messages to make them more consistently displaying the\n  current type and the value of the validated attribute.\n* Added Makefile workflow to help creating a virtualenv, building, installing,\n  releasing etc. actions much more easier.\n* Added `tox` config to run the test with Python 3.8 to 3.13.\n* Increased test coverage to 99.76%.\n* Updated documentation theme to `furo`.\n* Renamed `OSX` to `macOS` where ever it is mentioned.\n* `Scene` is now deriving from `Task`.\n* `Shot.sequences` is now `Shot.sequence` and it is many-to-one.\n* `Shot.scenes` is now `Shot.scene` and it is many-to-one.\n* Added the `Variant` class to allow variants to be approved and managed\n  individually.\n* Added `Review.version` attribute to relate a `Version` instance to the\n  review.\n* Removed the `Version.variant_name` attribute. The migration alembic script\n  will create `Variant` instances for each `Version.variant_name` under the\n  container `Task` to hold the information.\n* `Version._template_variables()` now finds the related `Asset`, `Shot` and\n  `Sequence` values and passes them in the returned dictionary.\n* All the enum values handled with arbitrary string lists or integer values are\n  now proper enum classes. As a result we now have `ScheduleConstraint`,\n  `TimeUnit`, `ScheduleModel`, `DependencyTarget`, `TraversalDirection`\n  enum classes which are removing the need of using fiddly strings as enum\n  values.\n* `StatusList`s that are created for super classes can now be used with the\n  derived classes, i.e. a status list created specifically for `Task` can now\n  be used with `Asset`, `Shot`, `Sequence` and `Scenes` and any future `Task`\n  derivatives.\n\n0.2.27\n======\n\n* Fixed a bug in ``Task.responsible`` attribute. This change has also slightly\n  changed how the ``Task.responsible`` attribute works. It still comes from the\n  parent if the ``Task.responsible`` is empty or None, but when queried it\n  causes the attribute to be filled with parent data. This is a slight change,\n  but may break some workflows.\n\n* Added ``ScheduleMixin.to_unit`` that converts the given ``seconds`` to the\n  given ``unit`` in consideration of the given ``schedule_model``.\n\n0.2.26\n======\n\n* ``Task.percent_complete`` value is now properly calculated for a parent Task\n  that contains a mixed type of \"effort\", \"duration\" and \"length\" based tasks.\n\n0.2.25.1\n========\n\n* **Update:** Updated the ``.travis.yml`` file to use PostgreSQL 13.3 and\n  Ubuntu 20.04 Focal Fossa.\n* **Update:** Updated the ``upload_to_pypi`` command to follow the current\n  Python packaging guide.\n* **Update:** Migrated from ``TravisCI.org`` to ``TravisCI.com``.\n* **Update:** Re-enabled concurrent testing in ``.travis.yml``.\n\n0.2.25\n======\n\n* **Update:** Stalker is now compatible with SQLAlchemy 1.4,\n  psycopg2-binary 2.86 and Python 3.9+. But more work still needs to be done to\n  make it SQLAlchemy 2.0 compatible.\n\n0.2.24.3\n========\n\nThis release is again mainly related to fixing failing tests.\n\n0.2.24.2\n========\n\nThis release is mainly related to cleaning up some complains that arose while\ntesting the library.\n\n* **Fix:** Fixed two tests which are testing the ``stalker.db`` module to\n  check the system against the correct Alembic revision id.\n\n* **Update:** Removed the unnecessary ``pytest.skip`` commands in the\n  ``Repository`` class tests which were shipping the tests if the OS is not\n  Windows. But they should work fine under all OSes.\n\n* **Update:** Updated all class documentation and removed the cancellation\n  character (which was apparently not good for PEP8)\n\n* **Fix:** Fixed some warnings about some regular expressions.\n\n\n0.2.24.1\n========\n\n* **Fix:** Fixed ``stalker.db`` module to check for the correct Alembic\n  revision id.\n\n\n0.2.24\n======\n\n* **New:** ``Repository`` instances now have a ``code`` attribute which is\n  used for generating the environment variables where in previous versions the\n  ``id`` attribute has been used which caused difficulties in transferring the\n  data to a different installation of Stalker. Also to make the system\n  backwards compatible, Stalker will still set the old ``id`` based environment\n  variables. But when asked for an environment variable it will return the\n  ``code`` based one. The ``code`` argument as usual has to be initialized on\n  ``Repository`` instance creation. That's why this version is slightly\n  backwards incompatible and needs the database to be updated with Alembic\n  (with the command ``alembic update head``).\n\n* **Fix:** ``Repository`` methods ``is_in_repo`` and ``find_repo`` are now case\n  insensitive for Windows paths.\n\n* **Update:** Updated ``Project`` class documentation and included information\n  about what is going to be deleted or how the delete operation will be\n  cascaded when a ``Project`` instance is deleted.\n\n0.2.23\n======\n\n* **Update:** Updated the ``setup.py`` to require ``psycopg2-binary`` instead\n  of ``psycopg2``. Also updated the configuration files for Docker and Travis.\n  This changes the requirement of psycopg2 to psycopg2-binary, which will make\n  it easier to get the installation to complete on e.g. CentOS 7 without\n  requiring pg_config.\n\n0.2.22\n======\n\n* **Fix:** Fixed ``TaskJugglerScheduler.schedule()`` method to correctly decode\n  byte data from ``sys.stderr`` to string for Python 3.x.\n\n* **Fix:** Fixed a couple of tests for TaskJuggler.\n\n* **Update:** Updated Classifiers information in ``setup.py``, removed Python\n  versions 2.6, 3.0, 3.1 and 3.2 from supported Python versions.\n\n* **Fix:** Removed Python 3.3 from TravisCI build which is not supported by\n  ``pytest`` apparently.\n\n* **Update:** Updated TravisCI config and removed Python 2.6 and added Python\n  3.6.\n\n* **Update:** Added a test case for an edge usage of FilenameTemplate.\n\n* **Update:** Updated .gitignore file to ignore PyTest cache folder.\n\n* **Update:** Updated the License file to correctly reflect the project license\n  of LGPLv3.\n\n* **Update:** Update copyright information.\n\n* **New:** Created ``make_html.bat`` for Windows.\n\n* **New:** Added support for Python wheel.\n\n\n0.2.21\n======\n\n* **New:** Switched from ``nose`` + ``unittest`` to ``pytest`` as the main\n  testing framework (with ``pytest-xdist`` tests complete 4x faster).\n\n* **New:** Added ``DBSession.save()`` shortcut method for convenience which\n  does an ``add`` or ``add_all`` (depending to the input) followed by a\n  ``commit`` at once.\n\n* **Update:** Updated the about page for a more appealing introduction to the\n  library.\n\n* **New:** Stalker now creates default ``StatusList`` for ``Project`` instances\n  on database initialization.\n\n* **Update:** SQLite3 support is back. In fact it was newer gone. For\n  simplicity of first time users the default database is again SQLite3. It was\n  dropped for the sake of adding more PostgreSQL oriented features. But then it\n  is recognized that the system can handle both. Though a two new Variant had\n  to be created for JSON and Datetime columns.\n\n* **Update:** With the reintroduction of SQLite3, the new JSON type column in\n  ``WorkingHours`` class has been upgraded to support SQLite3. So with SQLite3\n  the column stores the data as TEXT but seamlessly convert them to JSON when\n  ORM loads or commits the data.\n\n* **New:** Added ``ConfigBase`` as a base class for ``Config`` to let it be\n  used in other config classes.\n\n* **Fix:** Fixed ``testing.create_db()`` and ``testing.drop_db()`` to fallback\n  to ``subprocess.check_call`` method for Python 2.6.\n\n* **Fix:** Fixed ``stalker.models.auth.User._validate_password()`` method to\n  work with Python 2.6.\n\n* **Update:** Updated all of the tests to use ``pytest`` style assertions to\n  support Python 2.6 along with 2.7 and 3.0+.\n\n* **Fix:** Fixed ``stalker.db.check_alembic_version()`` function to invalidate\n  the connection, so it is not possible to continue with the current session,\n  preventing users to ignore the raised ``ValueError`` when the\n  ``alembic_version`` of the database is not matching the ``alembic_version``\n  of Stalker's current version.\n\n\n0.2.20\n======\n\n* **New:** Added ``goods`` attribute to the ``Client`` class. To allow special\n  priced ``Goods`` to be created for individual clients.\n\n* **Fix:** The ``WorkingHours`` class is now derived from ``Entity`` thus it is\n  not stored in a ``PickleType`` column in ``Studio`` anymore. (issue: #44)\n\n* **Update:** Updated ``appveyor.yml`` to match ``travis.yml``.\n\n\n0.2.19\n======\n\n* **Update:** Updated the ``stalker.config.Config.database_engine_settings`` to\n  point the test database.\n\n* **Fix:** Fixed a bug in ``stalker.testing.UnitTestDBBase.setUp()`` where it\n  was not considering the existence of the ``STALKER_PATH`` environment\n  variable while doing the tests.\n\n* **Update:** Removed debug message from ``db.setup()`` which was revealing the\n  database password.\n\n* **Update:** Updated the ``UnitTestDBBase``, it now creates its own test\n  database, which allows all the tests to run in an individual database. Thus,\n  the tests can now be run in ``multiprocess`` mode which speeds things a lot.\n\n* **Fix:** Removed any module level imports of ``stalker.defaults`` variable,\n  which can be changed by a Studio (or by tests) and should always be\n  refreshed.\n\n* **Update:** Removed the module level import of the\n  ``stalker.db.session.DBSession`` in ``stalker.db``, so it is not possible to\n  use ``db.DBSession`` anymore.\n\n* **Update:** The import statements that imports ``stalker.defaults`` moved to\n  local scopes to allow runtime changes to the ``defaults`` to be reflected\n  correctly.\n\n* **Update:** Added Python fall back mode to\n  ``stalker.shot.Shot._check_code_availability()`` which runs when there is no\n  database.\n\n* **Update:** ``stalker.models.task.TimeLog._validate_task()`` is now getting\n  the ``Status`` instances from the ``StatusList`` that is attached to the\n  ``Task`` instance instead of doing a database query.\n\n* **Update:** ``stalker.models.task.TimeLog._validate_resource()`` is now\n  falling back to a Python implementation if there is no database connection.\n\n* **Update:** ``stalker.models.task.Task._total_logged_seconds_getter()`` is\n  now hundreds of times faster when there is a lot of ``TimeLog`` instances\n  attached to the ``Task``.\n\n* **Update:** In ``stalker.models.task.Task`` class, methods those were doing a\n  database query to get the required ``Status`` instances are now using the\n  attached ``StatusList`` instance to get them.\n\n* **Fix:** A possible ``auto_flush`` is prevented in ``Ticket`` class.\n\n* **Update:** ``Version.latest_version`` property is now able to fall back to a\n  pure Python implementation when there is no database connection.\n\n* **Update:** The default log level has been increased from ``DEBUG`` to\n  ``INFO``.\n\n* **Update:** In an attempt to speed up tests, a lot of tests that doesn't need\n  an active Database has been updated to use the regular ``unittest.TestCase``\n  instead of ``stalker.testing.TestBase`` and as a result running all of the\n  tests are now 2x faster.\n\n* **Fix:** ``TimeLogs`` are now correctly reflected in UTC in a tj3 file.\n\n* **Fix:** Fixed a lot of tests which were raising Warnings and surprisingly\n  considered as Errors in TravisCI.\n\n* **Fix:** ``to_tjp`` methods of SOM classes that is printing a Datetime object\n  are now printing the dates in UTC.\n\n* **Fix:** Fixed ``stalker.models.auth.Permission`` to be hashable for Python\n  3.\n\n* **Fix:** Fixed ``stalker.models.auth.AuthenticationLog`` to be sortable for\n  Python 3.\n\n* **Fix:** Fixed ``stalker.models.version.Version.latest_version`` property for\n  Python 3.\n\n* **Fix:** Fixed tests of ``Permission`` class to check for correct exception\n  messages in Python 3.\n\n* **Update:** Replaced the ``assertEquals`` and ``assertNotEquals`` calls which\n  are deprecated in Python 3 with ``assertEqual`` and ``assertNotEquals`` calls\n  respectively.\n\n* **Fix:** Fixed tests for ``User`` and ``Version`` classes to not to cause the\n  ``id column is None`` warnings of SQLAlchemy to be emitted.\n\n\n0.2.18\n======\n\n* **Update:** Support for DB backends other than Postgresql has been dropped.\n  This is done to greatly benefit from a code that is highly optimized only\n  for one DB backend. With This all of the tests should be inherited from the\n  ``stalker.tests.UnitTestDBBase`` class.\n\n* **New:** All the DateTime fields in Stalker are now TimeZone aware and\n  Stalker stores the DateTime values in UTC. Naive datetime values are not\n  supported anymore. You should use a library like ``pytz`` to supply timezone\n  information as shown below::\n\n    import datetime\n    import pytz\n    from stalker import db, SimpleEntity\n    new_simple_entity = SimpleEntity(\n        name='New Simple Entity',\n        date_created = datetime.datetime.now(tzinfo=pytz.utc)\n    )\n\n* **Fix:** The default values for ``date_created`` and ``date_updated`` has now\n  been properly set to a partial function that returns the current time.\n\n* **Fix:** Previously it was possible to enter two TimeLogs for the same\n  resource in the same datetime range by committing the data from two different\n  sessions simultaneously. Thus the database was not aware that it should\n  prevent that. Now with the new PostgreSQL only implementation and the\n  ``ExcludeConstraint`` of PostgreSQL an ``IntegrityError`` is raised by the\n  database backend when something like that happens.\n\n* **Update:** All the tests those are checking the system against an Exception\n  is being raised or not are now checking also the exception message.\n\n* **Update:** In the ``TimeLog`` class, the raised ``OverBookedException``\n  message has now been made clear by adding the start and end date values of\n  the clashing TimeLog instance.\n\n* **Update:** Removed the unnecessary ``computed_start`` and ``computed_end``\n  columns from ``Task`` class, which are already defined in the\n  ``DateRangeMixin`` which is a super for the Task class.\n\n0.2.17.6\n========\n\n* **Fix:** Fixed a bug in ``ProjectMixin`` where a proper cascade was not\n  defined and the ``Delete`` operations to the ``Projects`` table were not\n  cascaded to the mixed-in classes properly.\n\n0.2.17.5\n========\n\n* **Fix:** Fixed the ``image_format`` attribute implementation in ``Shot``\n  class. Now it will not copy the value of ``Project.image_format`` directly on\n  ``__init__`` but instead will only store the value if the ``image_format``\n  argument in ``__init__`` or ``Shot.image_format`` attribute is set to\n  something.\n\n0.2.17.4\n========\n\n* **Update:** Updated the comment sections of all of the source files to\n  correctly show that Stalker is LGPL v3 (not v2.1).\n\n0.2.17.3\n========\n\n* **New:** Added ``Shot.fps`` attribute to hold the fps information per shot.\n* **Update:** Added the necessary alembic revision to reflect the changes in\n  the ``Version_Inputs`` table.\n\n0.2.17.2\n========\n\n* **Fix:** Fixed ``Version_Inputs`` table to correctly take care of\n  ``DELETE``s on the ``Versions`` table. So now it is possible to delete a\n  ``Version`` instance without first cleaning the ``Link`` instances that is\n  related to that ``Version`` instance.\n\n* **Update:** Changed the ``id`` attribute name from ``info_id`` to ``log_id``\n  in ``AuthenticationLog`` class.\n\n* **Update:** Started moving towards PostgreSQL only implementation. Merged the\n  ``DatabaseModelTester`` class and ``DatabaseModelsPostgreSQLTester`` class.\n\n* **Fix:** Fixed an autoflush issue in\n  ``stalker.models.review.Review.finalize_review_set()``.\n\n0.2.17.1\n========\n\n* **Fix:** Fixed alembic revision\n\n0.2.17\n======\n\n* **New:** Added ``AuthenticationLog`` class to hold user login/logout info.\n* **New:** Added ``stalker.testing`` module to simplify testing setup.\n\n0.2.16.4\n========\n\n* **Fix:** Fixed alembic revision.\n\n0.2.16.3\n========\n\n* **New:** ``ProjectUser`` now also holds a new field called ``rate``. The\n  default value is equal to the ``ProjectUser.user.rate``. It is a way to hold\n  the rate of a user on a specific project.\n\n* **New:** Added the ``Invoice`` class.\n\n* **New:** Added the ``Payment`` class.\n\n* **New:** Added two simple mixins ``AmountMixin`` and ``UnitMixin``.\n\n* **Update:** ``Good`` class is now mixed in with the new ``UnitMixin`` class.\n\n* **Update:** ``BudgetEntry`` class is now mixed in with the new\n  ``AmountMixin`` and ``UnitMixin`` classes.\n\n0.2.16.2\n========\n\n* **New:** ``Group`` permissions can now be set on ``__init__()`` with the\n  ``permissions`` argument.\n\n0.2.16.1\n========\n\n* **Fix:** As usual after a new release that changes database schema, fixed the\n  corresponding Alembic revision (92257ba439e1).\n\n0.2.16\n======\n\n* **New:** ``Budget`` instances are now statusable.\n\n* **Update:** Updated documentation to include database migration instructions\n  with Alembic.\n\n0.2.15.2\n========\n\n* **Fix:** Fixed a typo in the error message in\n  ``User._validate_email_format()`` method.\n\n* **Fix:** Fixed a query-invoked auto-flush problem in\n  ``Task.update_parent_statuses()`` method.\n\n0.2.15.1\n========\n\n* **Fix:** Fixed alembic revision (f2005d1fbadc), it will now drop any existing\n  constraints before re-creating them. And the downgrade function will not\n  remove the constraints.\n\n0.2.15\n======\n\n* **New:** ``db.setup()`` now checks for ``alembic_version`` before setting up\n  a connection to the database and raises a ``ValueError`` if the database\n  alembic version is not matching the current implementation of Stalker.\n\n* **Fix:** ``db.init()`` sets the ``created_by`` and ``updated_by``\n  attributes to ``admin`` user if there is one while creating entity statuses.\n\n* **New:** Created ``create_sdist.cmd`` and ``upload_to_pypi.cmd`` for Windows.\n\n* **New:** ``Project`` to ``Client`` relation is now a many-to-many relation,\n  thus it is possible to set multiple Clients for each project with each client\n  having their own roles in a specific project.\n\n* **Update:** ``ScheduleMixin.schedule_timing`` attribute is now Nullable.\n\n* **Update:** ``ScheduleMixin.schedule_unit`` attribute is now Nullable.\n\n0.2.14\n======\n\n* **Fix:** Fixed ``Task.path`` to always return a path with forward slashes.\n\n* **New:** Introducing ``EntityGroups`` that lets one to group a bunch of\n  ``SimpleEntity`` instances together, it can be used in grouping tasks even if\n  they are in different places on the project task hierarchy or even in\n  different projects.\n\n* **Update:** ``Task.percent_complete`` is now correctly calculated for a\n  ``Duration`` based task by using the ``Task.start`` and ``Task.end``\n  attribute values.\n\n* **Fix:** Fixed ``stalker.models.task.update_time_log_task_parents_for_end()``\n  event to work with SQLAlchemy v1.0.\n\n* **New:** Added an option called ``__dag_cascade__`` to the ``DAGMixin`` to\n  control cascades on mixed in class. The default value is \"all, delete\".\n  Change it to \"save-update, merge\" if you don't want the children also be\n  deleted when the parent is deleted.\n\n* **Fix:** Fixed a bug in ``Version`` class that occurs when a version instance\n  that is a parent of other version instances is deleted, the child versions\n  are also deleted (fixed through DAGMixin class).\n\n0.2.13.3\n========\n\n* **Fix:** Fixed a bug in ``Review.finalize_review_set()`` for tasks that are\n  sent to review and still have some extra time were not clamped to their total\n  logged seconds when the review set is all approved.\n\n0.2.13.2\n========\n\n* **New:** Removed ``msrp``, ``cost`` and ``unit`` arguments from\n  ``BudgetEntry.__init__()`` and added a new ``good`` argument to get all of\n  the data from the related ``Good`` instance. But the ``msrp``, ``cost`` and\n  ``unit`` attributes of ``BudgetEntry`` class are still there to store the\n  values that may not correlate with the related ``Good`` in future.\n\n0.2.13.1\n========\n\n* **Fix:** Fixed a bug in ``Review.finalize_review_set()`` which causes Task\n  instances to not to get any status update if the revised task is a second\n  degree dependee to that particular task.\n\n0.2.13\n======\n\n* **New:** ``Project`` instances can now have multiple repositories. Thus the\n  ``repository`` attribute is renamed to ``repositories``. And the order of the\n  items in the ``repositories`` attribute is restored correctly.\n\n* **New:** ``stalker.db.init()`` now automatically creates environment\n  variables for each repository in the database.\n\n* **New:** Added a new ``after_insert`` which listens ``Repository`` instance\n  ``insert`` instances to automatically add environment variables for the newly\n  inserted repositories.\n\n* **Update:** ``Repository.make_relative()`` now handles paths with environment\n  variables.\n\n* **Fix:** Fixed ``TaskJugglerScheduler`` to correctly generate task absolute\n  paths for PostgreSQL DB.\n\n* **New:** ``Repository.path`` is now writable and sets the correct path\n  (``linux_path``, ``windows_path``, or ``osx_path``) according to the current\n  system.\n\n* **New:** Setting either of the ``Repository.path``,\n  ``Repository.linux_path``, ``Repository.windows_path``,\n  ``Repository.osx_path`` attributes will update the related environment\n  variable if the system and attribute are matching to each other, setting the\n  ``linux_path`` on Linux or setting the ``windows_path`` on Windows or setting\n  the ``osx_path`` on OSX will update the environment variable.\n\n* **New:** Added ``Task.good`` attribute to easily connect tasks to ``Good``\n  instances.\n\n* **New:** Added new methods to ``Repository`` to help managing paths:\n\n  * ``Repository.find_repo()`` to find a repo from a given path. This is a\n    class method so it can be directly used with the Repository class.\n  * ``Repository.to_os_independent_path()`` to convert the given path to a OS\n    independent path which uses environment variables. Again this is a class\n    method too so it can be directly used with the Repository class.\n  * ``Repository.env_var`` a new property that returns the related environment\n    variable name of a repo instance. This is an instance property::\n\n    .. code=block:: python\n\n      # with default settings\n      repo  = Repository(...)\n      repo.env_var  # should print something like \"REPO131\" which will be used\n      #               in paths as \"$REPO131\"\n\n* **Fix:** Fixed ``User.company_role`` attribute which is a relationship to\n  the ``ClienUser`` to cascade ``all, delete-orphan`` to prevent\n  AssertionErrors when a Client instance is removed from the ``User.companies``\n  collection.\n\n0.2.12.1\n========\n\n* **Update:** ``Version`` class is now mixed with the ``DAGMixin``, so all the\n  parent/child relation is coming from the DAGMixin.\n\n* **Update:** ``DAGMixin.walk_hierarchy()`` is updated to walk the hierarchy in\n  ``Depth First`` mode by default (method=0) instead of ``Breadth First`` mode\n  (method=1).\n\n* **Fix:** Fixed ``alembic_revision`` on database initialization.\n\n0.2.12\n======\n\n* **Fix:** Fixed importing of ``ProjectUser`` directly from ``stalker``\n  namespace.\n\n* **Fix:** Fixed importing of ``ClientUser`` directly from ``stalker``\n  namespace.\n\n* **New:** Added two new columns to the ``BudgetEntry`` class to allow more\n  detailed info to be hold.\n\n* **New:** Added a new Mixin called ``DAGMixin`` to create parent/child\n  relation between mixed in class.\n\n* **Update:** The ``Task`` class is now mixed with the ``DAGMixin``, so all the\n  parent/child relation is coming from the DAGMixin.\n\n* **New:** Added a new class called ``Good`` to hold details about the\n  commercial items/services sold in the Studio.\n\n* **New:** Added a new class called ``PriceList`` to create price lists from\n  Goods.\n\n0.2.11\n======\n\n* **New:** User instances now have a new attribute called ``rate`` to track\n  their cost as a resource.\n\n* **New:** Added two new classes called ``Budget`` and ``BudgetEntry`` to\n  record Project budgets in a simple way.\n\n* **New:** Added a new class called **Role** to manage user roles in different\n  Departments, Clients and Projects.\n\n* **New:** User and Department relation is updated to include the role of the\n  user in that department in a more flexible way by using the newly introduced\n  Role class and some association proxy tricks.\n\n* **New:** Also updated the User to Project relation to include the role of the\n  user in that Project by using an associated Role class.\n\n* **Update:** Department.members attribute is renamed to **users** (and removed\n  the *synonym* property).\n\n* **Update:** Removed ``Project.lead`` attribute use ``Role`` instead.\n\n* **Update:** Removed ``Department.lead`` attribute use ``Role`` instead.\n\n* **Update:** Because the ``Project.lead`` attribute is removed, it is now\n  possible to have tasks with no responsible.\n\n* **Update:** Client to User relation is updated to use an association proxy\n  which makes it possible to set a Role for each User for each Client it is\n  assigned to.\n\n* **Update:** Renamed User.company to User.companies as the relation is now\n  able to handle more than one Client instances for the User company.\n\n* **Update:** Task Status Workflow has been updated to convert the status of a\n  DREV task to HREV instead of WIP when the dependent tasks has been set to\n  CMPL. Also the timing of the task is expanded by the value of\n  ``stalker.defaults.timing_resolution`` if it doesn't have any effort left\n  (generally true for CMPL tasks) to allow the resource to review and decide if\n  he/she needs more time to do any update on the task and also give a chance of\n  setting the Task status to WIP by creating a time log.\n\n* **New:** It is now possible to schedule only a desired set of projects by\n  passing a **projects** argument to the TaskJugglerScheduler.\n\n* **New:** Task.request_review() and Review.finalize() will not cap the timing\n  of the task until it is approved and also Review.finalize() will extend the\n  timing of the task if the total timing of the given revisions are not fitting\n  in to the left timing.\n\n0.2.10.5\n========\n\n* **Update:** TaskJuggler output is now written to debug output once per line.\n\n0.2.10.4\n========\n\n* **New:** '@' character is now allowed in Entity nice name.\n\n0.2.10.3\n========\n\n* **New:** '@' character is now allowed in Version take names.\n\n0.2.10.2\n========\n\n* **Fix:** Fixed a bug in\n  ``stalker.models.schedulers.TaskJugglerScheduler._create_tjp_file_content()``\n  caused by non-ascii task names.\n\n* **Fix:** Removed the residual ``RootFactory`` class reference from\n  documentation.\n\n* **New:** Added to new functions called ``utc_to_local`` and ``local_to_utc``\n  for UTC to Local time and vice versa conversion.\n\n0.2.10.1\n========\n\n* **Fix:** Fixed a bug where for a WIP Task with no time logs (apparently\n  something went wrong) and no dependencies using\n  ``Task.update_status_with_dependent_statuses()`` will convert the status to\n  RTS.\n\n0.2.10\n======\n\n* **New:** It is now possible to track the Edit information per Shot using the\n  newly introduced ``source_in``, ``source_out`` and ``record_in`` along with\n  existent ``cut_in`` and ``cut_out`` attributes.\n\n0.2.9.2\n=======\n\n* **Fix:** Fixed MySQL initialization problem in ``stalker.db.init()``.\n\n0.2.9.1\n=======\n\n* **New:** As usual, after a new release, fixed a bug in\n  ``stalker.db.create_entity_statuses()`` caused by the behavioral change of\n  the ``map`` built-in function in Python 3.\n\n0.2.9\n=====\n\n* **New:** Added a new class called ``Daily`` which will help managing\n  ``Version`` outputs (Link instances including Versions itself) as a group.\n\n* **New:** Added a new status list for ``Daily`` class which contains two\n  statuses called \"Open\" and \"Closed\".\n\n* **Update:** Setting the ``Version.take_name`` to a value other than a string\n  will now raise a ``TypeError``.\n\n0.2.8.4\n=======\n\n* **Fix:** Fixed ``SimpleEntity._validate_name()`` method for unicode strings.\n\n0.2.8.3\n=======\n\n* **Fix:** Fixed str/unicode errors due to the code written for Python3\n  compatibility.\n\n* **Update:** Removed ``Task.is_complete`` attribute. Use the status \"CMPL\"\n  instead of this attribute.\n\n0.2.8.2\n=======\n\n* **Fix:** Fixed ``stalker.db.create_alembic_table()`` again to prevent extra\n  row insertion.\n\n0.2.8.1.1\n=========\n\n* **Fix:** Fixed ``stalker.db.create_alembic_table()`` function to handle the\n  situation where the table is already created.\n\n0.2.8.1\n=======\n\n* **Fix:** Fixed ``stalker.db.create_alembic_table()`` function, it is not\n  using the ``alembic`` library anymore to create the ``alembic_version``\n  table, which was the proper way of doing it but it created a lot of problems\n  when Stalker is installed as a package.\n\n0.2.8\n=====\n\n* **Update:** Stalker is now Python3 compatible.\n\n* **New:** Added a new class called ``Client`` which can be used to track down\n  information about the clients of ``Projects``. Also added ``Project.client``\n  and ``User.company`` attributes which are referencing a Client instance\n  allowing to add clients as normal users.\n\n* **New:** ``db.init()`` now creates ``alembic_version`` table and stamps the\n  most recent version number to that table allowing newly initialized databases\n  to be considered in head revision.\n\n* **Fix:** Fixed ``Version._format_take_name()`` method. It is now possible to\n  use multiple underscore characters in ``Version.take_name`` attribute.\n\n0.2.7.6\n=======\n\n* **Update:** Removed ``TimeLog._expand_task_schedule_timing()`` method which\n  was automatically adjusting the ``schedule_timing`` and ``schedule_unit`` of\n  a Task to total duration of the TimeLogs of that particular task, thus\n  increasing the schedule info with the entered time logs.\n\n  But it was setting the ``schedule_timing`` to 0 in some certain cases and it\n  was unnecessary because the main purpose of this method was to prevent\n  TaskJuggler to raise any errors related to the inconsistencies between the\n  schedule values and the duration of TimeLogs and TaskJuggler has never given\n  a real error about that situation.\n\n0.2.7.5\n=======\n\n* **Fix:** Fixed Task parent/child relationship, previously setting the parent\n  of a task to None was cascading a delete operation due to the\n  \"all, delete-orphan\" setting of the Task parent/child relationship, this is\n  updated to be \"all, delete\" and it is now safe to set the parent to None\n  without causing the task to be deleted.\n\n0.2.7.4\n=======\n\n* **Fix:** Fixed the following columns column type from String to Text:\n\n    * Permissions.class_name\n    * SimpleEntities.description\n    * Links.full_path\n    * Structures.custom_template\n    * FilenameTemplates.path\n    * FilenameTemplates.filename\n    * Tickets.summary\n    * Wiki.title\n    * Wiki.content\n\n  and specified a size for the following columns:\n\n    * SimpleEntities.html_class -> String(32)\n    * SimpleEntities.html_style -> String(32)\n    * FilenameTemplates.target_entity_type -> String(32)\n\n  to be compatible with MySQL.\n\n* **Update:** It is now possible to create TimeLog instances for a Task with\n  PREV status.\n\n0.2.7.3\n=======\n\n* **Fix:** Fixed ``Task.update_status_with_dependent_statuses()`` method for a\n  Task where there is no dependency but the status is DREV. Now calling\n  ``Task.update_status_with_dependent_statuses()`` will set the status to RTS\n  if there is no ``TimeLog`` for that task and will set the status to WIP if\n  the task has time logs.\n\n0.2.7.2\n=======\n\n* **Update:** ``TaskJugglerScheduler`` is now 466x faster when dumping all the\n  data to TJP file. So with this new update it is taking only 1.5 seconds to\n  dump ~20k tasks to a valid TJP file where it was around ~10 minutes in\n  previous implementation. The speed enhancements is available only to\n  PostgreSQL dialect for now.\n\n0.2.7.1\n=======\n\n* **Fix:** Fixed TimeLog output in one line per task in ``Task.to_tjp()``.\n\n* **New:** Added ``TaskJugglerScheduler`` now accepts a new argument called\n  ``compute_resources`` which when set to True will also consider\n  `Task.alternative_resources` attribute and will fill\n  ``Task.computed_resources`` attribute for each Task. With\n  ``TaskJugglerScheduler`` when the total number of Task is around 15k it will\n  take around 7 minutes to generate this data, so by default it is set to\n  False.\n\n0.2.7\n=====\n\n* **New:** Added ``efficiency`` attribute to ``User`` class. See User\n  documentation for more info.\n\n0.2.6.14\n========\n\n* **Fix:** Fixed an **autoflush** problem in ``Studio.schedule()`` method.\n\n0.2.6.13\n========\n\n* **New:** Added ``Repository.make_relative()`` method, which makes the given\n  path to relative to the repository root. It considers that the path is\n  already in the repository. So for now, be careful about not to pass a path\n  outside of the repository.\n\n0.2.6.12\n========\n\n* **Update:** ``TaskJugglerScheduler.schedule()`` method now uses the\n  ``Studio.start`` and ``Studio.end`` values for the scheduling range instead\n  of the hardcoded dates.\n\n0.2.6.11\n========\n\n* **Update:** ``Task.create_time_log()`` method now returns the created\n  ``TimeLog`` instance.\n\n0.2.6.10\n========\n\n* **Fix:** Fixed an ``autoflush`` issue in\n  ``Task.update_status_with_children_statuses()`` method.\n\n0.2.6.9\n=======\n\n* **Update:** ``Studio.is_scheduling`` and ``Studio.is_scheduling_by``\n  attributes will not be updated or checked at the beginning of the\n  ``Studio.schedule()`` method. It is the duty of the user to check those\n  attributes before calling ``Studio.schedule()``. This is done in this way\n  because without being able to do a db commit inside ``Studio.schedule()``\n  method (which is the case with transaction managers which may be used in web\n  applications like **Stalker Pyramid**) it is not possible to persist and thus\n  use those variables. So, to be able to use those attributes meaningfully the\n  user should set them. Those variables will be set to False and None\n  accordingly by the ``Studio.schedule()`` method after the scheduling is done.\n\n0.2.6.8\n=======\n\n* **Fix:** Fixed a deadlock in ``TaskJugglerScheduler.schedule()`` method\n  related with the ``Popen.stderr.readlines()`` blocking the TaskJuggler\n  process without being able to read the output buffer.\n\n0.2.6.7\n=======\n\n* **Update:** ``TaskJugglerScheduler.schedule()`` is now using bulk inserts and\n  updates which is way faster than doing it with pure Python. Use\n  ``parsing_method`` (0: SQL, 1: Python) to choose between SQL or Pure Python\n  implementation. Also updated ``Studio.schedule()`` to take in a\n  ``parsing_method`` parameter.\n\n0.2.6.6\n=======\n\n* **Update:** The ``cut_in``, ``cut_out`` and ``cut_duration`` attribute\n  behaviour and the attribute order is updated in ``Shot`` class. So, if three\n  of the values are given, then the ``cut_duration`` attribute value will be\n  calculated from ``cut_in`` and ``cut_out`` attribute values. In any case\n  ``cut_out`` precedes ``cut_duration``, and if none of them given ``cut_in``\n  and ``cut_duration`` values will default to 1 and ``cut_out`` will be\n  calculated by using ``cut_in`` and ``cut_duration``.\n\n0.2.6.5\n=======\n\n* **New:** Entity to Note relation is now Many-to-Many. So one Note can now be\n  assigned more than one Entity.\n\n* **New:** Added alembic revision for ``Entity_Notes`` table creation and data\n  migration from ``Notes`` table to ``Entity_Notes`` table. So all notes are\n  preserved.\n\n* **Fix:** Fixed ``Shot.cut_duration`` attribute initialization on ``Shot``\n  instances restored from database.\n\n* **Fix:** Fixed ``Studios.is_scheduling_by`` relationship configuration, which\n  was wrongly referencing the ``Studios.last_scheduled_by_id`` column instead\n  of ``Studios.is_scheduled_by_id`` column.\n\n0.2.6.4\n=======\n\n* **New:** Added a ``Task.review_set(review_number)`` method to get the desired\n  set of reviews. It will return the latest set of reviews if ``review_number``\n  is skipped or it is None.\n\n* **Update:** Removed ``Task.approve()`` it was making things complex than it\n  should be.\n\n0.2.6.3\n=======\n\n* **Fix:** Added ``Page`` to ``class_names`` in ``db.init()``.\n\n* **Fix:** Fixed ``TimeLog`` tjp representation to use bot the ``start`` and\n  ``end`` date values instead of the ``start`` and ``duration``. This is much\n  better because it is independent from the timing resolution settings.\n\n0.2.6.2\n=======\n\n* **Fix:** Fixed ``stalker.models.studio.schedule()`` method, and prevented it\n  to call ``DBSession.commit()`` which causes errors if there is a transaction\n  manager.\n\n* **Fix:** Fixed ``stalker.models._parse_csv_file()`` method for empty\n  computed resources list.\n\n0.2.6.1\n=======\n\n* **New:** ``stalker.models.task.TimeLog`` instances are now checking if the\n  dependency relation between the task that receives the time log and the tasks\n  that the task depends on will be violated in terms of the start and end dates\n  and raises a ``DependencyViolationError`` if it is the case.\n\n0.2.6\n=====\n\n* **New:** Added ``stalker.models.wiki.Page`` class, for holding a per Project\n  wiki.\n\n0.2.5.5\n=======\n\n* **Fix:** ``Review.task`` attribute now accepts None but this is mainly done\n  to allow its relation to the ``Task`` instance can be broken when it needs to\n  be deleted without issuing a database commit.\n\n0.2.5.4\n=======\n\n* **Update:** The following column names are updated:\n  \n  * ``Tasks._review_number`` to ``Tasks.review_number``\n  * ``Tasks._schedule_seconds`` to ``Tasks.schedule_seconds``\n  * ``Tasks._total_logged_seconds`` to ``Tasks.total_logged_seconds``\n  * ``Reviews._review_number`` to ``Reviews.review_number``\n  * ``Shots._cut_in`` to ``Shots.cut_in``\n  * ``Shots._cut_out`` to ``Shots.cut_out``\n  \n  Also updated alembic migration to create columns with those names.\n\n* **Update:** Updated Alembic revision ``433d9caaafab`` (the one related with\n  stalker 2.5 update) to also include following updates:\n  \n  * Create StatusLists for Tasks, Asset, Shot and Sequences and add all the\n    Statuses in the Task Status Workflow.\n  * Remove ``NEW`` from all of the status lists of Task, Asset, Shot and\n    Sequence.\n  * Update all the ``PREV`` tasks to ``WIP`` to let them use the new Review\n    Workflow.\n  * Update the ``Tasks.review_number`` to 0 for all tasks.\n  * Create StatusLists and Statuses (``NEW``, ``RREV``, ``APP``) for Reviews.\n  * Remove any other status then defined in the Task Status Workflow from Task,\n    Asset, Shot and Sequence status list.\n\n0.2.5.3\n=======\n\n* **Fix:** Fixed a bug in ``Task`` class where trying to remove the\n  dependencies will raise an ``AttributeError`` caused by the\n  ``Task._previously_removed_dependent_tasks`` attribute.\n\n0.2.5.2\n=======\n\n* **New:** Task instances now have two new properties called ``path`` and\n  ``absolute_path``. As in Version instances, these are the rendered version\n  of the related FilenameTemplate object in the related Project. The ``path``\n  attribute is Repository root relative and ``absolute_path`` is the absolute\n  path including the OS dependent Repository path.\n\n* **Update:** Updated alembic revision with revision number \"433d9caaafab\" to\n  also create Statuses introduced with Stalker v0.2.5.\n\n0.2.5.1\n=======\n\n* **Update:** ``Version.__repr__`` results with a more readable string.\n\n* **New:** Added a generalized generator called\n  ``stalker.models.walk_hierarchy()`` that walks and yields the entities over\n  the given attribute in DFS or BFS fashion.\n\n* **New:** Added ``Task.walk_hierarchy()`` which iterates over the hierarchy of\n  the task. It walks in a breadth first fashion. Use ``method=0`` to walk in\n  depth first.\n\n* **New:** Added ``Task.walk_dependencies()`` which iterates over the\n  dependencies of the task. It walks in a breadth first fashion. Use\n  ``method=0`` to walk in depth first.\n\n* **New:** Added ``Version.walk_hierarchy()`` which iterates over the hierarchy\n  of the version. It walks in a depth first fashion. Use ``method=1`` to walk\n  in breadth first.\n\n* **New:** Added ``Version.walk_inputs()`` which iterates over the inputs of\n  the version. It walks in a depth first fashion. Use ``method=1`` to walk in\n  breath first.\n\n* **Update:** ``stalker.models.check_circular_dependency()`` function is now\n  using ``stalker.models.walk_hierarchy()`` instead of recursion over itself,\n  which makes it more robust in deep hierarchies.\n\n* **Fix:** ``db.init()`` now updates the statuses of already created status\n  lists for ``Task``, ``Asset``, ``Shot`` and ``Sequence`` classes.\n\n0.2.5\n=====\n\n* **Update:** ``Revision`` class is renamed to ``Review`` and introduced a\n  couple of new attributes.\n\n* **New:** Added a new workflow called \"Task Review Workflow\". Please see the\n  documentation about the new workflow.\n\n* **Update:** ``Task.responsible`` attribute is now a list which allows\n  multiple responsible to be set for a ``Task``.\n\n* **New:** Because of the new \"Task Review Workflow\" task statuses which are\n  normally created in Stalker Pyramid are now automatically created in Stalker\n  database initialization. The new statuses are\n  **Waiting For Dependency (WFD)**, **Ready To Start (RTS)**,\n  **Work In Progress (WIP)**, **Pending Review (PREV)**,\n  **Has Revision (HREV)**, **On Hold (OH)**, **Stopped (STOP)** and\n  **Completed (CMPL)** are all used in ``Task``, ``Asset``, ``Shot`` and\n  ``Sequence`` status lists by default.\n\n* **New:** Because of the new \"Task Review Workflow\" also a status list for\n  ``Review`` class is created by default. It contains the statuses of\n  **New (NEW)**, **Requested Revision (RREV)** and **Approved (APP)**.\n\n* **Fix:** ``Users.login`` column is now unique.\n\n* **Update:** Ticket workflow in config is now using the proper status names\n  instead of the lower case names of the statuses.\n\n* **New:** Added a new exception called **StatusError** which states the entity\n  status is not suitable for the action it is applied to.\n\n* **New:** ``Studio`` instance now stores the scheduling state to the database\n  to prevent two scheduling process to override each other. It also stores the\n  last schedule message and the last schedule date and the id of the user who\n  has done the scheduling.\n\n* **New:** The **Task Dependency** relation is now using an\n  **Association Object** instead of just a **Secondary Table**. The\n  ``Task.depends`` and ``Task.dependent_of`` attributes are now\n  *association_proxies*.\n\n  Also added extra parameters like ``dependency_target``, ``gap_timing``,\n  ``gap_unit`` and ``gap_model`` to the dependency relation. So all of the\n  dependency relations are now able to hold those extra information.\n\n  Updated the ``task_tjp_template`` to reflect the details of the dependencies\n  that a task has.\n\n* **New:** ``ScheduleMixin`` class now has some default class attributes that\n  will allow customizations in inherited classes. This is mainly done for\n  ``TaskDependency`` class and for ``the gap_timing``, ``gap_unit``,\n  ``gap_model`` attributes which are in fact synonyms of ``schedule_timing``,\n  ``schedule_unit`` and ``schedule_model`` attributes coming from the\n  ``ScheduleMixin`` class. So by using the ``__default_schedule_attr_name__``\n  Stalker is able to display error messages complaining about ``gap_timing``\n  attribute instead of ``schedule_timing`` etc.\n\n* **New:** Updating a task by calling ``Task.request_revision()`` will now set\n  the ``TaskDependency.dependency_target`` to **'onstart'** for tasks those are\n  depending to the revised task and updated to have a status of **DREV**,\n  **OH** or **STOP**. Thus, TaskJuggler will be able to continue scheduling\n  these tasks even if the tasks are now working together.\n\n* **Update:** Updated the TaskJuggler templates to make the tjp output a little\n  bit more readable.\n\n* **New:** ``ScheduleMixin`` now creates more localized (to the mixed in class)\n  column and enum type names in the mixed in classes.\n\n  For example, it creates the ``TaskScheduleModel`` enum type for ``Task``\n  class and for ``TaskDependency`` it creates ``TaskDependencyGapModel`` with\n  the same setup following the ``{{class_name}}{{attr_name}}Model`` template.\n\n  Also it creates ``schedule_model`` column for ``Task``, and ``gap_model`` for\n  ``TaskDependency`` class.\n\n* **Update:** Renamed the ``TaskScheduleUnit`` enum type name to ``TimeUnit``\n  in ``ScheduleMixin``.\n\n0.2.4\n=====\n\n* **New:** Added new class called ``Revision`` to hold info about Task\n  revisions.\n\n* **Update:** Renamed ``ScheduleMixin`` to ``DateRangeMixin``.\n\n* **New:** Added a new mixin called ``ScheduleMixin`` (replacing the old one)\n  which adds attributes like ``schedule_timing``, ``schedule_unit``,\n  ``schedule_model`` and ``schedule_constraint``.\n\n* **New:** Added ``Task.tickets`` and ``Task.open_tickets`` properties.\n\n* **Update:** Removed unnecessary arguments (``project_lead``, ``tasks``,\n  ``watching``, ``last_login``) from User class.\n\n* **Update:** The ``timing_resolution`` attribute is moved from the\n  ``DateRangeMixin`` to ``Studio`` class. So instances of classes like\n  ``Project`` or ``Task`` will not have their own timing resolution anymore.\n\n* **New:** The ``Studio`` instance now overrides the values on\n  ``stalker.defaults`` on creation and on load, and also the ``db.setup()``\n  function lets the first ``Studio`` instance that it finds to update the\n  defaults. So it is now possible to use ``stalker.defaults`` all the time\n  without worrying about the Studio settings.\n\n* **Update:** The ``Studio.yearly_working_days`` value is now always an\n  integer.\n\n* **New:** Added a new method ``ScheduleMixin.least_meaningful_time_unit()`` to\n  calculate the most appropriate timing unit and the value of the given seconds\n  which represents an interval of time.\n  \n  So it will convert 3600 seconds to 1 hours, and 8424000 seconds to 1 years if\n  it represents working time (``as_working_time=True``) or 2340 hours if it is\n  representing the calendar time.\n\n* **New:** Added a new method to ``ScheduleMixin`` called ``to_seconds()``. The\n  ``to_seconds()`` method converts the given schedule info values\n  (``schedule_timing``, ``schedule_unit``, ``schedule_model``) to seconds\n  considering if the given ``schedule_model`` is work time based ('effort' or\n  'length') or calendar time based ('duration').\n\n* **New:** Added a new method to ``ScheduleMixin`` called ``schedule_seconds``\n  which you may recognise from ``Task`` class. What it does is pretty much the\n  same as in the ``Task`` class, it converts the given schedule info values to\n  seconds.\n\n* **Update:** In ``DateRangeMixin``, when the ``start``, ``end`` or\n  ``duration`` arguments given so that the duration is smaller then the\n  ``defaults.timing_resolution`` the ``defaults.timing_resolution`` will be\n  used as the ``duration`` and the ``end`` will be recalculated by anchoring\n  the ``start`` value.\n\n* **New:** Adding a ``TimeLog`` to a ``Task`` and extending its schedule info\n  values now will always use the least meaningful timing unit. So expanding a\n  task from 16 hours to 18 hours will result a task with 2 days of schedule\n  (considering the ``daily_working_hours = 9``).\n\n* **Update:** Moved the ``daily_working_hours`` attribute from ``Studio`` class\n  to ``WorkingHours`` class as it was much related to this one then ``Studio``\n  class. Left a property with the same name in the ``Studio`` class, so it will\n  still function as it was before but there will be no column in the database\n  for that attribute anymore.\n\n0.2.3.5\n=======\n\n* **Fix:** Fixed a bug in ``stalker.models.auth.LocalSession`` where stalker\n  was complaining about \"copy_reg\" module, it seems that it is related to\n  `this bug`_.\n\n  .. _this bug: http://www.archivum.info/python-bugs-list@python.org/2007-04/msg00222.html\n\n0.2.3.4\n=======\n\n* **Update:** Fixed a little bug in Link.extension property setter.\n\n* **New:** Moved the stalker.models.env.EnvironmentBase class to\n  \"Anima Tools\" python module.\n\n* **Fix:** Fixed a bug in stalker.models.task.Task._responsible_getter() where\n  it was always returning the greatest parents responsible as the responsible\n  for the child task when the responsible is set to None for the child.\n\n* **New:** Added ``stalker.models.version.Version.naming_parents`` which\n  returns a list of parents starting from the closest parent Asset, Shot or\n  Sequence.\n\n* **New:** ``stalker.models.version.Version.nice_name`` now generates a name\n  starting from the closest Asset, Shot or Sequence parent.\n\n0.2.3.3\n=======\n\n* **New:** ``Ticket`` action methods (``resolve``, ``accept``, ``reassign``,\n  ``reopen``) now return the created ``TicketLog`` instance.\n\n0.2.3.2\n=======\n\n* **Update:** Added tests for negative or zero fps value in Project class.\n\n* **Fix:** Minor fix to ``schedule_timing`` argument in Task class, where IDEs\n  where assuming that the value passed to the ``schedule_timing`` should be\n  integer where as it accepts floats also.\n\n* **Update:** Removed ``bg_color`` and ``fg_color`` attributes (and columns)\n  from Status class. Use SimpleEntity.html_class and SimpleEntity.html_style\n  attributes instead.\n\n* **New:** Added ``Project.open_tickets`` property.\n\n0.2.3.1\n=======\n\n* **Fix:** Fixed an inconvenience in SimpleEntity.__init__() when a\n  date_created argument with a value is later than datetime.datetime.now() is\n  supplied and the date_updated argument is skipped or given as None, then the\n  date_updated attribute value was generated from datetime.datetime.now() this\n  was causing an unnecessary ValueError. This is fixed by directly copying the\n  date_created value to date_updated value when it is skipped or None.\n\n0.2.3\n=====\n\n* **New:** SimpleEntity now have two new attributes called ``html_style`` and\n  ``html_class`` which can be used in storing cosmetic html values.\n\n0.2.2.3\n=======\n\n* **Update:** Note.content attribute is now a synonym of the Note.description\n  attribute.\n\n0.2.2.2\n=======\n\n* **Update:** Studio.schedule() now returns information about how much did it\n  take to schedule the tasks.\n\n* **Update:** Studio.to_tjp() now returns information about how much did it\n  take to complete the conversion.\n\n0.2.2.1\n=======\n\n* **Fix:** Task.percent_complete() now calculates the percent complete\n  correctly.\n\n0.2.2\n=====\n\n* **Update:** Added cascade attributes to all necessary relations for all the\n  classes.\n\n* **Update:** The Version class is not mixed with the StatusMixin anymore. So\n  the versions are not going to be statusable anymore. Also created alembic\n  revision (a6598cde6b) for that update.\n\n0.2.1.2\n=======\n\n* **Update:** TaskJugglerScheduler and the Studio classes are now returning the\n  stderr message out of their ``schedule()`` methods.\n\n0.2.1.1\n=======\n\n* **Fix:** Disabled some deep debug messages on\n  TaskJugglerScheduler._parse_csv_file().\n\n* **Fix:** Fixed a flush issue related to the Task.parent attribute which is\n  lazily loaded in Task._schedule_seconds_setter().\n\n0.2.1\n=====\n\n* **Fix:** As usual distutil thinks ``0.2.0`` is a lower version number than\n  ``0.2.0.rc5`` (I should have read the documentation again and used\n  ``0.2.0.c5`` instead of ``0.2.0.rc5``) so this is a dummy update to just to\n  fix the version number.\n\n0.2.0\n=====\n\n* **Update:** Vacation tjp template now includes the time values of the start\n  and end dates of the Vacation instance.\n\n0.2.0.rc5\n=========\n\n* **Update:** For a container task, ``Task.total_logged_seconds`` and\n  ``Task.schedule_seconds`` attributes are now using the info of the child\n  tasks. Also these attributes are cached to database, so instead of querying\n  the child tasks all the time, the calculated data is cached and whenever a\n  TimeLog is created or updated for a child task (which changes the\n  ``total_logged_seconds`` for the child task) or the ``schedule_timing`` or\n  ``schedule_unit`` attributes are updated, the cached values are updated on\n  the parents. Allowing Stalker to display percent_complete info of a container\n  task without loading any of its children.\n\n* **New:** Added ``Task.percent_complete`` attribute, which calculates the\n  percent of completeness of the task based on the\n  ``Task.total_logged_seconds`` and ``Task.schedule_seconds`` attributes.\n\n* **Fix:** Added ``TimeLog.__eq__()`` operator to more robustly check if the\n  time logs are overlapping.\n\n* **New:** Added ``Project.percent_complete``,\n  ``Percent.total_logged_seconds`` and ``Project.schedule_seconds`` attributes.\n\n* **Update:** ``ScheduleMixin._validate_dates()`` does not set the date values\n  anymore, it just return the calculated and validated ``start``, ``end`` and\n  ``duration`` values.\n\n* **Update:** ``Vacation`` now can be created without a ``User`` instance,\n  effectively making the ``Vacation`` a ``Studio`` wide vacation, which applies\n  to all users.\n\n* **Update:** ``Vacation.__strictly_typed__`` is updated to ``False``, so there\n  is no need to create a ``Type`` instance to be able to create a ``Vacation``.\n\n* **New:** ``Studio.vacations`` property now returns the ``Vacation`` instances\n  which has no *user*.\n\n* **Update:** ``Task.start`` and ``Task.end`` values are no more read from\n  children Tasks for a container task over and over again but calculated\n  whenever the start and end values of a child task are changed or a new child\n  is appended or removed.\n\n* **Update:** ``SimpleEntity.description`` validation routine doesn't convert\n  the input to string anymore, but checks the given description value against\n  being a string or unicode instance.\n\n* **New:** Added ``Ticket.summary`` field.\n\n* **Fix:** Fixed ``Link.extension``, it is now accepting unicode.\n\n0.2.0.rc4\n=========\n\n* **New:** Added a new attribute to ``Version`` class called\n  ``latest_version`` which holds the latest version in the version queue.\n\n* **New:** To optimize the database connection times, ``stalker.db.setup()``\n  will not try to initialize the database every time it is called anymore. This\n  leads a ~4x speed up in database connection setup. To initialize a newly\n  created database please use::\n\n    # for a newly created database\n    from stalker import db\n    db.setup() # connects to database\n    db.init()  # fills some default values to be used with Stalker\n\n    # for any subsequent access just use (don't need to call db.init())\n    db.setup()\n\n* **Update:** Removed all ``__init_on_load()`` methods from all of the classes.\n  It was causing SQLAlchemy to eagerly load relations, thus slowing down\n  queries in certain cases (especially in ``Task.parent`` -> ``Task.children``\n  relation).\n\n* **Fix:** Fixed ``Vacation`` class tj3 format.\n\n* **Fix:** ``Studio.now`` attribute was not properly working when the\n  ``Studio`` instance has been restored from database.\n\n0.2.0.rc3\n=========\n\n* **New:** Added a new attribute to ``Task`` class called ``responsible``.\n\n* **Update:** Removed ``Sequence.lead_id`` use ``Task.reponsible`` instead.\n\n* **Update:** Updated documentation to include documentation about\n  Configuring Stalker with ``config.py``.\n\n* **Update:** The ``duration`` argument in ``Task`` class is removed. It is\n  somehow against the idea of having ``schedule_model`` and ``schedule_timing``\n  arguments (``schedule_model='duration'`` is kind of the same).\n\n* **Update:** Updated ``Task`` class documentation.\n\n0.2.0.rc2\n=========\n\n* **New:** Added ``Version.created_with`` attribute to track the environment or\n  host program name that a particular ``Version`` instance is created with.\n\n0.2.0.rc1\n=========\n\n* **Update:** Moved the Pyramid part of the system to another package called\n  ``stalker_pyramid``.\n\n* **Fix:** Fixed ``setup.py`` where importing ``stalker`` to get the\n  ``__version__`` variable causing problems.\n\n0.2.0.b9\n========\n\n* **New:** Added ``Version.latest_published_version`` and\n  ``Version.is_latest_published_version()``.\n\n* **Fix:** Fixed ``Version.__eq__()``, now Stalker correctly distinguishes\n  different Version instances.\n\n* **New:** Added ``Repository.to_linux_path()``,\n  ``Repository.to_windows_path()``, ``Repository.to_osx_path()`` and\n  ``Repository.to_native_path()`` to the ``Repository`` class.\n\n* **New:** Added ``Repository.is_in_repo(path)`` which checks if the given\n  path is in this repo.\n\n0.2.0.b8\n========\n\n* **Update:** Renamed **Version.version_of** attribute to **Version.task**.\n\n* **Fix:** Fixed **Version.version_number** where it was not possible to have\n  a version number bigger than 2.\n\n* **Fix:** In **db.setup()** Ticket statuses are only created if there aren't\n  any.\n\n* **Fix:** Added **Vacation** class to the registered class list in\n  stalker.db.\n\n0.2.0.b7\n========\n\n* **Update:** **Task.schedule_constraint** is now reflected to the tjp file\n  correctly.\n\n* **Fix:** **check_circular_dependency()** now checks if the **entity** and\n  the **other_entity** are the same.\n\n* **Fix:** **Task.to_tjp()** now correctly add the dependent tasks of a\n  container task.\n\n* **Fix:** **Task.__eq__()** now correctly considers the parent, depends,\n  resources, start and end dates.\n\n* **Update:** **Task.priority** is now reflected in tjp file if it is\n  different than the default value (500).\n\n* **New::** Added a new class called **Vacation** to hold user vacations.\n\n* **Update:** Removed dependencies to ``pyramid.security.Allow`` and\n  ``pyramid.security.Deny`` in couple of packages.\n\n* **Update:** Changed the way the ``stalker.defaults`` is created.\n\n* **Fix:** **EnvironmentBase.get_version_from_full_path()**,\n  **EnvironmentBase.get_versions_from_path()**,\n  **EnvironmentBase.trim_repo_path()**, **EnvironmentBase.find_repo** methods\n  are now working properly.\n\n* **Update:** Added **Version.absolute_full_path** property which renders the\n  absolute full path which also includes the repository path.\n\n* **Update:** Added **Version.absolute_path** property which renders the\n  absolute path which also includes the repository path.\n\n0.2.0.b6\n========\n\n* **Fix:** Fixed **LocalSession._write_data()**, previously it was not\n  creating the local session folder.\n\n* **New:** Added a new method called **LocalSession.delete()** to remove the\n  local session file.\n\n* **Update:** **Link.full_path** can now be set to an empty string. This is\n  updated in this way for **Version** class.\n\n* **Update:** Updated the formatting of **SimpleEntity.nice_name**, it is now\n  possible to have uppercase letters and camel case format will be preserved.\n\n* **Update**: **Version.take_name** formatting is enhanced.\n\n* **New**: **Task** class is now mixed in with **ReferenceMixin** making it\n  unnecessary to have **Asset**, **Shot** and **Sequence** classes all mixed\n  in individually. Thus removed the **ReferenceMixin** from **Asset**,\n  **Shot** and **Sequence** classes.\n\n* **Update**: Added **Task.schedule_model** validation and its tests.\n\n* **New**: Added **ScheduleMixin.total_seconds** and\n  **ScheduleMixin.computed_total_seconds**.\n\n0.2.0.b5\n========\n\n* **New:** **Version** class now has two new attributes called ``parent`` and\n  ``children`` which will be used in tracking of the history of Version\n  instances and track which Versions are derived from which Version.\n\n* **New:** **Versions** instances are now derived from **Link** class and not\n  **Entity**.\n\n* **Update:** Added new revisions to **alembic** to reflect the change in\n  **Versions** table.\n\n* **Update:** **Links.path** is renamed to **Links.full_path** and added\n  three new attributes called **path**, **filename** and **extension**.\n\n* **Update:** Added new revisions to alembic to reflect the change in\n  **Links** table.\n\n* **New:** Added a new class called **LocalSession** to store session data in\n  users local filesystem. It is going to be replaced with some other system\n  like **Beaker**.\n\n* **Fix:** Database part of Stalker can now be imported without depending to\n  **Pyramid**.\n\n* **Fix:** Fixed documentation errors that **Sphinx** complained about.\n\n0.2.0.b4\n========\n\n* No changes in SOM.\n\n0.2.0.b3\n========\n\n* **Update:** FilenameTemplate's are not ``strictly typed`` anymore.\n\n* **Update:** Removed the FilenameTemplate type initialization, FilenameTemplates\n  do not depend on Types anymore.\n\n* **Update:** Added back the ``plural_class_name`` (previously ``plural_name``)\n  property to the ORMClass class, so all the classes in SOM now have this new\n  property. \n\n* **Update:** Added ``accepts_references`` attribute to the EntityType class.\n\n* **New:** The Link class has a new attribute called ``original_filename`` to\n  store the original file names of link files.\n\n* **New:** Added **alembic** to the project requirements.\n\n* **New:** Added alembic migrations which adds the ``accepts_references`` column\n  to ``EntityTypes`` table and ``original_name`` to the ``Links`` table.\n\n0.2.0.b2\n========\n\n* Stalker is now compatible with Python 2.6.\n* Task:\n\n  * **Update:** Tasks now have a new attribute called ``watchers`` which holds a\n    list of User instances watching the particular Task.\n\n  * **Update:** Users now have a new attribute called ``watching`` which is a\n    list of Task instances that this user is watching.\n\n* TimeLog:\n\n  * **Update:** TimeLog instances will expand Task.schedule_timing value\n    automatically if the total amount of logged time is more than the\n    schedule_timing value.\n\n  * **Update:** TimeLogs are now considered while scheduling the task.\n\n  * **Fix:** TimeLogs raises OverBookedError when appending the same TimeLog\n    instance to the same resource.\n\n* Auth:\n\n  * **Fix:** The default ACLs for determining the permissions are now working\n    properly.\n\n0.2.0.b1\n========\n\n* WorkingHours.is_working_hour() is working now.\n\n* WorkingHours class is moved from stalker.models.project to\n  stalker.models.studio module.\n\n* ``daily_working_hours`` attribute is moved from\n  stalker.models.project.Project to stalker.models.studio.Studio class.\n\n* Repository path variables now ends with a forward slash even if it is not\n  given.\n\n* Updated Project classes validation messages to correlate with Stalker\n  standard.\n\n* Implementation of the Studio class is finished. The scheduling works like a\n  charm.\n\n* It is now possible to use any characters in SimpleEntity.name and the derived\n  classes.\n\n* Booking class is renamed to TimeLog.\n\n0.2.0.a10\n=========\n\n* Added new attribute to WorkingHours class called ``weekly_working_hours``,\n  which calculates the weekly working hours based on the working hours defined\n  in the instance.\n\n* Task class now has a new attribute called ``schedule_timing`` which is\n  replacing the ``effort``, ``length`` and ``duration`` attributes. Together\n  with the ``schedule_model`` attribute it will be used in scheduling the Task.\n\n* Updated the config system to the one used in oyProjectManager (based on\n  Sphinx config system). Now to reach the defaults::\n\n    # instead of doing the following\n    from stalker.conf import defaults # not valid anymore\n    \n    # use this\n    from stalker import defaults\n  \n  If the above idiom is used, the old ``defaults`` module behaviour is\n  retained, so no code change is required other than the new lower case config\n  variable names.\n\n0.2.0.a9\n========\n\n* A new property called ``to_tjp`` added to the SimpleEntity class which needs\n  to be implemented in the child and is going to be used in TaskJuggler\n  integration.\n\n* A new attribute called ``is_scheduled`` added to Task class and it is going\n  to be used in Gantt charts. Where it will lock the class and will not try\n  to snap it to anywhere if it is scheduled.\n\n* Changed the ``resolution`` attribute name to ``timing_resolution`` to comply\n  with TaskJuggler.\n\n* ScheduleMixin:\n\n  * Updated ScheduleMixin class documentation.\n\n  * There are two new read-only attributes called ``computed_start`` and\n    ``computed_end``. These attributes will be used in storing of the values\n    calculated by TaskJuggler, and will be used in Gantt Charts if available.\n\n  * Added ``computed_duration``.\n\n* Task:\n\n  * Arranged the TaskJuggler workflow.\n\n  * The task will use the effort > length > duration attributes in `to_tjp`\n    property.\n\n* Changed the license of Stalker from BSD-2 to LGPL 2.1. Any version previous\n  to 0.2.0.a9 will be still BSD-2 and any version from and including 0.2.0.a9\n  will be distributed under LGPL 2.1 license.\n\n* Added new types of classes called Schedulers which are going to be used in\n  scheduling the tasks.\n\n* Added TaskJugglerScheduler, it uses the given project and schedules its\n  tasks.\n\n0.2.0.a8\n========\n\n* TagSelect now can be filled by setting its ``value`` attribute (Ex:\n  TagSelect.set('value', data))\n\n* Added a new method called ``is_root`` to Task class. It is true for tasks\n  where there are no parents.\n\n* Added a new attribute called ``users`` to the Department class which is a\n  synonym for the ``members`` attribute.\n\n* Task:\n\n  * Task class is now preventing one of the dependents to be set as the parent\n    of a task.\n\n  * Task class is now preventing one of the parents to be set as the one of the\n    dependents of a task.\n\n  * Fixed ``autoflush`` bugs in Task class.\n\n* Fixed `admin` users department initialization.\n\n* Added ``thumbnail`` attribute to the SimpleEntity class which is a reference\n  to a Link instance, showing the path of the thumbnail.\n\n* Fixed Circular Dependency bug in Task class, where a parent of a newly\n  created task is depending to another task which is set as the dependee for\n  this newly created task (T1 -> T3 -> T2 -> T1 (parent relation) -> T3 -> T2\n  etc.).\n\n0.2.0.a7\n========\n\n* Changed these default setting value names to corresponding new names:\n\n  * ``DEFAULT_TASK_DURATION`` -> ``TASK_DURATION``\n  * ``DEFAULT_TASK_PRIORITY`` -> ``TASK_PRIORITY``\n  * ``DEFAULT_VERSION_TAKE_NAME`` -> ``VERSION_TAKE_NAME``\n  * ``DEFAULT_TICKET_LABEL`` -> ``TICKET_LABEL``\n  * ``DEFAULT_ACTIONS`` -> ``ACTIONS``\n  * ``DEFAULT_BG_COLOR`` -> ``BG_COLOR``\n  * ``DEFAULT_FG_COLOR`` -> ``FG_COLOR``\n\n* stalker.conf.defaults:\n\n  * Added default settings for project working hours (``WORKING_HOURS``,\n    ``DAY_ORDER``, ``DAILY_WORKING_HOURS``)\n\n  * Added a new variable for setting the task time resolution called\n    ``TIME_RESOLUTION``.\n\n* stalker.models.project.Project:\n\n  * Removed Project.project_tasks attribute, use Project.tasks directly to get\n    all the Tasks in that project. For root task you can do a quick query::\n\n      Task.query.filter(Task.project==proj_id).filter(Task.parent==None).all()\n    \n    This will also return the Assets, Sequences and Shots in that project,\n    which are also Tasks.\n\n  * Users are now assigned to Projects by appending them to the Project.users\n    list. This is done in this way to allow a reduced list of resources to be\n    shown in the Task creation dialogs.\n\n  * Added a new helper class for Project working hour management, called\n    WorkingHours.\n\n  * Added a new attribute to Project class called ``working_hours`` which holds\n    stalker.models.project.WorkingHours instances to manage the Project working\n    hours. It will directly be passed to TaskJuggler.\n\n* stalker.models.task.Task:\n\n  * Removed the Task.task_of attribute, use Task.parent to get the owner of\n    this Task.\n\n  * Task now has two new attributes called Task.parent and Task.children which\n    allow more complex Task-to-Task relation.\n\n  * Secondary table name for holding Task to Task dependency relation is\n    renamed from ``Task_Tasks`` to ``Task_Dependencies``.\n\n  * check_circular_dependency function is now accepting a third argument which\n    is the name of the attribute to be investigated for circular relationship.\n    It is done in that way to be able to use the same function in searching for\n    circular relations both in parent/child and depender/dependee relations.\n\n* ScheduleMixin:\n\n  * Added a new attribute to ScheduleMixin for time resolution adjustment.\n    Default value is 1 hour and can be set with\n    stalker.conf.defaults.TIME_RESOLUTION. Any finer time than the resolution\n    is rounded to the closest multiply of the resolution. It is possible to set\n    it from microseconds to years. Although 1 hour is a very reasonable\n    resolution which is also the default resolution for TaskJuggler.\n\n  * ScheduleMixin now uses datetime.datetime for the start and end attributes.\n\n  * Renamed the ``start_date`` attribute to ``start``.\n\n  * Renamed the ``end_date`` attribute to ``end``\n\n* Removed the TaskableEntity.\n\n* Asset, Sequence and Shot classes are now derived from Task class allowing\n  more complex Task relation combined with the new parent/child relation of\n  Tasks. Use Asset.children or Asset.tasks to reach the child tasks of that\n  asset (same with Sequence and Shot classes).\n\n* stalker.models.shot.Shot:\n\n  * Removed the sequence and introduced sequences attribute in Shot class. Now\n    one shot can be in more than one Sequence. Allowing more complex\n    Shot/Sequence relations..\n\n  * Shots can now be created without a Sequence instance. The sequence\n    attribute is just used to group the Shots.\n\n  * Shots now have a new attribute called ``scenes``, holding Scene instances.\n    It is created to group same shots occurring in the same scenes.\n\n* In tests all the Warnings are now properly handled as Warnings.\n\n* stalker.models.ticket.Ticket:\n\n  * Ticket instances are now tied to Projects and it is now possible to create\n    Tickets without supplying a Version. They are free now.\n\n  * It is now possible to link any SimpleEntity to a Ticket.\n\n  * The Ticket Workflow is now fully customizable. Use\n    stalker.conf.defaults.TICKET_WORKFLOW dictionary to define the workflow and\n    stalker.conf.defaults.TICKET_STATUS_ORDER for the order of the ticket\n    statuses.\n\n* Added a new class called ``Scene`` to manage Shots with another property.\n\n* Removed the ``output_path`` attribute in FilenameTemplate class.\n\n* Grouped the templates for each entity under a directory with the entity name.\n\n0.2.0.a6\n========\n\n* Users now can have more than one Department.\n\n* User instances now have two new properties for getting the user tickets\n  (User.tickets) and the open tickets (User.open_tickets).\n\n* New shortcut Task.project returns the Task.task_of.project value.\n\n* Shot and Asset creation dialogs now automatically updated with the given\n  Project instance info.\n\n* User overview page is now reflection the new design.\n\n0.2.0.a5\n========\n\n* The ``code`` attribute of the SimpleEntity is now introduced as a separate\n  mixin. To let it be used by the classes it is really needed.\n\n* The ``query`` method is now converted to a property so it is now possible to\n  use it like a property as in the SQLAlchemy.orm.Session as shown below::\n\n    from stalker import Project\n    Project.query.all() # instead of Project.query().all()\n\n* ScheduleMixin.due_date is renamed to ScheduleMixin.end_date.\n\n* Added a new class attribute to SimpleEntity called ``__auto_name__`` which\n  controls the naming of the instances and instances derived from SimpleEntity.\n  If ``__auto_name__`` is set to True the ``name`` attribute of the instance\n  will be automatically generated and it will have the following format::\n\n    {{ClassName}}_{{UUID4}}\n    \n  Here are a couple of naming examples::\n\n    Ticket_74bb46b0-29de-4f3e-b4e6-8bcf6aed352d\n    Version_2fa5749e-8cdb-4887-aef2-6d8cec6a4faa\n\n* Fixed an autoflush issue with SQLAlchemy in StatusList class. Now the status\n  column is again not nullable in StatusMixin.\n\n0.2.0.a4\n========\n\n* Added a new class called EntityType to hold all the available class names and\n  capabilities.\n\n* Version class now has a new attribute called ``inputs`` to hold the inputs of\n  the current Version instance. It is a list of Link instances.\n\n* FilenameTemplate classes ``path`` and ``filename`` attributes are no more\n  converted to string, so given a non string value will raise TypeError.\n\n* Structure.custom_template now only accepts strings and None, setting it to\n  anything else will raise a TypeError.\n\n* Two Type's for FilenameTemplate's are created by default when initializing\n  the database, first is called \"Version\" and it is used to define\n  FilenameTemplates which are used for placing Version source files. The second\n  one is called \"Reference\" and it is used when injecting references to a given\n  class. Along with the FilenameTemplate.target_entity_type this will allow one\n  to create two different FilenameTemplates for one class::\n\n    # first get the Types\n    vers_type = Type.query()\\\n                .filter_by(target_entity_type=\"FilenameTemplate\")\\\n                .filter_by(type=\"Version\")\\\n                .first()\n    \n    ref_type = Type.query()\\\n               .filter_by(target_entity_type=\"FilenameTemplate\")\\\n               .filter_by(type=\"Reference\")\\\n               .first()\n    \n    # lets create a FilenameTemplate for placing Asset Version files.\n    f_ver = FilenameTemplate(\n        target_entity_type=\"Asset\",\n        type=vers_type,\n        path=\"Assets/{{asset.type.code}}/{{asset.code}}/{{task.type.code}}\",\n        filename=\"{{asset.code}}_{{version.take_name}}_{{task.type.code}}_v{{'%03d'|version.version_number}}{{link.extension}}\"\n        output_path=\"{{version.path}}/Outputs/{{version.take_name}}\"\n    )\n    \n    # and now define a FilenameTemplate for placing Asset Reference files.\n    # no need to have an output_path here...\n    f_ref = FilenameTemplate(\n        target_entity_type=\"Asset\",\n        type=ref_type,\n        path=\"Assets/{{asset.type.code}}/{{asset.code}}/References\",\n        filename=\"{{link.type.code}}/{{link.id}}{{link.extension}}\"\n    )\n\n* stalker.db.register() now accepts only real classes instead of class names.\n  This way it can store more information about classes.\n\n* Status.bg_color and Status.fg_color attributes are now simple integers. And\n  the Color class is removed.\n\n* StatusMixin.status is now a ForeignKey to a the Statuses table, thus it is a\n  real Status instance instead of an integer showing the index of the Status in\n  the related StatusList. This way the Status of the object will not change if\n  the content of the StatusList is changed.\n\n* Added new attribute Project.project_tasks which holds all the direct or\n  indirect Tasks created for that project.\n\n* User.login_name is renamed to User.login.\n\n* Removed the ``first_name``, ``last_name`` and ``initials`` attributes from\n  User class. Now the ``name`` and ``code`` attributes are going to be used,\n  thus the ``name`` attribute is no more the equivalent of ``login`` and the\n  ``code`` attribute is doing what was ``initials`` doing previously.\n\n0.2.0.a3\n========\n\n* Status class now has two new attributes ``bg_color`` and ``fg_color`` to hold\n  the UI colors of the Status instance. The colors are Color instances.\n\n0.2.0.a2\n========\n\n* SimpleEntity now has an attribute called ``generic_data`` which can hold any\n  kind of ``SOM`` object inside and it is a list.\n\n* Changed the formatting rules for the ``name`` in SimpleEntity class, now it\n  can start with a number, and it is not allowed to have multiple whitespace\n  characters following each other.\n\n* The ``source`` attribute in Version is renamed to ``source_file``.\n\n* The ``version`` attribute in Version is renamed to ``version_number``.\n\n* The ``take`` attribute in Version is renamed to ``take_name``.\n\n* The ``version_number`` in Version is now generated automatically if it is\n  skipped or given as None or it is too low where there is already a version\n  number for the same Version series (means attached to the same Task and has\n  the same ``take_name``.\n\n* Moved the User class to ``stalker.models.auth module``.\n\n* Removed the ``stalker.ext.auth`` module because it is not necessary anymore.\n  Thus the User now handles all the password conversions by itself.\n\n* ``PermissionGroup`` is renamed back to Group\n  again to match with the general naming of the authorization concept.\n\n* Created two new classes for the Authorization system, first one is called\n  Permission and the second one is a Mixin which is called ACLMixin which adds\n  ACLs to the mixed in class. For now, only the User and Group classes are\n  mixed with this mixin by default.\n\n* The declarative Base class of SQLAlchemy is now created by binding it to a\n  ORMClass (a random name) which lets all the derived class to have a method\n  called ``query`` which will bypass the need of calling\n  ``DBSession.query(class_)`` but instead just call ``class_.query()``::\n\n    from stalker.models.auth import User\n    user_1 = User.query().filter_by(name='a user name').first()\n\n\n0.2.0.a1\n========\n\n* Changed the ``db.setup`` arguments. It is now accepting a dictionary instead\n  of just a string to comply with the SQLAlchemy scaffold and this dictionary\n  should contain keys for the SQLAlchemy engine setup. There is another utility\n  that comes with Pyramid to setup the database under the `scripts` folder, it\n  is also working without any problem with stalker.db.\n\n* The ``session`` variable is renamed to ``DBSession`` and is now a scopped\n  session, so there is no need to use ``DBSession.commit`` it will be handled\n  by the system it self.\n\n* Even though the ``DBSession`` is using the Zope Transaction Manager extension\n  normally, in the database tests no extension is used because the transaction\n  manager was swallowing all errors and it was a little weird to try to catch\n  this errors out of the ``with`` block.\n\n* Refactored the code, all the models are now in separate python files, but can\n  be directly imported from the main stalker module as shown::\n\n    from stalker import User, Department, Task\n  \n  By using this kind of organization, both development and usage will be eased\n  out.\n\n* ``task_of`` now only accepts TaskableEntity instances.\n\n* Updated the examples. It is now showing how to extend SOM correctly. \n\n* Updated the references to the SOM classes in docstrings and rst files.\n\n* Removed the ``Review`` class. And introduced the much handier Ticket class.\n  Now reviewing a data is the process of creating Ticket's to that data.\n\n* The database is now initialized with a StatusList and a couple of Statuses\n  appropriate for Ticket instances.\n\n* The database is now initialized with two Type instances ('Enhancement' and\n  'Defect') suitable for Ticket instances.\n\n* StatusMixin now stores the status attribute as an Integer showing the index\n  of the Status in the ``status_list`` attribute but when asked for the value\n  of ``StatusMixin.status`` attribute it will return a proper Status instance\n  and the attribute can be set with an integer or with a proper Status\n  instance.\n"
  },
  {
    "path": "CHANGELOG_OLD.rst",
    "content": "0.1.2.a5\n========\n\n* :class:`~stalker.core.models.SimpleEntity`.\\\n  :attr:`~stalker.core.models.SimpleEntity.name` attribute doesn't accept\n  anything other than a string or unicode anymore.\n* All the error messages are now showing both the class and attribute names,\n  and for TypeErrors it also shows the current given data type which raised\n  the error and the desired type.\n* Fixed a bug that causes recursion in queries to\n  :class:`~stalker.core.models.StatusList` instances.\n* Removed the ``FilenameTemplate.output_file_code`` attribute cause it is not\n  needed.\n* Removed the ``FilenameTemplate.output_is_relative`` attribute, now all the\n  path values are :class:`~stalker.models.repository.Repository` relative.\n* Renamed the ``path_code`` to ``path``, ``file_code`` to ``filename`` and\n  ``output_path_code`` to ``output_path`` in\n  :class:`~stalker.models.template.FilenameTemplate``\\ .\n\n0.1.2.a4\n========\n\n* Added database tests for:\n  * :class:`~stalker.core.models.Review`\n  * :class:`~stalker.core.models.Task`\n  * :class:`~stalker.core.models.Version`\n* The ``published`` attribute in :class:`~stalker.core.models.Version` is\n  renamed to :attr:`~stalker.core.models.Version.is_published`.\n* :class:`~stalker.core.models.Structure` is not ``strictly_typed`` anymore.\n* The initialization of ``status_list`` attribute in classes which are mixed\n  with :class:`~stalker.core.models.StatusMixin` is now automatically done if\n  there is a database connection (stalker.db.session is not None) and there is\n  a suitable :class:`~stalker.core.models.StatusList` instance in the database\n  whom :attr:`~stalker.core.models.StatusList.target_entity_type` attribute is\n  set to the mixed-in class name.\n* Finished the tests for :class:`~stalker.core.models.Booking` class.\n* The :class:`~stalker.core.models.Booking` class now checks if there is more\n  than one booking is creating with overlapping time interval and issue a\n  :class:`~stalker.core.errors.OverBookedWarning`.\n* Cleaned up the code style.\n* Moved the ``target_entity_type`` functionality to a new mixin class called\n  :class:`~stalker.core.models.TargetEntityTypeMixin`\\ .\n  :class:`~stalker.core.models.StatusList`,\n  :class:`~stalker.core.models.FilenameTemplate`, and\n  :class:`~stalker.core.models.Type` classes are mixin with this new class.\n* Included the ``pyseq`` library to the dependency list.\n\n0.1.2.a3\n========\n\n* stalker.__version__ is fixed for PyPI\n\n0.1.2.a2\n========\n\n* All the models are now converted to SQLAlchemy's Declarative.\n* Because of the move to the SQLAlchemy Declarative extension the\n  stalker.etx.ValidatedList is deprecated. SQLAlchemy is doing (in most of the\n  aspects) a much better job.\n* The ``stalker.core.mixins`` module is merged with :mod:`~stalker.core.models`\n  module.\n* Becase all the models are declaratively defined and thus they have their\n  mappers and tables inside, the ``stalker.db.mapper``, ``stalker.db.tables``\n  and the ``stalker.db.mixins`` modules are removed.\n* Added the :class:`~stalker.core.models.Version` class with all its tests.\n* Fixed :attr:`~stalker.core.models.Project.assets` and\n  :attr:`~stalker.core.models.Project.sequences` attributes in\n  :class:`~stalker.core.models.Project` class. It is now using the\n  ``db.query`` to get the info out of the database, which is much easier than\n  setting up a complex relation.\n* Fixed the tests for the database. It is now working properly with the SOM\n  which is using SQLAlchemy declarative. There are still missing tests though.\n* The :class:`~stalker.core.models.Project` and\n  :class:`~stalker.core.models.Structure` classes are not\n  ``__strictly_typed__`` anymore. It was a bump on the road to make them\n  strictly typed.\n\n0.1.2.a1\n========\n\n* Started to switch to SQLAlchemy ORM Declarative for SOM, implemented these\n  classes successfully:\n\n  SimpleEntity, Type, Tag, Note, ImageFormat, Status, StatusList, Repository,\n  Structure, FilenameTemplate, Department, Link, ReferenceMixin, StatusMixin,\n  ScheduleMixin, Project, Sequence, Shot, Asset, Review.\n\n* Empty :class:`~stalker.core.models.StatusList`\\ s are now allowed. The\n  validation overhead is left to the SOM user.\n* Removed the ``TaskMixin`` on the way of moving to declarative. It was not\n  possible to add a one-to-many relation to the\n  :class:`~stalker.core.model.Task`\\ s\n  :attr:`~stalker.core.models.Task.task_of` attribute from all the mixed-in\n  classes. So the solution was to introduce a new\n  :class:`~stalker.core.models.TaskableEntity` (yaykh!) inheriting from\n  :class:`~stalker.core.models.Entity`.\n* The :attr:`~stalker.core.models.SimpleEntity.name` attribute in\n  :attr:`~stalker.core.models.SimpleEntity` is no more forced to be unique.\n  This releases the :attr:`~stalker.core.models.Shot.name` attribute in the\n  :class:`~stalker.core.models.Shot` class to be anything it wants (not just\n  uuid4 sequences to get unique names).\n* From now on, the :attr:`~stalker.core.models.SimpleEntity.code` attribute in\n  :class:`~stalker.core.models.SimpleEntity` class is not going to change when\n  the :attr:`~stalker.core.models.SimpleEntity.name` attribute is changed.\n* The :attr:`~stalker.core.models.SimpleEntity.name` attribute in\n  :class:`~stalker.core.models.SimpleEntity` is going to be copied from the\n  :attr:`~stalker.core.models.SimpleEntity.code` attribute if the ``name``\n  argument is skipped, None or empty string.\n* The ``ReviewMixin`` is removed.\n* The :class:`~stalker.core.models.Review` class is now inheriting from the\n  :class:`~stalker.core.models.SimpleEntity`.\n* The :class:`~stalker.core.models.Entity` now has a new attribute called\n  :attr:`~stalker.core.models.Entity.reviews` to store a list of\n  :class:`~stalker.core.models.Review` instances.\n\n0.1.1.a10\n=========\n\n* :class:`~stalker.core.mixins.TaskMixin` from now on doesn't have a ``tasks``\n  argument in its ``__init__`` method.\n* Each of the mixin classes now has their own test modules.\n* In :class:`~stalker.core.models.Shot`, now the\n  :attr:`~stalker.core.models.Shot.cut_out` attribute is mapped to the\n  database instead of the :attr:`~stalker.core.models.Shot.cut_duration`.\n* In :class:`~stalker.core.models.Task` the ``part_of`` attribute is renamed\n  to :attr:`~stalker.core.models.Task.task_of` to reflect its duty clearly.\n* Removed the ``ProjectMixin``. The ``project`` attribute has been moved to\n  the :class:`~stalker.core.mixins.TaskMixin`. Now anything mixed with the\n  :class:`~stalker.core.mixins.TaskMixin` also has a\n  :attr:`~stalker.core.mixins.TaskMixin.project` attribute.\n* :attr:`~stalker.core.models.Task.task_of` attribute in\n  :class:`~stalker.core.models.Task` class now accepts anything that has been\n  derived from :class:`~stalker.core.mixins.TaskMixin` or anything that has\n  both a ``tasks`` attribute and a ``project`` attribute but use the\n  :class:`~stalker.core.mixins.TaskMixin` preferably.\n* :class:`~stalker.core.models.Sequence` now doesn't accept any ``shots``\n  argument. There is no way to create a :class:`~stalker.core.models.Shot`\n  without passing a :class:`~stalker.core.models.Sequence` instance.\n* All the classes that needs to be initialized properly now has a method\n  called __init_on_load__ which is called by SQLAlchemy on load.\n* Fixed the :attr:`~stalker.core.models.Task.task_of` attribute in\n  :class:`~stalker.core.models.Task` and\n  :attr:`~stalker.core.mixins.TaskMixin.tasks` attribute in\n  :class:`~stalker.core.mixins.TaskMixin`, they are now updating each other\n  correctly.\n* Added :attr:`~stalker.core.models.Shot.assets` to the\n  :class:`~stalker.core.models.Shot` class to track down which asset is used\n  in this shot.\n* Merged the ``ProjectMixinDB`` with ``TaskMixinDB`` and\n  removed the ``ProjectMixinDB`` from the database part of the mixins.\n* The :class:`~stalker.core.models.Project` doesn't accept an ``assets`` nor\n  a ``sequences`` arguments anymore. Which was meaningless previously, cause\n  it is not possible to create an :class:`~stalker.core.models.Asset` or a\n  :class:`~stalker.core.models.Sequence` without specifying the\n  :class:`~stalker.core.models.Project` first.\n* From now on it is not possible to create a\n  :class:`~stalker.core.models.Project` instance without passing a\n  :class:`~stalker.core.models.Repository` instance to it.\n* The :class:`~stalker.core.models.Asset` now updates the\n  :attr:`~stalker.core.models.Project.assets` attribute in the\n  :class:`~stalker.core.models.Project` class.\n* From now on none of the tests are using the Mocker library. Thus all the\n  little changes to any of the classes are present in all the tests which are\n  using those classes. This makes the tests more robust and current.\n* Fixed latex PDF output of the documentation, now the formatting is nice and\n  correct.\n* :class:`~stalker.core.models.Repository` now replaces backward slashes with\n  forward slashes in the given path arguments and attributes.\n* The ``filename`` attribute has been removed from the\n  :class:`~stalker.core.models.Link` class. And it doesn't need an\n  ``filename`` argument anymore. The :attr:`~stalker.core.models.Link.path`\n  is enough to hold the necessary data.\n* The :class:`~stalker.core.models.Link` is not strictly typed anymore. So\n  you can skip the ``type`` argument while creating a\n  :class:`~stalker.core.models.Link` instance.\n* Fixed the ``Mutable Default`` problem in the following classes:\n  * :class:`~stalker.core.models.Department` classes ``members`` argument.\n  * :class:`~stalker.core.models.Entity` classes ``tags`` and ``notes``\n    argument.\n  * :class:`~stalker.core.models.StatusList` classes ``statuses`` argument\n  * :class:`~stalker.core.models.Project` classes ``assets`` argument\n  * :class:`~stalker.core.models.Assets` classes ``shots`` argument\n  * :class:`~stalker.core.models.User` classes ``permission_groups``,\n    ``projects_lead``, ``sequences_lead`` and tasks attributes.\n* The ``milestone`` attribute is renamed to\n  :attr:`~stalker.core.models.Task.is_milestone` in\n  :class:`~stalker.core.models.Task` class.\n* The ``complete`` attribute is renamed to\n  :attr:`~stalker.core.models.Task.is_complete`` in\n  :class:`~stalker.core.models.Task` class.\n* Replaced the python property idiom which uses a function which contains an\n  fget, an fset functions and a doc string variable and returns the result of\n  locals() with the property idiom that uses @property and @x.setter. Thus\n  dropped the support for python versions <= 2.5. This is done to increase the\n  PyLint rate. And with its final state, the PyLint rate of Stalker increased\n  from around 1 to around 9.\n* Reintroduced the :class:`~stalker.core.mixins.ProjectMixin` and the\n  :class:`~stalker.core.mixins.TaskMixin` is now inherited from\n  :class:`~stalker.core.mixins.ProjectMixin`. It is done in that way to allow\n  other types to have relation with a :class:`~stalker.core.models.Project`\n  instance. Without the :class:`~stalker.core.mixins.ProjectMixin` it was\n  going to introduce some code repetition. Also updated the database part of\n  the TaskMixin and created a helper class for the ProjectMixin.\n* Added an attribute called \"__stalker_version__\" to the\n  :class:`~stalker.core.models.SimpleEntity` to track down in which version of\n  Stalker that data is created. This is mostly related with the database part.\n* Renamed ``stalker.db.mixin`` module to :mod:`stalker.db.mixins`.\n* Renamed ``stalker.core.models.Comment`` class to\n  :class:`~stalker.core.models.Review`.\n* The :attr:`~stalker.core.models.Review.to` attribute in\n  :class:`~stalker.core.models.Review` class now accepts anything which has a\n  list-like attribute called \"reviews\".\n* :class:`~stalker.ext.validatedList.ValidatedList` now works uniquely. Means\n  the list of items are always unique.\n* The :attr:`~stalker.core.mixins.TaskMixin.tasks` attribute in\n  :class:`~stalker.core.mixins.TaskMixin` is not read-only anymore. But will\n  produce RuntimeError if removing items will produce orphan children.\n* Optimized the back reference update procedure in\n  :class:`~stalker.core.models.Task` and\n  :class:`~stalker.core.mixins.TaskMixin` classes. They are not touching\n  their internal variables anymore.\n* Fixed backreference updates of :class:`~stalker.core.models.Task` classes\n  :attr:`~stalker.core.models.Task.resources` attribute.\n* Fixed ``__setslice__`` method in\n  :class:`~stalker.ext.validatedList.ValidatedList`. It is now correctly\n  passing the added and removed elements to the given ``validator`` function.\n\n0.1.1.a9\n========\n\n* Introduced :class:`~stalker.core.models.Type`. A new class to define\n  **types**. With this introduction, all the classes deriving from\n  ``TypeEntity`` (and the TypeEntity it self) are removed from Stalker.\n* Added an attribute called ``type`` to the\n  :class:`~stalker.core.models.SimpleEntity`. Which will be used to create new\n  types to the derived classes.\n* Introduced a new attribute called ``__strictly_typed__`` to all the classes\n  (by the means of the EntityMeta), which will force the class to have a\n  proper (not None) ``type``.\n* :class:`~stalker.core.models.SimpleEntity` now has its own test module.\n* :class:`~stalker.core.models.TypeTemplate` is renamed to\n  :class:`~stalker.core.models.FilenameTemplate` to reflect its duty more\n  clearly.\n* fixed the tests for the :mod:`~stalker.db`. Previously each of the tests\n  were creating an instance of a specific class then storing it in the\n  database, retrieved it back and then comparing the instances, one just\n  created and one queried from the database. The problem was that, SA was\n  returning the same instance (can be checked with id(instance)) so in any\n  case they were equal, it was not possible to compare them and get a\n  meaningful difference to see if the database part worked properly. Now, all\n  the attributes of the original instance are stored in new variables and\n  then the original instance is deleted, and the a new one is retrieved back\n  from the database, and all the attributes are compared with the stored ones.\n  (probably there are other good ways)\n* fixed :attr:`~stalker.core.models.SimpleEntity.nice_name` in the\n  :class:`~stalker.core.models.SimpleEntity`, if the instance is created by\n  using ``__new__`` (like in SA) then the\n  :attr:`~stalker.core.models.SimpleEntity.nice_name` attribute was not\n  initialized correctly.\n* fixed mapping of :attr:`~stalker.core.models.Department.lead` attribute in\n  :class:`~stalker.core.models.Department` class.\n* fixed :attr:`~stalker.core.models.ImageFormat.device_aspect` attribute in\n  :class:`~stalker.core.models.ImageFormat`, it is now correctly calculated\n  when the instance is created with ``__new__`` instead of ``__init__``\n* fixed mapping of :attr:`~stalker.core.models.Project.users` attribute in\n  :class:`~stalker.core.models.Project` class.\n* fixed mapping of :attr:`~stalker.core.models.Project.duration` attribute in\n  :class:`~stalker.core.models.Project` class (also possibly fixed all the\n  classes mixed with :class:`~stalker.core.mixins.ScheduleMixin`)\n* fixed mapping of :attr:`~stalker.core.models.Sequence.lead` attribute in\n  :class:`~stalker.core.models.Sequence` class\n* updated the behavior of :attr:`~stalker.core.models.Project.users`\n  attribute in :class:`~stalker.core.models.Project` class. Now the list of\n  :class:`~stalker.core.models.User`\\ s are gathered from the\n  :class:`~stalker.core.models.Task`\\ s of the\n  :class:`~stalker.core.models.Project` and from the\n  :class:`~stalker.core.models.Sequence`\\ s,\n  the :class:`~stalker.core.models.Shot`\\ s and\n  the :class:`~stalker.core.models.Asset`\\ s of the same\n  :class:`~stalker.core.models.Project`.\n* updated the behavior of :attr:`~stalker.core.models.User.project` attribute\n  in class :class:`~stalker.core.models.User` class. Now the list of\n  :class:`~stalker.core.models.Project`\\ s are gathered from all the\n  :class:`~stalker.core.models.Task`\\ s assigned to the current\n  :class:`~stalker.core.models.User`.\n* The ``Group`` class is renamed to ``PermissionGroup``.\n* The default duration for the :class:`~stalker.core.mixins.ScheduleMixin`\n  is now defined by the :attr:`stalker.conf.defaults.DEFAULT_TASK_DURATION`\n  attribute.\n* The :class:`~stalker.core.mixins.ScheduleMixin` class now accepts a third\n  argument called ``duration``.\n* The :attr:`~stalker.core.mixins.ScheduleMixin.duration` attribute in the\n  :class:`~stalker.core.mixins.ScheduleMixin` is now a settable. See the\n  :class:`~stalker.core.mixins.ScheduleMixin` class documentation for details.\n* The :attr:`~stalker.core.mixins.ScheduleMixin.due_date` in\n  :class:`~stalker.core.mixins.ScheduleMixin` doesn't accept\n  ``datetime.timedelta`` objects anymore.\n* The behavior of :attr:`~stalker.core.mixins.ScheduleMixin.start`,\n  :attr:`~stalker.core.mixins.ScheduleMixin.due_date` and\n  :attr:`~stalker.core.mixins.ScheduleMixin.duration` in\n  :class:`~stalker.core.mixins.ScheduleMixin` is updated.\n* Added :class:`~stalker.core.errors.CircularDependencyError`.\n* All the ``ValueError``\\ s are converted to ``TypeError``\\ s in the\n  :class:`~stalker.ext.validatedList.ValidatedList`.\n* Finished the implementation of :class:`~stalker.core.models.Task` class.\n* Updated the formatting of the :attr:`~stalker.core.models.SimpleEntity.code`\n  attribute in :class:`~stalker.core.models.SimpleEntity` class. See the\n  documentation of the :class:`~stalker.core.models.SimpleEntity` class for\n  details.\n* Updated the :attr:`~stalker.core.models.User.code` attribute in\n  :class:`~stalker.core.models.User`.\n* Updated all the exceptions raised by the SOM classes. Now they are correctly\n  raising ``TypeError`` and ValueError``\\ s.\n* added a new mixin class called :class:`~stalker.core.mixins.ProjectMixin`.\n* :class:`~stalker.core.models.Sequence`,\n  :class:`~stalker.core.models.Asset` and :class:`~stalker.core.models.Task`\n  classes are now using the\n  :class:`~stalker.core.mixins.ProjectMixin` mixin class instead of\n  implementing this common feature by them self.\n* added :class:`~stalker.db.mixin.ProjectMixinDB` for classes which are mixed\n  with :class:`~stalker.core.mixins.ProjectMixin`.\n* :class:`~stalker.ext.validatedList.ValidatedList` now accepts a third\n  argument called the ``validator`` which should be a callable, which is\n  called when any of the methods of the\n  :class:`~stalker.ext.validatedList.ValidatedList` is called and the list of\n  elements are modified. The list of elements modified will be passed to the\n  validator function where the first argument is a list containing the\n  elements added and the last argument is a list contatining the elements\n  removed.\n* :attr:`~stalker.core.models.Task.resources` in\n  :class:`~stalker.core.models.Task` is now updating the\n  :attr:`~stalker.core.models.User.tasks` attribute in the\n  :class:`~stalker.core.models.User` class.\n* ``tasks`` is not an argument for the :class:`~stalker.core.models.User`\n  anymore. It was meaningles to have the :class:`~stalker.core.models.Task`\\ s\n  in the initialization of the :class:`~stalker.core.models.User` instances.\n* :class:`~stalker.core.models.User` classes\n  :attr:`~stalker.core.models.User.projects` attribute is now gathered by\n  looking at the :attr:`~stalker.core.models.Task.project` attribute of the\n  :class:`~stalker.core.models.Task`\\ s in the\n  :attr:`~stalker.core.models.User.tasks` attribute.\n* :class:`~stalker.core.models.StatusList` now accepts classes for the\n  ``target_entity_type`` argument.\n* :class:`~stalker.core.mixins.ReferenceMixin` now accepts anything derived\n  from the :class:`~stalker.core.models.Entity`.\n* :class:`~stalker.core.models.Task` class now has a\n  :attr:`~stalker.core.models.Task.part_of` attribute which accepts\n  :class:`~stalker.core.models.SimpleEntity` instances and shows which\n  entity is this task a part of.\n\n0.1.1.a8\n========\n\n* From now on an :class:`~stalker.core.models.Asset` instance can not be\n  created without a :class:`~stalker.core.models.AssetType` object defining\n  the type of the current :class:`~stalker.core.models.Asset`. This is done to\n  prevent creation of :class:`~stalker.core.models.Asset`\\ s without a certain\n  type.\n* Fixed :class:`~stalker.core.models.Project` where it was not raising a\n  ValueError properly for :attr:`~stalker.core.models.Project.sequence`,\n  :attr:`~stalker.core.models.Project.assets` and\n  :attr:`~stalker.core.models.Project.users` attributes when the assigned\n  value is not iterable.\n* Fixed :class:`~stalker.core.models.Department` where it was not raising a\n  ValueError properly for :attr:`~stalker.core.models.Department.members`\n  attribute when the assigned value is not iterable.\n* Changed the representaion string of the :class:`~stalker.core.models.Shot`\n  to <Shot (Shot.code, Shot.code)> because the\n  :attr:`~stalker.core.models.Shot.name` is not meaningful.\n* Changed the way :class:`~stalker.core.models.EntityMeta` metaclass working.\n  It is now using the ``__new__`` method and the ``dict_`` of the class to\n  set the attributes.\n* The :class:`~stalker.core.models.EntityMeta` now adds another attribute\n  called ``plural_name`` to the classes, which shows the plural form of the\n  class name. By default it tries to set it to a good name using plural form\n  rules of English but if the name has an irregular plural name (or it is not\n  in English) you can override this attribute by adding ``plural_name`` to the\n  class attributes::\n\n    from stalker.core.models import SimpleEntity\n\n    class MyEntity(SimpleEntity):\n        plural_name = \"MyEntities\"\n        pass\n\n* From now on the table names are in the following format:\n  * The plural name of the class if the table belongs to one class\n  * The class1.__name__ + \"_\" + class2.plural_name if the table is a join\n    table\n* Updated the table names in the :mod:`stalker.db.mixin` module\n\n0.1.1.a7\n========\n\n* Updated the :ref:`roadmap_toplevel` to reflect the current development\n  history and cycle\n* Merged all the model classes which were previously in separate files in to\n  :mod:`stalker.core.models` module, to make it easy to use (and possibly hard\n  to develop)\n* All the references to modules or classes or anything in the source codes are\n  now represented by an absolute path in the docs (\n  :class:`stalker.core.models.User` instead of\n  :class:`~stalker.core.models.User`)\n* moved the :mod:`stalker.db.auth` to :mod:`stalker.ext.auth`\n* :class:`stalker.core.models.User` class now uses the\n  :func:`stalker.ext.auth.set_password` and\n  :func:`stalker.ext.auth.check_password` utility functions to handle\n  passwords. The user passwords are now always hidden, but not strongly\n  encrypted.\n* The :func:`stalker.ext.auth.session` renamed to\n  :func:`stalker.ext.auth.create_session` to reflect its functionality\n  properly. And removed the return value from the function. Now it doesn't\n  return any bool value but None. To check if the user is already logged in\n  use :const:`stalker.ext.auth.SESSION` dictionary as follows::\n\n    from stalker.ext import auth\n\n    # initialize the session\n    auth.create_session()\n\n    # check if there is a user\n    if auth.SESSION_KEY in auth.SESSION:\n        print \"There is a logged in user\"\n    else:\n        print \"There is no logged in user\"\n\n* :func:`stalker.ext.auth.authenticate` updated to use\n  :func:`stalker.ext.auth.check_password`\n* Fixed the :attr:`~stalker.core.models.User.last_login` attribute in the\n  database mapper, it was set as a *synonym* for it self.\n* Removed the ``tearDown`` methods in :mod:`tests.db.test_db`, there are\n  problems with cleaning the mappers and disposing the engine, so instead of\n  killing them the db.setup is called over and over again with different in\n  memory databases.\n* From now on the :attr:`~stalker.core.models.SimpleEntity.code` attribute\n  doesn't format the given string value too heavily, to allow more individual\n  naming conventions to work with Stalker.\n* Updated :ref:`contribute_toplevel`\n* Renamed the :class:`~stalker.core.models.PipelineStep` to\n  :class:`~stalker.core.models.TaskType` and changed the idea behind the\n  relation between :class:`~stalker.core.models.AssetType` and\n  :class:`~stalker.core.models.Task`\n* :class:`stalker.core.models.AssetType` classes\n  :attr:`~stalker.core.models.AssetType.pipeline_steps` attribute has been\n  renamed to :attr:`~stalker.core.models.AssetType.task_types`\n* Fixed a little error in the mapper of :class:`stalker.core.models.Structure`\n* Re-implemented the :func:`stalker.ext.auth.login` function and updated the\n  tests accordingly.\n* All the error classes in :mod:`stalker.core.models` moved to\n  :mod:`stalker.core.errors`\n* Added a new error class called :class:`stalker.core.errors.DBError`\n* Fixed a bug in :const:`stalker.db.__mappers__`, it is now possible to add\n  new mappers without deleting the previous\n  :const:`stalker.conf.defaults.MAPPERS` list.\n* Removed the :class:`~stalker.core.models.AssetType` and derived the\n  :class:`~stalker.core.models.Shot` and :class:`~stalker.core.models.Asset`\n  classes from :class:`~stalker.core.models.Entity`.\n* Moved the mixin classes from :mod:`stalker.core.models` to\n  :mod:`stalker.core.mixins`\n* Introduced the :class:`~stalker.core.mixins.TaskMixin` which gives the\n  ability to connect a list of tasks to the mixed in class. Also added the\n  mapper setup for this mixin.\n* :class:`~stalker.ext.validatedList.ValidatedList` now accepts string values\n  for the ``type_`` argument.\n* Added :class:`~stalker.core.models.Shot` class and test for it.\n* Updated the :class:`~stalker.core.models.Sequence` database tests according\n  to new rules introduced with the :class:`~stalker.core.models.Shot` class.\n* :class:`~stalker.ext.validatedList.ValidatedList` now imports the given\n  types lazily when the type is given as a string path.\n* :class:`~stalker.core.models.Sequence` now needs a\n  :class:`~stalker.core.models.Project` instance to be created.\n* It is now possible to assign :class:`~stalker.core.models.Task`\\ s to\n  :class:`~stalker.core.models.Project` and\n  :class:`~stalker.core.models.Sequence`\\ s. Also updated the tests for this\n  change.\n* Removed the ``shots`` argument from the\n  :class:`~stalker.core.models.Sequence` class initialization. Because there\n  is no way to create a :class:`~stalker.core.models.Shot` without a\n  :class:`~stalker.core.models.Sequence` instance.\n* Added tests for mixin initialization for\n  :class:`~stalker.core.models.Project`,\n  :class:`~stalker.core.models.Sequence`, :class:`~stalker.core.models.Shot`\n  and :class:`~stalker.core.models.Asset` classes.\n* Fixed a bug in :class:`~stalker.core.models.Project` where it was always\n  initializing the references with an empty list no matter what is given.\n* Fixed a bug in :class:`~stalker.core.models.Project` where it was always\n  initializing the :attr:`~stalker.core.models.Project.start` to\n  **datetime.date.today** and the\n  :attr:`~stalker.core.models.project.due_date` to 10 days later then the\n  :attr:`~stalker.core.models.project.start` no matter what are given.\n* Fixed a bug in :class:`~stalker.core.mixins.ReferenceMixin` where it was not\n  initializing the reference attribute correctly.\n* Fixed a bug in :class:`~stalker.core.models.Asset` where the\n  :attr:`~stalker.core.models.Asset.project` attribute was not correctly\n  getting the given :class:`~stalker.core.models.Project` instance.\n* Added the mappers and tables for :class:`~stalker.core.models.Shot` class.\n* Updated database model tests to test all the attributes of the models.\n\n0.1.1.a6\n========\n\n* updated/fixed tests for :class:`stalker.ext.validatedList.ValidatedList`\n* updated a couple of tests to increase tests coverage\n* :class:`stalker.core.models.status.Status` class instances now can be\n  compared to string or unicode values\n* A :class:`stalker.core.models.status.Status` object in a\n  :class:`stalker.core.models.status.StatusList` can now be accessed by its\n  name as the index in :class:`stalker.core.models.status.StatusList` only\n  while getting the item.\n* Added :class:`stalker.core.models.mixin.ScheduleMixin` which introduces date\n  variables like, start, due_date and duration to the mixed in class.\n* Removed some parts of the :class:`stalker.core.models.project.Project` class\n  which are now satisfied by the\n  :class:`stalker.core.models.mixin.ScheduleMixin`\n* Improved the implementation of the :mod:`stalker.db.auth` module\n* removed the ``stalker.db.__setup__`` module which were helping to reference\n  the variables in :mod:`stalker.db` module but it is not needed any more\n* It is now possible to initialize a\n  :class:`stalker.core.models.project.Project` object without a\n  :class:`stalker.core.models.repository.Repository`,\n  :class:`stalker.core.models.structure.Structure` or an\n  :class:`stalker.core.models.imageFormat.ImageFormat` or a\n  :class:`stalker.core.models.types.ProjectType`\n* Updated the :ref:`tutorial_toplevel`\n* From now on, in a :class:`stalker.core.models.entity.SimpleEntity`,\n  setting the code attribute to None or empty string will not raise any\n  ``ValueError``\\ s but will re-initialize the ``code`` value from the\n  ``nice_name`` attribute.\n* Implemented :class:`stalker.core.models.sequence.Sequence` class along with\n  its tests.\n* added :class:`stalker.core.models.sequence.Sequence` equality tests.\n* improved :class:`stalker.core.models.project.Project` equality tests.\n* Implemented :class:`stalker.core.models.assetBase.AssetBase` class along\n  with its tests.\n* The **index.rst** of the documentation now references the **README** from\n  the project root.\n* added the basic implementation of :class:`stalker.core.models.task.Task`\n  and :class:`stalker.core.models.shot.Shot` and mapped them very basically\n  to be able to test the dependent classes like\n  :class:`stalker.core.models.assetBase.AssetBase` and\n  :class:`stalker.core.models.sequence.Sequence`\n* Added mappers and tables for\n  :class:`stalker.core.models.assetBase.AssetBase`\n* Now all the mixin classes have proper :func:`__init__` methods, and in a\n  mixed class, the mixin classes' :func:`__init__` method can be called\n  directly by giving the current object instance (*self*) like shown below::\n\n    class ANewEntity(entity.SimpleEntity, mixin.StatusMixin):\n        def __init__(self, **kwargs):\n            super(ANewEntity, self).__init__(**kwargs)\n            mixin.StatusMixin.__init__(self, **kwargs)\n\n  and it can be repeated for any number of mixins in class inheritance path.\n* Added the **CHANGELOG** to the documentation, and updated all formating of\n  the mentioned references inside the file.\n\n0.1.1.a5\n========\n\n* removed the :class:`stalker.core.models.entity.StatusedEntity` and its\n  tests, with the introduction of\n  :class:`stalker.core.models.mixin.StatusMixin`, it is not necessary any\n  more\n* added camera_lens.py to the examples, which shows how to extend SOM in its\n  very basic form, also added tests testing this example\n* changed the database uri for the **DatabaseTester**, it now uses an in\n  memory SQLite database instead a file based one.\n* Updated the version numbers in the roadmap\n* Added ``last_login`` attribute to :class:`stalker.core.models.user.User`\n  class tables and mapped it\n* because it was taking too much space in the diffs the VUE file which shows\n  the design sketches has been removed from the trunk\n* added the :class:`stalker.ext.validatedList.ValidatedList` class which is a\n  list derivative that accepts only one type of object.\n* these SOM classes listed below uses\n  :class:`stalker.ext.validatedList.ValidatedList` in their list attributes\n  requiring specific types of objects to be assigned:\n  * :class:`stalker.core.models.entity.Entity`\n  * :class:`stalker.core.models.status.StatusList`\n  * :class:`stalker.core.models.structure.Structure`\n  * :class:`stalker.core.models.types.AssetType`\n  * :class:`stalker.core.models.department.Department`\n  * :class:`stalker.core.models.user.User`\n  * :class:`stalker.core.models.mixin.ReferenceMixin`\n* added tests of the :class:`stalker.core.models.project.Project` class\n* completed the first implementation of the\n  :class:`stalker.core.models.project.Project` class\n* to be able to use *assertIsInstance* method of\n  :class:`mocker.MockerTestCase` all the\n  :class:`unittest.TestCase` test classes are converted to\n  :class:`mocker.MockerTestCase`\n* changed the design of the **stalker.db.mixins.ReferenceMixin.setup**\n  and **stalker.db.mixins.StatusMixin.setup** to organize the mixin\n  classes' database setup helper functions, now they are converted to classes\n  with a classmethod called :meth:`stalker.db.mixin.ReferenceMixinDB.setup`\n  doing all the functionality of the previous setup function and placed them\n  under the :mod:`stalker.db.mixin` module.\n* added persistence tests for :class:`stalker.core.models.project.Project`\n* fixed secondary table generation for\n  :class:`stalker.core.models.mixin.ReferenceMixin`, the table is now\n  created only if it doesn't exists already, and it is retrieved from\n  :attr:`stalker.db.metadata` if it exists\n\n0.1.1.a4\n========\n\n* changed the arguments of the\n  :func:`stalker.db.mixins.ReferenceMixin.setup` function, to allow\n  carrying the data from one to the next mixin (this part still needs a lot of\n  attention)\n* removed the unnecessary ``statusedEntity_statuses`` secondary table, because\n  one :class:`stalker.core.models.entity.StatusedEntity` owns just one\n  :class:`stalker.core.models.status.StatusList` its a **many2one** relation,\n  so no need to have a secondary table\n* introduced the :class:`stalker.core.models.mixin.StatusMixin` (will replace\n  StatusedEntity soon)\n* Added a new example for the usage of\n  :class:`stalker.core.models.mixin.StatusMixin`\n* Updated the :func:`stalker.db.mixins.ReferenceMixin.setup` function, now it\n  takes three arguments, the *class*, the *table* and the *mapper_options*\n  dictionary.\n\n0.1.1.a3\n========\n\n* Removed the included *tests* from the egg build\n* Added/fixed equality and inequality operators for classes:\n  * :class:`stalker.core.models.department.Department`\n  * :class:`stalker.core.models.entity.StatusedEntity`\n* :class:`stalker.core.models.entity.SimpleEntity` now has a \\*\\*kwargs in the\n  :func:`__init__` so it doesn't give ``TypeError`` for extra keywords\n* added :class:`stalker.core.models.entity.EntityMeta` metaclass which adds\n  ``entity_type`` attribute and sets its value to the unicode version of the\n  name of the class\n* the :class:`stalker.core.models.entity.SimpleEntity` uses the\n  :class:`stalker.core.models.entity.EntityMeta` metaclass to automatically\n  add all the ``entity_type`` attribute to all the derived classes\n* all the mappers now uses the ``ClassName.entity_type`` class attribute as\n  the polymorphic discriminator (polymorphic identity)\n* instead of *LBYL* moving toward *EAFP* idiom for all the models in the\n  :mod:`stalker.core`\n* :class:`stalker.core.models.status.StatusList` now supports indexing\n* :class:`stalker.core.models.status.StatusList` now has an\n  ``target_entity_type`` attribute which accepts strings with the class name\n  and shows the compatible class of this\n  :class:`stalker.core.models.status.StatusList`\n* :meth:`stakler.core.models.status.StatusList.__eq__` now checks for the\n  ``target_entity_type`` also\n* :class:`stalker.core.models.status.StatusedEntity` now checks for the given\n  :attr:`stalker.core.models.StatusList.target_entity_type` for\n  compatibility with the current class\n* All the validation methods in the :mod:`stalker.core.models` now has the\n  **validate** word in their name instead of **check**\n* Little fixes:\n  * the mapper of :class:`stalker.core.models.types.TypeTemplate` was trying\n     to setup a synonym to a parameter with the same name (file_code)\n  * :class:`stalker.core.models.user.User` classes ``_sequence_lead``\n       attribute renamed to ``_sequences_lead``\n* Added persistence tests for\n  :class:`stalker.core.models.entity.StatusedEntity`\n* Added :func:`stalker.utils.path_to_exec` which converts the given module\n  full paths to an executable python code which imports the given python\n  object to the current namespace\n* Added ``entity_types`` table to hold the possible entity types in Stalker.\n  The content of the table comes from the\n  :const:`stalker.conf.defaults.CORE_MODEL_CLASSES` list. And possibly going\n  to be extended by the users.\n* Added :func:`stalker.db.__setup__.__fill_entity_types_table__` which fills\n  the ``entity_types`` table with default values.\n* :class:`stalker.core.models.user.User` class now has ``initials`` attribute,\n  which is automatically calculated from the first and last name if there is\n  no one given.\n* Started developing the :class:`stalker.core.models.message.Message` class\n* Added the :mod:`stalker.core.models.mixin` module which holds the common\n  mixins.\n* Added the :class:`stalker.core.models.mixin.ReferenceMixin` class which\n  gives reference abilities to mixed in classes.\n* Added the database part of the\n  :class:`stalker.core.models.mixin.ReferenceMixin`. Now it is possible to\n  create a new type of entity and mix it with ReferenceMixin and also persist\n  it in the database. But it needs a lot of effort before to have something\n  usable.\n* Added **examples** module, which holds usage examples and recipes\n* Added an example about how to create a new mixed in entity type for SOM.\n\n0.1.1.a2\n========\n\n* Updated the Tutorial\n* Added *code* attribute to :class:`stalker.core.models.entity.SimpleEntity`\n* Updated the :class:`stalker.core.models.user.User` class for the new *code*\n  attribute, and also updated the tests to add tests for *code* attribute\n  (simply copied the test code from ``SimpleEntityTester``, bad code\n  repetition, need to change it later, by may be inheriting the test case from\n  the other one)\n* Updated the database tables and mappers for the new *code* attribute\n* Removed the clashing *code* attribute from\n  :class:`stalker.core.models.pipelineStep.PipelineStep` class and the tables\n  and mappers.\n* Added :class:`stalker.core.models.note.Note` class\n* Added ``notes`` table and a mapper for\n  :class:`stalker.core.models.note.Note` class\n* Added *note* attribute to :class:`stalker.core.models.entity.Entity` class\n* Fixed ``EntityTester`` in tests\n* Added ``__repr__`` to entity classes\n* Added tests for persistence of :class:`stalker.core.models.note.Note`` class\n* Added equality (__eq__) and inequality (__ne__) operators for classes:\n  * :class:`stalker.core.models.user.User`\n  * :class:`stalker.core.models.tag.Tag`\n  * :class:`stalker.core.models.status.Status`\n  * :class:`stalker.core.models.status.StatusList`\n  * :class:`stalker.core.models.imageFormat.ImageFormat`\n  * :class:`stalker.core.models.repository.Repository`\n  * :class:`stalker.core.models.pipelineStep.PipelineStep`\n  * :class:`stalker.core.models.structure.Structure`\n  * :class:`stalker.core.models.types.AssetType`\n  * :class:`stalker.core.models.types.LinkType`\n  * :class:`stalker.core.models.entity.TypeEntity`\n  * :class:`stalker.core.models.types.ProjectType`\n* :class:`stalker.core.models.Status` classes' short_name attribute has been\n  removed, from now on the ``code`` attribute will be used, also updated the\n  database tables and mappers\n* The :attr:`stalker.core.models.user.User.login_name` is now superior to the\n  :attr:`stalker.core.models.user.User.name` attribute, giving both of them as\n  arguments will lead the ``login_name`` to be used as both the ``login_name``\n  and the ``name``\n\n0.1.1.a1\n========\n\n* Fixed a couple of documentation errors like:\n  * :ref:`inheritance_diagram_toplevel` had references to modules\n  * A couple of docstring documentation errors in\n     :class:`stalker.core.models.structure.Structure`,\n     :class:`stalker.core.models.user.User` and\n     :class:`stalker.core.models.types.TypeTemplate` classes\n* Updated :ref:`installation_toplevel`\n* Added :ref:`tutorial_toplevel` page to the documentation\n* All the classes, functions from **SQLAlchemy** are now imported to the\n  ``sqlalchemy`` namespace, this will let the **Sphinx** to correctly include\n  classes, functions from **Stalker** only\n* Removed the ``db.meta module``, now all the functionalities supplied by\n  ``stalker.db.meta`` are supplied by ``db`` itself (``db.meta.session`` -->\n  ``db.session`` etc.)\n* Added ``query`` variable to :mod:`stalker.db` module so instead of\n  ``db.session.query`` now ``db.query`` can be used\n* Updated :func:`stalker.db.auth.login_required` decorator function, it now\n  accepts a ``view`` function\n* Added :func:`stalker.db.auth.permission_required` decorator function\n* ``name`` attribute of :class:`stalker.core.models.entity.SimpleEntity` is\n  not any more forced to start with an upper case letter\n* From now on ``login_name`` is now a *synonym* for ``name`` in\n  :class:`stalker.core.models.user.User` class and just the ``name`` attribute\n  is going to be stored in the database\n* To make things simple all the properties with name **type_** is now using\n  the name **type** even though it is a Python keyword, Python is clever\n  enough to understand what is meant\n\n0.1.1.a0\n========\n\n* Changed the version number scheme a little bit to follow the setuptools guide\n\n0.1.0.20110111.1\n================\n\n* Persistence tests for Link is now fixed\n* Now every table correctly has a ``primary_key``\n\n0.1.0.20110110.1\n================\n\n* Added :ref:`installation_toplevel` to the documentation\n* Updated **README** file for **PyPI**\n* Added the package to **PyPI**\n* Fixed ``StatusedEntityTester`` test suit, now it properly creates mock\n  :class:`satlker.coer.models.status.StatusList` object for the ``__eq__`` and\n  ``__ne__`` tests\n* Updated tables and mappers for\n  :class:`stalker.core.models.typeEntity.TypeTemplate`\n* Updated mappers for :class:`stalker.core.models.typeEntity.AssetType`\n* :class:`stalker.core.models.entity.TypeEntity` class is moved to\n  ``entity.py``, right beside the other entity classes\n* ``typeEntity.py`` renamed to ``types.py``\n* Created tables and mappers for:\n  * :class:`stalker.core.models.structure.Structure`\n  * :class:`stalker.core.models.entity.TypeEntity`\n  * :class:`stalker.core.models.types.TypeTemplate`\n  * :class:`stalker.core.models.types.AssetType`\n  * :class:`stalker.core.models.types.LinkType`\n  * :class:`stalker.core.models.types.ProjectType`\n* Updated ``simpleEntities`` table, now the ``name`` by itself is not a\n  *unique constraint*, but added an explicit ``UniqueConstraint`` on ``name``\n  and ``entity_type`` columns to allow entities with different types to have\n  the same name, also added test for that.\n* Fixed all the errors in ``test_db.py``, there are only failures left.\n* Added tests for :class:`stalker.core.models.link.Link`, all the test are\n  green for :class:`stalker.core.models.link.Link` except the persistence\n  tests.\n\n0.1.0.20110108.1\n================\n\n* ``Template`` class is renamed to ``TypeTemplate`` and moved inside\n  ``stalker.core.models.typeEntity`` to prevent the name clashing with\n  **Jinja2** Template class\n* added ``__eq__`` to :class:`stalker.core.models.entity.SimpleEntity` and\n  still trying to add it to the derived classes\n* organized the project structure to conform setup tools for **PyPI**\n\n0.1.0.20110107.2\n================\n\n* updating the db tests\n* stalker.core.models.user.User class is now allowed to have its department\n  to be set None\n\n0.1.0.20110107.1\n================\n\n* organized the existent tests\n\n0.1.0.20110106.2\n================\n\n* added nice_name property to the stalker.core.models.entity.SimpleEntity\n  class\n* added tests for stalker.core.models.structure.Structure class\n* implemented the stalker.core.models.structure.Structure class\n* added last_login attribute to the stalker.core.models.user.User class and\n  added all the tests\n\n0.1.0.20110106.1\n================\n\n* re-introduced the link.Link, which has a general meaning than\n  reference.Reference (I bet it will be reference again tomorrow)\n* stalker.models moved to stalker.core.models\n* renamed tests/z_db to tests/db, because the sqlalchemy/mocker problem is\n  solved by moving the models to core/models\n\n0.1.0.20110105\n==============\n\n* improved the stalker.models.template.Template class documentation, and added\n  an example showing the usage of it.\n\n0.1.0.20110104\n==============\n\n* removed the link.Link and introduced reference.Reference and\n  typeEntity.ReferenceType classes, which I think are more organized then the\n  previous design.\n* reorganized the AssetType and ReferenceType objects by introducing the new\n  TypeEntity class and deriving the AssetType and ReferenceType from this\n  class\n* added ProjectType class to hold different project types (like Commercial,\n  Film, Still etc., it is different than having a Commercial Structure object)\n* removed AssetTemplate and ReferenceTemplate concepts and generalized the\n  Template class by adding a `type` parameter to it, which accepts TypeEntity\n  and classes derived from TypeEntity.\n\n0.1.0.20110103.2\n================\n\n* added login_required decorator to the stalker.db.auth module, but the\n  implementation is not done yet\n\n0.1.0.20110103\n==============\n\n* user.User._password is now scrambled, but the password property uses the raw\n  password\n* added stalker.db.auth for authentication, removed the db.login function.\n\n0.1.0.20110102\n==============\n\n* added the error.LoginError exception for login errors\n* started to add tests for db.login function\n\n0.1.0.20101231\n==============\n\n* moved the login function to the db.__init__ to let it used just after\n  setting up the database without importing any new module\n* updated the example in the docstring of the template.AssetTemplate\n\n0.1.0.20101229.3\n================\n\n* generalized the Template class. Now every Entity can be assigned to a\n  template, it is not limited with Assets or References.\n\n0.1.0.20101229.2\n================\n\n* entity.SimpleEntity.name now can have white spaces, but not at the beginning\n  or end, just in the middle\n* done mapping template.Template class\n\n0.1.0.20101229.1\n================\n\n* trying to create a session system with Beaker, to hold user login\n  information\n* done mapping assetType.AssetType class\n* done mapping pipelineStep class\n\n0.1.0.20101228.1\n================\n\n* added repositories table and mapper for the repository.Repository class\n* added imageFormats table and mapper for the imageFormat.ImageFormat class\n* renamed extensions module to ext\n* added roadmap to docs\n\n0.1.0.20101228\n==============\n\n* created the block of database tests\n* added stalker.db.meta.__mappers__ list to hold the mappers and use it to\n  check if anything is already mapped\n* added tests for db initialization\n* removed the whole stalker.models.unit module from SOM, only TimeUnit was\n  usable in some cases, but in fact it is also not important, the only object\n  using TimeUnit was the Project class and it can go well without it. Don't\n  need to make things more complex than it needs to be.\n* increased the version number to 0.1.0 to follow the stalker roadmap\n\n0.0.1.20101227\n==============\n\n* the test_db is converted to a proper unittest which is testing all the\n  models one by one\n* test/db renamed to test/z_db to let nose run it latest to solve the problem\n  about mocker and sqlalchemy fighting each other.\n* Mapping syntax is changed a little bit, now to do the mapping, the\n  <mapper>.setup() function needs to be called to be able to do the mapping\n  any time\n* started adding tests for every class in SOM\n\n0.0.1.20101226\n==============\n\n* in user.User the last_name attribute could be an empty string\n* removed SimpleEntity, TaggedEntity and introduced StatusedEntity to make the\n  inheritance clear and let users to find somebody to blame by moving all the\n  audit information to the the SimpleEntity class in which everything is\n  inherited from. Now even a Tag has audit information.\n\n0.0.1.20101225\n==============\n\n* entity.AuditEntity.created_by can now be None (for now)\n* user.User.last_name can now be None, to let users like admin have no last\n  name\n* creating tables for catch the general inheritance of the entity classes\n* entitiy.SimpleEntity.name's first letter is not capitalized any more\n* department.Department class now accepts Null for lead attribute (for now\n  again)\n\n0.0.1.20101224\n==============\n\n* started playing with the SQLAlchemy side of the system\n\n0.0.1.20101223\n==============\n\n* updating the documentation\n* AuditEntity now accepts None for updated_by attribute when it an object is\n  created, but sets it to the same value with created_by attribute\n\n0.0.1.20101219\n==============\n\n* started to implement:\n  * a database entry point\n  * a customizable object model and database tables\n  * an automatic mapper to map the objects and tables together according to\n     user settings\n\n  things can change a lot in this phase, I'm just trying to figure out the\n  best possible way to do it.\n\n* added a new entity type called TaggedEntity which derives from SimpleEntity,\n  and moved all the tag related attributes of SimpleEntity to TaggedEntity,\n  and all the child classes deriving from SimpleEntity now derives from\n  TaggedEntity, also moved the tests related with tag in SimpleEntity to\n  TaggedEntity.\n* tag.Tag now derives from the SimpleEntity and doesn't add any other\n  attribute to its super.\n* updated tests for tag.Tag\n* updated docs for TaggedEntity\n* finished implementing the Department object and its tests\n* removed the notes attribute from the Entity class\n\n0.0.1.20101209\n==============\n\n* added the inheritance diagram as an rst page to reference it anywhere needed\n* added the empty classes for:\n  * Asset\n  * AssetBase\n  * Booking\n  * Shot\n  * Structure\n  * Template\n  * Version\n\n* added the Department class\n* added inheritance diagrams to the autosummary pages of the classes\n"
  },
  {
    "path": "COPYING",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <http://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<http://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<http://www.gnu.org/philosophy/why-not-lgpl.html>.\n"
  },
  {
    "path": "COPYING.LESSER",
    "content": "                   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n\n  This version of the GNU Lesser General Public License incorporates\nthe terms and conditions of version 3 of the GNU General Public\nLicense, supplemented by the additional permissions listed below.\n\n  0. Additional Definitions.\n\n  As used herein, \"this License\" refers to version 3 of the GNU Lesser\nGeneral Public License, and the \"GNU GPL\" refers to version 3 of the GNU\nGeneral Public License.\n\n  \"The Library\" refers to a covered work governed by this License,\nother than an Application or a Combined Work as defined below.\n\n  An \"Application\" is any work that makes use of an interface provided\nby the Library, but which is not otherwise based on the Library.\nDefining a subclass of a class defined by the Library is deemed a mode\nof using an interface provided by the Library.\n\n  A \"Combined Work\" is a work produced by combining or linking an\nApplication with the Library.  The particular version of the Library\nwith which the Combined Work was made is also called the \"Linked\nVersion\".\n\n  The \"Minimal Corresponding Source\" for a Combined Work means the\nCorresponding Source for the Combined Work, excluding any source code\nfor portions of the Combined Work that, considered in isolation, are\nbased on the Application, and not on the Linked Version.\n\n  The \"Corresponding Application Code\" for a Combined Work means the\nobject code and/or source code for the Application, including any data\nand utility programs needed for reproducing the Combined Work from the\nApplication, but excluding the System Libraries of the Combined Work.\n\n  1. Exception to Section 3 of the GNU GPL.\n\n  You may convey a covered work under sections 3 and 4 of this License\nwithout being bound by section 3 of the GNU GPL.\n\n  2. Conveying Modified Versions.\n\n  If you modify a copy of the Library, and, in your modifications, a\nfacility refers to a function or data to be supplied by an Application\nthat uses the facility (other than as an argument passed when the\nfacility is invoked), then you may convey a copy of the modified\nversion:\n\n   a) under this License, provided that you make a good faith effort to\n   ensure that, in the event an Application does not supply the\n   function or data, the facility still operates, and performs\n   whatever part of its purpose remains meaningful, or\n\n   b) under the GNU GPL, with none of the additional permissions of\n   this License applicable to that copy.\n\n  3. Object Code Incorporating Material from Library Header Files.\n\n  The object code form of an Application may incorporate material from\na header file that is part of the Library.  You may convey such object\ncode under terms of your choice, provided that, if the incorporated\nmaterial is not limited to numerical parameters, data structure\nlayouts and accessors, or small macros, inline functions and templates\n(ten or fewer lines in length), you do both of the following:\n\n   a) Give prominent notice with each copy of the object code that the\n   Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the object code with a copy of the GNU GPL and this license\n   document.\n\n  4. Combined Works.\n\n  You may convey a Combined Work under terms of your choice that,\ntaken together, effectively do not restrict modification of the\nportions of the Library contained in the Combined Work and reverse\nengineering for debugging such modifications, if you also do each of\nthe following:\n\n   a) Give prominent notice with each copy of the Combined Work that\n   the Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the Combined Work with a copy of the GNU GPL and this license\n   document.\n\n   c) For a Combined Work that displays copyright notices during\n   execution, include the copyright notice for the Library among\n   these notices, as well as a reference directing the user to the\n   copies of the GNU GPL and this license document.\n\n   d) Do one of the following:\n\n       0) Convey the Minimal Corresponding Source under the terms of this\n       License, and the Corresponding Application Code in a form\n       suitable for, and under terms that permit, the user to\n       recombine or relink the Application with a modified version of\n       the Linked Version to produce a modified Combined Work, in the\n       manner specified by section 6 of the GNU GPL for conveying\n       Corresponding Source.\n\n       1) Use a suitable shared library mechanism for linking with the\n       Library.  A suitable mechanism is one that (a) uses at run time\n       a copy of the Library already present on the user's computer\n       system, and (b) will operate properly with a modified version\n       of the Library that is interface-compatible with the Linked\n       Version.\n\n   e) Provide Installation Information, but only if you would otherwise\n   be required to provide such information under section 6 of the\n   GNU GPL, and only to the extent that such information is\n   necessary to install and execute a modified version of the\n   Combined Work produced by recombining or relinking the\n   Application with a modified version of the Linked Version. (If\n   you use option 4d0, the Installation Information must accompany\n   the Minimal Corresponding Source and Corresponding Application\n   Code. If you use option 4d1, you must provide the Installation\n   Information in the manner specified by section 6 of the GNU GPL\n   for conveying Corresponding Source.)\n\n  5. Combined Libraries.\n\n  You may place library facilities that are a work based on the\nLibrary side by side in a single library together with other library\nfacilities that are not Applications and are not covered by this\nLicense, and convey such a combined library under terms of your\nchoice, if you do both of the following:\n\n   a) Accompany the combined library with a copy of the same work based\n   on the Library, uncombined with any other library facilities,\n   conveyed under the terms of this License.\n\n   b) Give prominent notice with the combined library that part of it\n   is a work based on the Library, and explaining where to find the\n   accompanying uncombined form of the same work.\n\n  6. Revised Versions of the GNU Lesser General Public License.\n\n  The Free Software Foundation may publish revised and/or new versions\nof the GNU Lesser General Public License from time to time. Such new\nversions will be similar in spirit to the present version, but may\ndiffer in detail to address new problems or concerns.\n\n  Each version is given a distinguishing version number. If the\nLibrary as you received it specifies that a certain numbered version\nof the GNU Lesser General Public License \"or any later version\"\napplies to it, you have the option of following the terms and\nconditions either of that published version or of any later version\npublished by the Free Software Foundation. If the Library as you\nreceived it does not specify a version number of the GNU Lesser\nGeneral Public License, you may choose any version of the GNU Lesser\nGeneral Public License ever published by the Free Software Foundation.\n\n  If the Library as you received it specifies that a proxy can decide\nwhether future versions of the GNU Lesser General Public License shall\napply, that proxy's public statement of acceptance of any version is\npermanent authorization for you to choose that version for the\nLibrary.\n"
  },
  {
    "path": "Dockerfile-py3.5",
    "content": "# This Dockerfile is based on: https://docs.docker.com/examples/postgresql_service/\n\nFROM ubuntu:16.04\n\nMAINTAINER fredrik@averpil.com\n\n# Add the PostgreSQL PGP key to verify their Debian packages.\n# It should be the same key as https://www.postgresql.org/media/keys/ACCC4CF8.asc\nRUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8\n\n# Add PostgreSQL's repository. It contains the most recent stable release\n#     of PostgreSQL, ``9.3``.\nRUN echo \"deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main\" > /etc/apt/sources.list.d/pgdg.list\n\n# Install everything in one enormous RUN command\n#  There are some warnings (in red) that show up during the build. You can hide\n#  them by prefixing each apt-get statement with DEBIAN_FRONTEND=noninteractive\nRUN apt-get update && \\\n\n    apt-get install -y \\\n    python3-software-properties python3-pip \\\n    software-properties-common \\\n    postgresql-9.3 postgresql-client-9.3 postgresql-contrib-9.3 postgresql-server-dev-9.3 \\\n    rubygems && \\\n\n    gem install taskjuggler && \\\n\n    pip3 install -U pip && \\\n    pip3 install sqlalchemy psycopg2-binary jinja2 alembic mako markupsafe python-editor nose coverage\n\n# Note: The official Debian and Ubuntu images automatically ``apt-get clean``\n# after each ``apt-get``\n\n# Run commands as the ``postgres`` user created by the ``postgres-9.3`` package when it was ``apt-get installed``\nUSER postgres\n\nRUN /etc/init.d/postgresql start && \\\n    psql -c \"CREATE DATABASE stalker_test;\" -U postgres && \\\n    psql -c \"CREATE USER stalker_admin WITH PASSWORD 'stalker';\" -U postgres && \\\n    /etc/init.d/postgresql stop\n\n# Adjust PostgreSQL configuration so that remote connections to the\n# database are possible.\n# RUN echo \"host all  all    0.0.0.0/0  md5\" >> /etc/postgresql/9.3/main/pg_hba.conf\n\n# And add ``listen_addresses`` to ``/etc/postgresql/9.3/main/postgresql.conf``\n# RUN echo \"listen_addresses='*'\" >> /etc/postgresql/9.3/main/postgresql.conf\n\n# Expose the PostgreSQL port\n# EXPOSE 5432\n\n# Add VOLUMEs to allow backup of config, logs and databases\n# VOLUME  [\"/etc/postgresql\", \"/var/log/postgresql\", \"/var/lib/postgresql\"]\n\nUSER root\n\n# Create symlink to TaskJuggler\n# RUN ln -s $(which tj3) /usr/local/bin/tj3\n\n# Set working directory\nWORKDIR /workspace\n\n# Embed wait-for-postgres.sh script into Dockerfile\nRUN echo '\\n\\\n# wait-for-postgres\\n\\\n\\n\\\n\nset -e\\n\\\n\\n\\\ncmd=\"$@\"\\n\\\ntimer=\"5\"\\n\\\n\\n\\\nuntil runuser -l postgres -c 'pg_isready' 2>/dev/null; do\\n\\\n    >&2 echo \"Postgres is unavailable - sleeping for $timer seconds\"\\n\\\n    sleep $timer\\n\\\ndone\\n\\\n\\n\\\n>&2 echo \"Postgres is up - executing command\"\\n\\\nexec $cmd\\n'\\\n>> /workspace/wait-for-postgres.sh\n\n# Make script executable\nRUN chmod +x /workspace/wait-for-postgres.sh\n\n# Execute this when running container\nENTRYPOINT \\\n\n            # Copy stalker into container's /workspace'\n            cp -r /stalker /workspace && \\\n\n            # Remove execution permissions within Stalker\n            chmod -R -x /workspace/stalker && \\\n\n            # Start PostgreSQL\n            runuser -l postgres -c '/usr/lib/postgresql/9.3/bin/postgres -D /var/lib/postgresql/9.3/main -c config_file=/etc/postgresql/9.3/main/postgresql.conf & ' && \\\n\n            # Wait for PostgresSQL\n            ./wait-for-postgres.sh nosetests /workspace/stalker --verbosity=1 --cover-erase --with-coverage --cover-package=stalker && \\\n\n            # Cleanly shut down PostgreSQL\n            /etc/init.d/postgresql stop\n"
  },
  {
    "path": "INSTALL",
    "content": "See docs/installation.html.\n"
  },
  {
    "path": "LICENSE",
    "content": "                   GNU LESSER GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n\n  This version of the GNU Lesser General Public License incorporates\nthe terms and conditions of version 3 of the GNU General Public\nLicense, supplemented by the additional permissions listed below.\n\n  0. Additional Definitions.\n\n  As used herein, \"this License\" refers to version 3 of the GNU Lesser\nGeneral Public License, and the \"GNU GPL\" refers to version 3 of the GNU\nGeneral Public License.\n\n  \"The Library\" refers to a covered work governed by this License,\nother than an Application or a Combined Work as defined below.\n\n  An \"Application\" is any work that makes use of an interface provided\nby the Library, but which is not otherwise based on the Library.\nDefining a subclass of a class defined by the Library is deemed a mode\nof using an interface provided by the Library.\n\n  A \"Combined Work\" is a work produced by combining or linking an\nApplication with the Library.  The particular version of the Library\nwith which the Combined Work was made is also called the \"Linked\nVersion\".\n\n  The \"Minimal Corresponding Source\" for a Combined Work means the\nCorresponding Source for the Combined Work, excluding any source code\nfor portions of the Combined Work that, considered in isolation, are\nbased on the Application, and not on the Linked Version.\n\n  The \"Corresponding Application Code\" for a Combined Work means the\nobject code and/or source code for the Application, including any data\nand utility programs needed for reproducing the Combined Work from the\nApplication, but excluding the System Libraries of the Combined Work.\n\n  1. Exception to Section 3 of the GNU GPL.\n\n  You may convey a covered work under sections 3 and 4 of this License\nwithout being bound by section 3 of the GNU GPL.\n\n  2. Conveying Modified Versions.\n\n  If you modify a copy of the Library, and, in your modifications, a\nfacility refers to a function or data to be supplied by an Application\nthat uses the facility (other than as an argument passed when the\nfacility is invoked), then you may convey a copy of the modified\nversion:\n\n   a) under this License, provided that you make a good faith effort to\n   ensure that, in the event an Application does not supply the\n   function or data, the facility still operates, and performs\n   whatever part of its purpose remains meaningful, or\n\n   b) under the GNU GPL, with none of the additional permissions of\n   this License applicable to that copy.\n\n  3. Object Code Incorporating Material from Library Header Files.\n\n  The object code form of an Application may incorporate material from\na header file that is part of the Library.  You may convey such object\ncode under terms of your choice, provided that, if the incorporated\nmaterial is not limited to numerical parameters, data structure\nlayouts and accessors, or small macros, inline functions and templates\n(ten or fewer lines in length), you do both of the following:\n\n   a) Give prominent notice with each copy of the object code that the\n   Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the object code with a copy of the GNU GPL and this license\n   document.\n\n  4. Combined Works.\n\n  You may convey a Combined Work under terms of your choice that,\ntaken together, effectively do not restrict modification of the\nportions of the Library contained in the Combined Work and reverse\nengineering for debugging such modifications, if you also do each of\nthe following:\n\n   a) Give prominent notice with each copy of the Combined Work that\n   the Library is used in it and that the Library and its use are\n   covered by this License.\n\n   b) Accompany the Combined Work with a copy of the GNU GPL and this license\n   document.\n\n   c) For a Combined Work that displays copyright notices during\n   execution, include the copyright notice for the Library among\n   these notices, as well as a reference directing the user to the\n   copies of the GNU GPL and this license document.\n\n   d) Do one of the following:\n\n       0) Convey the Minimal Corresponding Source under the terms of this\n       License, and the Corresponding Application Code in a form\n       suitable for, and under terms that permit, the user to\n       recombine or relink the Application with a modified version of\n       the Linked Version to produce a modified Combined Work, in the\n       manner specified by section 6 of the GNU GPL for conveying\n       Corresponding Source.\n\n       1) Use a suitable shared library mechanism for linking with the\n       Library.  A suitable mechanism is one that (a) uses at run time\n       a copy of the Library already present on the user's computer\n       system, and (b) will operate properly with a modified version\n       of the Library that is interface-compatible with the Linked\n       Version.\n\n   e) Provide Installation Information, but only if you would otherwise\n   be required to provide such information under section 6 of the\n   GNU GPL, and only to the extent that such information is\n   necessary to install and execute a modified version of the\n   Combined Work produced by recombining or relinking the\n   Application with a modified version of the Linked Version. (If\n   you use option 4d0, the Installation Information must accompany\n   the Minimal Corresponding Source and Corresponding Application\n   Code. If you use option 4d1, you must provide the Installation\n   Information in the manner specified by section 6 of the GNU GPL\n   for conveying Corresponding Source.)\n\n  5. Combined Libraries.\n\n  You may place library facilities that are a work based on the\nLibrary side by side in a single library together with other library\nfacilities that are not Applications and are not covered by this\nLicense, and convey such a combined library under terms of your\nchoice, if you do both of the following:\n\n   a) Accompany the combined library with a copy of the same work based\n   on the Library, uncombined with any other library facilities,\n   conveyed under the terms of this License.\n\n   b) Give prominent notice with the combined library that part of it\n   is a work based on the Library, and explaining where to find the\n   accompanying uncombined form of the same work.\n\n  6. Revised Versions of the GNU Lesser General Public License.\n\n  The Free Software Foundation may publish revised and/or new versions\nof the GNU Lesser General Public License from time to time. Such new\nversions will be similar in spirit to the present version, but may\ndiffer in detail to address new problems or concerns.\n\n  Each version is given a distinguishing version number. If the\nLibrary as you received it specifies that a certain numbered version\nof the GNU Lesser General Public License \"or any later version\"\napplies to it, you have the option of following the terms and\nconditions either of that published version or of any later version\npublished by the Free Software Foundation. If the Library as you\nreceived it does not specify a version number of the GNU Lesser\nGeneral Public License, you may choose any version of the GNU Lesser\nGeneral Public License ever published by the Free Software Foundation.\n\n  If the Library as you received it specifies that a proxy can decide\nwhether future versions of the GNU Lesser General Public License shall\napply, that proxy's public statement of acceptance of any version is\npermanent authorization for you to choose that version for the\nLibrary.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include *.ini *.cfg\n\ninclude alembic.ini\ninclude CHANGELOG.rst\ninclude COPYING\ninclude COPYING.LESSER\ninclude INSTALL\ninclude MANIFEST.in\ninclude README.rst\ninclude stalker/VERSION\ninclude TODO\ninclude VERSION\n\nprune docs/build\nprune docs/source/generated"
  },
  {
    "path": "Makefile",
    "content": "SHELL:=bash\nPACKAGE_NAME=stalker\nNUM_CPUS = $(shell nproc ||  grep -c '^processor' /proc/cpuinfo)\nSETUP_PY_FLAGS = --use-distutils\nVERSION := $(shell cat VERSION)\nVERSION_FILE=$(CURDIR)/src/stalker/VERSION\nVIRTUALENV_DIR:=.venv\nSYSTEM_PYTHON?=python3\n\nall: build FORCE\n\n.PHONY: help\nhelp:\n\t@echo \"\"\n\t@echo \"Available targets:\"\n\t@make -qp | grep -o '^[a-z0-9-]\\+' | sort\n\n.PHONY: venv\nvenv:\n\t@printf \"\\n\\033[36m--- $@: Creating Local virtualenv '$(VIRTUALENV_DIR)' using '$(SYSTEM_PYTHON)' ---\\033[0m\\n\"\n\t$(SYSTEM_PYTHON) -m venv $(VIRTUALENV_DIR)\n\nbuild:\n\t@printf \"\\n\\033[36m--- $@: Building ---\\033[0m\\n\"\n\techo -e \"\\n\\033[36m--- $@: Local install into virtualenv '$(VIRTUALENV_DIR)' ---\\033[0m\\n\";\n\tsource ./$(VIRTUALENV_DIR)/bin/activate; \\\n\techo -e \"\\n\\033[36m--- $@: Using python interpretter '`which python`' ---\\033[0m\\n\"; \\\n\tpip install -r requirements.txt; \\\n\tpip install -r requirements-dev.txt; \\\n\tpython -m build;\n\n.PHONY: install\ninstall:\n\t@printf \"\\n\\033[36m--- $@: Installing $(PACKAGE_NAME) to virtualenv at '$(VIRTUALENV_DIR)' using '$(SYSTEM_PYTHON)' ---\\033[0m\\n\"\n\tsource ./$(VIRTUALENV_DIR)/bin/activate; \\\n\tpip install ./dist/$(PACKAGE_NAME)-$(VERSION)-*.whl --force-reinstall;\n\nclean: FORCE\n\t@printf \"\\n\\033[36m--- $@: Clean ---\\033[0m\\n\"\n\t-rm -rf .pytest_cache\n\t-rm -f .coverage*\n\t-rm -rf .mypy_cache\n\t-rm -rf .tox\n\t-rm -rf dist\n\t-rm -rf build\n\t-rm -rf docs/build\n\t-rm -rf docs/source/generated/*\n\t-rm -rf htmlcov\n\nclean-all: clean\n\t@printf \"\\n\\033[36m--- $@: Clean All---\\033[0m\\n\"\n\t-rm -f INSTALLED_FILES\n\t-rm -f setuptools-*.egg\n\t-rm -f use-distutils\n\t-rm -Rf src/$(PACKAGE_NAME).egg-info\n\t-rm -Rf $(VIRTUALENV_DIR)\n\nhtml:\n\t./setup.py readme\n\nnew-release:\n\t@printf \"\\n\\033[36m--- $@: Generating New Release ---\\033[0m\\n\"\n\tgit add $(VERSION_FILE)\n\tgit commit -m \"Version $(VERSION)\"\n\tgit push\n\tgit checkout main\n\tgit pull\n\tgit merge develop\n\tgit tag $(VERSION)\n\tgit push origin main --tags\n\tsource ./$(VIRTUALENV_DIR)/bin/activate; \\\n\techo -e \"\\n\\033[36m--- $@: Using python interpretter '`which python`' ---\\033[0m\\n\"; \\\n\tpip install -r requirements.txt; \\\n\tpip install -r requirements-dev.txt; \\\n\tpython -m build; \\\n\ttwine check dist/$(PACKAGE_NAME)-$(VERSION).tar.gz; \\\n\ttwine upload dist/$(PACKAGE_NAME)-$(VERSION).tar.gz;\n\n.PHONY: tests\ntests:\n\t@printf \"\\n\\033[36m--- $@: Run Tests ---\\033[0m\\n\"\n\techo -e \"\\n\\033[36m--- $@: Using virtualenv at '$(VIRTUALENV_DIR)' ---\\033[0m\\n\";\n\tsource ./$(VIRTUALENV_DIR)/bin/activate; \\\n\techo -e \"\\n\\033[36m--- $@: Using python interpretter '`which python`' ---\\033[0m\\n\"; \\\n\tSQLALCHEMY_WARN_20=1 PYTHONPATH=src pytest -n auto -W ignore -W always::DeprecationWarning --color=yes --cov=src --cov-report term --cov-report html --cov-append --cov-fail-under 99 tests;\n\n.PHONY: docs\ndocs:\n\tcd docs && $(MAKE) html\n\n# https://www.gnu.org/software/make/manual/html_node/Force-Targets.html\nFORCE:\n"
  },
  {
    "path": "README.md",
    "content": "[![license](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](http://www.gnu.org/licenses/lgpl-3.0)\n[![Supported Python versions](https://img.shields.io/pypi/pyversions/stalker.svg)](https://pypi.python.org/pypi/stalker)\n[![Unit Tests](https://github.com/eoyilmaz/stalker/actions/workflows/pytest.yml/badge.svg)](https://github.com/eoyilmaz/stalker/actions/workflows/pytest.yml)\n[![PyPI Version](https://img.shields.io/pypi/v/stalker.svg)](https://pypi.python.org/pypi/stalker)\n[![PyPI Downloads](https://static.pepy.tech/badge/stalker)](https://pepy.tech/projects/stalker)\n\nAbout\n=====\n\nStalker is an Open Source Production Asset Management (ProdAM) Library designed \nspecifically for Animation and VFX Studios. But it can be used for any kind of\nprojects from any other industry. Stalker is licensed under LGPL v3.\n\nFeatures\n========\n\nStalker has the following features:\n\n * Designed for **Animation and VFX Studios** (but not limited to).\n * OS independent, can work simultaneously with **Windows**, **macOS** and\n   **Linux**.\n * Supplies excellent **Project Management** capabilities, i.e. scheduling and\n   tracking tasks, milestones and deadlines (via **TaskJuggler**).\n * Powerful **Asset management** capabilities, allows tracking of asset\n   references in shots, scenes, sequences and projects.\n * Customizable object model (**Stalker Object Model - SOM**).\n * Uses **TaskJuggler** as the project planing and tracking backend.\n * Mainly developed for **PostgreSQL** in mind but **SQLite3** is also\n   supported.\n * Can be connected to all the major 3D animation packages like **Maya,\n   Houdini, Nuke, Fusion, DaVinci Resolve, Blender** etc. and any application\n   that has a Python API, and for **Adobe Suite** applications like\n   **Adobe Photoshop** through ``win32com`` or ``comtypes`` libraries.\n * Developed with religious **TDD** practices.\n\nStalker is mainly build over the following OpenSource libraries:\n\n * [Python](https://www.python.org)\n * [PostgreSQL](https://www.postgresql.org/)\n * [SQLAlchemy](https://www.sqlalchemy.org/)\n * [Jinja2](https://jinja.palletsprojects.com/en/stable/)\n * [TaskJuggler](https://taskjuggler.org/)\n\nAs Stalker is a Python library and doesn't supply any graphical UI you can use\nother tools like [Stalker Pyramid](https://github.com/eoyilmaz/stalker_pyramid)\nwhich is a Pyramid Web Application and [Anima](https://github.com/eoyilmaz/anima)\nwhich has PyQt/PySide UIs for applications like Houdini, Maya, Blender, Nuke,\nFusion, DaVinci Resolve, Photoshop and many more.\n\nInstallation\n============\n\nSimply use:\n\n```shell\npip install stalker\n```\n\nExamples\n========\n\nLet's play with **Stalker**.\n\nBecause Stalker uses SQLAlchemy, it is very easy to retrieve complex data.\nLet's say that you want to query all the Shot Lighting tasks where a specific\nasset is referenced:\n\n```python\nfrom stalker import Asset, File, Shot, Version\n\nmy_asset = Asset.query.filter_by(name=\"My Asset\").first()\n# Let's assume we have multiple Versions created for this Asset already\nmy_asset_version = my_asset.versions[0]\n# get a file from that version\nmy_asset_version_file = my_asset_version.files[0]\n# now get any other Lighting Versions that is referencing this file\nrefs = (\n    Version.query\n        .join(File, Version.files)\n        .filter(Version.name==\"Lighting\")\n        .filter(File.references.contains(my_asset_version_file))\n        .all()\n)\n```\n\nLet's say you want to get all the tasks assigned to you in a specific Project:\n\n```python\nfrom stalker import Project, Task, User\n\nme = User.query.filter_by(name=\"Erkan Ozgur Yilmaz\").first()\nmy_project = Project.query.filter_by(name=\"My Project\").first() \nquery = Task.query.filter_by(project=my_project).filter(Task.resources.contains(me))\nmy_tasks = query.all()\n```\n\nYou can further query let's say your WIP tasks by adding more criteria to the ``query``\nobject:\n\n```python\nfrom stalker import Status\n\nwip = Status.query.filter_by(code=\"WIP\").first()\nquery = query.filter_by(status=wip)\nmy_wip_tasks = query.all()\n```\n\nand that's the way to get complex data in Stalker.\n\nSee more detailed examples in [API Tutorial](https://pythonhosted.org/stalker/tutorial.html).\n"
  },
  {
    "path": "TODO.rst",
    "content": "TODO\n====\n\n* **Update:** Better support ``duration`` Tasks.\n\n  The current status workflow is not working with **duration** based tasks.\n  Task statuses should be updated automatically for duration tasks according to\n  their computed start and computed end date values.\n\n* Auto history generation.\n\n  Automatically record all kind of CRUD actions to create a history for any\n  attribute present in an entity.\n\n* SCM Integration:\n\n  The repository can be a local path, and the project can be managed with an\n  SCM, preferably with Mercurial.\n\n* Per user settings file:\n\n  To let Pipeline TDs easily setup a new workstation with a setup script,\n  a predefined file let say with a name of \".strc\" can be placed in to the \n  users home folder and Stalker can search for this file and parse it to get \n  things like the database server path, user name and the password.\n\n  There could be also an $STRC environment variable which is showing a common\n  place lets say in the fileserver, which also may have a \".strc\" file. In \n  this way it will be easy to setup only one \".strc\" file for the whole studio.\n\n* ``__tablename__`` and ``__mapper_args__``:\n  \n  The duty of the ``__tablename__`` and ``__mapper_args__`` variables are \n  very common to any class in the SOM. It can be gathered in a mixin and the\n  :class:`~stalker.core.models.SimpleEntity` can be mixed with this class and\n  the rest will have their table name and polymorphic identity by default. \n\n* use pyseq for file sequence handling:\n\n  PySeq is a great, simple library which handles all the file sequence actions.\n  It would be great to use it in the :class:`~stalker.core.models.Link` \n  instances. So, the :class:`~stalker.core.models.Link` class can also hold a\n  string which can be uncompressed with the pyseq.uncompress function::\n    \n    from pyseq import uncompress\n    seq = uncompress(\"./tests/012_vb_110_v001.%04d.png 1-10\",\n                     format=\"%h%p%t %r\")\n\n* Hidden keyword arguments:\n  \n  Because of the heavy inheritance, it is not very clear what parameters are \n  needed to initialize a class. A simple solution is to repeat all the \n  parameters of the inherited class in the __init__ of the child class.\n\nDONE\n====\n\n* Update error messages:\n\n  Not all error message are clear. Generally, because of the heavy\n  inheritance, it is not very obvious which class gave the error. Writing down\n  the class name should help the user to understand at least what class is\n  giving the error message.\n\n* Drop support to any database other then PostgreSQL.\n\n  This is needs to be done mainly to benefit from a system that is highly\n  optimized for one database in mind. There are some certain functionality that\n  is only supported in PostgreSQL and will make Stalker better but break the\n  compatibility to any other DB. Also trying to support SQLite3 with its lack\n  of support to some key functionality stabs Stalker in back.\n\n* Test CRUD:\n  \n  The database tests should test if all the Create, Read, Update and Delete\n  operations are happening properly.\n\n* Plural name:\n\n  The plural name attribute needs to be reintroduced.\n  \n* datetime instead of date\n\n  In the Task class all the time calculation should be done over the\n  datetime.datetime class instead of datetime.date object. This will let us\n  to increase the granularity of the scheduling.\n\n* ``end_date`` attribute in DateRangeMixin:\n  \n  there could be a synonym for the ``due_date`` attribute in the\n  :class:`~stalker.core.models.DateRangeMixin`\\ . Which will be meaningful for\n  :class:`~stalker.core.models.Booking` class.\n* Logging:\n\n  Use the Python logging module to output Debug messages.\n\n* StatusMixin and string indices:\n\n  StatusMixin should be able to set the status with a string, because it is\n  possible to use strings in StatusList.\n\n* Start & End Date For Classes Mixed With TaskMixin:\n\n  All the classes which are mixed with TaskMixin should have a start and end\n  date attribute which will be set to the start date of the first task to the\n  due_date of the last task.\n\n* Refactor target_entity_type attributes\n\n  StatusList, FilenameTemplate and Type classes are using the same\n  target_entity_type attribute, create a mixin for that.\n\n* OverBookingWarning:\n  \n  Create a new Warning for the :class:`~stalker.core.models.Booking` class \n  which will be emitted when a resource is booked for the same time period \n  more than once.\n\n* Auto StatusList connection:\n\n  StatusLists can be automatically connected to the created instance if there\n  is already a database setup and a StatusList instance already defined for\n  the current class. This means mixing the model part with the control part\n  but it is acceptable.\n\n* Stop the fight between SimpleEntity.name and SimpleEntity.code.\n\n  Currently name superseeds code, but it is annoying to change the code over\n  and over again just because the name is changed. So change the behaviour to\n  something like that; the code is only updated to the same value with name if\n  it is set to None or empty string. In any other case the code should remain\n  in the same value.\n\n* SQLAlchemy ORM Declarative:\n\n  Use declarative for the whole system. It started to make no sense to use\n  classical approach with Python objects and it started to be very hard to try\n  to update all the relations which is handled automatically by SQLAlchemy.\n  Besides, the work done by all the attributes which are using ValidatedList\n  is replaced with a neat system whenever the mapping has occured. Which is\n  the usage case %90 of the time.\n\n  Tests are going to be nearly the same. The only programming overhead is the\n  implementation itself.\n\n  Mixin classes also needs some attention, but as far as I see it is\n  successfully handled withing declarative approach.\n\n* \"__stalker_version__\" in SimpleEntity:\n\n  Create an attribute called __stalker_version__ in the SimpleEntity, and\n  automatically update it to the current version string of Stalker to be able\n  to see with which version of Stalker this data is created, mainly important\n  for the database part.\n\n* Replace all the Mocker based tests with Unittest's which are using real\n  objects. It was necessary to use the Mocker library while designing the rest\n  of the system, but it is now making things complex and started to hide the\n  changes of one object from the others in the system.\n\n* Convert all the list comparison test to assertItemsEqual\n\n* Add a slot in the ValidatedList which will hold the callable for the\n  validation process when any of the objects are changed (set, remove, delete\n  etc.) to allow the callable to be called when something has changed. This\n  will allow more control on the list, e.g. this will help controling the\n  relation of the classes to each other.\n\n* Check FilenameTemplate class documentation.\n\n* Check database part of all the previous Type dependent classes (Link, Asset,\n  Project, Task)\n\n* Update the exceptions. Check if a proper exception is raised instead of\n  raising ValueErrors all the time.\n\n* A Status in StatusList should be accessed by its name used as the index\n\n* A status should be comparable with a string like project.status==\"complete\"\n  or project.status==\"cmplt\"\n\n* for an object which stores a list of other objects, stalker is validating if\n  the list is gathered from the correct type of objects, for example,\n  StatusList objects only accepts a list of Status objects. Stalker is able to\n  check if the elements in a list are Status objects when a list is assigned\n  to the StatusList.statuses attribute, but it can not check anything if the\n  list element is changed individually afterwards. This behaviour should be\n  extended with a validating system which is able to track changes on list\n  elements.\n  \n  SOLUTION:\n  \n    Added the ValidatedList list variant which does all the necessary things\n    explained in the problem.\n"
  },
  {
    "path": "alembic/README",
    "content": "Generic single-database configuration."
  },
  {
    "path": "alembic/env.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Setup environment for migration.\"\"\"\nfrom logging.config import fileConfig\n\nfrom alembic import context\n\nfrom sqlalchemy import engine_from_config, pool\n\nfrom stalker.db.declarative import Base\n\n# this is the Alembic Config object, which provides\n# access to the values within the .ini file in use.\nconfig = context.config\n\n# Interpret the config file for Python logging.\n# This line sets up loggers basically.\nfileConfig(config.config_file_name)\n\n# add your model's MetaData object here\n# for 'autogenerate' support\n# from myapp import mymodel\n# target_metadata = mymodel.Base.metadata\n\ntarget_metadata = Base.metadata\n\n# other values from the config, defined by the needs of env.py,\n# can be acquired:\n# my_important_option = config.get_main_option(\"my_important_option\")\n# ... etc.\n\n\ndef run_migrations_offline():\n    \"\"\"Run migrations in 'offline' mode.\n\n    This configures the context with just a URL\n    and not an Engine, though an Engine is acceptable\n    here as well.  By skipping the Engine creation\n    we don't even need a DBAPI to be available.\n\n    Calls to context.execute() here emit the given string to the\n    script output.\n\n    \"\"\"\n    url = config.get_main_option(\"sqlalchemy.url\")\n    context.configure(url=url)\n\n    with context.begin_transaction():\n        context.run_migrations()\n\n\ndef run_migrations_online():\n    \"\"\"Run migrations in 'online' mode.\n\n    In this scenario we need to create an Engine\n    and associate a connection with the context.\n\n    \"\"\"\n    engine = engine_from_config(\n        config.get_section(config.config_ini_section),\n        prefix=\"sqlalchemy.\",\n        poolclass=pool.NullPool,\n    )\n\n    connection = engine.connect()\n    context.configure(connection=connection, target_metadata=target_metadata)\n\n    try:\n        with context.begin_transaction():\n            context.run_migrations()\n    finally:\n        connection.close()\n\n\nif context.is_offline_mode():\n    run_migrations_offline()\nelse:\n    run_migrations_online()\n"
  },
  {
    "path": "alembic/script.py.mako",
    "content": "\"\"\"${message}\n\nRevision ID: ${up_revision}\nRevises: ${down_revision}\nCreate Date: ${create_date}\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n${imports if imports else \"\"}\n\n# revision identifiers, used by Alembic.\nrevision = ${repr(up_revision)}\ndown_revision = ${repr(down_revision)}\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    ${upgrades if upgrades else \"pass\"}\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    ${downgrades if downgrades else \"pass\"}\n"
  },
  {
    "path": "alembic/versions/0063f547dc2e_updated_version_inputs_table.py",
    "content": "\"\"\"updated version_inputs table.\n\nRevision ID: 0063f547dc2e\nRevises: a9319b19f7be\nCreate Date: 2016-11-29 14:08:41.335000\n\"\"\"\n\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = \"0063f547dc2e\"\ndown_revision = \"a9319b19f7be\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.drop_constraint(\n        \"Version_Inputs_link_id_fkey\", \"Version_Inputs\", type_=\"foreignkey\"\n    )\n    op.create_foreign_key(\n        None,\n        \"Version_Inputs\",\n        \"Links\",\n        [\"link_id\"],\n        [\"id\"],\n        onupdate=\"CASCADE\",\n        ondelete=\"CASCADE\",\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_constraint(None, \"Version_Inputs\", type_=\"foreignkey\")\n    op.create_foreign_key(\n        \"Version_Inputs_link_id_fkey\", \"Version_Inputs\", \"Links\", [\"link_id\"], [\"id\"]\n    )\n"
  },
  {
    "path": "alembic/versions/019378697b5b_rename_depends_to_to_depends_on.py",
    "content": "\"\"\"Rename depends_to to depends_on\n\nRevision ID: 019378697b5b\nRevises: feca9bac7d5a\nCreate Date: 2024-11-01 13:59:11.513575\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"019378697b5b\"\ndown_revision = \"feca9bac7d5a\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.alter_column(\n        \"Task_Dependencies\", \"depends_to_id\", new_column_name=\"depends_on_id\"\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.alter_column(\n        \"Task_Dependencies\", \"depends_on_id\", new_column_name=\"depends_to_id\"\n    )\n"
  },
  {
    "path": "alembic/versions/101a789e38ad_created_task_responsible.py",
    "content": "\"\"\"Created \"Task.responsible\" attribute.\n\nRevision ID: 101a789e38ad\nRevises: 59092d41175c\nCreate Date: 2013-06-24 12:32:04.852386\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"101a789e38ad\"\ndown_revision = \"59092d41175c\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    try:\n        op.drop_column(\"Sequences\", \"lead_id\")\n        op.add_column(\"Tasks\", sa.Column(\"responsible_id\", sa.Integer(), nullable=True))\n    except sa.exc.OperationalError:\n        pass\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    try:\n        op.drop_column(\"Tasks\", \"responsible_id\")\n        op.add_column(\"Sequences\", sa.Column(\"lead_id\", sa.INTEGER(), nullable=True))\n    except sa.exc.OperationalError:\n        pass\n"
  },
  {
    "path": "alembic/versions/1181305d3001_added_client_id_column_to_goods_table.py",
    "content": "\"\"\"Added client_id column to Goods table.\n\nRevision ID: 1181305d3001\nRevises: 31b1e22b455e\nCreate Date: 2017-05-17 18:17:46.555000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"1181305d3001\"\ndown_revision = \"31b1e22b455e\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"Goods\", sa.Column(\"client_id\", sa.Integer(), nullable=True))\n    op.create_foreign_key(None, \"Goods\", \"Clients\", [\"client_id\"], [\"id\"])\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_constraint(None, \"Goods\", type_=\"foreignkey\")\n    op.drop_column(\"Goods\", \"client_id\")\n"
  },
  {
    "path": "alembic/versions/130a7697cd79_vacation_user_can_now_be_nullable.py",
    "content": "\"\"\"Vacation.user can now be nullable.\n\nRevision ID: 130a7697cd79\nRevises: 57a5949c7f29\nCreate Date: 2013-08-02 19:58:59.638085\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"130a7697cd79\"\ndown_revision = \"57a5949c7f29\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.alter_column(\"Vacations\", \"user_id\", existing_type=sa.INTEGER(), nullable=True)\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.alter_column(\"Vacations\", \"user_id\", existing_type=sa.INTEGER(), nullable=False)\n"
  },
  {
    "path": "alembic/versions/174567b9c159_note_content.py",
    "content": "\"\"\"'Note.content' is now a synonym of 'Note.description'.\n\nRevision ID: 174567b9c159\nRevises: a6598cde6b\nCreate Date: 2013-11-14 13:38:02.566201\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"174567b9c159\"\ndown_revision = \"a6598cde6b\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.drop_column(\"Notes\", \"content\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\"Notes\", sa.Column(\"content\", sa.VARCHAR(), nullable=True))\n"
  },
  {
    "path": "alembic/versions/182f44ce5f07_added_users_company_and_projects_client.py",
    "content": "\"\"\"added \"Users.company\" and \"Projects.client\" columns and a new Clients table.\n\nRevision ID: 182f44ce5f07\nRevises: 59bfe820c369\nCreate Date: 2014-05-29 11:33:02.313000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"182f44ce5f07\"\ndown_revision = \"59bfe820c369\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # Create Clients table\n    op.create_table(\n        \"Clients\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n\n    # Users table\n    op.add_column(\"Users\", sa.Column(\"company_id\", sa.Integer(), nullable=True))\n    op.create_foreign_key(\n        name=None,\n        source=\"Users\",\n        referent=\"Clients\",\n        local_cols=[\"company_id\"],\n        remote_cols=[\"id\"],\n    )\n\n    # Projects table\n    op.add_column(\"Projects\", sa.Column(\"client_id\", sa.Integer(), nullable=True))\n    op.create_foreign_key(\n        name=None,\n        source=\"Projects\",\n        referent=\"Clients\",\n        local_cols=[\"client_id\"],\n        remote_cols=[\"id\"],\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_column(\"Users\", \"company_id\")\n    op.drop_column(\"Projects\", \"client_id\")\n    op.drop_table(\"Clients\")\n"
  },
  {
    "path": "alembic/versions/1875136a2bfc_removed_version_variant_name_attribute.py",
    "content": "\"\"\"Removed Version.variant_name attribute\n\nRevision ID: 1875136a2bfc\nRevises: a2007ad7f535\nCreate Date: 2024-11-28 10:18:56.634490\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\nimport stalker\n\n\n# revision identifiers, used by Alembic.\nrevision = \"1875136a2bfc\"\ndown_revision = \"a2007ad7f535\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # create a new Variant task for all the Versions as their new parents.\n    op.execute(\n        f\"\"\"\n        -- add temporary column to simple entities\n        ALTER TABLE \"SimpleEntities\" ADD temp_variant_parent_id integer;\n\n        -- insert a new Variant for each distinct Version.variant_name\n        WITH sel1 as (\n            SELECT\n                subtable.task_id as task_id,\n                'Variant' as entity_type,\n                subtable.name as name,\n                'Created by alembic revision: {revision}' as description,\n                (SELECT CAST(NOW() at time zone 'utc' AS timestamp)) as date_created,\n                (SELECT CAST(NOW() at time zone 'utc' AS timestamp)) as date_updated,\n                '' as html_style,\n                '' as html_class,\n                '{stalker.__version__}' as stalker_version,\n                subtable.project_id as project_id,\n                false as is_milestone,\n                subtable.allocation_strategy as allocation_strategy,\n                subtable.persistent_allocation as persistent_allocation,\n                500 as priority,\n                10 as bid_timing,\n                'min' as bid_unit,\n                0 as schedule_seconds,\n                0 as total_logged_seconds,\n                0 as review_number,\n                subtable.good_id as good_id,\n                subtable.status_id as status_id,\n                (SELECT id FROM \"StatusLists\" WHERE \"StatusLists\".target_entity_type = 'Variant') as status_list_id,\n                subtable.start as start,\n                subtable.duration as duration,\n                subtable.computed_end as computed_end,\n                subtable.computed_start as computed_start,\n                subtable.end as end,\n                subtable.schedule_timing as schedule_timing,\n                subtable.schedule_unit as schedule_unit,\n                subtable.schedule_constraint as schedule_constraint,\n                subtable.schedule_model as schedule_model,\n                subtable.parent_id as parent_id\n            FROM (\n                SELECT\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Versions\".task_id))))[1] as task_id,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Versions\".variant_name))))[1] as name,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".project_id))))[1] as project_id,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".status_id))))[1] as status_id,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".allocation_strategy))))[1] as allocation_strategy,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".persistent_allocation))))[1] as persistent_allocation,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".good_id))))[1] as good_id,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".schedule_constraint))))[1] as schedule_constraint,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".schedule_model))))[1] as schedule_model,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".parent_id))))[1] as parent_id,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".start))))[1] as start,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".duration))))[1] as duration,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".computed_end))))[1] as computed_end,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".computed_start))))[1] as computed_start,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".end))))[1] as end,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".schedule_timing))))[1] as schedule_timing,\n                    (ARRAY_AGG(DISTINCT(COALESCE(\"Version_Tasks\".schedule_unit))))[1] as schedule_unit\n                FROM \"Versions\"\n                JOIN \"SimpleEntities\" AS \"Version_SimpleEntities\" ON \"Versions\".id = \"Version_SimpleEntities\".id\n                JOIN \"Tasks\" AS \"Version_Tasks\" ON \"Versions\".task_id = \"Version_Tasks\".id\n                JOIN \"StatusLists\" AS \"Variant_StatusLists\" ON \"Variant_StatusLists\".target_entity_type = 'Variant'\n                GROUP BY\n                    \"Versions\".task_id,\n                    \"Versions\".variant_name\n                ORDER BY\n                    \"Versions\".task_id\n            ) as subtable\n        ),\n        ins1 as (\n            INSERT INTO \"SimpleEntities\" (\n                entity_type,\n                name,\n                description,\n                date_created,\n                date_updated,\n                html_style,\n                html_class,\n                stalker_version,\n                temp_variant_parent_id\n            ) (\n                SELECT\n                    sel1.entity_type,\n                    sel1.name,\n                    sel1.description,\n                    sel1.date_created,\n                    sel1.date_updated,\n                    sel1.html_style,\n                    sel1.html_class,\n                    sel1.stalker_version,\n                    sel1.task_id -- use task_id as parent_id\n                FROM sel1\n            )\n            RETURNING id as variant_id, name as variant_name, temp_variant_parent_id as variant_parent_id\n        ),\n        ins2 as (\n            INSERT INTO \"Entities\" (id) (SELECT ins1.variant_id FROM ins1)\n        ), ins3 AS (\n            INSERT INTO \"Tasks\" (\n                id,\n                project_id,\n                is_milestone,\n                allocation_strategy,\n                persistent_allocation,\n                priority,\n                bid_timing,\n                bid_unit,\n                schedule_seconds,\n                total_logged_seconds,\n                review_number,\n                good_id,\n                status_id,\n                status_list_id,\n                start,\n                duration,\n                computed_end,\n                computed_start,\n                \"end\",\n                schedule_timing,\n                schedule_unit,\n                schedule_constraint,\n                schedule_model,\n                parent_id\n            )\n            (\n                SELECT\n                    ins1.variant_id,\n                    sel1.project_id,\n                    sel1.is_milestone,\n                    sel1.allocation_strategy,\n                    sel1.persistent_allocation,\n                    sel1.priority,\n                    sel1.bid_timing,\n                    CAST(sel1.bid_unit as public.\"TimeUnit\"),\n                    sel1.schedule_seconds,\n                    sel1.total_logged_seconds,\n                    sel1.review_number,\n                    sel1.good_id,\n                    sel1.status_id,\n                    sel1.status_list_id,\n                    sel1.start,\n                    sel1.duration,\n                    sel1.computed_end,\n                    sel1.computed_start,\n                    sel1.end,\n                    sel1.schedule_timing,\n                    sel1.schedule_unit,\n                    sel1.schedule_constraint,\n                    sel1.schedule_model,\n                    sel1.task_id -- use the original task as the parent of the new Variant\n                FROM sel1, ins1\n                WHERE sel1.name = ins1.variant_name AND sel1.task_id = ins1.variant_parent_id\n            )\n        )\n        INSERT INTO \"Variants\" (id) (SELECT ins1.variant_id FROM ins1);\n\n        -- Update the Versions to use the new Variants as parents\n        UPDATE \"Versions\"\n        SET task_id = subtable.variant_id\n        FROM (\n            SELECT\n                \"Versions\".id as version_id,\n                \"Versions\".variant_name as version_variant_name,\n                \"Versions\".task_id as version_task_id,\n                \"Tasks\".id as task_id,\n                \"Variant_Tasks\".parent_id as variant_parent_id,\n                \"Variant_Tasks\".id as variant_id,\n                \"Variant_SimpleEntities\".name as variant_name\n            FROM \"Versions\"\n            JOIN \"Tasks\" ON \"Versions\".task_id = \"Tasks\".id\n            JOIN \"Tasks\" AS \"Variant_Tasks\" ON \"Variant_Tasks\".parent_id = \"Tasks\".id\n            JOIN \"SimpleEntities\" AS \"Variant_SimpleEntities\" ON \"Variant_Tasks\".id = \"Variant_SimpleEntities\".id\n            WHERE \"Versions\".variant_name = \"Variant_SimpleEntities\".name\n            ORDER BY \"Versions\".id\n        ) as subtable\n        WHERE \"Versions\".id = subtable.version_id;\n\n\n        -- Remove the temporary column\n        ALTER TABLE \"SimpleEntities\" DROP COLUMN temp_variant_parent_id;\n\n        -- And drop the Versions.variant_name column\n        ALTER TABLE \"Versions\" DROP COLUMN variant_name;\n        \"\"\"\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\n        \"Versions\",\n        sa.Column(\n            \"variant_name\", sa.VARCHAR(length=256), autoincrement=False, nullable=True\n        ),\n    )\n\n    op.execute(\n        \"\"\"\n        -- Update Version.variant_name with parent names\n        UPDATE \"Versions\" SET (variant_name, task_id) = (subtable.variant_name, subtable.task_id)\n        FROM (\n            SELECT\n                \"Versions\".id as version_id,\n                \"Variant_SimpleEntities\".name as variant_name,\n                \"Variant_Tasks\".parent_id as task_id\n            FROM \"Versions\"\n            JOIN \"SimpleEntities\" as \"Variant_SimpleEntities\" on \"Versions\".task_id = \"Variant_SimpleEntities\".id\n            JOIN \"Tasks\" as \"Variant_Tasks\" on \"Versions\".task_id = \"Variant_Tasks\".id\n        ) as subtable\n        WHERE subtable.version_id = \"Versions\".id;\n\n        -- Remove all the variants that had a version before\n        -- match by the Variant.name = Version.variant_name under the same parent task\n        DELETE FROM \"Variants\"\n        WHERE \"Variants\".id IN (\n            SELECT\n                DISTINCT(\"Variants\".id) --,\n                -- \"Variant_SimpleEntities\".name,\n                -- \"Variant_Tasks\".parent_id,\n                -- \"Versions\".variant_name,\n                -- \"Versions\".task_id\n            FROM \"Variants\"\n            JOIN \"Tasks\" AS \"Variant_Tasks\" ON \"Variants\".id = \"Variant_Tasks\".id\n            JOIN \"Versions\" ON \"Variant_Tasks\".parent_id = \"Versions\".task_id\n            JOIN \"SimpleEntities\" AS \"Variant_SimpleEntities\" ON \"Variants\".id = \"Variant_SimpleEntities\".id\n            WHERE \"Variant_SimpleEntities\".name = \"Versions\".variant_name\n        );\n    \"\"\"\n    )\n"
  },
  {
    "path": "alembic/versions/1c9c9c28c102_price_lists_and_goods.py",
    "content": "\"\"\"Add PriceLists and Goods.\n\nRevision ID: 1c9c9c28c102\nRevises: 856e70016b2\nCreate Date: 2015-01-26 13:05:50.050345\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"1c9c9c28c102\"\ndown_revision = \"856e70016b2\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"PriceLists\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n    op.create_table(\n        \"Goods\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.Column(\"cost\", sa.Float(), nullable=True),\n        sa.Column(\"msrp\", sa.Float(), nullable=True),\n        sa.Column(\"unit\", sa.String(length=64), nullable=True),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n    op.create_table(\n        \"PriceList_Goods\",\n        sa.Column(\"price_list_id\", sa.Integer(), nullable=False),\n        sa.Column(\"good_id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"good_id\"],\n            [\"Goods.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"price_list_id\"],\n            [\"PriceLists.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"price_list_id\", \"good_id\"),\n    )\n    with op.batch_alter_table(\"BudgetEntries\", schema=None) as batch_op:\n        batch_op.add_column(sa.Column(\"cost\", sa.Float(), nullable=True))\n        batch_op.add_column(sa.Column(\"msrp\", sa.Float(), nullable=True))\n        batch_op.add_column(sa.Column(\"price\", sa.Float(), nullable=True))\n        batch_op.add_column(sa.Column(\"realized_total\", sa.Float(), nullable=True))\n        batch_op.add_column(sa.Column(\"unit\", sa.String(length=64), nullable=True))\n\n    with op.batch_alter_table(\"Budgets\", schema=None) as batch_op:\n        batch_op.add_column(sa.Column(\"parent_id\", sa.Integer(), nullable=True))\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    with op.batch_alter_table(\"Budgets\", schema=None) as batch_op:\n        batch_op.drop_column(\"parent_id\")\n\n    with op.batch_alter_table(\"BudgetEntries\", schema=None) as batch_op:\n        batch_op.drop_column(\"unit\")\n        batch_op.drop_column(\"realized_total\")\n        batch_op.drop_column(\"price\")\n        batch_op.drop_column(\"msrp\")\n        batch_op.drop_column(\"cost\")\n\n    op.drop_table(\"PriceList_Goods\")\n    op.drop_table(\"Goods\")\n    op.drop_table(\"PriceLists\")\n"
  },
  {
    "path": "alembic/versions/21b88ed3da95_added_referencemixin.py",
    "content": "\"\"\"Added ReferenceMixin to Task.\n\nRevision ID: 21b88ed3da95\nRevises: 4664d72ce1e1\nCreate Date: 2013-05-31 12:08:59.425539\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"21b88ed3da95\"\ndown_revision = \"4664d72ce1e1\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    try:\n        op.create_table(\n            \"Task_References\",\n            sa.Column(\"task_id\", sa.Integer(), nullable=False),\n            sa.Column(\"link_id\", sa.Integer(), nullable=False),\n            sa.ForeignKeyConstraint(\n                [\"link_id\"],\n                [\"Links.id\"],\n            ),\n            sa.ForeignKeyConstraint(\n                [\"task_id\"],\n                [\"Tasks.id\"],\n            ),\n            sa.PrimaryKeyConstraint(\"task_id\", \"link_id\"),\n        )\n    except sa.exc.OperationalError:\n        pass\n\n    op.drop_table(\"Asset_References\")\n    op.drop_table(\"Shot_References\")\n    op.drop_table(\"Sequence_References\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.create_table(\n        \"Sequence_References\",\n        sa.Column(\"sequence_id\", sa.INTEGER(), autoincrement=False, nullable=False),\n        sa.Column(\"link_id\", sa.INTEGER(), autoincrement=False, nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"link_id\"], [\"Links.id\"], name=\"Sequence_References_link_id_fkey\"\n        ),\n        sa.ForeignKeyConstraint(\n            [\"sequence_id\"],\n            [\"Sequences.id\"],\n            name=\"Sequence_References_sequence_id_fkey\",\n        ),\n        sa.PrimaryKeyConstraint(\n            \"sequence_id\", \"link_id\", name=\"Sequence_References_pkey\"\n        ),\n    )\n    op.create_table(\n        \"Shot_References\",\n        sa.Column(\"shot_id\", sa.INTEGER(), autoincrement=False, nullable=False),\n        sa.Column(\"link_id\", sa.INTEGER(), autoincrement=False, nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"link_id\"], [\"Links.id\"], name=\"Shot_References_link_id_fkey\"\n        ),\n        sa.ForeignKeyConstraint(\n            [\"shot_id\"], [\"Shots.id\"], name=\"Shot_References_shot_id_fkey\"\n        ),\n        sa.PrimaryKeyConstraint(\"shot_id\", \"link_id\", name=\"Shot_References_pkey\"),\n    )\n    op.create_table(\n        \"Asset_References\",\n        sa.Column(\"asset_id\", sa.INTEGER(), autoincrement=False, nullable=False),\n        sa.Column(\"link_id\", sa.INTEGER(), autoincrement=False, nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"asset_id\"], [\"Assets.id\"], name=\"Asset_References_asset_id_fkey\"\n        ),\n        sa.ForeignKeyConstraint(\n            [\"link_id\"], [\"Links.id\"], name=\"Asset_References_link_id_fkey\"\n        ),\n        sa.PrimaryKeyConstraint(\"asset_id\", \"link_id\", name=\"Asset_References_pkey\"),\n    )\n    op.drop_table(\"Task_References\")\n"
  },
  {
    "path": "alembic/versions/2252e51506de_multiple_repositories.py",
    "content": "\"\"\"Multiple Repositories per Project.\n\nRevision ID: 2252e51506de\nRevises: 1c9c9c28c102\nCreate Date: 2015-01-28 00:46:29.139946\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"2252e51506de\"\ndown_revision = \"1c9c9c28c102\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"Project_Repositories\",\n        sa.Column(\"project_id\", sa.Integer(), nullable=False),\n        sa.Column(\"repo_id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint([\"project_id\"], [\"Projects.id\"]),\n        sa.ForeignKeyConstraint([\"repo_id\"], [\"Repositories.id\"]),\n        sa.PrimaryKeyConstraint(\"project_id\", \"repo_id\"),\n    )\n\n    # before dropping repository column, carry all the data to the new table\n    op.execute(\n        'insert into \"Project_Repositories\"'\n        \"   select id, repository_id \"\n        '   from \"Projects\"'\n    )\n\n    with op.batch_alter_table(\"Projects\", schema=None) as batch_op:\n        batch_op.drop_column(\"repository_id\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    with op.batch_alter_table(\"Projects\", schema=None) as batch_op:\n        batch_op.add_column(\n            sa.Column(\"repository_id\", sa.INTEGER(), autoincrement=False, nullable=True)\n        )\n\n    # before dropping Project_Repositories, carry all the data back,\n    # note that only the first repository found per project will be\n    # restored to the Project.repository_id column\n    op.execute(\"\"\"\n        UPDATE \"Projects\" SET repository_id = (\n            SELECT\n                repo_id\n            FROM \"Project_Repositories\"\n            WHERE project_id = \"Projects\".id LIMIT 1\n        )\"\"\"\n    )\n\n    op.drop_table(\"Project_Repositories\")\n"
  },
  {
    "path": "alembic/versions/23dff41c95ff_removed_tasks_is_complete_column.py",
    "content": "\"\"\"Removed Tasks.is_complete column.\n\nRevision ID: 23dff41c95ff\nRevises: 5999269aad30\nCreate Date: 2014-06-11 14:00:00.559122\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"23dff41c95ff\"\ndown_revision = \"5999269aad30\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.drop_column(\"Tasks\", \"is_complete\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\"Tasks\", sa.Column(\"is_complete\", sa.BOOLEAN(), nullable=True))\n"
  },
  {
    "path": "alembic/versions/255ee1f9c7b3_added_payments_table.py",
    "content": "\"\"\"Added Payments table.\n\nRevision ID: 255ee1f9c7b3\nRevises: ea28a39ba3f5\nCreate Date: 2016-08-18 03:19:22.301000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"255ee1f9c7b3\"\ndown_revision = \"ea28a39ba3f5\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"Payments\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.Column(\"invoice_id\", sa.Integer(), nullable=True),\n        sa.Column(\"amount\", sa.Float(), nullable=True),\n        sa.Column(\"unit\", sa.String(length=64), nullable=True),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"invoice_id\"],\n            [\"Invoices.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_table(\"Payments\")\n"
  },
  {
    "path": "alembic/versions/258985128aff_create_entitygroups_table.py",
    "content": "\"\"\"create EntityGroups table.\n\nRevision ID: 258985128aff\nRevises: 39d3c16ff005\nCreate Date: 2016-05-16 16:06:39.389000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"258985128aff\"\ndown_revision = \"39d3c16ff005\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"EntityGroups\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n    op.create_table(\n        \"EntityGroup_Entities\",\n        sa.Column(\"entity_group_id\", sa.Integer(), nullable=False),\n        sa.Column(\"other_entity_id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"entity_group_id\"],\n            [\"EntityGroups.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"other_entity_id\"],\n            [\"SimpleEntities.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"entity_group_id\", \"other_entity_id\"),\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_table(\"EntityGroup_Entities\")\n    op.drop_table(\"EntityGroups\")\n"
  },
  {
    "path": "alembic/versions/25b3eba6ffe7_derive_version_from.py",
    "content": "\"\"\"Derive Version from Link instead of Entity.\n\nRevision ID: 25b3eba6ffe7\nRevises: 53d8127d8560\nCreate Date: 2013-05-22 16:51:53.136718\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\nfrom stalker import defaults, log\n\n\n# revision identifiers, used by Alembic.\nrevision = \"25b3eba6ffe7\"\ndown_revision = \"53d8127d8560\"\n\nlogger = log.get_logger(__name__)\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    try:\n        op.drop_column(\"Versions\", \"source_file_id\")\n    except (sa.exc.OperationalError, sa.exc.InternalError):\n        # SQLite doesn't support it\n        pass\n\n    try:\n        op.create_foreign_key(None, \"Versions\", \"Links\", [\"id\"], [\"id\"])\n        # FOREIGN KEY ( id ) REFERENCES Entities ( id ) DEFERRABLE INITIALLY DEFERRED\n        # FOREIGN KEY ( id ) REFERENCES Links ( id ) DEFERRABLE INITIALLY DEFERRED\n    except NotImplementedError:\n        # there is no way to create the foreign key in SQLite\n        # and it is incredibly hard to upgrade it\n        # so I opt to skip this part for SQLite and loose data\n\n        # so create a new table with the name Versions_New and create columns\n        # The DDL of the new table\n        # + id INTEGER PRIMARY KEY NOT NULL,\n        # + version_of_id INTEGER NOT NULL,\n        # + take_name TEXT,\n        # + version_number INTEGER NOT NULL,\n        # + parent_id INTEGER,\n        # + is_published TEXT,\n        # + status_id INTEGER NOT NULL,\n        # + status_list_id INTEGER NOT NULL,\n        # + FOREIGN KEY ( status_list_id ) REFERENCES StatusLists ( id )\n        #                                                 DEFERRABLE INITIALLY DEFERRED,\n        # + FOREIGN KEY ( status_id ) REFERENCES Statuses ( id )\n        #                                                 DEFERRABLE INITIALLY DEFERRED,\n        # + FOREIGN KEY ( parent_id ) REFERENCES Versions ( id )\n        #                                                 DEFERRABLE INITIALLY DEFERRED,\n        # + FOREIGN KEY ( version_of_id ) REFERENCES Tasks ( id )\n        #                                                 DEFERRABLE INITIALLY DEFERRED,\n        # + FOREIGN KEY ( id ) REFERENCES Links ( id ) DEFERRABLE INITIALLY DEFERRED\n\n        op.create_table(\n            \"Versions_New\",\n            sa.Column(\"id\", sa.Integer, sa.ForeignKey(\"Links.id\"), primary_key=True),\n            sa.Column(\n                \"version_of_id\", sa.Integer, sa.ForeignKey(\"Tasks.id\"), nullable=False\n            ),\n            sa.Column(\"take_name\", sa.String(256), default=defaults.version_take_name),\n            sa.Column(\"version_number\", sa.Integer, default=1, nullable=False),\n            sa.Column(\"parent_id\", sa.Integer, sa.ForeignKey(\"Versions.id\")),\n            sa.Column(\"is_published\", sa.Boolean, default=False),\n            sa.Column(\n                \"status_id\", sa.Integer, sa.ForeignKey(\"Statuses.id\"), nullable=False\n            ),\n            sa.Column(\n                \"status_list_id\",\n                sa.Integer,\n                sa.ForeignKey(\"StatusLists.id\"),\n                nullable=False,\n            ),\n        )\n\n        # *********************************************************************\n        # SKIP THIS PART\n        # then copy the data from the original table to the new table\n        # s = sa.sql.select(\n        #     ['Versions', 'Links']\n        # ).where('Versions.c.source_link_id == Links.c.id')\n        # result = op.execute(s)\n        #\n        # if result:\n        #     data = []\n        #     for row in result:\n        #        # get the source Link\n        #        # data.append()\n        # *********************************************************************\n\n        # and then delete the original table\n        op.drop_table(\"Versions\")\n        # and rename the new table to the old one\n        op.rename_table(\"Versions_New\", \"Versions\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\"Versions\", sa.Column(\"source_file_id\", sa.INTEGER(), nullable=True))\n    op.create_foreign_key(None, \"Versions\", \"Entities\", [\"id\"], [\"id\"])\n"
  },
  {
    "path": "alembic/versions/275bdc106fd5_added_ticket_summary.py",
    "content": "\"\"\"Added \"Ticket.summary\".\n\nRevision ID: 275bdc106fd5\nRevises: 130a7697cd79\nCreate Date: 2013-08-07 00:19:39.414232\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"275bdc106fd5\"\ndown_revision = \"130a7697cd79\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"Tickets\", sa.Column(\"summary\", sa.String(), nullable=True))\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_column(\"Tickets\", \"summary\")\n"
  },
  {
    "path": "alembic/versions/2aeab8b376dc_fg_color_bg_color.py",
    "content": "\"\"\"Remove Statuses.bg_color and Statuses.fg_color columns.\n\nRevision ID: 2aeab8b376dc\nRevises: 5168cc8552a3\nCreate Date: 2013-11-18 23:44:49.428028\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"2aeab8b376dc\"\ndown_revision = \"5168cc8552a3\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.drop_column(\"Statuses\", \"bg_color\")\n    op.drop_column(\"Statuses\", \"fg_color\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\"Statuses\", sa.Column(\"fg_color\", sa.INTEGER(), nullable=True))\n    op.add_column(\"Statuses\", sa.Column(\"bg_color\", sa.INTEGER(), nullable=True))\n"
  },
  {
    "path": "alembic/versions/2e4a3813ae76_created_daily_class.py",
    "content": "\"\"\"Created Daily class and the \"Daily Statuses\" status list and the status Open.\n\nRevision ID: 2e4a3813ae76\nRevises: 23dff41c95ff\nCreate Date: 2014-06-23 17:14:33.013543\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\nimport stalker\n\n\n# revision identifiers, used by Alembic.\nrevision = \"2e4a3813ae76\"\ndown_revision = \"23dff41c95ff\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"Dailies\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.Column(\"status_id\", sa.Integer(), nullable=False),\n        sa.Column(\"status_list_id\", sa.Integer(), nullable=False),\n        sa.Column(\"project_id\", sa.Integer(), nullable=True),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"project_id\"], [\"Projects.id\"], name=\"project_x_id\", use_alter=True\n        ),\n        sa.ForeignKeyConstraint(\n            [\"status_id\"],\n            [\"Statuses.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"status_list_id\"],\n            [\"StatusLists.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n\n    op.create_table(\n        \"Daily_Links\",\n        sa.Column(\"daily_id\", sa.Integer(), nullable=False),\n        sa.Column(\"link_id\", sa.Integer(), nullable=False),\n        sa.Column(\"rank\", sa.Integer(), nullable=True),\n        sa.ForeignKeyConstraint(\n            [\"daily_id\"],\n            [\"Dailies.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"link_id\"],\n            [\"Links.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"daily_id\", \"link_id\"),\n    )\n\n    # create new Statuses\n    #\n    # 'Open', 'OPEN',\n\n    def create_status(name, code):\n        # Insert in to SimpleEntities\n        op.execute(\n            f\"\"\"INSERT INTO \"SimpleEntities\" (entity_type, name, description,\n            created_by_id, updated_by_id, date_created, date_updated, type_id,\n            thumbnail_id, html_style, html_class, stalker_version)\n            VALUES ('Status', '{name}', '', NULL, NULL,\n            (SELECT CAST(NOW() at time zone 'utc' AS timestamp)),\n            (SELECT CAST(NOW() at time zone 'utc' AS timestamp)),\n            NULL,\n            NULL,\n            '', '', '{stalker.__version__}')\"\"\"\n        )\n\n        # insert in to Entities and Statuses\n        op.execute(\n            f\"\"\"INSERT INTO \"Entities\" (id)\n            VALUES ((\n              SELECT id\n              FROM \"SimpleEntities\"\n              WHERE \"SimpleEntities\".name = '{name}'\n            ));\n            INSERT INTO \"Statuses\" (id, code)\n            VALUES ((\n              SELECT id\n              FROM \"SimpleEntities\"\n              WHERE \"SimpleEntities\".name = '{name}'), '{code}');\"\"\"\n        )\n\n    create_status(\"Open\", \"OPEN\")\n\n    # Create Review StatusList\n    # Insert in to SimpleEntities\n    op.execute(\n        f\"\"\"INSERT INTO \"SimpleEntities\" (entity_type, name, description,\n        created_by_id, updated_by_id, date_created, date_updated, type_id,\n        thumbnail_id, html_style, html_class, stalker_version)\n        VALUES ('StatusList', 'Daily Statuses', '', NULL, NULL,\n        (SELECT CAST(NOW() at time zone 'utc' AS timestamp)),\n        (SELECT CAST(NOW() at time zone 'utc' AS timestamp)), NULL, NULL,\n        '', '', '{stalker.__version__}')\"\"\"\n    )\n\n    # insert in to Entities and StatusLists\n    op.execute(\n        \"\"\"INSERT INTO \"Entities\" (id)\n        VALUES ((\n            SELECT id\n            FROM \"SimpleEntities\"\n            WHERE \"SimpleEntities\".name = 'Daily Statuses'\n        ));\n        INSERT INTO \"StatusLists\" (id, target_entity_type)\n        VALUES ((\n            SELECT id\n            FROM \"SimpleEntities\"\n            WHERE \"SimpleEntities\".name = 'Daily Statuses'), 'Daily');\"\"\"\n    )\n\n    # Add Review Statues To StatusList_Statuses\n    # Add new Task statuses to StatusList\n    op.execute(\n        \"\"\"INSERT INTO \"StatusList_Statuses\" (status_list_id, status_id)\n        VALUES\n            ((SELECT id FROM \"StatusLists\" WHERE target_entity_type = 'Daily'),\n            (SELECT id FROM \"Statuses\" WHERE code = 'OPEN')),\n            ((SELECT id FROM \"StatusLists\" WHERE target_entity_type = 'Daily'),\n            (SELECT id FROM \"Statuses\" WHERE code = 'CLS'))\n        \"\"\"\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_table(\"Daily_Links\")\n    op.drop_table(\"Dailies\")\n\n    # Delete Open Status\n    op.execute(\n        \"\"\"DELETE FROM \"StatusList_Statuses\" WHERE\n    status_id IN (select id FROM \"SimpleEntities\" WHERE name = 'Open');\n    DELETE FROM \"Statuses\" WHERE\n        id IN (select id FROM \"SimpleEntities\" WHERE name = 'Open');\n    DELETE FROM \"Entities\" WHERE\n        id IN (select id FROM \"SimpleEntities\" WHERE name = 'Open');\n    DELETE FROM \"SimpleEntities\" WHERE name = 'Open';\n    \"\"\"\n    )\n\n    # Delete Daily Statuses\n    op.execute(\n        \"\"\"\n    DELETE FROM \"StatusList_Statuses\"\n    WHERE status_list_id=(SELECT id FROM \"SimpleEntities\" WHERE name='Daily Statuses');\n    DELETE FROM \"StatusLists\"\n    WHERE id=(SELECT id FROM \"SimpleEntities\" WHERE name='Daily Statuses');\n    DELETE FROM \"Entities\"\n    WHERE id=(SELECT id FROM \"SimpleEntities\" WHERE name='Daily Statuses');\n    DELETE FROM \"SimpleEntities\" WHERE\n    name = 'Daily Statuses';\n    \"\"\"\n    )\n"
  },
  {
    "path": "alembic/versions/2f55dc4f199f_wiki_page.py",
    "content": "\"\"\"Add Wiki Page.\n\nRevision ID: 2f55dc4f199f\nRevises: 433d9caaafab\nCreate Date: 2014-03-24 16:52:45.127579\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"2f55dc4f199f\"\ndown_revision = \"433d9caaafab\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"Pages\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.Column(\"title\", sa.String(), nullable=True),\n        sa.Column(\"content\", sa.String(), nullable=True),\n        sa.Column(\"project_id\", sa.Integer(), nullable=True),\n        sa.ForeignKeyConstraint([\"id\"], [\"Entities.id\"]),\n        sa.ForeignKeyConstraint(\n            [\"project_id\"], [\"Projects.id\"], name=\"project_x_id\", use_alter=True\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_table(\"Pages\")\n"
  },
  {
    "path": "alembic/versions/30c576f3691_budget_and_budget_entry.py",
    "content": "\"\"\"Added Budget and BudgetEntry tables.\n\nRevision ID: 30c576f3691\nRevises: 409d2d73ca30\nCreate Date: 2014-11-20 22:49:37.015323\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"30c576f3691\"\ndown_revision = \"409d2d73ca30\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"Budgets\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.Column(\"project_id\", sa.Integer(), nullable=True),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"project_id\"],\n            [\"Projects.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n    op.create_table(\n        \"BudgetEntries\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.Column(\"budget_id\", sa.Integer(), nullable=True),\n        sa.Column(\"amount\", sa.Float(), nullable=True),\n        sa.ForeignKeyConstraint(\n            [\"budget_id\"],\n            [\"Budgets.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_table(\"BudgetEntries\")\n    op.drop_table(\"Budgets\")\n"
  },
  {
    "path": "alembic/versions/31b1e22b455e_added_exclude_and_check_constraints_to_.py",
    "content": "\"\"\"Added exclude and check constraints to TimeLogs table.\n\nRevision ID: 31b1e22b455e\nRevises: c5607b4cfb0a\nCreate Date: 2017-03-10 02:14:38.330000\n\"\"\"\n\nfrom alembic import op\n\nfrom sqlalchemy import CheckConstraint\n\nfrom stalker import log\n\n# revision identifiers, used by Alembic.\nrevision = \"31b1e22b455e\"\ndown_revision = \"c5607b4cfb0a\"\nlogger = log.get_logger(__name__)\n\n\ndef upgrade():\n    \"\"\"Add CheckConstraint and an ExcludeConstraint for the TimeLogs table.\"\"\"\n    # First cleanup TimeLogs table\n    logger.info(\"Removing duplicate TimeLog entries\")\n    op.execute(\n        \"\"\"-- first remove direct duplicates\n        with cte as (\n            select\n                row_number() over (partition by resource_id, start) as rn,\n                id,\n                start,\n                \"end\",\n                resource_id\n            from \"TimeLogs\"\n            where exists (\n                select\n                    1\n                from \"TimeLogs\" as tlogs\n                where tlogs.start <= \"TimeLogs\".start\n                    and \"TimeLogs\".start < tlogs.end\n                    and tlogs.id != \"TimeLogs\".id\n                    and tlogs.resource_id = \"TimeLogs\".resource_id\n            )\n            order by start\n        ) delete from \"TimeLogs\"\n        where \"TimeLogs\".id in (select id from cte where rn > 1);\"\"\"\n    )\n\n    logger.info(\n        \"Removing contained TimeLog entries (TimeLogs surrounded by other \" \"TimeLogs\"\n    )\n    op.execute(\n        \"\"\"-- remove any contained (TimeLogs surrounded by other TimeLogs) TimeLogs\n        with cte as (\n            select\n                \"TimeLogs\".id,\n                \"TimeLogs\".start,\n                \"TimeLogs\".end,\n                \"TimeLogs\".resource_id\n            from \"TimeLogs\"\n            join \"TimeLogs\" as tlogs on\n                \"TimeLogs\".start > tlogs.start and \"TimeLogs\".start < tlogs.end\n                and \"TimeLogs\".end > tlogs.start and \"TimeLogs\".end < tlogs.end\n                and \"TimeLogs\".resource_id = tlogs.resource_id\n        ) delete from \"TimeLogs\"\n        where \"TimeLogs\".id in (select id from cte);\"\"\"\n    )\n\n    logger.info(\"Trimming residual overlapping TimeLog.end values\")\n    op.execute(\n        \"\"\"-- then trim the end dates of the TimeLogs that are still overlapping with others\n        update \"TimeLogs\"\n        set \"end\" = (\n            select\n                tlogs.start\n            from \"TimeLogs\" as tlogs\n            where \"TimeLogs\".start < tlogs.start and \"TimeLogs\".end > tlogs.start\n                and \"TimeLogs\".resource_id = tlogs.resource_id\n            limit 1\n        )\n        where \"TimeLogs\".end - \"TimeLogs\".start > interval '10 min'\n            and exists(\n                select\n                    1\n                from \"TimeLogs\" as tlogs\n                where \"TimeLogs\".start < tlogs.start and \"TimeLogs\".end > tlogs.start\n                    and \"TimeLogs\".resource_id = tlogs.resource_id\n            );\n        \"\"\"\n    )\n\n    logger.info(\"Trimming residual overlapping TimeLog.start values\")\n    op.execute(\n        \"\"\"-- then trim the start dates of the TimeLogs that are still overlapping with\n        -- others (there may be 10 min TimeLogs left in the previous query)\n        update \"TimeLogs\"\n        set start = (\n            select\n                tlogs.end\n            from \"TimeLogs\" as tlogs\n            where \"TimeLogs\".start > tlogs.start and \"TimeLogs\".start < tlogs.end\n                and \"TimeLogs\".resource_id = tlogs.resource_id\n            limit 1\n        )\n        where \"TimeLogs\".end - \"TimeLogs\".start > interval '10 min'\n            and exists(\n                select\n                    1\n                from \"TimeLogs\" as tlogs\n                where \"TimeLogs\".start > tlogs.start and \"TimeLogs\".start < tlogs.end\n                    and \"TimeLogs\".resource_id = tlogs.resource_id\n                limit 1\n            );\n        \"\"\"\n    )\n\n    logger.info(\"Adding CheckConstraint(end > start) to TimeLogs table\")\n    with op.batch_alter_table(\n        \"TimeLogs\", table_args=(CheckConstraint('\"end\" > start'))\n    ):\n        logger.info(\"Adding ExcludeConstraint to TimeLogs table\")\n    from stalker.models.task import TimeLog, add_exclude_constraint\n\n    conn = op.get_bind()\n    add_exclude_constraint(TimeLog.__table__, conn)\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # Drop ExcludeConstraint and functions\n    # Sadly we can not reintroduce the deleted data in the upgrade() function\n    logger.info(\"Dropping CheckConstraint(end > start)\")\n    op.execute(\"\"\"ALTER TABLE \"TimeLogs\" DROP CONSTRAINT IF EXISTS TimeLogs_check;\"\"\")\n\n    logger.info('Dropping \"TimeLogs\".overlapping_time_logs function')\n    op.execute(\n        \"\"\"ALTER TABLE \"TimeLogs\" DROP CONSTRAINT IF EXISTS overlapping_time_logs;\"\"\"\n    )\n\n    logger.info(\"Dropping ts_to_box function\")\n    op.execute(\n        \"DROP FUNCTION IF EXISTS \"\n        \"ts_to_box(timestamp with time zone, timestamp with time zone);\"\n    )\n\n    logger.info(\"Dropping btree_gist extension\")\n    op.execute(\"\"\"DROP EXTENSION IF EXISTS btree_gist;\"\"\")\n"
  },
  {
    "path": "alembic/versions/39d3c16ff005_budget_entries_good_id.py",
    "content": "\"\"\"Added BudgetEntries.good_id.\n\nRevision ID: 39d3c16ff005\nRevises: eaed49db6d9\nCreate Date: 2015-02-15 02:29:26.301437\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"39d3c16ff005\"\ndown_revision = \"eaed49db6d9\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    with op.batch_alter_table(\"BudgetEntries\", schema=None) as batch_op:\n        batch_op.add_column(sa.Column(\"good_id\", sa.Integer(), nullable=True))\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    with op.batch_alter_table(\"BudgetEntries\", schema=None) as batch_op:\n        batch_op.drop_column(\"good_id\")\n"
  },
  {
    "path": "alembic/versions/3be540ad3a93_added_version_revision_number_attribute.py",
    "content": "\"\"\"Added Version.revision_number attribute\n\nRevision ID: 3be540ad3a93\nRevises: 1875136a2bfc\nCreate Date: 2024-12-04 17:04:37.174269\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"3be540ad3a93\"\ndown_revision = \"1875136a2bfc\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.execute(\n        \"\"\"ALTER TABLE \"Versions\" ADD revision_number integer NOT NULL DEFAULT 1;\"\"\"\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # because we are removing the revision_number column,\n    # add the 1000 * (revision_number - 1) to all the version numbers\n    # to preserve the version sequences, intact...\n    op.execute(\n        \"\"\"UPDATE \"Versions\" SET version_number = (1000 * (revision_number - 1) + version_number);\"\"\"\n    )\n    op.execute(\"\"\"ALTER TABLE \"Versions\" DROP COLUMN revision_number;\"\"\")\n"
  },
  {
    "path": "alembic/versions/409d2d73ca30_user_rate.py",
    "content": "\"\"\"Added \"Users.rate\".\n\nRevision ID: 409d2d73ca30\nRevises: 5814290f49c7\nCreate Date: 2014-11-20 22:47:56.013644\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"409d2d73ca30\"\ndown_revision = \"5814290f49c7\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"Users\", sa.Column(\"rate\", sa.Float(), nullable=True))\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_column(\"Users\", \"rate\")\n"
  },
  {
    "path": "alembic/versions/433d9caaafab_task_review_status_workflow.py",
    "content": "\"\"\"Task review/status workflow.\n\nRevision ID: 433d9caaafab\nRevises: 46775e4a3d96\nCreate Date: 2014-01-31 01:51:08.457109\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\nfrom sqlalchemy.dialects import postgresql\n\nimport stalker\n\n# revision identifiers, used by Alembic.\nrevision = \"433d9caaafab\"\ndown_revision = \"46775e4a3d96\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # Enum Types\n    time_unit_enum = postgresql.ENUM(\n        \"min\", \"h\", \"d\", \"w\", \"m\", \"y\", name=\"TimeUnit\", create_type=False\n    )\n    review_schedule_model_enum = postgresql.ENUM(\n        \"effort\", \"length\", \"duration\", name=\"ReviewScheduleModel\", create_type=False\n    )\n\n    task_dependency_target_enum = postgresql.ENUM(\n        \"onend\", \"onstart\", name=\"TaskDependencyTarget\", create_type=False\n    )\n\n    task_dependency_gap_model = postgresql.ENUM(\n        \"length\", \"duration\", name=\"TaskDependencyGapModel\", create_type=False\n    )\n\n    resource_allocation_strategy_enum = postgresql.ENUM(\n        \"minallocated\",\n        \"maxloaded\",\n        \"minloaded\",\n        \"order\",\n        \"random\",\n        name=\"ResourceAllocationStrategy\",\n        create_type=False,\n    )\n\n    # Reviews\n    op.create_table(\n        \"Reviews\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.Column(\"task_id\", sa.Integer(), nullable=False),\n        sa.Column(\"reviewer_id\", sa.Integer(), nullable=False),\n        sa.Column(\"review_number\", sa.Integer(), nullable=True),\n        sa.Column(\"schedule_timing\", sa.Float(), nullable=True),\n        sa.Column(\"schedule_unit\", time_unit_enum, nullable=False),\n        sa.Column(\"schedule_constraint\", sa.Integer(), nullable=False),\n        sa.Column(\"schedule_model\", review_schedule_model_enum, nullable=False),\n        sa.Column(\"status_id\", sa.Integer(), nullable=False),\n        sa.Column(\"status_list_id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"SimpleEntities.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"reviewer_id\"],\n            [\"Users.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"status_id\"],\n            [\"Statuses.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"status_list_id\"],\n            [\"StatusLists.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"task_id\"],\n            [\"Tasks.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n\n    # Task_Responsible\n    op.create_table(\n        \"Task_Responsible\",\n        sa.Column(\"task_id\", sa.Integer(), nullable=False),\n        sa.Column(\"responsible_id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint([\"responsible_id\"], [\"Users.id\"]),\n        sa.ForeignKeyConstraint([\"task_id\"], [\"Tasks.id\"]),\n        sa.PrimaryKeyConstraint(\"task_id\", \"responsible_id\"),\n    )\n\n    # Task_Alternative_Resources\n    op.create_table(\n        \"Task_Alternative_Resources\",\n        sa.Column(\"task_id\", sa.Integer(), nullable=False),\n        sa.Column(\"resource_id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"resource_id\"],\n            [\"Users.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"task_id\"],\n            [\"Tasks.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"task_id\", \"resource_id\"),\n    )\n\n    # Task Computed Resources\n    op.create_table(\n        \"Task_Computed_Resources\",\n        sa.Column(\"task_id\", sa.Integer(), nullable=False),\n        sa.Column(\"resource_id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"resource_id\"],\n            [\"Users.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"task_id\"],\n            [\"Tasks.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"task_id\", \"resource_id\"),\n    )\n\n    # EntityTypes\n    op.add_column(\"EntityTypes\", sa.Column(\"dateable\", sa.Boolean(), nullable=True))\n\n    # Projects\n    op.drop_column(\"Projects\", \"timing_resolution\")\n\n    # Studios\n    op.add_column(\"Studios\", sa.Column(\"is_scheduling\", sa.Boolean(), nullable=True))\n    op.add_column(\n        \"Studios\", sa.Column(\"is_scheduling_by_id\", sa.Integer(), nullable=True)\n    )\n    op.add_column(\n        \"Studios\", sa.Column(\"last_schedule_message\", sa.PickleType(), nullable=True)\n    )\n    op.add_column(\n        \"Studios\", sa.Column(\"last_scheduled_at\", sa.DateTime(), nullable=True)\n    )\n    op.add_column(\n        \"Studios\", sa.Column(\"last_scheduled_by_id\", sa.Integer(), nullable=True)\n    )\n    op.add_column(\n        \"Studios\", sa.Column(\"scheduling_started_at\", sa.DateTime(), nullable=True)\n    )\n    op.drop_column(\"Studios\", \"daily_working_hours\")\n\n    # Task Dependencies\n\n    # *************************************************************************\n    # dependency_target - onend by default\n    op.add_column(\n        \"Task_Dependencies\",\n        sa.Column(\"dependency_target\", task_dependency_target_enum, nullable=True),\n    )\n    # fill data\n    op.execute(\"\"\"UPDATE \"Task_Dependencies\" SET dependency_target = 'onend'\"\"\")\n\n    # alter column to be nullable false\n    op.alter_column(\n        \"Task_Dependencies\", \"dependency_target\", existing_nullable=True, nullable=False\n    )\n    # *************************************************************************\n\n    op.alter_column(\n        \"Task_Dependencies\", \"depends_to_task_id\", new_column_name=\"depends_to_id\"\n    )\n\n    # *************************************************************************\n    # gap_constraint column - 0 by default\n    op.add_column(\n        \"Task_Dependencies\", sa.Column(\"gap_constraint\", sa.Integer(), nullable=True)\n    )\n    # fill data\n    op.execute(\"\"\"UPDATE \"Task_Dependencies\" SET gap_constraint = 0 \"\"\")\n\n    # alter column to be nullable false\n    op.alter_column(\n        \"Task_Dependencies\", \"gap_constraint\", existing_nullable=True, nullable=False\n    )\n    # *************************************************************************\n\n    # *************************************************************************\n    # gap_model - length by default\n    op.add_column(\n        \"Task_Dependencies\",\n        sa.Column(\"gap_model\", task_dependency_gap_model, nullable=True),\n    )\n    # fill data\n    op.execute(\"\"\"UPDATE \"Task_Dependencies\" SET gap_model = 'length'\"\"\")\n\n    # alter column to be nullable false\n    op.alter_column(\n        \"Task_Dependencies\", \"gap_model\", existing_nullable=True, nullable=False\n    )\n    # *************************************************************************\n\n    # *************************************************************************\n    # gap_timing - 0 by default\n    op.add_column(\n        \"Task_Dependencies\", sa.Column(\"gap_timing\", sa.Float(), nullable=True)\n    )\n    op.add_column(\n        \"Task_Dependencies\", sa.Column(\"gap_unit\", time_unit_enum, nullable=True)\n    )\n    # fill data\n    op.execute(\"\"\"UPDATE \"Task_Dependencies\" SET gap_timing = 0\"\"\")\n\n    # alter column to be nullable false\n    op.alter_column(\n        \"Task_Dependencies\", \"gap_timing\", existing_nullable=True, nullable=False\n    )\n    # *************************************************************************\n\n    # Tasks\n    op.add_column(\"Tasks\", sa.Column(\"review_number\", sa.Integer(), nullable=True))\n\n    # *************************************************************************\n    # allocation_strategy - minallocated by default\n    op.add_column(\n        \"Tasks\",\n        sa.Column(\n            \"allocation_strategy\", resource_allocation_strategy_enum, nullable=True\n        ),\n    )\n    # fill data\n    op.execute(\"\"\"UPDATE \"Tasks\" SET allocation_strategy = 'minallocated'\"\"\")\n\n    # alter column to be nullable false\n    op.alter_column(\n        \"Tasks\", \"allocation_strategy\", existing_nullable=True, nullable=False\n    )\n    # *************************************************************************\n\n    # *************************************************************************\n    # persistent_allocation - True by default\n    op.add_column(\n        \"Tasks\", sa.Column(\"persistent_allocation\", sa.Boolean(), nullable=True)\n    )\n    # fill data\n    op.execute(\"\"\"UPDATE \"Tasks\" SET persistent_allocation = TRUE\"\"\")\n\n    # alter column to be nullable false\n    op.alter_column(\n        \"Tasks\", \"persistent_allocation\", existing_nullable=True, nullable=False\n    )\n    # *************************************************************************\n\n    op.drop_column(\"Tasks\", \"timing_resolution\")\n\n    op.drop_column(\"TimeLogs\", \"timing_resolution\")\n    op.create_unique_constraint(None, \"Users\", [\"login\"])\n\n    op.drop_column(\"Vacations\", \"timing_resolution\")\n\n    # before dropping responsible_id column from the Tasks table\n    # move the data to the Task_Responsible table\n    op.execute(\n        'insert into \"Task_Responsible\" '\n        \"   select id, responsible_id \"\n        '   from \"Tasks\" where responsible_id is not NULL'\n    )\n\n    # now drop the data\n    op.drop_column(\"Tasks\", \"responsible_id\")\n\n    # create new Statuses\n    #\n    # 'Waiting For Dependency', 'WFD',\n    # 'Dependency Has Revision','DREV',\n    # 'On Hold',                'OH',\n    # 'Stopped',                'STOP',\n\n    def create_status(name, code):\n        # Insert in to SimpleEntities\n        op.execute(\n            f\"\"\"INSERT INTO \"SimpleEntities\" (entity_type, name, description,\n            created_by_id, updated_by_id, date_created, date_updated, type_id,\n            thumbnail_id, html_style, html_class, stalker_version)\n            VALUES ('Status', '{name}', '', NULL, NULL,\n            (SELECT CAST(NOW() at time zone 'utc' AS timestamp)),\n            (SELECT CAST(NOW() at time zone 'utc' AS timestamp)),\n            NULL,\n            NULL,\n            '', '', '{stalker.__version__}')\"\"\"\n        )\n\n        # insert in to Entities and Statuses\n        op.execute(\n            f\"\"\"INSERT INTO \"Entities\" (id)\n            VALUES ((\n              SELECT id\n              FROM \"SimpleEntities\"\n              WHERE \"SimpleEntities\".name = '{name}'\n            ));\n            INSERT INTO \"Statuses\" (id, code)\n            VALUES ((\n              SELECT id\n              FROM \"SimpleEntities\"\n              WHERE \"SimpleEntities\".name = '{name}'), '{code}');\"\"\"\n        )\n\n    create_status(\"Waiting For Dependency\", \"WFD\")\n    create_status(\"Dependency Has Revision\", \"DREV\")\n    create_status(\"On Hold\", \"OH\")\n    create_status(\"Stopped\", \"STOP\")\n\n    # Review Statuses\n    create_status(\"Requested Revision\", \"RREV\")\n    create_status(\"Approved\", \"APP\")\n\n    # Add new Task statuses to StatusList\n    def update_status_lists(entity_type, status_code):\n        op.execute(\n            f\"\"\"\n            CREATE OR REPLACE FUNCTION\n                add_status_to_status_list(status_list_id INT, status_id INT)\n                    RETURNS VOID AS $$\n            BEGIN\n                INSERT INTO \"StatusList_Statuses\" (status_list_id, status_id)\n                VALUES (status_list_id, status_id);\n            EXCEPTION WHEN OTHERS THEN\n                -- do nothning\n            END;\n            $$\n            LANGUAGE 'plpgsql';\n\n            select NULL from add_status_to_status_list(\n            (SELECT id FROM \"StatusLists\" WHERE target_entity_type = '{entity_type}'),\n            (SELECT id FROM \"Statuses\" WHERE code = '{status_code}')\n        );\"\"\"\n        )\n\n    # Task\n    for t in [\"Task\", \"Asset\", \"Shot\", \"Sequence\"]:\n        for s in [\"WFD\", \"RTS\", \"WIP\", \"OH\", \"STOP\", \"PREV\", \"HREV\", \"DREV\", \"CMPL\"]:\n            update_status_lists(t, s)\n\n    # drop function\n    op.execute(\"drop function add_status_to_status_list(integer, integer);\")\n\n    # Remove NEW from Task, Asset, Shot and Sequence StatusList\n    op.execute(\n        \"\"\"DELETE FROM \"StatusList_Statuses\"\nWHERE status_list_id = (SELECT id FROM \"StatusLists\"\n  WHERE target_entity_type = 'Task')\nAND status_id = (SELECT id FROM \"Statuses\" WHERE \"Statuses\".code = 'NEW')\n\"\"\"\n    )\n    op.execute(\n        \"\"\"DELETE FROM \"StatusList_Statuses\"\nWHERE status_list_id = (SELECT id FROM \"StatusLists\"\nWHERE target_entity_type = 'Asset')\nAND status_id = (SELECT id FROM \"Statuses\" WHERE \"Statuses\".code = 'NEW')\n\"\"\"\n    )\n    op.execute(\n        \"\"\"DELETE FROM \"StatusList_Statuses\"\nWHERE status_list_id = (SELECT id FROM \"StatusLists\"\nWHERE target_entity_type = 'Shot')\nAND status_id = (SELECT id FROM \"Statuses\" WHERE \"Statuses\".code = 'NEW')\n\"\"\"\n    )\n    op.execute(\n        \"\"\"DELETE FROM \"StatusList_Statuses\"\nWHERE status_list_id = (SELECT id FROM \"StatusLists\"\nWHERE target_entity_type = 'Sequence')\nAND status_id = (SELECT id FROM \"Statuses\" WHERE \"Statuses\".code = 'NEW')\n\"\"\"\n    )\n\n    # Create Review StatusList\n    # Insert in to SimpleEntities\n    op.execute(\n        f\"\"\"INSERT INTO \"SimpleEntities\" (entity_type, name, description,\ncreated_by_id, updated_by_id, date_created, date_updated, type_id,\nthumbnail_id, html_style, html_class, stalker_version)\nVALUES ('StatusList', 'Review Status List', '', NULL, NULL,\n(SELECT CAST(NOW() at time zone 'utc' AS timestamp)),\n(SELECT CAST(NOW() at time zone 'utc' AS timestamp)), NULL, NULL,\n'', '', '{stalker.__version__}')\"\"\"\n    )\n\n    # insert in to Entities and StatusLists\n    op.execute(\n        \"\"\"INSERT INTO \"Entities\" (id)\nVALUES ((\n  SELECT id\n  FROM \"SimpleEntities\"\n  WHERE \"SimpleEntities\".name = 'Review Status List'\n));\nINSERT INTO \"StatusLists\" (id, target_entity_type)\nVALUES ((\n  SELECT id\n  FROM \"SimpleEntities\"\n  WHERE \"SimpleEntities\".name = 'Review Status List'), 'Review');\"\"\"\n    )\n\n    # Add Review Statues To StatusList_Statuses\n    # Add new Task statuses to StatusList\n    op.execute(\n        \"\"\"INSERT INTO \"StatusList_Statuses\" (status_list_id, status_id)\nVALUES\n    ((SELECT id FROM \"StatusLists\" WHERE target_entity_type = 'Review'),\n    (SELECT id FROM \"Statuses\" WHERE code = 'NEW')),\n    ((SELECT id FROM \"StatusLists\" WHERE target_entity_type = 'Review'),\n    (SELECT id FROM \"Statuses\" WHERE code = 'RREV')),\n    ((SELECT id FROM \"StatusLists\" WHERE target_entity_type = 'Review'),\n    (SELECT id FROM \"Statuses\" WHERE code = 'APP'))\n\"\"\"\n    )\n\n    # Update all NEW Tasks to WFD\n    op.execute(\n        \"\"\"update \"Tasks\"\nset status_id = (select id from \"Statuses\" where code='WFD')\nwhere status_id = (select id from \"Statuses\" where code='NEW')\"\"\"\n    )\n\n    # Update all PREV Tasks to WIP\n    op.execute(\n        \"\"\"update \"Tasks\"\nset status_id = (select id from \"Statuses\" where code='WIP')\nwhere status_id = (select id from \"Statuses\" where code='PREV')\"\"\"\n    )\n\n    # delete any other status from Task, Asset, Shot and Sequence Status Lists\n    map(\n        lambda x: op.execute(\n            \"\"\"DELETE FROM \"StatusList_Statuses\"\n        WHERE status_list_id=(\n          SELECT id\n          FROM \"StatusLists\"\n          WHERE target_entity_type='{}')\n          AND status_id in (\n            SELECT id\n            FROM \"Statuses\"\n            WHERE code NOT IN\n            ('WFD', 'RTS', 'WIP', 'OH', 'STOP', 'PREV', 'HREV', 'DREV', 'CMPL')\n        );\"\"\".format(\n                x\n            )\n        ),\n        [\"Task\", \"Asset\", \"Shot\", \"Sequence\"],\n    )\n\n    # Update Tasks.review_number to 0 for all tasks\n    op.execute(\"\"\"update \"Tasks\" set review_number = 0\"\"\")\n\n    # Shots._cut_in -> Shots.cut_in\n    op.alter_column(\"Shots\", \"_cut_in\", new_column_name=\"cut_in\")\n\n    # Shots._cut_out -> Shots.cut_out\n    op.alter_column(\"Shots\", \"_cut_out\", new_column_name=\"cut_out\")\n\n    # Tasks._schedule_seconds -> Tasks.schedule_seconds\n    op.alter_column(\"Tasks\", \"_schedule_seconds\", new_column_name=\"schedule_seconds\")\n\n    # Tasks._total_logged_seconds -> Tasks.total_logged_seconds\n    op.alter_column(\n        \"Tasks\", \"_total_logged_seconds\", new_column_name=\"total_logged_seconds\"\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\n        \"Vacations\",\n        sa.Column(\"timing_resolution\", postgresql.INTERVAL(), nullable=True),\n    )\n    # op.drop_constraint(None, 'Users')\n    op.add_column(\n        \"TimeLogs\", sa.Column(\"timing_resolution\", postgresql.INTERVAL(), nullable=True)\n    )\n    op.add_column(\"Tasks\", sa.Column(\"responsible_id\", sa.INTEGER(), nullable=True))\n\n    # restore data\n    op.execute(\n        \"\"\"\n        UPDATE\n           \"Tasks\"\n        SET\n            responsible_id = t2.responsible_id\n        FROM (\n            SELECT task_id, responsible_id\n            FROM \"Task_Responsible\"\n        ) as t2\n        WHERE \"Tasks\".id = t2.task_id\n    \"\"\"\n    )\n\n    op.add_column(\n        \"Tasks\", sa.Column(\"timing_resolution\", postgresql.INTERVAL(), nullable=True)\n    )\n    op.drop_column(\"Tasks\", \"persistent_allocation\")\n    op.drop_column(\"Tasks\", \"allocation_strategy\")\n    op.drop_column(\"Tasks\", \"review_number\")\n    op.alter_column(\n        \"Task_Dependencies\", \"depends_to_id\", new_column_name=\"depends_to_task_id\"\n    )\n    op.drop_column(\"Task_Dependencies\", \"gap_unit\")\n    op.drop_column(\"Task_Dependencies\", \"gap_timing\")\n    op.drop_column(\"Task_Dependencies\", \"gap_model\")\n    op.drop_column(\"Task_Dependencies\", \"gap_constraint\")\n    op.drop_column(\"Task_Dependencies\", \"dependency_target\")\n    op.add_column(\n        \"Studios\", sa.Column(\"daily_working_hours\", sa.INTEGER(), nullable=True)\n    )\n    op.drop_column(\"Studios\", \"scheduling_started_at\")\n    op.drop_column(\"Studios\", \"last_scheduled_by_id\")\n    op.drop_column(\"Studios\", \"last_scheduled_at\")\n    op.drop_column(\"Studios\", \"last_schedule_message\")\n    op.drop_column(\"Studios\", \"is_scheduling_by_id\")\n    op.drop_column(\"Studios\", \"is_scheduling\")\n    op.add_column(\n        \"Projects\", sa.Column(\"timing_resolution\", postgresql.INTERVAL(), nullable=True)\n    )\n    op.drop_column(\"EntityTypes\", \"dateable\")\n    op.drop_table(\"Task_Alternative_Resources\")\n    op.drop_table(\"Task_Computed_Resources\")\n    op.drop_table(\"Reviews\")\n    # will loose all the responsible data, change if you care!\n    op.drop_table(\"Task_Responsible\")\n\n    # Update all WFD Tasks to NEW\n    op.execute(\n        \"\"\"update \"Tasks\"\n        set status_id = (select id from \"Statuses\" where code='NEW')\n        where status_id = (select id from \"Statuses\" where code='WFD')\n        \"\"\"\n    )\n\n    # Update all OH Tasks to WIP\n    op.execute(\n        \"\"\"update \"Tasks\"\n        set status_id = (select id from \"Statuses\" where code='WIP')\n        where status_id = (select id from \"Statuses\" where code='OH')\n        \"\"\"\n    )\n\n    # Update all STOP or DREV Tasks to CMPL\n    op.execute(\n        \"\"\"update \"Tasks\"\n        set status_id = (select id from \"Statuses\" where code='WIP')\n        where status_id in (select id from \"Statuses\" where code in ('STOP', 'DREV'))\n        \"\"\"\n    )\n\n    op.execute(\n        \"\"\"update \"Tasks\"\n        set status_id = (select id from \"Statuses\" where code='WIP')\n        where status_id = (select id from \"Statuses\" where code='STOP')\n        \"\"\"\n    )\n\n    # Delete Statuses\n    op.execute(\n        \"\"\"DELETE FROM \"StatusList_Statuses\" WHERE\n    status_id IN (\n    select id FROM \"SimpleEntities\" WHERE\n    name IN ('Waiting For Dependency', 'Dependency Has Revision', 'On Hold',\n    'Stopped', 'Requested Revision', 'Approved'));\n    DELETE FROM \"Statuses\" WHERE\n      id IN (select id FROM \"SimpleEntities\" WHERE\n    name IN ('Waiting For Dependency', 'Dependency Has Revision', 'On Hold',\n    'Stopped', 'Requested Revision', 'Approved'));\n    DELETE FROM \"Entities\" WHERE\n    id IN (select id FROM \"SimpleEntities\" WHERE\n    name IN ('Waiting For Dependency', 'Dependency Has Revision', 'On Hold',\n    'Stopped', 'Requested Revision', 'Approved'));\n    DELETE FROM \"SimpleEntities\" WHERE\n    name IN ('Waiting For Dependency', 'Dependency Has Revision', 'On Hold',\n    'Stopped', 'Requested Revision', 'Approved');\n    \"\"\"\n    )\n\n    # Delete Review Status List\n    op.execute(\n        \"\"\"\n    DELETE FROM \"StatusList_Statuses\"\n    WHERE status_list_id=(\n        SELECT id FROM \"SimpleEntities\"\n        WHERE name='Review Status List'\n    );\n    DELETE FROM \"StatusLists\"\n    WHERE id=(SELECT id FROM \"SimpleEntities\" WHERE name='Review Status List');\n    DELETE FROM \"Entities\"\n    WHERE id=(SELECT id FROM \"SimpleEntities\" WHERE name='Review Status List');\n    DELETE FROM \"SimpleEntities\" WHERE\n    name = 'Review Status List';\n    \"\"\"\n    )\n\n    # column name changes\n    # Shots._cut_in -> Shots.cut_in\n    op.alter_column(\"Shots\", \"cut_in\", new_column_name=\"_cut_in\")\n\n    # Shots._cut_out -> Shots.cut_out\n    op.alter_column(\"Shots\", \"cut_out\", new_column_name=\"_cut_out\")\n\n    # Tasks._schedule_seconds -> Tasks.schedule_seconds\n    op.alter_column(\"Tasks\", \"schedule_seconds\", new_column_name=\"_schedule_seconds\")\n\n    # Tasks._total_logged_seconds -> Tasks.total_logged_seconds\n    op.alter_column(\n        \"Tasks\", \"total_logged_seconds\", new_column_name=\"_total_logged_seconds\"\n    )\n"
  },
  {
    "path": "alembic/versions/4400871fa852_scene_is_now_deriving_from_task.py",
    "content": "\"\"\"Scene is now deriving from Task\n\nRevision ID: 4400871fa852\nRevises: ec1eb2151bb9\nCreate Date: 2024-11-15 13:16:53.885627\n\"\"\"\n\nfrom alembic import op\n\nimport stalker\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"4400871fa852\"\ndown_revision = \"ec1eb2151bb9\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # Update the Scenes.id to be a foreign key to Tasks.id\n    op.drop_constraint(\"Scenes_id_fkey\", \"Scenes\", type_=\"foreignkey\")\n    op.create_foreign_key(\"Scenes_id_fkey\", \"Scenes\", \"Tasks\", [\"id\"], [\"id\"])\n\n    # Create a StatusList for Scenes\n    # Create a SimpleEntity for the StatusList\n    op.execute(\n        \"\"\"\n    INSERT INTO \"SimpleEntities\" (\n        entity_type,\n        name,\n        description,\n        created_by_id,\n        updated_by_id,\n        date_created,\n        date_updated,\n        generic_text,\n        html_style,\n        html_class,\n        stalker_version\n    ) VALUES (\n        'StatusList',\n        'Scene Statuses',\n        '',\n        3,\n        3,\n        (SELECT CAST(NOW() at time zone 'utc' AS timestamp)),\n        (SELECT CAST(NOW() at time zone 'utc' AS timestamp)),\n        '',\n        '',\n        '',\n        '{stalker_version}'\n    )\"\"\".format(\n            stalker_version=stalker.__version__\n        )\n    )\n    # Insert the same data to the Entities\n    op.execute(\n        \"\"\"\n    INSERT INTO \"Entities\" (id) VALUES (\n        (SELECT \"SimpleEntities\".id FROM \"SimpleEntities\" WHERE \"SimpleEntities\".name = 'Scene Statuses')\n    )\n    \"\"\"\n    )\n    # Insert the same to the StatusLists\n    op.execute(\n        \"\"\"INSERT INTO \"StatusLists\" (id, target_entity_type) VALUES (\n        (SELECT \"SimpleEntities\".id FROM \"SimpleEntities\" WHERE \"SimpleEntities\".name = 'Scene Statuses'),\n        'Scene'\n    )\n    \"\"\"\n    )\n    # Create the same StatusList -> Status relation of a Task\n    op.execute(\n        \"\"\"INSERT INTO \"StatusList_Statuses\" (status_list_id, status_id)\n    SELECT\n        \"SimpleEntities\".id,\n        \"StatusList_Statuses\".status_id\n    FROM \"SimpleEntities\", \"StatusList_Statuses\"\n    WHERE \"SimpleEntities\".name = 'Scene Statuses'\n        AND \"StatusList_Statuses\".status_list_id = (\n            SELECT \"SimpleEntities\".id FROM \"SimpleEntities\" WHERE \"SimpleEntities\".name = 'Task Statuses'\n        )\n    \"\"\"\n    )\n\n    # Because Scene class is now deriving from Task\n    # we need create a Task for each Scene in the database,\n    # with the same id of the Scene\n    # carry on the data: id, project_id\n    op.execute(\n        \"\"\"\n        INSERT INTO \"Tasks\" (\n            id,\n            project_id,\n            allocation_strategy,\n            persistent_allocation,\n            status_id,\n            status_list_id,\n            schedule_model,\n            schedule_constraint\n        ) SELECT\n            \"Scenes\".id,\n            \"Scenes\".project_id,\n            'minallocated',\n            TRUE,\n            (SELECT \"Statuses\".id FROM \"Statuses\" WHERE \"Statuses\".code = 'CMPL'),\n            (SELECT \"SimpleEntities\".id FROM \"SimpleEntities\" WHERE \"SimpleEntities\".name = 'Scene Statuses'),\n            'effort',\n            0\n        FROM \"Scenes\"\n    \"\"\"\n    )\n\n    # drop the project_id column in Scenes table\n    with op.batch_alter_table(\"Scenes\", schema=None) as batch_op:\n        batch_op.drop_column(\"project_id\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # Add the project_id column back to the Scenes table\n    op.add_column(\"Scenes\", sa.Column(\"project_id\", sa.Integer(), nullable=False))\n    # Add the project_id data back to the Scenes table\n    op.execute(\n        \"\"\"UPDATE \"Scenes\" SET project_id = (\n            SELECT \"Tasks\".project_id FROM \"Tasks\" WHERE \"Tasks\".id = (\n                SELECT \"Scenes\".id FROM \"Scenes\"\n            )\n        )\"\"\"\n    )\n    # set the project_id column not nullable\n    op.execute(\"\"\"ALTER TABLE \"Scenes\" ALTER COLUMN project_id SET NOT NULL\"\"\")\n    # Remove the scene entries from Tasks table\n    op.execute(\"\"\"DELETE FROM \"Tasks\" WHERE id IN (SELECT id  FROM \"Scenes\")\"\"\")\n    # Remove the StatusList entries from StatusList_Statuses\n    op.execute(\n        \"\"\"DELETE FROM \"StatusList_Statuses\" WHERE status_list_id = (\n            SELECT id FROM \"SimpleEntities\"\n            WHERE \"SimpleEntities\".name = 'Scene Statuses'\n        )\n        \"\"\"\n    )\n    # Remove the StatusList from StatusLists Table\n    op.execute(\"\"\"DELETE FROM \"StatusLists\" WHERE target_entity_type = 'Scene'\"\"\")\n    # Remove the StatusList from Entities Table\n    op.execute(\n        \"\"\"DELETE FROM \"Entities\" WHERE id IN (\n            SELECT\n                \"SimpleEntities\".id\n            FROM \"SimpleEntities\"\n            WHERE \"SimpleEntities\".name = 'Scene Statuses'\n        )\n        \"\"\"\n    )\n    # Remove the StatusList from SimpleEntities Table\n    op.execute(\"\"\"DELETE FROM \"SimpleEntities\" WHERE name = 'Scene Statuses'\"\"\")\n\n    # Update the Scenes.id to be a foreign key to Entities.id\n    op.drop_constraint(\"Scenes_id_fkey\", \"Scenes\", type_=\"foreignkey\")\n    op.create_foreign_key(\"Scenes_id_fkey\", \"Scenes\", \"Entities\", [\"id\"], [\"id\"])\n"
  },
  {
    "path": "alembic/versions/4664d72ce1e1_renamed_link_path_to_full_path.py",
    "content": "\"\"\"Renamed \"Link.path\" to\" Link.full_path\".\n\nRevision ID: 4664d72ce1e1\nRevises: 25b3eba6ffe7\nCreate Date: 2013-05-23 18:46:18.218662\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"4664d72ce1e1\"\ndown_revision = \"25b3eba6ffe7\"\n\n\ndef upgrade():\n    \"\"\"Create full_path column.\"\"\"\n    try:\n        op.alter_column(\"Links\", \"path\", new_column_name=\"full_path\")\n    except sa.exc.OperationalError:\n        # SQLite3\n        # create new table\n        op.create_table(\n            \"Links_Temp\",\n            sa.Column(\"id\", sa.Integer, sa.ForeignKey(\"Entities.id\"), primary_key=True),\n            sa.Column(\"original_filename\", sa.String(256), nullable=True),\n            sa.Column(\"full_path\", sa.String),\n        )\n\n        sa.sql.table(\n            \"Links_Temp\",\n            sa.Column(\"id\", sa.Integer, sa.ForeignKey(\"Entities.id\"), primary_key=True),\n            sa.Column(\"original_filename\", sa.String(256), nullable=True),\n            sa.Column(\"full_path\", sa.String),\n        )\n\n        sa.sql.table(\n            \"Links\",\n            sa.Column(\"id\", sa.Integer, sa.ForeignKey(\"Entities.id\"), primary_key=True),\n            sa.Column(\"original_filename\", sa.String(256), nullable=True),\n            sa.Column(\"path\", sa.String),\n        )\n\n        # copy data from Links.path to Links_Temp.full_path\n        op.execute(\n            'INSERT INTO \"Links_Temp\" '\n            'SELECT \"Links\".id, \"Links\".original_filename, \"Links\".path '\n            'FROM \"Links\"'\n        )\n\n        # drop the Links table and rename Links_Temp to Links\n        op.drop_table(\"Links\")\n        op.rename_table(\"Links_Temp\", \"Links\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    try:\n        op.alter_column(\"Links\", \"path\", new_column_name=\"full_path\")\n    except sa.exc.OperationalError:\n        # SQLite3\n        # create new table\n        op.create_table(\n            \"Links_Temp\",\n            sa.Column(\"id\", sa.Integer, sa.ForeignKey(\"Entities.id\"), primary_key=True),\n            sa.Column(\"original_filename\", sa.String(256), nullable=True),\n            sa.Column(\"path\", sa.String),\n        )\n\n        sa.sql.table(\n            \"Links_Temp\",\n            sa.Column(\"id\", sa.Integer, sa.ForeignKey(\"Entities.id\"), primary_key=True),\n            sa.Column(\"original_filename\", sa.String(256), nullable=True),\n            sa.Column(\"path\", sa.String),\n        )\n\n        sa.sql.table(\n            \"Links\",\n            sa.Column(\"id\", sa.Integer, sa.ForeignKey(\"Entities.id\"), primary_key=True),\n            sa.Column(\"original_filename\", sa.String(256), nullable=True),\n            sa.Column(\"full_path\", sa.String),\n        )\n\n        # copy data from Links.path to Links_Temp.full_path\n        op.execute(\n            'INSERT INTO \"Links_Temp\" '\n            'SELECT \"Links\".id, \"Links\".original_filename, \"Links\".full_path '\n            'FROM \"Links\"'\n        )\n\n        # drop the Links table and rename Links_Temp to Links\n        op.drop_table(\"Links\")\n        op.rename_table(\"Links_Temp\", \"Links\")\n"
  },
  {
    "path": "alembic/versions/46775e4a3d96_create_enum_types.py",
    "content": "\"\"\"Create enum types.\n\nRevision ID: 46775e4a3d96\nRevises: 2aeab8b376dc\nCreate Date: 2014-01-31 03:08:36.445876\n\"\"\"\n\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = \"46775e4a3d96\"\ndown_revision = \"2aeab8b376dc\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # rename types\n    op.execute('ALTER TYPE \"TaskScheduleUnit\" RENAME TO \"TimeUnit\";')\n\n    # create new types\n    op.execute(\n        \"\"\"CREATE TYPE \"ResourceAllocationStrategy\" AS ENUM\n            ('minallocated', 'maxloaded', 'minloaded', 'order', 'random');\n        CREATE TYPE \"TaskDependencyGapModel\" AS ENUM ('length', 'duration');\n        CREATE TYPE \"TaskDependencyTarget\" AS ENUM ('onend', 'onstart');\n        CREATE TYPE \"ReviewScheduleModel\"\n            AS ENUM ('effort', 'length', 'duration');\n    \"\"\"\n    )\n\n    # update the Task column to use the TimeUnit type instead of TaskBidUnit\n    op.execute(\n        \"\"\"\n        ALTER TABLE \"Tasks\" ALTER COLUMN bid_unit TYPE \"TimeUnit\"\n            USING ((bid_unit::text)::\"TimeUnit\");\n        \"\"\"\n    )\n\n    # remove unnecessary types\n    op.execute('DROP TYPE IF EXISTS \"TaskBidUnit\" CASCADE;')\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # add necessary types\n    op.execute(\n        \"\"\"CREATE TYPE \"TaskBidUnit\" AS ENUM\n        ('min', 'h', 'd', 'w', 'm', 'y');\n        \"\"\"\n    )\n\n    # update the Task column to use the TimeUnit type instead of TaskBidUnit\n    op.execute(\n        \"\"\"\n        ALTER TABLE \"Tasks\" ALTER COLUMN bid_unit TYPE \"TaskBidUnit\"\n            USING ((bid_unit::text)::\"TaskBidUnit\");\n        \"\"\"\n    )\n\n    # rename types\n    op.execute('ALTER TYPE \"TimeUnit\" RENAME TO \"TaskScheduleUnit\";')\n\n    # create new types\n    op.execute(\n        \"\"\"\n        DROP TYPE IF EXISTS \"ResourceAllocationStrategy\" CASCADE;\n        DROP TYPE IF EXISTS \"TaskDependencyGapModel\" CASCADE;\n        DROP TYPE IF EXISTS \"TaskDependencyTarget\" CASCADE;\n        DROP TYPE IF EXISTS \"ReviewScheduleModel\" CASCADE;\n        \"\"\"\n    )\n"
  },
  {
    "path": "alembic/versions/4a836cf73bcf_create_entitytype_accepts_references.py",
    "content": "\"\"\"Create EntityType.accepts_references.\n\nRevision ID: 4a836cf73bcf\nRevises: None\nCreate Date: 2013-05-15 16:27:05.983849\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"4a836cf73bcf\"\ndown_revision = None\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    try:\n        op.add_column(\"EntityTypes\", sa.Column(\"accepts_references\", sa.Boolean))\n    except (sa.exc.OperationalError, sa.exc.ProgrammingError):\n        # the column already exists\n        pass\n\n    try:\n        op.add_column(\"Links\", sa.Column(\"original_filename\", sa.String(256)))\n    except (sa.exc.OperationalError, sa.exc.ProgrammingError, sa.exc.InternalError):\n        # the column already exists\n        pass\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # no drop column in SQLite so this will not work for SQLite databases\n    op.drop_column(\"EntityTypes\", \"accepts_references\")\n    op.drop_column(\"Links\", \"original_filename\")\n"
  },
  {
    "path": "alembic/versions/5078390e5527_shot_scene_relation_is_now_many_to_one.py",
    "content": "\"\"\"Shot Scene relation is now many-to-one\n\nRevision ID: 5078390e5527\nRevises: e25ec9930632\nCreate Date: 2024-11-18 11:35:10.872216\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"5078390e5527\"\ndown_revision = \"e25ec9930632\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # Add scene_id column\n    op.add_column(\"Shots\", sa.Column(\"scene_id\", sa.Integer(), nullable=True))\n\n    # Create foreign key constraint\n    op.create_foreign_key(None, \"Shots\", \"Scenes\", [\"scene_id\"], [\"id\"])\n\n    # Migrate the data\n    op.execute(\n        \"\"\"UPDATE \"Shots\" SET scene_id = (\n            SELECT scene_id\n                FROM \"Shot_Scenes\"\n                WHERE \"Shot_Scenes\".shot_id = \"Shots\".id LIMIT 1\n        )\"\"\"\n    )\n\n    # Drop Shot_Scenes Table\n    op.execute(\"\"\"DROP TABLE \"Shot_Scenes\" \"\"\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # Add Shot_Scenes Table\n    op.create_table(\n        \"Shot_Scenes\",\n        sa.Column(\"shot_id\", sa.Integer(), nullable=False),\n        sa.Column(\"scene_id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"shot_id\"],\n            [\"Shots.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"scene_id\"],\n            [\"Scenes.id\"],\n        ),\n    )\n\n    # Transfer Data\n    op.execute(\n        \"\"\"\n    UPDATE \"Shot_Scenes\" SET shot_id, scene_id = (\n        SELECT id, scene_id FROM \"Shots\" WHERE \"Shots\".scene_id != NULL\n    )\n    \"\"\"\n    )\n\n    # Drop foreign key constraint\n    op.drop_constraint(\"Shots_scene_id_fkey\", \"Shots\", type_=\"foreignkey\")\n\n    # drop Shots.scene_id column\n    op.drop_column(\"Shots\", \"scene_id\")\n"
  },
  {
    "path": "alembic/versions/5168cc8552a3_html_style_html_class.py",
    "content": "\"\"\"Added html_style and html_class columns to SimpleEntities.\n\nRevision ID: 5168cc8552a3\nRevises: 174567b9c159\nCreate Date: 2013-11-14 23:03:55.413681\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"5168cc8552a3\"\ndown_revision = \"174567b9c159\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"SimpleEntities\", sa.Column(\"html_class\", sa.String(), nullable=True))\n    op.add_column(\"SimpleEntities\", sa.Column(\"html_style\", sa.String(), nullable=True))\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_column(\"SimpleEntities\", \"html_style\")\n    op.drop_column(\"SimpleEntities\", \"html_class\")\n"
  },
  {
    "path": "alembic/versions/5355b569237b_version_version_of_r.py",
    "content": "\"\"\"'Version.version_of' renamed to \"Version.task\".\n\nRevision ID: 5355b569237b\nRevises: 6297277da38\nCreate Date: 2013-06-10 11:47:28.984222\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"5355b569237b\"\ndown_revision = \"6297277da38\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    try:\n        op.alter_column(\"Versions\", \"version_of_id\", new_column_name=\"task_id\")\n    except sa.exc.OperationalError:\n        # SQLite3\n        # just create the new column\n        # and copy data\n        op.add_column(\n            \"Versions\",\n            \"task_id\",\n            sa.Column(sa.Integer, sa.ForeignKey(\"Tasks.id\"), nullable=False),\n        )\n        # copy data from Links.path to Links_Temp.full_path\n        op.execute(\n            \"\"\"INSERT INTO \"Versions\".task_id\n            SELECT \"Versions\".version_of_id FROM \"Versions\"\n            \"\"\"\n        )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    try:\n        op.alter_column(\"Versions\", \"task_id\", new_column_name=\"version_of_id\")\n    except sa.exc.OperationalError:\n        # SQLite3\n        # just create the new column\n        # and copy data\n        op.add_column(\n            \"Versions\",\n            \"version_of_id\",\n            sa.Column(sa.Integer, sa.ForeignKey(\"Tasks.id\"), nullable=False),\n        )\n        op.execute(\n            \"\"\"INSERT INTO \"Versions\".version_of_id\n            SELECT \"Versions\".task_id\n            FROM \"Versions\"\n            \"\"\"\n        )\n"
  },
  {
    "path": "alembic/versions/53d8127d8560_parent_child_relatio.py",
    "content": "\"\"\"parent child relation in Versions.\n\nRevision ID: 53d8127d8560\nRevises: 4a836cf73bcf\nCreate Date: 2013-05-22 12:44:05.626047\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"53d8127d8560\"\ndown_revision = \"4a836cf73bcf\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    try:\n        op.add_column(\"Versions\", sa.Column(\"parent_id\", sa.Integer(), nullable=True))\n    except (sa.exc.OperationalError, sa.exc.InternalError):\n        pass\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_column(\"Versions\", \"parent_id\")\n"
  },
  {
    "path": "alembic/versions/57a5949c7f29_cache_for_total_logged_seconds.py",
    "content": "\"\"\"Created cache columns for total_logged_seconds and schedule_seconds attributes.\n\nRevision ID: 57a5949c7f29\nRevises: 101a789e38ad\nCreate Date: 2013-07-31 16:57:17.674995\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"57a5949c7f29\"\ndown_revision = \"101a789e38ad\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"Tasks\", sa.Column(\"_schedule_seconds\", sa.Integer(), nullable=True))\n    op.add_column(\n        \"Tasks\", sa.Column(\"_total_logged_seconds\", sa.Integer(), nullable=True)\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_column(\"Tasks\", \"_total_logged_seconds\")\n    op.drop_column(\"Tasks\", \"_schedule_seconds\")\n"
  },
  {
    "path": "alembic/versions/5814290f49c7_added_shot_source_in_shot_source_out_record_in.py",
    "content": "\"\"\"Added Shot.source_in, Shot.source_out and Shot.record_in attributes.\n\nRevision ID: 5814290f49c7\nRevises: 2e4a3813ae76\nCreate Date: 2014-09-22 15:25:29.618377\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"5814290f49c7\"\ndown_revision = \"2e4a3813ae76\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"Shots\", sa.Column(\"record_in\", sa.Integer(), nullable=True))\n    op.add_column(\"Shots\", sa.Column(\"source_in\", sa.Integer(), nullable=True))\n    op.add_column(\"Shots\", sa.Column(\"source_out\", sa.Integer(), nullable=True))\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_column(\"Shots\", \"source_out\")\n    op.drop_column(\"Shots\", \"source_in\")\n    op.drop_column(\"Shots\", \"record_in\")\n"
  },
  {
    "path": "alembic/versions/583875229230_good_task_relation.py",
    "content": "\"\"\"Added Tasks.good_id column.\n\nRevision ID: 583875229230\nRevises: 2252e51506de\nCreate Date: 2015-02-07 18:53:04.343928\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"583875229230\"\ndown_revision = \"2252e51506de\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    with op.batch_alter_table(\"Tasks\", schema=None) as batch_op:\n        batch_op.add_column(sa.Column(\"good_id\", sa.Integer(), nullable=True))\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    with op.batch_alter_table(\"Tasks\", schema=None) as batch_op:\n        batch_op.drop_column(\"good_id\")\n"
  },
  {
    "path": "alembic/versions/59092d41175c_added_version_created_with.py",
    "content": "\"\"\"Added Version.created_with.\n\nRevision ID: 59092d41175c\nRevises: 5355b569237b\nCreate Date: 2013-06-19 15:31:53.547392\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"59092d41175c\"\ndown_revision = \"5355b569237b\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    try:\n        op.add_column(\n            \"Versions\", sa.Column(\"created_with\", sa.String(length=256), nullable=True)\n        )\n    except sa.exc.OperationalError:\n        pass\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    try:\n        op.drop_column(\"Versions\", \"created_with\")\n    except sa.exc.OperationalError:\n        pass\n"
  },
  {
    "path": "alembic/versions/5999269aad30_added_generic_text_attribute.py",
    "content": "\"\"\"Added generic_text attribute on SimpleEntity.\n\nRevision ID: 5999269aad30\nRevises: 182f44ce5f07\nCreate Date: 2014-06-02 15:17:27.961000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"5999269aad30\"\ndown_revision = \"182f44ce5f07\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"SimpleEntities\", sa.Column(\"generic_text\", sa.Text()))\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_column(\"SimpleEntities\", \"generic_text\")\n"
  },
  {
    "path": "alembic/versions/59bfe820c369_resource_efficiency.py",
    "content": "\"\"\"Added \"User.efficiency\" column.\n\nRevision ID: 59bfe820c369\nRevises: af869ddfdf9\nCreate Date: 2014-04-26 23:50:53.880274\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"59bfe820c369\"\ndown_revision = \"af869ddfdf9\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"Users\", sa.Column(\"efficiency\", sa.Float(), nullable=True))\n    # set default value\n    op.execute('update \"Users\" set efficiency = 1.0')\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_column(\"Users\", \"efficiency\")\n"
  },
  {
    "path": "alembic/versions/6297277da38_added_vacation_class.py",
    "content": "\"\"\"Added Vacation class.\n\nRevision ID: 6297277da38\nRevises: 21b88ed3da95\nCreate Date: 2013-06-07 16:03:08.412610\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"6297277da38\"\ndown_revision = \"21b88ed3da95\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    try:\n        op.drop_table(\"User_Vacations\")\n    except sa.exc.OperationalError:\n        pass\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.create_table(\n        \"User_Vacations\",\n        sa.Column(\"user_id\", sa.INTEGER(), autoincrement=False, nullable=False),\n        sa.Column(\"vacation_id\", sa.INTEGER(), autoincrement=False, nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"user_id\"], [\"Users.id\"], name=\"User_Vacations_user_id_fkey\"\n        ),\n        sa.ForeignKeyConstraint(\n            [\"vacation_id\"], [\"Vacations.id\"], name=\"User_Vacations_vacation_id_fkey\"\n        ),\n        sa.PrimaryKeyConstraint(\"user_id\", \"vacation_id\", name=\"User_Vacations_pkey\"),\n    )\n"
  },
  {
    "path": "alembic/versions/644f5251fc0d_remove_project_active_attribute.py",
    "content": "\"\"\"Remove Project.active attribute\n\nRevision ID: 644f5251fc0d\nRevises: 5078390e5527\nCreate Date: 2024-11-18 12:47:09.673241\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"644f5251fc0d\"\ndown_revision = \"5078390e5527\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # just remove the \"active\" column\n    with op.batch_alter_table(\"Projects\", schema=None) as batch_op:\n        batch_op.drop_column(\"active\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    with op.batch_alter_table(\"Projects\", schema=None) as batch_op:\n        batch_op.add_column(sa.Column(\"active\", sa.Boolean(), nullable=True))\n\n    # restore the value by checking the status\n    op.execute(\n        \"\"\"UPDATE \"Projects\" SET active = (\n        SELECT\n            (\n                CASE WHEN \"Projects\".status_id = (\n                    SELECT\n                        \"Statuses\".id\n                    FROM \"Statuses\"\n                    WHERE \"Statuses\".code = 'WIP'\n                )\n                THEN true ELSE false END\n            ) as active\n        FROM \"Projects\"\n    )\n    \"\"\"\n    )\n"
  },
  {
    "path": "alembic/versions/745b210e6907_fix_non_existing_thumbnails.py",
    "content": "\"\"\"Fix none-existing thumbnails.\n\nRevision ID: 745b210e6907\nRevises: f2005d1fbadc\nCreate Date: 2016-06-27 17:52:24.381000\n\"\"\"\n\nfrom alembic import op\n\n# revision identifiers, used by Alembic.\nrevision = \"745b210e6907\"\ndown_revision = \"258985128aff\"\n\n\ndef upgrade():\n    \"\"\"Fix SimpleEntities with none-existing thumbnail_id's.\"\"\"\n    op.execute(\n        \"\"\"\n        UPDATE \"SimpleEntities\" SET thumbnail_id = NULL\n        WHERE \"SimpleEntities\".thumbnail_id is not NULL\n            and not exists(\n                select\n                    thum.id\n                from \"SimpleEntities\" as thum\n                where thum.id = \"SimpleEntities\".thumbnail_id\n            )\n        \"\"\"\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # do nothing\n    pass\n"
  },
  {
    "path": "alembic/versions/856e70016b2_roles.py",
    "content": "\"\"\"Added Roles.\n\nRevision ID: 856e70016b2\nRevises: 30c576f3691\nCreate Date: 2014-11-26 00:25:29.543411\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"856e70016b2\"\ndown_revision = \"30c576f3691\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"Roles\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n\n    op.create_table(\n        \"Client_Users\",\n        sa.Column(\"uid\", sa.Integer(), nullable=False),\n        sa.Column(\"cid\", sa.Integer(), nullable=False),\n        sa.Column(\"rid\", sa.Integer(), nullable=True),\n        sa.ForeignKeyConstraint(\n            [\"cid\"],\n            [\"Clients.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"rid\"],\n            [\"Roles.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"uid\"],\n            [\"Users.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"uid\", \"cid\"),\n    )\n\n    #\n    # read Users.client_id and create Client_Users entries accordingly\n    #\n\n    op.rename_table(\"User_Groups\", \"Group_Users\")\n    op.rename_table(\"User_Departments\", \"Department_Users\")\n\n    op.add_column(\"Department_Users\", sa.Column(\"rid\", sa.Integer(), nullable=True))\n    op.add_column(\"Project_Users\", sa.Column(\"rid\", sa.Integer(), nullable=True))\n\n    op.drop_column(\"Departments\", \"lead_id\")\n    op.drop_column(\"Projects\", \"lead_id\")\n    op.drop_column(\"Users\", \"company_id\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\"Projects\", sa.Column(\"lead_id\", sa.INTEGER(), nullable=True))\n    op.add_column(\"Departments\", sa.Column(\"lead_id\", sa.INTEGER(), nullable=True))\n\n    op.drop_column(\"Project_Users\", \"rid\")\n    op.drop_column(\"Department_Users\", \"rid\")\n\n    op.rename_table(\"Department_Users\", \"User_Departments\")\n    op.rename_table(\"Group_Users\", \"User_Groups\")\n\n    op.add_column(\"Users\", sa.Column(\"company_id\", sa.INTEGER(), nullable=True))\n\n    op.drop_table(\"Client_Users\")\n\n    op.drop_table(\"Roles\")\n"
  },
  {
    "path": "alembic/versions/91ed52b72b82_created_variant_class.py",
    "content": "\"\"\"Created Variant class.\n\nRevision ID: 91ed52b72b82\nRevises: 644f5251fc0d\nCreate Date: 2024-11-22 07:57:46.848687\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"91ed52b72b82\"\ndown_revision = \"644f5251fc0d\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"Variants\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Tasks.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n    op.alter_column(\n        \"Projects\",\n        \"fps\",\n        existing_type=sa.REAL(),\n        type_=sa.Float(precision=3),\n        existing_nullable=True,\n    )\n    op.alter_column(\n        \"Shots\",\n        \"fps\",\n        existing_type=sa.REAL(),\n        type_=sa.Float(precision=3),\n        existing_nullable=True,\n    )\n\n    # create Variant Status Lists\n    op.execute(\n        \"\"\"\n        WITH ins1 AS (\n            INSERT INTO \"SimpleEntities\" (\n               entity_type,\n               name,\n               description,\n               date_created,\n               date_updated,\n               html_style,\n               html_class,\n               stalker_version\n            )\n            VALUES (\n                'StatusList',\n                'Variant Statuses',\n                'Created by alembic revision: 91ed52b72b82',\n                (SELECT CAST(NOW() at time zone 'utc' AS timestamp)),\n                (SELECT CAST(NOW() at time zone 'utc' AS timestamp)),\n                '',\n                '',\n                '1.0.0.dev1'\n            )\n            RETURNING id as variant_status_list_id\n        ),\n        ins2 AS (\n            INSERT INTO \"Entities\" (id) (SELECT ins1.variant_status_list_id FROM ins1)\n        )\n        INSERT INTO \"StatusLists\" (id, target_entity_type) (SELECT ins1.variant_status_list_id, 'Variant' FROM ins1);\n    \"\"\"\n    )\n\n    # Add the same statuses of Task StatusList to Variant StatusList\n    op.execute(\n        \"\"\"\n        INSERT INTO \"StatusList_Statuses\" (status_list_id, status_id) (\n            SELECT\n                (SELECT id FROM \"StatusLists\" WHERE target_entity_type = 'Variant') as status_list_id,\n                \"StatusList_Statuses\".status_id\n            FROM \"StatusList_Statuses\"\n            WHERE \"StatusList_Statuses\".status_list_id = (\n                SELECT id FROM \"StatusLists\" WHERE target_entity_type = 'Task'\n            )\n        )\n        \"\"\"\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # ### commands auto generated by Alembic - please adjust! ###\n    op.alter_column(\n        \"Shots\",\n        \"fps\",\n        existing_type=sa.Float(precision=3),\n        type_=sa.REAL(),\n        existing_nullable=True,\n    )\n    op.alter_column(\n        \"Projects\",\n        \"fps\",\n        existing_type=sa.Float(precision=3),\n        type_=sa.REAL(),\n        existing_nullable=True,\n    )\n    # remove Variant Status List Statuses\n    op.execute(\n        \"\"\"\n        DELETE FROM \"StatusList_Statuses\"\n        WHERE \"StatusList_Statuses\".status_list_id = (\n            SELECT id FROM \"StatusLists\" WHERE \"StatusLists\".target_entity_type = 'Variant'\n        )\n    \"\"\"\n    )\n    # remove Variant Status Lists\n    op.execute(\n        \"\"\"\n        WITH del1 AS (\n            DELETE FROM \"StatusLists\"\n               WHERE \"StatusLists\".target_entity_type = 'Variant'\n               RETURNING \"StatusLists\".id as deleted_status_list_id\n        ), del2 AS (\n            DELETE FROM \"Entities\" WHERE \"Entities\".id = (SELECT del1.deleted_status_list_id FROM del1)\n        )\n        DELETE FROM \"SimpleEntities\" WHERE \"SimpleEntities\".id = (SELECT del1.deleted_status_list_id FROM del1)\n    \"\"\"\n    )\n    op.drop_table(\"Variants\")\n"
  },
  {
    "path": "alembic/versions/92257ba439e1_budget_is_now_statusable.py",
    "content": "\"\"\"Budget is now statusable.\n\nRevision ID: 92257ba439e1\nRevises: f2005d1fbadc\nCreate Date: 2016-07-28 13:20:27.397000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"92257ba439e1\"\ndown_revision = \"f2005d1fbadc\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"Budgets\", sa.Column(\"status_id\", sa.Integer(), nullable=True))\n    op.add_column(\"Budgets\", sa.Column(\"status_list_id\", sa.Integer(), nullable=True))\n    op.create_foreign_key(None, \"Budgets\", \"Statuses\", [\"status_id\"], [\"id\"])\n    op.create_foreign_key(None, \"Budgets\", \"StatusLists\", [\"status_list_id\"], [\"id\"])\n\n    # create a dummy status list for budgets\n    op.execute(\n        \"\"\"insert into \"SimpleEntities\" (name, entity_type)\n        values ('Dummy Budget StatusList', 'StatusList');\n        insert into \"Entities\" (id)\n          select\n            \"SimpleEntities\".id\n          from \"SimpleEntities\"\n          where \"SimpleEntities\".entity_type = 'StatusList' and\n            \"SimpleEntities\".name = 'Dummy Budget StatusList'\n        ;\n        insert into \"StatusLists\" (id, target_entity_type)\n            select\n              \"SimpleEntities\".id,\n              'Budget'\n            from \"SimpleEntities\"\n            where \"SimpleEntities\".entity_type = 'StatusList' and\n                  \"SimpleEntities\".name = 'Dummy Budget StatusList'\n        ;\n        insert into \"StatusList_Statuses\"\n            select\n                \"SimpleEntities\".id,\n                \"Statuses\".id\n            from \"SimpleEntities\", \"Statuses\"\n            where \"SimpleEntities\".name = 'Dummy Budget StatusList'\n            order by \"Statuses\".id\n            limit 1\n        ;\n        update \"Budgets\"\n          set status_id = (\n            select \"Statuses\".id\n            from \"Statuses\"\n            order by \"Statuses\".id limit 1\n          )\n        ;\n        update \"Budgets\"\n          set status_list_id = (\n            select \"SimpleEntities\".id\n            from \"SimpleEntities\"\n            where \"SimpleEntities\".name = 'Dummy Budget StatusList'\n          )\n        ;\n        \"\"\"\n    )\n    # now alter column to be non nullable\n    op.alter_column(\"Budgets\", \"status_id\", nullable=False)\n    op.alter_column(\"Budgets\", \"status_list_id\", nullable=False)\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.execute(\n        \"\"\"\n        ALTER TABLE public.\"Budgets\" DROP CONSTRAINT \"Budgets_status_id_fkey\";\n        ALTER TABLE public.\"Budgets\" DROP CONSTRAINT \"Budgets_status_list_id_fkey\";\n        ALTER TABLE public.\"Budgets\" DROP COLUMN status_id;\n        ALTER TABLE public.\"Budgets\" DROP COLUMN status_list_id;\n        \"\"\"\n    )\n\n    # remove 'Dummy Budget StatusList' if it exists\n    op.execute(\n        \"\"\"\n          delete\n          from \"StatusList_Statuses\"\n          where \"StatusList_Statuses\".status_list_id = (\n            select\n              id\n            from \"SimpleEntities\"\n            where \"SimpleEntities\".name = 'Dummy Budget StatusList'\n          )\n          ;\n          delete\n          from \"StatusLists\"\n          where \"StatusLists\".id = (\n            select\n              id\n            from \"SimpleEntities\"\n            where \"SimpleEntities\".name = 'Dummy Budget StatusList'\n          )\n          ;\n          delete\n          from \"Entities\"\n          where \"Entities\".id = (\n            select\n              id\n            from \"SimpleEntities\"\n            where \"SimpleEntities\".name = 'Dummy Budget StatusList'\n          )\n          ;\n          delete\n          from \"SimpleEntities\"\n          where \"SimpleEntities\".name = 'Dummy Budget StatusList'\n          ;\n        \"\"\"\n    )\n"
  },
  {
    "path": "alembic/versions/9f9b88fef376_link_renamed_to_file.py",
    "content": "\"\"\"Link renamed to File\n\nRevision ID: 9f9b88fef376\nRevises: 3be540ad3a93\nCreate Date: 2025-01-14 15:37:15.746961\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"9f9b88fef376\"\ndown_revision = \"3be540ad3a93\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # -------------------------------------------------------------------------\n    # drop constraints first\n    op.drop_constraint(\"Links_id_fkey\", table_name=\"Links\", type_=\"foreignkey\")\n    op.drop_constraint(\"Daily_Links_link_id_fkey\", \"Daily_Links\", type_=\"foreignkey\")\n    op.drop_constraint(\"Project_References_link_id_fkey\", \"Project_References\", type_=\"foreignkey\")\n    op.drop_constraint(\"Task_References_link_id_fkey\", \"Task_References\", type_=\"foreignkey\")\n    op.drop_constraint(\"Version_Inputs_link_id_fkey\", \"Version_Inputs\", type_=\"foreignkey\")\n    op.drop_constraint(\"Version_Outputs_link_id_fkey\", \"Version_Outputs\", type_=\"foreignkey\")\n    op.drop_constraint(\"Versions_id_fkey\", \"Versions\", type_=\"foreignkey\")\n    op.drop_constraint(\"Daily_Links_daily_id_fkey\", table_name=\"Daily_Links\", type_=\"foreignkey\")\n    op.drop_constraint(\"Daily_Links_pkey\", table_name=\"Daily_Links\")\n    op.drop_constraint(\"Version_Outputs_version_id_fkey\", \"Version_Outputs\", type_=\"foreignkey\")\n    op.drop_constraint(\"Version_Outputs_pkey\", table_name=\"Version_Outputs\", type_=\"primary\")\n    op.drop_constraint(\"Version_Inputs_version_id_fkey\", table_name=\"Version_Inputs\", type_=\"foreignkey\")\n    op.drop_constraint(\"Version_Inputs_pkey\", table_name=\"Version_Inputs\", type_=\"primary\")\n    op.drop_constraint(\"xc\", \"SimpleEntities\", type_=\"foreignkey\")\n    op.drop_constraint(\"xu\", \"SimpleEntities\", type_=\"foreignkey\")\n    op.drop_constraint(\"y\", \"SimpleEntities\", type_=\"foreignkey\")\n    op.drop_constraint(\"z\", \"SimpleEntities\", type_=\"foreignkey\")\n    # link_id_fkey\n    # Links_pkey -> Files_pkey\n    # This requires a lot of other constraints to be dropped first!!!\n    op.drop_constraint(\"Links_pkey\", \"Links\", type_=\"primary\")\n\n    # -------------------------------------------------------------------------\n    # rename tables\n    op.rename_table(\"Links\", \"Files\")\n    op.rename_table(\"Daily_Links\", \"Daily_Files\")\n    op.rename_table(\"Version_Outputs\", \"Version_Files\")\n    # -------------------------------------------------------------------------\n    # Rename Version_Inputs to File_References:\n    #\n    #   This table is storing which Version was referencing which other\n    #   version.\n    #\n    #   Data needs to be migrated in tandem with the data moved to the\n    #   Version_Files table.\n    op.rename_table(\"Version_Inputs\", \"File_References\")\n\n    # -------------------------------------------------------------------------\n    # create columns\n    # create \"Files\".created_with\n    op.add_column(\n        \"Files\",\n        sa.Column(\"created_with\", sa.String(length=256), nullable=True),\n    )\n\n    # -------------------------------------------------------------------------\n    # rename columns\n    op.alter_column(\"Daily_Files\", \"link_id\", new_column_name=\"file_id\")\n    op.alter_column(\"File_References\", \"link_id\", new_column_name=\"reference_id\")\n    op.alter_column(\"File_References\", \"version_id\", new_column_name=\"file_id\")\n    op.alter_column(\"Project_References\", \"link_id\", new_column_name=\"reference_id\")\n    op.alter_column(\"Task_References\", \"link_id\", new_column_name=\"reference_id\")\n    op.alter_column(\"Version_Files\", \"link_id\", new_column_name=\"file_id\")\n\n    # migrate data\n    # Update \"SimpleEntities\".entity_type to 'File'\n    # and replace the 'Link_%' in the name with 'File_%'\n    op.execute(\n        \"\"\"UPDATE \"SimpleEntities\"\n        SET (entity_type, name) = ('File', REPLACE(\"SimpleEntities\".name, 'Link_', 'File_'))\n        WHERE \"SimpleEntities\".entity_type = 'Link'\n        \"\"\"\n    )\n    # Update \"EntityTypes\".name for 'Link' to 'File'\n    op.execute(\n        \"\"\"UPDATE \"EntityTypes\"\n        SET name = 'File'\n        WHERE \"EntityTypes\".name = 'Link'\n        \"\"\"\n    )\n    # Update any Types where target_entity_type == 'Link' to 'File'\n    op.execute(\n        \"\"\"UPDATE \"Types\"\n        SET target_entity_type = 'File'\n        WHERE target_entity_type = 'Link'\n        \"\"\"\n    )\n    # migrate the created_with data from Versions to the Files table\n    op.execute(\n        \"\"\"UPDATE \"Files\" SET created_with = \"Versions\".created_with\n        FROM \"Versions\"\n        WHERE \"Versions\".id = \"Files\".id\n        \"\"\"\n    )\n    # -------------------------------------------------------------------------\n    # Migrate Files that were Versions before, to new entries...\n    #   - Go back to Files tables\n    #   - Search for Files that have the same ids with Versions\n    #   - Create a new entry with the same data to Files, Entities and SimpleEntities\n    #     tables.\n    #   - Add them to the Version_Files tables, as they were previous versions\n    #   - Delete the old files\n    op.execute(\n        f\"\"\"\n        -- reshuffle names for entities that have autoname and that are clashing\n        UPDATE \"SimpleEntities\"\n            SET name = (\"SimpleEntities\".entity_type || '_' || gen_random_uuid())\n        WHERE name in (\n            SELECT\n                name\n            FROM \"SimpleEntities\"\n            WHERE length(name) > 37 -- entity_type_uuid4\n            GROUP BY name\n            HAVING COUNT(*) > 1\n            ORDER BY name\n        );\n\n        -- create temp storage for data coming from \"Files\" table\n        ALTER TABLE \"SimpleEntities\"\n            ADD original_filename character varying(256) COLLATE pg_catalog.\"default\",\n            ADD full_path text COLLATE pg_catalog.\"default\",\n            ADD created_from_version_id integer,\n            ADD created_with character varying(256);\n\n        -- create new entry for all Files that were originally Versions\n        WITH sel1 as (\n            SELECT\n                \"File_SimpleEntities\".id,\n                'File' as entity_type,\n                REPLACE(\"File_SimpleEntities\".name, 'Version_', 'File_') as entity_name,\n                \"File_SimpleEntities\".description,\n                \"File_SimpleEntities\".created_by_id,\n                \"File_SimpleEntities\".updated_by_id,\n                \"File_SimpleEntities\".date_created,\n                \"File_SimpleEntities\".date_updated,\n                \"File_SimpleEntities\".type_id,\n                \"File_SimpleEntities\".generic_text,\n                \"File_SimpleEntities\".thumbnail_id,\n                \"File_SimpleEntities\".html_style,\n                \"File_SimpleEntities\".html_class,\n                \"File_SimpleEntities\".stalker_version,\n                \"Files\".original_filename,\n                \"Files\".full_path,\n                \"Files\".created_with\n            FROM \"Files\"\n            JOIN \"SimpleEntities\" AS \"File_SimpleEntities\" ON \"Files\".id = \"File_SimpleEntities\".id\n            WHERE \"File_SimpleEntities\".entity_type = 'Version'\n            ORDER BY \"File_SimpleEntities\".id\n        ), ins1 as (\n            INSERT INTO \"SimpleEntities\" (\n                entity_type,\n                name,\n                description,\n                created_by_id,\n                updated_by_id,\n                date_created,\n                date_updated,\n                type_id,\n                html_style,\n                html_class,\n                stalker_version,\n                original_filename,\n                full_path,\n                created_with,\n                created_from_version_id\n            ) (\n                SELECT\n                    sel1.entity_type,\n                    sel1.entity_name,\n                    sel1.description,\n                    sel1.created_by_id,\n                    sel1.updated_by_id,\n                    sel1.date_created,\n                    sel1.date_updated,\n                    sel1.type_id,\n                    sel1.html_style,\n                    sel1.html_class,\n                    sel1.stalker_version,\n                    sel1.original_filename,\n                    sel1.full_path,\n                    sel1.created_with,\n                    sel1.id as created_from_version_id\n                FROM sel1\n            )\n            RETURNING id as file_id, name as entity_name\n        )\n        INSERT INTO \"Entities\" (id) (SELECT ins1.file_id FROM ins1);\n\n        -- Insert into Files\n        INSERT INTO \"Files\" (id, original_filename, full_path, created_with)\n        (\n            SELECT\n                \"SimpleEntities\".id,\n                \"SimpleEntities\".original_filename,\n                \"SimpleEntities\".full_path,\n                \"SimpleEntities\".created_with\n            FROM \"SimpleEntities\"\n            WHERE \"SimpleEntities\".created_from_version_id IS NOT NULL\n        );\n\n        -- Insert into Version_Files\n        INSERT INTO \"Version_Files\" (version_id, file_id)\n        (\n            SELECT\n                \"SimpleEntities\".created_from_version_id as version_id,\n                \"SimpleEntities\".id as file_id\n            FROM \"SimpleEntities\"\n            WHERE \"SimpleEntities\".created_from_version_id IS NOT NULL\n        );\n\n        -- Update File_References\n        -- so that the newly created Files\n        -- are referencing the newly create other Files and not the old versions\n\n        -- Update the file_id column first\n        UPDATE \"File_References\" SET file_id = sel1.file_id\n        FROM (\n            SELECT\n                id as file_id,\n                created_from_version_id\n            FROM \"SimpleEntities\"\n            WHERE created_from_version_id IS NOT NULL\n        ) as sel1\n        WHERE \"File_References\".file_id = sel1.created_from_version_id;\n\n        -- then the reference_id column\n        UPDATE \"File_References\" SET reference_id = sel1.file_id\n        FROM (\n            SELECT\n                id as file_id,\n                created_from_version_id\n            FROM \"SimpleEntities\"\n            WHERE created_from_version_id IS NOT NULL\n        ) as sel1\n        WHERE \"File_References\".reference_id = sel1.created_from_version_id;\n\n        -- Drop all the Files that previously was a Version\n\n        -- Remove constraints first (Otherwise it will be incredibly slow!)\n        -- ALTER TABLE \"SimpleEntities\" DROP CONSTRAINT \"SimpleEntities_thumbnail_id_fkey\";\n        -- ALTER TABLE \"Daily_Files\" DROP CONSTRAINT \"Daily_Files_file_id_fkey\";\n        -- ALTER TABLE \"Project_References\" DROP CONSTRAINT \"Project_References_reference_id_fkey\";\n        -- ALTER TABLE \"Task_References\" DROP CONSTRAINT \"Task_References_reference_id_fkey\";\n        -- ALTER TABLE \"File_References\" DROP CONSTRAINT \"File_References_file_id_fkey\";\n        -- ALTER TABLE \"File_References\" DROP CONSTRAINT \"File_References_reference_id_fkey\";\n        -- ALTER TABLE \"Version_Files\" DROP CONSTRAINT \"Version_Files_file_id_fkey\";\n        -- ALTER TABLE \"Files\" DROP CONSTRAINT \"Files_id_fkey\";\n\n        -- Really delete data now\n        DELETE FROM \"Files\"\n        WHERE id in (\n            SELECT\n                id\n            FROM \"SimpleEntities\"\n            WHERE entity_type = 'Version'\n        );\n\n        -- Recreate constraints\n--         ALTER TABLE \"SimpleEntities\"\n--             ADD CONSTRAINT \"SimpleEntities_thumbnail_id_fkey\" FOREIGN KEY (thumbnail_id)\n--                 REFERENCES public.\"Files\" (id) MATCH SIMPLE\n--                 ON UPDATE NO ACTION\n--                 ON DELETE NO ACTION;\n--         ALTER TABLE \"Daily_Files\"\n--             ADD CONSTRAINT \"Daily_Files_file_id_fkey\" FOREIGN KEY (file_id)\n--                 REFERENCES public.\"Files\" (id) MATCH SIMPLE\n--                 ON UPDATE NO ACTION\n--                 ON DELETE NO ACTION;\n--         ALTER TABLE \"Project_References\"\n--             ADD CONSTRAINT \"Project_References_reference_id_fkey\" FOREIGN KEY (reference_id)\n--                 REFERENCES public.\"Files\" (id) MATCH SIMPLE\n--                 ON UPDATE NO ACTION\n--                 ON DELETE NO ACTION;\n--         ALTER TABLE \"Task_References\"\n--             ADD CONSTRAINT \"Task_References_reference_id_fkey\" FOREIGN KEY (reference_id)\n--                 REFERENCES public.\"Files\" (id) MATCH SIMPLE\n--                 ON UPDATE NO ACTION\n--                 ON DELETE NO ACTION;\n--         ALTER TABLE \"File_References\"\n--             ADD CONSTRAINT \"File_References_file_id_fkey\" FOREIGN KEY (file_id)\n--                 REFERENCES public.\"Files\" (id) MATCH SIMPLE\n--                 ON UPDATE NO ACTION\n--                 ON DELETE NO ACTION,\n--             ADD CONSTRAINT \"File_References_reference_id_fkey\" FOREIGN KEY (reference_id)\n--                 REFERENCES public.\"Files\" (id) MATCH SIMPLE\n--                 ON UPDATE NO ACTION\n--                 ON DELETE NO ACTION;\n--         ALTER TABLE \"Version_Files\"\n--             ADD CONSTRAINT \"Version_Files_file_id_fkey\" FOREIGN KEY (file_id)\n--                 REFERENCES public.\"Files\" (id) MATCH SIMPLE\n--                 ON UPDATE CASCADE\n--                 ON DELETE CASCADE;\n--         ALTER TABLE \"Files\"\n--             ADD CONSTRAINT \"Files_id_fkey\" FOREIGN KEY (id)\n--                 REFERENCES public.\"Entities\" (id) MATCH SIMPLE\n--                 ON UPDATE NO ACTION\n--                 ON DELETE NO ACTION;\n\n        -- delete temp data in \"SimpleEntities\"\n        ALTER TABLE \"SimpleEntities\"\n            DROP COLUMN original_filename,\n            DROP COLUMN full_path,\n            DROP COLUMN created_from_version_id,\n            DROP COLUMN created_with;\n        \"\"\"\n    )\n\n    # recreate constraints\n    op.create_foreign_key(\"Files_id_fkey\", \"Files\", \"Entities\", [\"id\"], [\"id\"])\n    op.create_primary_key(\"Files_pkey\", \"Files\", [\"id\"])\n    op.create_foreign_key(\n        \"Daily_Files_daily_id_fkey\",\n        \"Daily_Files\",\n        \"Dailies\",\n        [\"daily_id\"],\n        [\"id\"],\n    )\n    op.create_foreign_key(\n        \"Daily_Files_file_id_fkey\",\n        \"Daily_Files\",\n        \"Files\",\n        [\"file_id\"],\n        [\"id\"],\n    )\n    op.create_primary_key(\"Daily_Files_pkey\", \"Daily_Files\", [\"daily_id\", \"file_id\"])\n    op.create_foreign_key(\n        \"Version_Files_version_id_fkey\",\n        \"Version_Files\",\n        \"Versions\",\n        [\"version_id\"],\n        [\"id\"],\n    )\n    op.create_foreign_key(\n        \"Version_Files_file_id_fkey\",\n        \"Version_Files\",\n        \"Files\",\n        [\"file_id\"],\n        [\"id\"],\n        onupdate=\"CASCADE\",\n        ondelete=\"CASCADE\",\n    )\n    op.create_primary_key(\n        \"Version_Files_pkey\", \"Version_Files\", [\"version_id\", \"file_id\"]\n    )\n    op.create_foreign_key(\n        \"File_References_file_id_fkey\",\n        \"File_References\",\n        \"Files\",\n        [\"file_id\"],\n        [\"id\"],\n    )\n    op.create_foreign_key(\n        \"File_References_reference_id_fkey\",\n        \"File_References\",\n        \"Files\",\n        [\"reference_id\"],\n        [\"id\"],\n    )\n    op.create_primary_key(\n        \"File_References_pkey\",\n        \"File_References\",\n        [\"file_id\", \"reference_id\"],\n    )\n    op.create_foreign_key(\n        \"Project_References_reference_id_fkey\",\n        \"Project_References\",\n        \"Files\",\n        [\"reference_id\"],\n        [\"id\"],\n    )\n    op.create_foreign_key(\n        \"SimpleEntities_thumbnail_id_fkey\",\n        \"SimpleEntities\",\n        \"Files\",\n        [\"thumbnail_id\"],\n        [\"id\"],\n        use_alter=True,\n    )\n    op.create_foreign_key(\n        \"SimpleEntities_created_by_id_fkey\",\n        \"SimpleEntities\",\n        \"Users\",\n        [\"created_by_id\"],\n        [\"id\"],\n        use_alter=True,\n    )\n    op.create_foreign_key(\n        \"SimpleEntities_updated_by_id_fkey\",\n        \"SimpleEntities\",\n        \"Users\",\n        [\"updated_by_id\"],\n        [\"id\"],\n        use_alter=True,\n    )\n    op.create_foreign_key(\n        \"SimpleEntities_type_id_fkey\",\n        \"SimpleEntities\",\n        \"Types\",\n        [\"type_id\"],\n        [\"id\"],\n        use_alter=True,\n    )\n    op.create_foreign_key(\n        \"Task_References_reference_id_fkey\",\n        \"Task_References\",\n        \"Files\",\n        [\"reference_id\"],\n        [\"id\"],\n    )\n    # Versions is now deriving from Entities\n    op.create_foreign_key(\"Versions_id_fkey\", \"Versions\", \"Entities\", [\"id\"], [\"id\"])\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # drop constraints first\n    op.drop_constraint(\"Daily_Files_pkey\", \"Daily_Files\", type_=\"primary\")\n    op.drop_constraint(\"Daily_Files_file_id_fkey\", \"Daily_Files\", type_=\"foreignkey\")\n    op.drop_constraint(\"Daily_Files_daily_id_fkey\", \"Daily_Files\", type_=\"foreignkey\")\n    op.drop_constraint(\"File_References_pkey\", \"File_References\", type_=\"primary\")\n    op.drop_constraint(\"File_References_file_id_fkey\", \"File_References\", type_=\"foreignkey\")\n    op.drop_constraint(\"File_References_reference_id_fkey\", \"File_References\", type_=\"foreignkey\")\n    op.drop_constraint(\"Files_pkey\", \"Files\", type_=\"primary\")\n    op.drop_constraint(\"Files_id_fkey\", \"Files\", type_=\"foreignkey\")\n    op.drop_constraint(\"Project_References_reference_id_fkey\", \"Project_References\", type_=\"foreignkey\")\n    op.drop_constraint(\"SimpleEntities_created_by_id_fkey\", \"SimpleEntities\", type_=\"foreignkey\")\n    op.drop_constraint(\"SimpleEntities_thumbnail_id_fkey\", \"SimpleEntities\", type_=\"foreignkey\")\n    op.drop_constraint(\"SimpleEntities_updated_by_id_fkey\", \"SimpleEntities\", type_=\"foreignkey\")\n    op.drop_constraint(\"SimpleEntities_type_id_fkey\", \"SimpleEntities\", type_=\"foreignkey\")\n    op.drop_constraint(\"Task_References_reference_id_fkey\", \"Task_References\", type_=\"foreignkey\")\n    op.drop_constraint(\"Version_Files_pkey\", \"Version_Files\", type_=\"primary\")\n    op.drop_constraint(\"Version_Files_file_id_fkey\", \"Version_Files\", type_=\"foreignkey\")\n    op.drop_constraint(\"Versions_id_fkey\", \"Versions\", type_=\"foreignkey\")\n\n    # rename tables\n    op.rename_table(\"Daily_Files\", \"Daily_Links\")\n    op.rename_table(\"Files\", \"Links\")\n    op.rename_table(\"File_References\", \"Version_Inputs\")\n    op.rename_table(\"Version_Files\", \"Version_Outputs\")\n\n    # rename columns\n    op.alter_column(\"Daily_Links\", \"file_id\", new_column_name=\"link_id\")\n    op.alter_column(\"Version_Outputs\", \"file_id\", new_column_name=\"link_id\")\n    op.alter_column(\"Version_Inputs\", \"file_id\", new_column_name=\"version_id\")\n    op.alter_column(\"Version_Inputs\", \"reference_id\", new_column_name=\"link_id\")\n    op.alter_column(\"Project_References\", \"reference_id\", new_column_name=\"link_id\")\n    op.alter_column(\"Task_References\", \"reference_id\", new_column_name=\"link_id\")\n\n    # migrate the data as much as you can\n    # op.execute(\n    #     \"\"\"\n    #     -- There are Versions where there are no corresponding input in the\n    #     -- Links table anymore\n    #     -- Update all the ids of the Links that are in the Version_Inputs with the\n    #     -- id of the version, so that we have a corresponding links for all versions\n    #     -- and then delete all the entries from the Entities and SimpleEntities tables\n    #     -- for those links.\n    #\n    #     UPDATE \"Links\" SET id = sel1.id FROM (\n    #         SELECT\n    #             link_id\n    #         FROM \"Version_Links\"\n    #     )\n    #\n    #     \"\"\"\n    # )\n\n    # -------------------------------------------------------------------------\n    # drop columns\n    # remove created_with from Versions table\n    op.drop_column(\"Links\", \"created_with\")\n\n    # recreate constraints\n    op.create_foreign_key(\"Daily_Links_daily_id_fkey\", \"Daily_Links\", \"Dailies\", [\"daily_id\"], [\"id\"])\n    op.create_foreign_key(\"Daily_Links_link_id_fkey\", \"Daily_Links\", \"Links\", [\"link_id\"], [\"id\"])\n    op.create_primary_key(\"Daily_Links_pkey\", \"Daily_Links\", [\"daily_id\", \"link_id\"])\n    op.create_primary_key(\"Links_pkey\", \"Links\", [\"id\"])\n    op.create_foreign_key(\"Link_id_fkey\", \"Links\", \"Entities\", [\"id\"], [\"id\"])\n    op.create_foreign_key(\"Project_References_link_id_fkey\", \"Project_References\", [\"link_id\"], [\"id\"])\n    op.create_foreign_key(\"Task_References_link_id_fkey\", \"Task_References\", \"Links\", [\"link_id\"], [\"id\"])\n    op.create_foreign_key(\"Version_Inputs_version_id_fkey\", \"Version_Inputs\", \"Versions\", [\"version_id\"], [\"id\"])\n    op.create_foreign_key(\"Version_Inputs_link_id_fkey\", \"Version_Inputs\", \"Links\", [\"link_id\"], [\"id\"])\n    op.create_primary_key(\"Version_Inputs_pkey\", \"Version_Inputs\", [\"version_id\", \"link_id\"])\n    op.create_primary_key(\"Version_Outputs_pkey\", \"Version_Outputs\", [\"version_id\", \"link_id\"])\n    op.create_foreign_key(\"Version_Outputs_link_id_fkey\", \"Versions_Outputs\", \"Links\", [\"link_id\"], [\"id\"])\n    op.create_foreign_key(\"Version_Outputs_version_id_fkey\", \"Version_Outputs\", \"Links\", [\"version_id\"], [\"id\"])\n    op.create_foreign_key(\"Versions_id_fkey\", \"Versions\", \"Links\", [\"id\"], [\"id\"])\n    op.create_foreign_key(\"xc\", \"SimpleEntities\", \"Users\", [\"created_by_id\"], [\"id\"], use_alter=True)\n    op.create_foreign_key(\"xu\", \"SimpleEntities\", \"Users\", [\"updated_by_id\"], [\"id\"], use_alter=True)\n    op.create_foreign_key(\"y\", \"SimpleEntities\", \"Types\", [\"type_id\"], [\"id\"], use_alter=True)\n    op.create_foreign_key(\"z\", \"SimpleEntities\", \"Links\", [\"thumbnail_id\"], [\"id\"], use_alter=True)\n"
  },
  {
    "path": "alembic/versions/a2007ad7f535_added_review_version_id_column.py",
    "content": "\"\"\"Added Review.version_id column\n\nRevision ID: a2007ad7f535\nRevises: 91ed52b72b82\nCreate Date: 2024-11-26 11:36:07.776169\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"a2007ad7f535\"\ndown_revision = \"91ed52b72b82\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"Reviews\", sa.Column(\"version_id\", sa.Integer(), nullable=True))\n    op.create_foreign_key(\n        \"Reviews_version_id_fkey\",\n        \"Reviews\",\n        \"Versions\",\n        [\"version_id\"],\n        [\"id\"],\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_constraint(\"Reviews_version_id_fkey\", \"Reviews\", type_=\"foreignkey\")\n    op.drop_column(\"Reviews\", \"version_id\")\n"
  },
  {
    "path": "alembic/versions/a6598cde6b_versions_are_not_mix.py",
    "content": "\"\"\"Versions are not mixed with StatusMixin anymore.\n\nRevision ID: a6598cde6b\nRevises: 275bdc106fd5\nCreate Date: 2013-10-25 17:35:42.953516\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"a6598cde6b\"\ndown_revision = \"275bdc106fd5\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.drop_column(\"Versions\", \"status_list_id\")\n    op.drop_column(\"Versions\", \"status_id\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\"Versions\", sa.Column(\"status_id\", sa.INTEGER(), nullable=False))\n    op.add_column(\"Versions\", sa.Column(\"status_list_id\", sa.INTEGER(), nullable=False))\n"
  },
  {
    "path": "alembic/versions/a9319b19f7be_added_shot_fps.py",
    "content": "\"\"\"Added \"shot.fps\".\n\nRevision ID: a9319b19f7be\nRevises: f16651477e64\nCreate Date: 2016-11-29 13:38:22.380000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"a9319b19f7be\"\ndown_revision = \"f16651477e64\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"Shots\", sa.Column(\"fps\", sa.Float(precision=3), nullable=True))\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_column(\"Shots\", \"fps\")\n"
  },
  {
    "path": "alembic/versions/af869ddfdf9_entity_to_note_relation_is_now_many_to_many.py",
    "content": "\"\"\"Entity to note relation is now many-to-many.\n\nRevision ID: af869ddfdf9\nRevises: 2f55dc4f199f\nCreate Date: 2014-04-06 09:20:44.509357\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"af869ddfdf9\"\ndown_revision = \"2f55dc4f199f\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"Entity_Notes\",\n        sa.Column(\"entity_id\", sa.Integer(), nullable=False),\n        sa.Column(\"note_id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"entity_id\"],\n            [\"Entities.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"note_id\"],\n            [\"Notes.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"entity_id\", \"note_id\"),\n    )\n    # before dropping notes entity_id column\n    # store all the entity_id values in the secondary table\n    op.execute(\n        \"\"\"\n      insert into \"Entity_Notes\"\n      select \"Notes\".entity_id, \"Notes\".id\n      from \"Notes\"\n      where \"Notes\".entity_id is not NULL\n      and exists(\n        select \"Entities\".id\n        from \"Entities\"\n        where \"Entities\".id = \"Notes\".entity_id\n      )\"\"\"\n    )\n\n    # now drop the entity_id column\n    op.drop_column(\"Notes\", \"entity_id\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\"Notes\", sa.Column(\"entity_id\", sa.INTEGER(), nullable=True))\n\n    # restore data\n    op.execute(\n        \"\"\"\n        UPDATE\n           \"Notes\"\n        SET\n            entity_id = \"Entity_Notes\".entity_id\n        FROM \"Entity_Notes\"\n        WHERE \"Notes\".id = \"Entity_Notes\".note_id\n    \"\"\"\n    )\n\n    op.drop_table(\"Entity_Notes\")\n"
  },
  {
    "path": "alembic/versions/bf67e6a234b4_added_revision_code_attribute.py",
    "content": "\"\"\"Added \"Repository.code\" attribute.\n\nRevision ID: bf67e6a234b4\nRevises: ed0167fff399\nCreate Date: 2020-01-01 09:50:19.086342\n\"\"\"\n\nimport logging\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"bf67e6a234b4\"\ndown_revision = \"ed0167fff399\"\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # add the column\n    logger.info(\"creating code column in Repositories table\")\n    op.add_column(\n        \"Repositories\", sa.Column(\"code\", sa.String(length=256), nullable=True)\n    )\n\n    # copy the name as code\n    logger.info(\n        \"filling data to the code column in Repositories table from \"\n        \"Repositories.name column\"\n    )\n    op.execute(\n        r\"\"\"UPDATE \"Repositories\"\n        SET code = (\n            SELECT REGEXP_REPLACE(name, '\\s+', '')\n            FROM \"SimpleEntities\" WHERE id=\"Repositories\".id\n        )\"\"\"\n    )\n    logger.info(\"set code column to not nullable\")\n    op.alter_column(\"Repositories\", \"code\", nullable=False)\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    logger.info(\"removing code column from Repositories table\")\n    op.drop_column(\"Repositories\", \"code\")\n"
  },
  {
    "path": "alembic/versions/c5607b4cfb0a_added_support_for_time_zones.py",
    "content": "\"\"\"Added support for time zones.\n\nRevision ID: c5607b4cfb0a\nRevises: 0063f547dc2e\nCreate Date: 2017-03-09 02:17:08.209000\n\"\"\"\n\nimport logging\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"c5607b4cfb0a\"\ndown_revision = \"0063f547dc2e\"\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.INFO)\n\ntables_to_update = {\n    \"AuthenticationLogs\": [\"date\"],\n    \"Tasks\": [\"computed_start\", \"computed_end\", \"start\", \"end\"],\n    \"Studios\": [\n        \"computed_start\",\n        \"computed_end\",\n        \"start\",\n        \"end\",\n        \"scheduling_started_at\",\n        \"last_scheduled_at\",\n    ],\n    \"SimpleEntities\": [\"date_created\", \"date_updated\"],\n    \"Projects\": [\"computed_start\", \"computed_end\", \"start\", \"end\"],\n    \"TimeLogs\": [\"computed_start\", \"computed_end\", \"start\", \"end\"],\n    \"Vacations\": [\"computed_start\", \"computed_end\", \"start\", \"end\"],\n}\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # Directly updating the columns will set the timezone of the datetime\n    # fields to the timezone of the machine that is running this code.\n    #\n    # Because the data in the database is already in UTC we need to update the\n    # data also to have their time values correctly shifted to UTC.\n    for table_name in tables_to_update:\n        logger.info(f\"upgrading table: {table_name}\")\n        with op.batch_alter_table(table_name) as batch_op:\n            for column_name in tables_to_update[table_name]:\n                logger.info(f\"altering column: {column_name}\")\n                batch_op.alter_column(column_name, type_=sa.DateTime(timezone=True))\n\n        sql = \"\"\"\n        -- Add the time zone offset\n        UPDATE\n          \"{table_name}\"\n        SET\n        \"\"\".format(\n            table_name=table_name\n        )\n\n        for i, column_name in enumerate(tables_to_update[table_name]):\n            if i > 0:\n                sql = \"{sql},\\n\".format(sql=sql)\n\n            # per column add\n            sql = f\"\"\"{sql}\n              \"{column_name}\" = (\n                SELECT\n                  aliased_table.{column_name}::timestamp at time zone 'utc'\n                FROM \"{table_name}\" as aliased_table\n                where aliased_table.id = \"{table_name}\".id\n              )\"\"\"\n\n        op.execute(sql)\n        logger.info(f\"done upgrading table: {table_name}\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # Removing the timezone info will not shift the time values. So shift the\n    # values by hand\n    for table_name in tables_to_update:\n        logger.info(f\"downgrading table: {table_name}\")\n        sql = f\"\"\"\n        -- Add the time zone offset\n        UPDATE\n          \"{table_name}\"\n        SET\n        \"\"\"\n\n        for i, column_name in enumerate(tables_to_update[table_name]):\n            if i > 0:\n                sql = f\"{sql},\\n\"\n\n            # per column add\n            sql = f\"\"\"{sql}\n                \"{column_name}\" = (\n                SELECT\n                    CAST(aliased_table.{column_name} at time zone 'utc'\n                    AS timestamp with time zone)\n                FROM \"{table_name}\" as aliased_table\n                where aliased_table.id = \"{table_name}\".id\n                )\"\"\"\n        op.execute(sql)\n        logger.info(f\"raw sql completed for table: {table_name}\")\n\n        with op.batch_alter_table(table_name) as batch_op:\n            for column_name in tables_to_update[table_name]:\n                batch_op.alter_column(column_name, type_=sa.DateTime(timezone=False))\n\n        logger.info(f\"done downgrading table: {table_name}\")\n"
  },
  {
    "path": "alembic/versions/d8421de6a206_added_project_users_rate_column.py",
    "content": "\"\"\"Added \"Project_Users.rate\".\n\nRevision ID: d8421de6a206\nRevises: 92257ba439e1\nCreate Date: 2016-08-17 19:27:00.358000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"d8421de6a206\"\ndown_revision = \"92257ba439e1\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.add_column(\"Project_Users\", sa.Column(\"rate\", sa.Float(), nullable=True))\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.execute(\"\"\"ALTER TABLE public.\"Project_Users\" DROP COLUMN IF EXISTS rate;\"\"\")\n"
  },
  {
    "path": "alembic/versions/e25ec9930632_shot_sequence_relation_is_now_many_to_.py",
    "content": "\"\"\"Shot Sequence relation is now many-to-one\n\nRevision ID: e25ec9930632\nRevises: 4400871fa852\nCreate Date: 2024-11-16 00:27:54.060738\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"e25ec9930632\"\ndown_revision = \"4400871fa852\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    # Add sequence_id column\n    op.add_column(\"Shots\", sa.Column(\"sequence_id\", sa.Integer(), nullable=True))\n\n    # Create foreign key constraint\n    op.create_foreign_key(None, \"Shots\", \"Sequences\", [\"sequence_id\"], [\"id\"])\n\n    # Migrate the data\n    op.execute(\n        \"\"\"UPDATE \"Shots\" SET sequence_id = (\n        SELECT sequence_id\n            FROM \"Shot_Sequences\"\n            WHERE \"Shot_Sequences\".shot_id = \"Shots\".id LIMIT 1\n    )\"\"\"\n    )\n\n    # Drop Shot_Sequences Table\n    op.execute(\"\"\"DROP TABLE \"Shot_Sequences\" \"\"\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    # Add Shot_Sequences Table\n    op.create_table(\n        \"Shot_Sequences\",\n        sa.Column(\"shot_id\", sa.Integer(), nullable=False),\n        sa.Column(\"sequence_id\", sa.Integer(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"shot_id\"],\n            [\"Shots.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"sequence_id\"],\n            [\"Sequences.id\"],\n        ),\n    )\n\n    # Transfer Data\n    op.execute(\n        \"\"\"\n    UPDATE \"Shot_Sequences\" SET shot_id, sequence_id = (\n        SELECT id, sequence_id FROM \"Shots\" WHERE \"Shots\".sequence_id != NULL\n    )\n    \"\"\"\n    )\n\n    # Drop foreign key constraint\n    op.drop_constraint(\"Shots_sequence_id_fkey\", \"Shots\", type_=\"foreignkey\")\n\n    # drop Shots.sequence_id column\n    op.drop_column(\"Shots\", \"sequence_id\")\n"
  },
  {
    "path": "alembic/versions/ea28a39ba3f5_added_invoices_table.py",
    "content": "\"\"\"Added Invoices table.\n\nRevision ID: ea28a39ba3f5\nRevises: 92257ba439e1\nCreate Date: 2016-08-17 19:21:40.428000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"ea28a39ba3f5\"\ndown_revision = \"d8421de6a206\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"Invoices\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.Column(\"budget_id\", sa.Integer(), nullable=True),\n        sa.Column(\"client_id\", sa.Integer(), nullable=True),\n        sa.Column(\"amount\", sa.Float(), nullable=True),\n        sa.Column(\"unit\", sa.String(length=64), nullable=True),\n        sa.ForeignKeyConstraint(\n            [\"budget_id\"],\n            [\"Budgets.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"client_id\"],\n            [\"Clients.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.drop_table(\"Invoices\")\n"
  },
  {
    "path": "alembic/versions/eaed49db6d9_added_position_column_to_Project_Repositories.py",
    "content": "\"\"\"Added position column to Project_Repositories table.\n\nRevision ID: eaed49db6d9\nRevises: 583875229230\nCreate Date: 2015-02-10 16:08:03.449570\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n# revision identifiers, used by Alembic.\nrevision = \"eaed49db6d9\"\ndown_revision = \"583875229230\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    with op.batch_alter_table(\"Project_Repositories\", schema=None) as batch_op:\n        batch_op.add_column(sa.Column(\"position\", sa.Integer(), nullable=True))\n        batch_op.alter_column(\"repo_id\", new_column_name=\"repository_id\")\n\n    # insert zeros as the position value\n    op.execute(\n        \"\"\"update \"Project_Repositories\"\n        set position=0\n        \"\"\"\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    with op.batch_alter_table(\"Project_Repositories\", schema=None) as batch_op:\n        batch_op.alter_column(\"repository_id\", new_column_name=\"repo_id\")\n        batch_op.drop_column(\"position\")\n"
  },
  {
    "path": "alembic/versions/ec1eb2151bb9_rename_version_take_name_to_version_.py",
    "content": "\"\"\"Rename Version.take_name to Version.variant_name\n\nRevision ID: ec1eb2151bb9\nRevises: 019378697b5b\nCreate Date: 2024-11-01 16:37:18.048904\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"ec1eb2151bb9\"\ndown_revision = \"019378697b5b\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.alter_column(\"Versions\", \"take_name\", new_column_name=\"variant_name\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.alter_column(\"Versions\", \"variant_name\", new_column_name=\"take_name\")\n"
  },
  {
    "path": "alembic/versions/ed0167fff399_added_workinghours_table.py",
    "content": "\"\"\"Added WorkingHours table.\n\nRevision ID: ed0167fff399\nRevises: 1181305d3001\nCreate Date: 2017-05-20 14:32:48.388000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\nfrom sqlalchemy.dialects import postgresql\n\n# revision identifiers, used by Alembic.\nrevision = \"ed0167fff399\"\ndown_revision = \"1181305d3001\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"WorkingHours\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.Column(\"working_hours\", sa.JSON(), nullable=True),\n        sa.Column(\"daily_working_hours\", sa.Integer(), nullable=True),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"Entities.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n\n    op.add_column(\"Studios\", sa.Column(\"working_hours_id\", sa.Integer(), nullable=True))\n    op.create_foreign_key(None, \"Studios\", \"WorkingHours\", [\"working_hours_id\"], [\"id\"])\n    op.drop_column(\"Studios\", \"working_hours\")\n    op.alter_column(\"Studios\", \"last_schedule_message\", type_=sa.Text)\n\n    # warn the user to recreate the working hours\n    # because of the nature of Pickle it is very hard to do it here\n    print(\"Warning! Can not keep WorkingHours data of Studios.\")\n    print(\"Please, recreate the WorkingHours for all Studio instances!\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\n        \"Studios\",\n        sa.Column(\n            \"working_hours\", postgresql.BYTEA(), autoincrement=False, nullable=True\n        ),\n    )\n    op.drop_constraint(\"Studios_working_hours_id_fkey\", \"Studios\", type_=\"foreignkey\")\n    op.drop_column(\"Studios\", \"working_hours_id\")\n    op.drop_table(\"WorkingHours\")\n    op.execute(\n        'ALTER TABLE \"Studios\"'\n        \"ALTER COLUMN last_schedule_message TYPE BYTEA \"\n        \"USING last_schedule_message::bytea\"\n    )\n    print(\"Warning! Can not keep WorkingHours instances.\")\n    print(\"Please, recreate the WorkingHours for all Studio instances!\")\n"
  },
  {
    "path": "alembic/versions/f16651477e64_added_authenticationlog_class.py",
    "content": "\"\"\"Added AuthenticationLog class.\n\nRevision ID: f16651477e64\nRevises: 255ee1f9c7b3\nCreate Date: 2016-11-15 00:22:16.438000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\nfrom sqlalchemy.dialects import postgresql\n\n# revision identifiers, used by Alembic.\nrevision = \"f16651477e64\"\ndown_revision = \"255ee1f9c7b3\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"AuthenticationLogs\",\n        sa.Column(\"id\", sa.Integer(), nullable=False),\n        sa.Column(\"uid\", sa.Integer(), nullable=False),\n        sa.Column(\n            \"action\",\n            sa.Enum(\"login\", \"logout\", name=\"AuthenticationActions\"),\n            nullable=False,\n        ),\n        sa.Column(\"date\", sa.DateTime(), nullable=False),\n        sa.ForeignKeyConstraint(\n            [\"id\"],\n            [\"SimpleEntities.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"uid\"],\n            [\"Users.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"id\"),\n    )\n    op.drop_column(\"Users\", \"last_login\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.add_column(\n        \"Users\",\n        sa.Column(\n            \"last_login\", postgresql.TIMESTAMP(), autoincrement=False, nullable=True\n        ),\n    )\n    op.drop_table(\"AuthenticationLogs\")\n"
  },
  {
    "path": "alembic/versions/f2005d1fbadc_added_projectclients.py",
    "content": "\"\"\"Added ProjectClients.\n\nRevision ID: f2005d1fbadc\nRevises: 258985128aff\nCreate Date: 2016-06-27 14:33:10.642000\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\nfrom sqlalchemy.dialects import postgresql\n\n# revision identifiers, used by Alembic.\nrevision = \"f2005d1fbadc\"\ndown_revision = \"745b210e6907\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.create_table(\n        \"Project_Clients\",\n        sa.Column(\"client_id\", sa.Integer(), nullable=False),\n        sa.Column(\"project_id\", sa.Integer(), nullable=False),\n        sa.Column(\"rid\", sa.Integer(), nullable=True),\n        sa.ForeignKeyConstraint(\n            [\"client_id\"],\n            [\"Clients.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"project_id\"],\n            [\"Projects.id\"],\n        ),\n        sa.ForeignKeyConstraint(\n            [\"rid\"],\n            [\"Roles.id\"],\n        ),\n        sa.PrimaryKeyConstraint(\"client_id\", \"project_id\"),\n    )\n\n    # before doing anything store current project clients\n    op.execute(\n        \"\"\"insert into \"Project_Clients\"\n          select client_id, id, NULL\n          from \"Projects\"\n          where \"Projects\".client_id is not NULL\n        \"\"\"\n    )\n\n    # create missing constraints if any\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"BudgetEntries\" DROP CONSTRAINT IF EXISTS \"BudgetEntries_good_id_fkey\";\n    ALTER TABLE \"BudgetEntries\"\n      ADD CONSTRAINT \"BudgetEntries_good_id_fkey\" FOREIGN KEY (good_id)\n          REFERENCES public.\"Goods\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"Budgets\" DROP CONSTRAINT IF EXISTS \"Budgets_parent_id_fkey\";\n    ALTER TABLE public.\"Budgets\"\n      ADD CONSTRAINT \"Budgets_parent_id_fkey\" FOREIGN KEY (parent_id)\n          REFERENCES public.\"Budgets\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"Dailies\" DROP CONSTRAINT IF EXISTS \"Dailies_project_id_fkey\";\n    ALTER TABLE public.\"Dailies\"\n      ADD CONSTRAINT \"Dailies_project_id_fkey\" FOREIGN KEY (project_id)\n          REFERENCES public.\"Projects\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"Department_Users\" DROP CONSTRAINT\n        IF EXISTS \"Department_Users_rid_fkey\";\n    ALTER TABLE public.\"Department_Users\"\n      ADD CONSTRAINT \"Department_Users_rid_fkey\" FOREIGN KEY (rid)\n          REFERENCES public.\"Roles\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"Pages\" DROP CONSTRAINT IF EXISTS \"Pages_project_id_fkey\";\n    ALTER TABLE public.\"Pages\"\n      ADD CONSTRAINT \"Pages_project_id_fkey\" FOREIGN KEY (project_id)\n          REFERENCES public.\"Projects\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"Project_Users\" DROP CONSTRAINT IF EXISTS \"Project_Users_rid_fkey\";\n    ALTER TABLE public.\"Project_Users\"\n      ADD CONSTRAINT \"Project_Users_rid_fkey\" FOREIGN KEY (rid)\n          REFERENCES public.\"Roles\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.drop_constraint(\"Projects_client_id_fkey\", \"Projects\", type_=\"foreignkey\")\n    op.drop_column(\"Projects\", \"client_id\")\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"Scenes\" DROP CONSTRAINT IF EXISTS \"Scenes_project_id_fkey\";\n    ALTER TABLE public.\"Scenes\"\n      ADD CONSTRAINT \"Scenes_project_id_fkey\" FOREIGN KEY (project_id)\n          REFERENCES public.\"Projects\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"SimpleEntities\" DROP CONSTRAINT IF EXISTS xu;\n    ALTER TABLE \"SimpleEntities\"\n      ADD CONSTRAINT xu FOREIGN KEY (updated_by_id)\n          REFERENCES public.\"Users\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"SimpleEntities\" DROP CONSTRAINT IF EXISTS z;\n    ALTER TABLE public.\"SimpleEntities\"\n      ADD CONSTRAINT z FOREIGN KEY (thumbnail_id)\n          REFERENCES public.\"Links\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"SimpleEntities\" DROP CONSTRAINT IF EXISTS y;\n    ALTER TABLE public.\"SimpleEntities\"\n      ADD CONSTRAINT y FOREIGN KEY (type_id)\n          REFERENCES public.\"Types\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"Studios\" DROP CONSTRAINT IF EXISTS \"Studios_last_scheduled_by_id_fkey\";\n    ALTER TABLE \"Studios\"\n      ADD CONSTRAINT \"Studios_last_scheduled_by_id_fkey\"\n        FOREIGN KEY (last_scheduled_by_id)\n          REFERENCES public.\"Users\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"Studios\" DROP CONSTRAINT IF EXISTS \"Studios_is_scheduling_by_id_fkey\";\n    ALTER TABLE \"Studios\"\n      ADD CONSTRAINT \"Studios_is_scheduling_by_id_fkey\"\n        FOREIGN KEY (is_scheduling_by_id)\n          REFERENCES public.\"Users\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n    op.alter_column(\n        \"Task_Dependencies\",\n        \"gap_timing\",\n        existing_type=postgresql.DOUBLE_PRECISION(precision=53),\n        nullable=True,\n    )\n\n    op.alter_column(\n        \"Task_Dependencies\",\n        \"gap_unit\",\n        existing_type=postgresql.VARCHAR(length=256),\n        nullable=True,\n    )\n\n    op.execute(\n        \"\"\"\n    ALTER TABLE \"Tasks\" DROP CONSTRAINT IF EXISTS \"Tasks_good_id_fkey\";\n    ALTER TABLE public.\"Tasks\"\n      ADD CONSTRAINT \"Tasks_good_id_fkey\" FOREIGN KEY (good_id)\n          REFERENCES public.\"Goods\" (id) MATCH SIMPLE\n          ON UPDATE NO ACTION ON DELETE NO ACTION;\n    \"\"\"\n    )\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.execute('ALTER TABLE \"Tasks\" DROP CONSTRAINT \"Tasks_good_id_fkey\"')\n    op.alter_column(\n        \"Task_Dependencies\",\n        \"gap_unit\",\n        existing_type=postgresql.VARCHAR(length=256),\n        nullable=True,\n    )\n    op.alter_column(\n        \"Task_Dependencies\",\n        \"gap_timing\",\n        existing_type=postgresql.DOUBLE_PRECISION(precision=53),\n        nullable=True,\n    )\n\n    op.add_column(\n        \"Projects\",\n        sa.Column(\"client_id\", sa.INTEGER(), autoincrement=False, nullable=True),\n    )\n    op.create_foreign_key(\n        \"Projects_client_id_fkey\", \"Projects\", \"Clients\", [\"client_id\"], [\"id\"]\n    )\n\n    # before dropping the Project_Clients, add the first client as the\n    # Project.client_id\n    op.execute(\n        \"\"\"\n        update \"Projects\"\n          set client_id = (\n            select\n              client_id\n            from \"Project_Clients\"\n            where project_id = \"Projects\".id limit 1\n          )\n        \"\"\"\n    )\n\n    op.drop_table(\"Project_Clients\")\n"
  },
  {
    "path": "alembic/versions/feca9bac7d5a_renamed_osx_to_macos.py",
    "content": "\"\"\"Renamed OSX to macOS\n\nRevision ID: feca9bac7d5a\nRevises: bf67e6a234b4\nCreate Date: 2024-11-01 12:22:24.818481\n\"\"\"\n\nfrom alembic import op\n\nimport sqlalchemy as sa\n\n\n# revision identifiers, used by Alembic.\nrevision = \"feca9bac7d5a\"\ndown_revision = \"bf67e6a234b4\"\n\n\ndef upgrade():\n    \"\"\"Upgrade the tables.\"\"\"\n    op.alter_column(\"Repositories\", \"osx_path\", new_column_name=\"macos_path\")\n\n\ndef downgrade():\n    \"\"\"Downgrade the tables.\"\"\"\n    op.alter_column(\"Repositories\", \"macos_path\", new_column_name=\"osx_path\")\n"
  },
  {
    "path": "alembic.ini",
    "content": "# A generic, single database configuration.\n\n[alembic]\n# path to migration scripts\nscript_location = alembic\n\n# template used to generate migration files\n# file_template = %%(rev)s_%%(slug)s\n\n# set to 'true' to run the environment during\n# the 'revision' command, regardless of autogenerate\n# revision_environment = false\n\n#sqlalchemy.url = sqlite:///%(here)s/stalker.db\nsqlalchemy.url = postgresql://stalker_admin:stalker@localhost:5432/stalker\n\n\n# Logging configuration\n[loggers]\nkeys = root,sqlalchemy,alembic\n\n[handlers]\nkeys = console\n\n[formatters]\nkeys = generic\n\n[logger_root]\nlevel = WARN\nhandlers = console\nqualname =\n\n[logger_sqlalchemy]\nlevel = WARN\nhandlers =\nqualname = sqlalchemy.engine\n\n[logger_alembic]\nlevel = INFO\nhandlers =\nqualname = alembic\n\n[handler_console]\nclass = StreamHandler\nargs = (sys.stderr,)\nlevel = NOTSET\nformatter = generic\n\n[formatter_generic]\nformat = %(levelname)-5.5s [%(name)s] %(message)s\ndatefmt = %H:%M:%S\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD   = sphinx-build\nPAPER         =\nBUILDDIR      = build\n\n# Internal variables.\nPAPEROPT_a4     = -D latex_paper_size=a4\nPAPEROPT_letter = -D latex_paper_size=letter\nALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source\n# the i18n builder cannot share the environment and doctrees with the others\nI18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source\n\n.PHONY: help\nhelp:\n\t@echo \"Please use \\`make <target>' where <target> is one of\"\n\t@echo \"  html       to make standalone HTML files\"\n\t@echo \"  dirhtml    to make HTML files named index.html in directories\"\n\t@echo \"  singlehtml to make a single large HTML file\"\n\t@echo \"  pickle     to make pickle files\"\n\t@echo \"  json       to make JSON files\"\n\t@echo \"  htmlhelp   to make HTML files and a HTML help project\"\n\t@echo \"  qthelp     to make HTML files and a qthelp project\"\n\t@echo \"  applehelp  to make an Apple Help Book\"\n\t@echo \"  devhelp    to make HTML files and a Devhelp project\"\n\t@echo \"  epub       to make an epub\"\n\t@echo \"  epub3      to make an epub3\"\n\t@echo \"  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\"\n\t@echo \"  latexpdf   to make LaTeX files and run them through pdflatex\"\n\t@echo \"  latexpdfja to make LaTeX files and run them through platex/dvipdfmx\"\n\t@echo \"  text       to make text files\"\n\t@echo \"  man        to make manual pages\"\n\t@echo \"  texinfo    to make Texinfo files\"\n\t@echo \"  info       to make Texinfo files and run them through makeinfo\"\n\t@echo \"  gettext    to make PO message catalogs\"\n\t@echo \"  changes    to make an overview of all changed/added/deprecated items\"\n\t@echo \"  xml        to make Docutils-native XML files\"\n\t@echo \"  pseudoxml  to make pseudoxml-XML files for display purposes\"\n\t@echo \"  linkcheck  to check all external links for integrity\"\n\t@echo \"  doctest    to run all doctests embedded in the documentation (if enabled)\"\n\t@echo \"  coverage   to run coverage check of the documentation (if enabled)\"\n\t@echo \"  dummy      to check syntax errors of document sources\"\n\n.PHONY: clean\nclean:\n\trm -rf $(BUILDDIR)/*\n\trm -rf source/generated/*\n\n.PHONY: html\nhtml:\n\t$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/html.\"\n\n.PHONY: dirhtml\ndirhtml:\n\t$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml\n\t@echo\n\t@echo \"Build finished. The HTML pages are in $(BUILDDIR)/dirhtml.\"\n\n.PHONY: singlehtml\nsinglehtml:\n\t$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml\n\t@echo\n\t@echo \"Build finished. The HTML page is in $(BUILDDIR)/singlehtml.\"\n\n.PHONY: pickle\npickle:\n\t$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle\n\t@echo\n\t@echo \"Build finished; now you can process the pickle files.\"\n\n.PHONY: json\njson:\n\t$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json\n\t@echo\n\t@echo \"Build finished; now you can process the JSON files.\"\n\n.PHONY: htmlhelp\nhtmlhelp:\n\t$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp\n\t@echo\n\t@echo \"Build finished; now you can run HTML Help Workshop with the\" \\\n\t      \".hhp project file in $(BUILDDIR)/htmlhelp.\"\n\n.PHONY: qthelp\nqthelp:\n\t$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp\n\t@echo\n\t@echo \"Build finished; now you can run \"qcollectiongenerator\" with the\" \\\n\t      \".qhcp project file in $(BUILDDIR)/qthelp, like this:\"\n\t@echo \"# qcollectiongenerator $(BUILDDIR)/qthelp/Stalker.qhcp\"\n\t@echo \"To view the help file:\"\n\t@echo \"# assistant -collectionFile $(BUILDDIR)/qthelp/Stalker.qhc\"\n\n.PHONY: applehelp\napplehelp:\n\t$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp\n\t@echo\n\t@echo \"Build finished. The help book is in $(BUILDDIR)/applehelp.\"\n\t@echo \"N.B. You won't be able to view it unless you put it in\" \\\n\t      \"~/Library/Documentation/Help or install it in your application\" \\\n\t      \"bundle.\"\n\n.PHONY: devhelp\ndevhelp:\n\t$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp\n\t@echo\n\t@echo \"Build finished.\"\n\t@echo \"To view the help file:\"\n\t@echo \"# mkdir -p $$HOME/.local/share/devhelp/Stalker\"\n\t@echo \"# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Stalker\"\n\t@echo \"# devhelp\"\n\n.PHONY: epub\nepub:\n\t$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub\n\t@echo\n\t@echo \"Build finished. The epub file is in $(BUILDDIR)/epub.\"\n\n.PHONY: epub3\nepub3:\n\t$(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3\n\t@echo\n\t@echo \"Build finished. The epub3 file is in $(BUILDDIR)/epub3.\"\n\n.PHONY: latex\nlatex:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\tcp texinputs/* $(BUILDDIR)/latex/\n\t@echo\n\t@echo \"Build finished; the LaTeX files are in $(BUILDDIR)/latex.\"\n\t@echo \"Run \\`make' in that directory to run these through (pdf)latex\" \\\n\t      \"(use \\`make latexpdf' here to do that automatically).\"\n\n.PHONY: latexpdf\nlatexpdf:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through pdflatex...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\n.PHONY: latexpdfja\nlatexpdfja:\n\t$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex\n\t@echo \"Running LaTeX files through platex and dvipdfmx...\"\n\t$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja\n\t@echo \"pdflatex finished; the PDF files are in $(BUILDDIR)/latex.\"\n\n.PHONY: text\ntext:\n\t$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text\n\t@echo\n\t@echo \"Build finished. The text files are in $(BUILDDIR)/text.\"\n\n.PHONY: man\nman:\n\t$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man\n\t@echo\n\t@echo \"Build finished. The manual pages are in $(BUILDDIR)/man.\"\n\n.PHONY: texinfo\ntexinfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo\n\t@echo \"Build finished. The Texinfo files are in $(BUILDDIR)/texinfo.\"\n\t@echo \"Run \\`make' in that directory to run these through makeinfo\" \\\n\t      \"(use \\`make info' here to do that automatically).\"\n\n.PHONY: info\ninfo:\n\t$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo\n\t@echo \"Running Texinfo files through makeinfo...\"\n\tmake -C $(BUILDDIR)/texinfo info\n\t@echo \"makeinfo finished; the Info files are in $(BUILDDIR)/texinfo.\"\n\n.PHONY: gettext\ngettext:\n\t$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale\n\t@echo\n\t@echo \"Build finished. The message catalogs are in $(BUILDDIR)/locale.\"\n\n.PHONY: changes\nchanges:\n\t$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes\n\t@echo\n\t@echo \"The overview file is in $(BUILDDIR)/changes.\"\n\n.PHONY: linkcheck\nlinkcheck:\n\t$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck\n\t@echo\n\t@echo \"Link check complete; look for any errors in the above output \" \\\n\t      \"or in $(BUILDDIR)/linkcheck/output.txt.\"\n\n.PHONY: doctest\ndoctest:\n\t$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest\n\t@echo \"Testing of doctests in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/doctest/output.txt.\"\n\n.PHONY: coverage\ncoverage:\n\t$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage\n\t@echo \"Testing of coverage in the sources finished, look at the \" \\\n\t      \"results in $(BUILDDIR)/coverage/python.txt.\"\n\n.PHONY: xml\nxml:\n\t$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml\n\t@echo\n\t@echo \"Build finished. The XML files are in $(BUILDDIR)/xml.\"\n\n.PHONY: pseudoxml\npseudoxml:\n\t$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml\n\t@echo\n\t@echo \"Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml.\"\n\n.PHONY: dummy\ndummy:\n\t$(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy\n\t@echo\n\t@echo \"Build finished. Dummy builder generates no files.\"\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\n\nREM Command file for Sphinx documentation\n\nset SPHINXBUILD=..\\..\\Scripts\\sphinx-build.exe\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=..\\..\\Scripts\\sphinx-build\n)\nset BUILDDIR=build\nset ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source\nset I18NSPHINXOPTS=%SPHINXOPTS% source\nif NOT \"%PAPER%\" == \"\" (\n\tset ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%\n\tset I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%\n)\n\nif \"%1\" == \"\" goto help\n\nif \"%1\" == \"help\" (\n\t:help\n\techo.Please use `make ^<target^>` where ^<target^> is one of\n\techo.  html       to make standalone HTML files\n\techo.  dirhtml    to make HTML files named index.html in directories\n\techo.  singlehtml to make a single large HTML file\n\techo.  pickle     to make pickle files\n\techo.  json       to make JSON files\n\techo.  htmlhelp   to make HTML files and a HTML help project\n\techo.  qthelp     to make HTML files and a qthelp project\n\techo.  devhelp    to make HTML files and a Devhelp project\n\techo.  epub       to make an epub\n\techo.  epub3      to make an epub3\n\techo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter\n\techo.  text       to make text files\n\techo.  man        to make manual pages\n\techo.  texinfo    to make Texinfo files\n\techo.  gettext    to make PO message catalogs\n\techo.  changes    to make an overview over all changed/added/deprecated items\n\techo.  xml        to make Docutils-native XML files\n\techo.  pseudoxml  to make pseudoxml-XML files for display purposes\n\techo.  linkcheck  to check all external links for integrity\n\techo.  doctest    to run all doctests embedded in the documentation if enabled\n\techo.  coverage   to run coverage check of the documentation if enabled\n\techo.  dummy      to check syntax errors of document sources\n\tgoto end\n)\n\nif \"%1\" == \"clean\" (\n\tfor /d %%i in (%BUILDDIR%\\*) do rmdir /q /s %%i\n\tdel /q /s %BUILDDIR%\\*\n\tgoto end\n)\n\n\nREM Check if sphinx-build is available and fallback to Python version if any\n%SPHINXBUILD% 1>NUL 2>NUL\nif errorlevel 9009 goto sphinx_python\ngoto sphinx_ok\n\n:sphinx_python\n\nset SPHINXBUILD=python -m sphinx.__init__\n%SPHINXBUILD% 2> nul\nif errorlevel 9009 (\n\techo.\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\n\techo.installed, then set the SPHINXBUILD environment variable to point\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\n\techo.may add the Sphinx directory to PATH.\n\techo.\n\techo.If you don't have Sphinx installed, grab it from\n\techo.http://sphinx-doc.org/\n\texit /b 1\n)\n\n:sphinx_ok\n\n\nif \"%1\" == \"html\" (\n\t%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The HTML pages are in %BUILDDIR%/html.\n\tgoto end\n)\n\nif \"%1\" == \"dirhtml\" (\n\t%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.\n\tgoto end\n)\n\nif \"%1\" == \"singlehtml\" (\n\t%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.\n\tgoto end\n)\n\nif \"%1\" == \"pickle\" (\n\t%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can process the pickle files.\n\tgoto end\n)\n\nif \"%1\" == \"json\" (\n\t%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can process the JSON files.\n\tgoto end\n)\n\nif \"%1\" == \"htmlhelp\" (\n\t%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can run HTML Help Workshop with the ^\n.hhp project file in %BUILDDIR%/htmlhelp.\n\tgoto end\n)\n\nif \"%1\" == \"qthelp\" (\n\t%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; now you can run \"qcollectiongenerator\" with the ^\n.qhcp project file in %BUILDDIR%/qthelp, like this:\n\techo.^> qcollectiongenerator %BUILDDIR%\\qthelp\\Stalker.qhcp\n\techo.To view the help file:\n\techo.^> assistant -collectionFile %BUILDDIR%\\qthelp\\Stalker.ghc\n\tgoto end\n)\n\nif \"%1\" == \"devhelp\" (\n\t%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished.\n\tgoto end\n)\n\nif \"%1\" == \"epub\" (\n\t%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The epub file is in %BUILDDIR%/epub.\n\tgoto end\n)\n\nif \"%1\" == \"epub3\" (\n\t%SPHINXBUILD% -b epub3 %ALLSPHINXOPTS% %BUILDDIR%/epub3\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The epub3 file is in %BUILDDIR%/epub3.\n\tgoto end\n)\n\nif \"%1\" == \"latex\" (\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished; the LaTeX files are in %BUILDDIR%/latex.\n\tgoto end\n)\n\nif \"%1\" == \"latexpdf\" (\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\n\tcd %BUILDDIR%/latex\n\tmake all-pdf\n\tcd %~dp0\n\techo.\n\techo.Build finished; the PDF files are in %BUILDDIR%/latex.\n\tgoto end\n)\n\nif \"%1\" == \"latexpdfja\" (\n\t%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex\n\tcd %BUILDDIR%/latex\n\tmake all-pdf-ja\n\tcd %~dp0\n\techo.\n\techo.Build finished; the PDF files are in %BUILDDIR%/latex.\n\tgoto end\n)\n\nif \"%1\" == \"text\" (\n\t%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The text files are in %BUILDDIR%/text.\n\tgoto end\n)\n\nif \"%1\" == \"man\" (\n\t%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The manual pages are in %BUILDDIR%/man.\n\tgoto end\n)\n\nif \"%1\" == \"texinfo\" (\n\t%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.\n\tgoto end\n)\n\nif \"%1\" == \"gettext\" (\n\t%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The message catalogs are in %BUILDDIR%/locale.\n\tgoto end\n)\n\nif \"%1\" == \"changes\" (\n\t%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.The overview file is in %BUILDDIR%/changes.\n\tgoto end\n)\n\nif \"%1\" == \"linkcheck\" (\n\t%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Link check complete; look for any errors in the above output ^\nor in %BUILDDIR%/linkcheck/output.txt.\n\tgoto end\n)\n\nif \"%1\" == \"doctest\" (\n\t%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Testing of doctests in the sources finished, look at the ^\nresults in %BUILDDIR%/doctest/output.txt.\n\tgoto end\n)\n\nif \"%1\" == \"coverage\" (\n\t%SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Testing of coverage in the sources finished, look at the ^\nresults in %BUILDDIR%/coverage/python.txt.\n\tgoto end\n)\n\nif \"%1\" == \"xml\" (\n\t%SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The XML files are in %BUILDDIR%/xml.\n\tgoto end\n)\n\nif \"%1\" == \"pseudoxml\" (\n\t%SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml.\n\tgoto end\n)\n\nif \"%1\" == \"dummy\" (\n\t%SPHINXBUILD% -b dummy %ALLSPHINXOPTS% %BUILDDIR%/dummy\n\tif errorlevel 1 exit /b 1\n\techo.\n\techo.Build finished. Dummy builder generates no files.\n\tgoto end\n)\n\n:end\n"
  },
  {
    "path": "docs/make_html.bat",
    "content": "..\\..\\sphinx-build.exe -b html -D graphviz_dot=\"dot.exe\" source build\\html"
  },
  {
    "path": "docs/source/_static/images/Task_Status_Workflow.vue",
    "content": "<!-- Tufts VUE 3.2.2 concept-map (Task_Status_Workflow.vue) 2015-06-18 -->\n<!-- Tufts VUE: http://vue.tufts.edu/ -->\n<!-- Do Not Remove: VUE mapping @version(1.1) jar:file:/home/eoyilmaz/.local/share/vue/VUE.jar!/tufts/vue/resources/lw_mapping_1_1.xml -->\n<!-- Do Not Remove: Saved date Thu Jun 18 04:28:12 EEST 2015 by eoyilmaz on platform Linux 4.0.4-301.fc22.x86_64 in JVM 1.8.0_45-b14 -->\n<!-- Do Not Remove: Saving version @(#)VUE: built May 23 2013 at 2146 by tomadm on Linux 2.6.18-348.2.1.el5 i386 JVM 1.7.0_21-b11(bits=32) -->\n<?xml version=\"1.0\" encoding=\"US-ASCII\"?>\n<LW-MAP xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:noNamespaceSchemaLocation=\"none\" ID=\"0\"\n    label=\"Task_Status_Workflow.vue\" created=\"1388347735325\" x=\"0.0\"\n    y=\"0.0\" width=\"1.4E-45\" height=\"1.4E-45\" strokeWidth=\"0.0\" autoSized=\"false\">\n    <resource referenceCreated=\"1434590892499\" size=\"35158\"\n        spec=\"/home/eoyilmaz/Documents/development/stalker/stalker/docs/source/_static/images/Task_Status_Workflow.vue\"\n        type=\"1\" xsi:type=\"URLResource\">\n        <title>Task_Status_Workflow.vue</title>\n        <property key=\"File\" value=\"/home/eoyilmaz/Documents/development/stalker/stalker/docs/source/_static/images/Task_Status_Workflow.vue\"/>\n    </resource>\n    <fillColor>#FFFFFF</fillColor>\n    <strokeColor>#404040</strokeColor>\n    <textColor>#000000</textColor>\n    <font>SansSerif-plain-14</font>\n    <URIString>http://vue.tufts.edu/rdf/resource/400bcd2639fad37c2b0b7e545266b52a</URIString>\n    <child ID=\"6\" label=\"WFD\" layerID=\"1\" created=\"1388347755161\"\n        x=\"455.60834\" y=\"18.66666\" width=\"82.3916\" height=\"79.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#D0D0D0</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd2939fad37c2b0b7e54a889347a</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"7\" label=\"RTS\" layerID=\"1\" created=\"1388347803612\"\n        x=\"622.94165\" y=\"121.83334\" width=\"82.3916\" height=\"80.83333\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FC938D</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd2c39fad37c2b0b7e54864540f3</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"8\" label=\"task.depends_on[*].approve&#xa;or no dependency\"\n        layerID=\"1\" created=\"1388347803614\" x=\"533.74115\" y=\"70.56363\"\n        width=\"121.582825\" height=\"63.072296\" strokeWidth=\"3.0\"\n        strokeStyle=\"4\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#EA2218</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd2d39fad37c2b0b7e546aff6d54</URIString>\n        <point1 x=\"535.24115\" y=\"72.06363\"/>\n        <point2 x=\"636.9042\" y=\"132.13593\"/>\n        <ID1 xsi:type=\"node\">6</ID1>\n        <ID2 xsi:type=\"node\">7</ID2>\n        <ctrlPoint0 x=\"605.57526\" y=\"97.49305\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"9\" label=\"WIP\" layerID=\"1\" created=\"1388347840853\"\n        x=\"826.5\" y=\"174.0\" width=\"77.5\" height=\"77.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FFC63B</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd4239fad37c2b0b7e547fce60e4</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"11\" label=\"PREV\" layerID=\"1\" created=\"1388347912335\"\n        x=\"1005.9166\" y=\"89.25001\" width=\"77.5\" height=\"76.75\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#83CEFF</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd4539fad37c2b0b7e543e5eb4f9</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"12\" label=\"request_review\" layerID=\"1\"\n        created=\"1388347928095\" x=\"893.0264\" y=\"134.53569\"\n        width=\"114.14453\" height=\"52.29561\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd4539fad37c2b0b7e54ddb1139f</URIString>\n        <point1 x=\"893.5264\" y=\"186.3313\"/>\n        <point2 x=\"1006.67096\" y=\"135.03569\"/>\n        <ID1 xsi:type=\"node\">9</ID1>\n        <ID2 xsi:type=\"node\">11</ID2>\n        <ctrlPoint0 x=\"933.5417\" y=\"149.29887\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"13\" label=\"HREV\" layerID=\"1\" created=\"1388347930535\"\n        x=\"1004.9166\" y=\"241.24998\" width=\"77.5\" height=\"75.75\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#DAA9FF</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd4739fad37c2b0b7e546a650ff6</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"14\" label=\"request_revision\" layerID=\"1\"\n        created=\"1388347934209\" x=\"1003.2942\" y=\"165.45312\" width=\"81.0\"\n        height=\"76.296875\" strokeWidth=\"1.0\" autoSized=\"false\"\n        controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd4839fad37c2b0b7e549280057f</URIString>\n        <point1 x=\"1044.1775\" y=\"165.95312\"/>\n        <point2 x=\"1043.6665\" y=\"241.25\"/>\n        <ID1 xsi:type=\"node\">11</ID1>\n        <ID2 xsi:type=\"node\">13</ID2>\n        <ctrlPoint0 x=\"1043.6665\" y=\"205.99997\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"15\" label=\"create_time_log\" layerID=\"1\"\n        created=\"1388347943236\" x=\"895.26306\" y=\"235.4754\"\n        width=\"110.37891\" height=\"41.964783\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd4939fad37c2b0b7e543c642f53</URIString>\n        <point1 x=\"1005.14197\" y=\"276.9402\"/>\n        <point2 x=\"895.76306\" y=\"235.9754\"/>\n        <ID1 xsi:type=\"node\">13</ID1>\n        <ID2 xsi:type=\"node\">9</ID2>\n        <ctrlPoint0 x=\"944.5415\" y=\"273.50342\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"17\" label=\"approve\" layerID=\"1\" created=\"1388347947963\"\n        x=\"1082.3657\" y=\"132.53693\" width=\"120.855835\" height=\"51.44342\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd4a39fad37c2b0b7e54d6134912</URIString>\n        <point1 x=\"1082.8657\" y=\"133.03693\"/>\n        <point2 x=\"1202.7216\" y=\"183.48035\"/>\n        <ID1 xsi:type=\"node\">11</ID1>\n        <ID2 xsi:type=\"node\">18</ID2>\n        <ctrlPoint0 x=\"1152.998\" y=\"142.97308\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"18\" label=\"CMPL\" layerID=\"1\" created=\"1388347956643\"\n        x=\"1193.9165\" y=\"169.00002\" width=\"77.5\" height=\"77.75\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#8AEE95</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd4b39fad37c2b0b7e54dc411cd0</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"21\" label=\"STOP\" layerID=\"1\" created=\"1388347990388\"\n        x=\"759.7791\" y=\"-2.179165\" width=\"84.66669\" height=\"68.66667\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#FFFFFF</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd4d39fad37c2b0b7e5415afe5b9</URIString>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"28\" label=\"resume\" layerID=\"1\" created=\"1388348620342\"\n        x=\"678.95337\" y=\"220.56944\" width=\"149.08716\" height=\"61.25334\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd5539fad37c2b0b7e543dfd69cc</URIString>\n        <point1 x=\"679.45337\" y=\"281.32278\"/>\n        <point2 x=\"827.5405\" y=\"221.06944\"/>\n        <ID1 xsi:type=\"node\">29</ID1>\n        <ID2 xsi:type=\"node\">9</ID2>\n        <ctrlPoint0 x=\"738.41797\" y=\"241.32248\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"29\" label=\"OH\" layerID=\"1\" created=\"1388348638483\"\n        x=\"612.24994\" y=\"267.83337\" width=\"77.5\" height=\"65.58331\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#D0D0D0</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd5739fad37c2b0b7e546148af1c</URIString>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"30\" label=\"hold\" layerID=\"1\" created=\"1388348691701\"\n        x=\"685.6064\" y=\"231.69934\" width=\"147.0138\" height=\"62.595337\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd5739fad37c2b0b7e543634001b</URIString>\n        <point1 x=\"832.1202\" y=\"232.19934\"/>\n        <point2 x=\"686.1064\" y=\"293.79468\"/>\n        <ID1 xsi:type=\"node\">9</ID1>\n        <ID2 xsi:type=\"node\">29</ID2>\n        <ctrlPoint0 x=\"749.16394\" y=\"281.52606\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"10\" label=\"create_time_log\" layerID=\"1\"\n        created=\"1388347840854\" x=\"704.5831\" y=\"164.18347\"\n        width=\"125.809326\" height=\"33.194763\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd4339fad37c2b0b7e54d84a5e4e</URIString>\n        <point1 x=\"705.0831\" y=\"164.68347\"/>\n        <point2 x=\"829.89246\" y=\"196.87823\"/>\n        <ID1 xsi:type=\"node\">7</ID1>\n        <ID2 xsi:type=\"node\">9</ID2>\n        <ctrlPoint0 x=\"765.09717\" y=\"168.25021\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"22\" label=\"stop\" layerID=\"1\" created=\"1388348026798\"\n        x=\"804.153\" y=\"65.987305\" width=\"37.611145\" height=\"116.98999\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd5339fad37c2b0b7e543f29cb47</URIString>\n        <point1 x=\"841.26416\" y=\"182.4773\"/>\n        <point2 x=\"804.653\" y=\"66.487305\"/>\n        <ID1 xsi:type=\"node\">9</ID1>\n        <ID2 xsi:type=\"node\">21</ID2>\n        <ctrlPoint0 x=\"810.37506\" y=\"143.81372\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"45\" label=\"resume\" layerID=\"1\" created=\"1388352320248\"\n        x=\"822.34424\" y=\"65.98755\" width=\"45.103394\" height=\"108.87671\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/4040cb2939fad37c2b0b7e5425f40da9</URIString>\n        <point1 x=\"822.84424\" y=\"66.48755\"/>\n        <point2 x=\"861.613\" y=\"174.36426\"/>\n        <ID1 xsi:type=\"node\">21</ID1>\n        <ID2 xsi:type=\"node\">9</ID2>\n        <ctrlPoint0 x=\"856.6667\" y=\"122.50004\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"46\" label=\"request_revision\" layerID=\"1\"\n        created=\"1389986937453\" x=\"1081.7737\" y=\"230.65527\"\n        width=\"120.53125\" height=\"47.582703\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a1b6ced67f0000013b244fea3bcf813b</URIString>\n        <point1 x=\"1201.8049\" y=\"231.15527\"/>\n        <point2 x=\"1082.2737\" y=\"277.73798\"/>\n        <ID1 xsi:type=\"node\">18</ID1>\n        <ID2 xsi:type=\"node\">13</ID2>\n        <ctrlPoint0 x=\"1142.9418\" y=\"275.55835\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"48\" label=\"DREV\" layerID=\"1\" created=\"1389987024340\"\n        x=\"1002.1918\" y=\"388.3916\" width=\"77.5\" height=\"77.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FFC63B</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a1b6ced77f0000013b244feaa9ee9d7b</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"53\" label=\"task.depends_on[*].request_revision\" layerID=\"1\"\n        created=\"1389987183648\" x=\"1076.3951\" y=\"244.95898\"\n        width=\"190.66138\" height=\"172.44278\" strokeWidth=\"3.0\"\n        strokeStyle=\"4\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#EA2218</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a1b6ced87f0000013b244feacfeea727</URIString>\n        <point1 x=\"1229.7809\" y=\"246.45898\"/>\n        <point2 x=\"1077.8951\" y=\"415.90176\"/>\n        <ID1 xsi:type=\"node\">18</ID1>\n        <ID2 xsi:type=\"node\">48</ID2>\n        <ctrlPoint0 x=\"1220.275\" y=\"373.5583\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"54\" label=\"task.depends_on[*].approve\" layerID=\"1\"\n        created=\"1389987236672\" x=\"857.20386\" y=\"293.70032\"\n        width=\"152.99811\" height=\"126.148285\" strokeWidth=\"3.0\"\n        strokeStyle=\"4\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#EA2218</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a1b6ceda7f0000013b244fea6bf684bb</URIString>\n        <point1 x=\"1003.224\" y=\"418.3486\"/>\n        <point2 x=\"1008.70197\" y=\"295.20032\"/>\n        <ID1 xsi:type=\"node\">48</ID1>\n        <ID2 xsi:type=\"node\">13</ID2>\n        <ctrlPoint0 x=\"827.44476\" y=\"378.53494\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"59\" label=\"task.depends_on[*].request_revision\" layerID=\"1\"\n        created=\"1390041848192\" x=\"863.7536\" y=\"243.94739\" width=\"160.0\"\n        height=\"158.71149\" strokeWidth=\"3.0\" strokeStyle=\"4\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#EA2218</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a574d2c37f00000148ec1d5416dda6df</URIString>\n        <point1 x=\"885.0193\" y=\"245.44739\"/>\n        <point2 x=\"1012.30347\" y=\"401.15887\"/>\n        <ID1 xsi:type=\"node\">9</ID1>\n        <ID2 xsi:type=\"node\">48</ID2>\n        <ctrlPoint0 x=\"938.8458\" y=\"335.15417\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"66\" label=\"create_time_log\" layerID=\"1\"\n        created=\"1390050103632\" x=\"1004.31006\" y=\"316.4297\" width=\"76.0\"\n        height=\"72.52734\" strokeWidth=\"1.0\" autoSized=\"false\"\n        controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a574d2c47f00000148ec1d54c76f7627</URIString>\n        <point1 x=\"1042.9695\" y=\"316.9297\"/>\n        <point2 x=\"1041.6505\" y=\"388.45703\"/>\n        <ID1 xsi:type=\"node\">13</ID1>\n        <ID2 xsi:type=\"node\">48</ID2>\n    </child>\n    <child ID=\"25\" label=\"create_time_log\" layerID=\"1\"\n        created=\"1388348049189\" x=\"741.6509\" y=\"245.725\"\n        width=\"114.953125\" height=\"92.27731\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd5439fad37c2b0b7e54b86d70b9</URIString>\n        <point1 x=\"856.104\" y=\"249.80566\"/>\n        <point2 x=\"846.275\" y=\"246.225\"/>\n        <ID1 xsi:type=\"node\">9</ID1>\n        <ctrlPoint0 x=\"829.0844\" y=\"360.01578\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"682.52496\" y=\"358.64685\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"70\" label=\"create_time_log\" layerID=\"1\"\n        created=\"1390055750897\" x=\"1065.9458\" y=\"436.4465\"\n        width=\"100.20093\" height=\"51.3685\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a5c84bd87f00000148ec1d5486b15f38</URIString>\n        <point1 x=\"1078.1873\" y=\"436.9465\"/>\n        <point2 x=\"1066.4458\" y=\"453.82083\"/>\n        <ID1 xsi:type=\"node\">48</ID1>\n        <ctrlPoint0 x=\"1201.5228\" y=\"470.24258\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"1091.9907\" y=\"516.3417\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"56\" label=\"task.depends_on[*].request_revision\" layerID=\"1\"\n        created=\"1390041758943\" x=\"468.4242\" y=\"93.67566\" width=\"160.0\"\n        height=\"64.96039\" strokeWidth=\"3.0\" strokeStyle=\"4\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#EA2218</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a574d2c17f00000148ec1d542c75e466</URIString>\n        <point1 x=\"623.46716\" y=\"157.13605\"/>\n        <point2 x=\"510.77924\" y=\"95.17566\"/>\n        <ID1 xsi:type=\"node\">7</ID1>\n        <ID2 xsi:type=\"node\">6</ID2>\n        <ctrlPoint0 x=\"529.72516\" y=\"145.34879\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"83\" label=\"delete_time_log\" layerID=\"1\"\n        created=\"1390229964313\" x=\"701.86884\" y=\"176.61932\"\n        width=\"125.67432\" height=\"31.027588\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b02ac6397f0000013e376dbaac4e08d3</URIString>\n        <point1 x=\"827.04315\" y=\"207.14691\"/>\n        <point2 x=\"702.36884\" y=\"177.11932\"/>\n        <ID1 xsi:type=\"node\">9</ID1>\n        <ID2 xsi:type=\"node\">7</ID2>\n        <ctrlPoint0 x=\"752.8457\" y=\"196.75125\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"85\" label=\"WFD\" layerID=\"1\" created=\"1390239171735\"\n        x=\"494.5249\" y=\"611.99994\" width=\"82.3916\" height=\"79.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#D0D0D0</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b951897f0000010f9ede6dbf93a108</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"86\" label=\"RTS\" layerID=\"1\" created=\"1390239171735\"\n        x=\"691.59143\" y=\"611.99994\" width=\"82.3916\" height=\"80.83333\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FC938D</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b9518a7f0000010f9ede6df7032851</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"87\" label=\"approve\" layerID=\"1\" created=\"1390239171735\"\n        x=\"575.43567\" y=\"630.1156\" width=\"117.87976\" height=\"13.479248\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b9518b7f0000010f9ede6d33187e87</URIString>\n        <point1 x=\"575.93567\" y=\"643.09485\"/>\n        <point2 x=\"692.8154\" y=\"643.04816\"/>\n        <ID1 xsi:type=\"node\">85</ID1>\n        <ID2 xsi:type=\"node\">86</ID2>\n        <ctrlPoint0 x=\"637.8251\" y=\"630.15967\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"88\" label=\"WIP\" layerID=\"1\" created=\"1390239171735\"\n        x=\"888.65796\" y=\"611.99994\" width=\"77.5\" height=\"77.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FFC63B</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b9518c7f0000010f9ede6d5646bd67</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"94\" label=\"approve\" layerID=\"1\" created=\"1390239171735\"\n        x=\"964.96716\" y=\"631.6112\" width=\"117.1167\" height=\"13.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b9518d7f0000010f9ede6d0afb73cb</URIString>\n        <point1 x=\"965.46716\" y=\"643.6913\"/>\n        <point2 x=\"1081.5839\" y=\"643.402\"/>\n        <ID1 xsi:type=\"node\">88</ID1>\n        <ID2 xsi:type=\"node\">95</ID2>\n        <ctrlPoint0 x=\"1027.0425\" y=\"632.6758\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"95\" label=\"CMPL\" layerID=\"1\" created=\"1390239171735\"\n        x=\"1080.8329\" y=\"611.99994\" width=\"77.5\" height=\"77.75\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#8AEE95</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b9518e7f0000010f9ede6de123e8b3</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"100\" label=\"create_time_log\" layerID=\"1\"\n        created=\"1390239171735\" x=\"772.4731\" y=\"630.88525\"\n        width=\"117.44147\" height=\"13.339111\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b9518f7f0000010f9ede6da65640f1</URIString>\n        <point1 x=\"772.9731\" y=\"643.72437\"/>\n        <point2 x=\"889.41455\" y=\"643.0438\"/>\n        <ID1 xsi:type=\"node\">86</ID1>\n        <ID2 xsi:type=\"node\">88</ID2>\n        <ctrlPoint0 x=\"830.01373\" y=\"631.3865\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"115\" label=\"delete_time_log\" layerID=\"1\"\n        created=\"1390239171735\" x=\"573.78766\" y=\"663.8028\"\n        width=\"120.32428\" height=\"17.02948\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b951907f0000010f9ede6d61625eb4</URIString>\n        <point1 x=\"693.61194\" y=\"664.3028\"/>\n        <point2 x=\"574.28766\" y=\"664.9955\"/>\n        <ID1 xsi:type=\"node\">86</ID1>\n        <ID2 xsi:type=\"node\">85</ID2>\n        <ctrlPoint0 x=\"628.64166\" y=\"684.0154\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"120\" label=\"delete_time_log\" layerID=\"1\"\n        created=\"1390239171735\" x=\"770.619\" y=\"665.2781\"\n        width=\"121.743774\" height=\"19.78424\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b951917f0000010f9ede6ddfc41ffa</URIString>\n        <point1 x=\"891.8628\" y=\"665.7781\"/>\n        <point2 x=\"771.119\" y=\"666.969\"/>\n        <ID1 xsi:type=\"node\">88</ID1>\n        <ID2 xsi:type=\"node\">86</ID2>\n        <ctrlPoint0 x=\"833.7622\" y=\"690.7511\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"122\" label=\"request_revision\" layerID=\"1\"\n        created=\"1390239225857\" x=\"961.28296\" y=\"665.8157\"\n        width=\"123.26221\" height=\"22.071533\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b951927f0000010f9ede6d857e3460</URIString>\n        <point1 x=\"1084.0452\" y=\"666.3157\"/>\n        <point2 x=\"961.78296\" y=\"667.92004\"/>\n        <ID1 xsi:type=\"node\">95</ID1>\n        <ID2 xsi:type=\"node\">88</ID2>\n        <ctrlPoint0 x=\"1016.5156\" y=\"695.65656\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"123\" label=\"CONTAINER TASK STATUS WORKFLOW\" layerID=\"1\"\n        created=\"1390239279430\" x=\"709.70917\" y=\"576.3718\" width=\"242.0\"\n        height=\"17.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b951937f0000010f9ede6df249fca0</URIString>\n        <richText>&lt;html&gt;\n  &lt;head color=\"#404040\" style=\"color: #404040\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; font-size: 11; font-family: Arial; color: #404040 }\n        ol { margin-top: 6; font-family: Arial; vertical-align: middle; margin-left: 30; font-size: 11; list-style-position: outside }\n        p { margin-top: 0; margin-left: 0; margin-right: 0; margin-bottom: 0; color: #404040 }\n        ul { margin-top: 6; font-size: 11; margin-left: 30; vertical-align: middle; list-style-position: outside; font-family: Arial }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p color=\"#404040\" style=\"text-align: center; color: #404040\"&gt;\n      CONTAINER TASK STATUS WORKFLOW\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>CONTAINER TASK STATUS WORKFLOW</label>\n    </child>\n    <child ID=\"125\" label=\"LEAF TASK STATUS WORKFLOW\" layerID=\"1\"\n        created=\"1390239332214\" x=\"796.04254\" y=\"-49.737953\"\n        width=\"242.0\" height=\"17.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b0b951957f0000010f9ede6d01435234</URIString>\n        <richText>&lt;html&gt;\n  &lt;head color=\"#404040\" style=\"color: #404040\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; font-size: 11; font-family: Arial; color: #404040 }\n        ol { margin-top: 6; font-family: Arial; vertical-align: middle; margin-left: 30; font-size: 11; list-style-position: outside }\n        p { margin-top: 0; margin-left: 0; margin-right: 0; margin-bottom: 0; color: #404040 }\n        ul { margin-top: 6; font-size: 11; margin-left: 30; vertical-align: middle; list-style-position: outside; font-family: Arial }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p color=\"#404040\" style=\"text-align: center; color: #404040\"&gt;\n      LEAF TASK STATUS WORKFLOW\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>LEAF TASK STATUS WORKFLOW</label>\n    </child>\n    <child ID=\"126\" label=\"resume\" layerID=\"1\" created=\"1390300976758\"\n        x=\"673.1849\" y=\"330.24115\" width=\"330.14667\" height=\"111.1467\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b465a9c87f00000160874ad883e2b183</URIString>\n        <point1 x=\"673.6849\" y=\"330.74115\"/>\n        <point2 x=\"1002.83154\" y=\"433.19635\"/>\n        <ID1 xsi:type=\"node\">29</ID1>\n        <ID2 xsi:type=\"node\">48</ID2>\n        <ctrlPoint0 x=\"778.7791\" y=\"470.26202\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"127\" label=\"hold\" layerID=\"1\" created=\"1390405947783\"\n        x=\"667.5642\" y=\"332.91675\" width=\"337.17053\" height=\"129.22067\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/baa74e4c7f000001444cc84d9fe05d8e</URIString>\n        <point1 x=\"1004.23474\" y=\"438.66983\"/>\n        <point2 x=\"668.0642\" y=\"333.41675\"/>\n        <ID1 xsi:type=\"node\">48</ID1>\n        <ID2 xsi:type=\"node\">29</ID2>\n        <ctrlPoint0 x=\"763.10004\" y=\"516.04285\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"132\" label=\"resume\" layerID=\"1\" created=\"1390424390043\"\n        x=\"836.1739\" y=\"-8.720401\" width=\"483.16998\" height=\"436.11707\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/bbd8eded7f000001444cc84d0c8a9afd</URIString>\n        <point1 x=\"836.6739\" y=\"18.19101\"/>\n        <point2 x=\"1079.6913\" y=\"426.89667\"/>\n        <ID1 xsi:type=\"node\">21</ID1>\n        <ID2 xsi:type=\"node\">48</ID2>\n        <ctrlPoint0 x=\"1215.7904\" y=\"-134.97574\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"1569.1111\" y=\"426.96072\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"133\" label=\"stop\" layerID=\"1\" created=\"1390424424438\"\n        x=\"834.6763\" y=\"-25.234804\" width=\"512.50903\" height=\"456.37933\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/bbd8edee7f000001444cc84da1d1e216</URIString>\n        <point1 x=\"1079.4398\" y=\"429.37418\"/>\n        <point2 x=\"835.1763\" y=\"15.500305\"/>\n        <ID1 xsi:type=\"node\">48</ID1>\n        <ID2 xsi:type=\"node\">21</ID2>\n        <ctrlPoint0 x=\"1616.1722\" y=\"463.98547\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"1246.1915\" y=\"-191.52309\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"134\" label=\"WFD\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1560.3584\" y=\"36.174225\" width=\"82.3916\" height=\"79.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#D0D0D0</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d21dc0a8012b17e4445f9ad9d4f0</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"135\" label=\"RTS\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1727.6917\" y=\"139.34091\" width=\"82.3916\" height=\"80.83333\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FC938D</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d21ec0a8012b17e4445fe5262599</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"136\" label=\"task.depends_on[*].approve&#xa;or no dependency\"\n        layerID=\"1\" created=\"1433612001033\" x=\"1638.491\" y=\"88.07114\"\n        width=\"121.58301\" height=\"63.07242\" strokeWidth=\"3.0\"\n        strokeStyle=\"4\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#EA2218</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d21ec0a8012b17e4445f7e54fab2</URIString>\n        <point1 x=\"1639.991\" y=\"89.57114\"/>\n        <point2 x=\"1741.6542\" y=\"149.64355\"/>\n        <ID1 xsi:type=\"node\">134</ID1>\n        <ID2 xsi:type=\"node\">135</ID2>\n        <ctrlPoint0 x=\"1710.3252\" y=\"115.00061\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"137\" label=\"WIP\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1931.25\" y=\"191.50757\" width=\"77.5\" height=\"77.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FFC63B</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d21fc0a8012b17e4445f9fb714d9</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"138\" label=\"PREV\" layerID=\"1\" created=\"1433612001033\"\n        x=\"2110.6665\" y=\"106.75757\" width=\"77.5\" height=\"76.75\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#83CEFF</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d220c0a8012b17e4445fa5c7307d</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"139\" label=\"request_review\" layerID=\"1\"\n        created=\"1433612001033\" x=\"1997.7762\" y=\"152.04327\"\n        width=\"114.14465\" height=\"52.295715\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d221c0a8012b17e4445f09711db8</URIString>\n        <point1 x=\"1998.2762\" y=\"203.83899\"/>\n        <point2 x=\"2111.421\" y=\"152.54327\"/>\n        <ID1 xsi:type=\"node\">137</ID1>\n        <ID2 xsi:type=\"node\">138</ID2>\n        <ctrlPoint0 x=\"2038.2917\" y=\"166.80643\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"140\" label=\"HREV\" layerID=\"1\" created=\"1433612001033\"\n        x=\"2109.6665\" y=\"258.75754\" width=\"77.5\" height=\"75.75\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#DAA9FF</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d222c0a8012b17e4445f68152c62</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"141\" label=\"request_revision\" layerID=\"1\"\n        created=\"1433612001033\" x=\"2108.0442\" y=\"182.95312\" width=\"81.0\"\n        height=\"76.30438\" strokeWidth=\"1.0\" autoSized=\"false\"\n        controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d222c0a8012b17e4445f68878e0b</URIString>\n        <point1 x=\"2148.9275\" y=\"183.45312\"/>\n        <point2 x=\"2148.4165\" y=\"258.7575\"/>\n        <ID1 xsi:type=\"node\">138</ID1>\n        <ID2 xsi:type=\"node\">140</ID2>\n        <ctrlPoint0 x=\"2148.4165\" y=\"223.50754\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"142\" label=\"create_time_log\" layerID=\"1\"\n        created=\"1433612001033\" x=\"2000.013\" y=\"252.98291\"\n        width=\"110.37891\" height=\"41.964813\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d223c0a8012b17e4445ffb86e992</URIString>\n        <point1 x=\"2109.8918\" y=\"294.44772\"/>\n        <point2 x=\"2000.513\" y=\"253.48291\"/>\n        <ID1 xsi:type=\"node\">140</ID1>\n        <ID2 xsi:type=\"node\">137</ID2>\n        <ctrlPoint0 x=\"2049.2915\" y=\"291.011\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"143\" label=\"approve\" layerID=\"1\" created=\"1433612001033\"\n        x=\"2187.1155\" y=\"150.0445\" width=\"120.8562\" height=\"51.443665\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d224c0a8012b17e4445f3f7e4d37</URIString>\n        <point1 x=\"2187.6155\" y=\"150.5445\"/>\n        <point2 x=\"2307.4717\" y=\"200.98816\"/>\n        <ID1 xsi:type=\"node\">138</ID1>\n        <ID2 xsi:type=\"node\">144</ID2>\n        <ctrlPoint0 x=\"2257.748\" y=\"160.48065\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"144\" label=\"CMPL\" layerID=\"1\" created=\"1433612001033\"\n        x=\"2298.6665\" y=\"186.50757\" width=\"77.5\" height=\"77.75\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#8AEE95</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d224c0a8012b17e4445f2622ddfe</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"145\" label=\"STOP\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1864.529\" y=\"15.3284\" width=\"84.66669\" height=\"68.66667\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#FFFFFF</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d225c0a8012b17e4445f95fe4ae5</URIString>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"146\" label=\"resume\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1783.7035\" y=\"238.07706\" width=\"149.08691\" height=\"61.253265\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d225c0a8012b17e4445f5cde7a27</URIString>\n        <point1 x=\"1784.2035\" y=\"298.83032\"/>\n        <point2 x=\"1932.2904\" y=\"238.57706\"/>\n        <ID1 xsi:type=\"node\">147</ID1>\n        <ID2 xsi:type=\"node\">137</ID2>\n        <ctrlPoint0 x=\"1843.168\" y=\"258.83005\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"147\" label=\"OH\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1717.0\" y=\"285.34094\" width=\"77.5\" height=\"65.58331\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#D0D0D0</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d226c0a8012b17e4445f58f103df</URIString>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"148\" label=\"hold\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1790.3564\" y=\"249.20691\" width=\"147.0138\" height=\"62.595306\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d226c0a8012b17e4445f20a567f8</URIString>\n        <point1 x=\"1936.8702\" y=\"249.70691\"/>\n        <point2 x=\"1790.8564\" y=\"311.30222\"/>\n        <ID1 xsi:type=\"node\">137</ID1>\n        <ID2 xsi:type=\"node\">147</ID2>\n        <ctrlPoint0 x=\"1853.914\" y=\"299.03363\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"149\" label=\"now &lt; start\" layerID=\"1\"\n        created=\"1433612001033\" x=\"1809.3333\" y=\"181.69104\"\n        width=\"125.809326\" height=\"33.194885\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d227c0a8012b17e4445f218056c0</URIString>\n        <point1 x=\"1809.8333\" y=\"182.19104\"/>\n        <point2 x=\"1934.6426\" y=\"214.38593\"/>\n        <ID1 xsi:type=\"node\">135</ID1>\n        <ID2 xsi:type=\"node\">137</ID2>\n        <ctrlPoint0 x=\"1869.8472\" y=\"185.75778\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"150\" label=\"stop\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1908.9031\" y=\"83.49414\" width=\"37.611084\" height=\"116.99072\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d228c0a8012b17e4445fb829c90a</URIString>\n        <point1 x=\"1946.0142\" y=\"199.98486\"/>\n        <point2 x=\"1909.4031\" y=\"83.99414\"/>\n        <ID1 xsi:type=\"node\">137</ID1>\n        <ID2 xsi:type=\"node\">145</ID2>\n        <ctrlPoint0 x=\"1915.125\" y=\"161.32129\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"151\" label=\"resume\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1927.0944\" y=\"83.49512\" width=\"45.103394\" height=\"108.87598\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d229c0a8012b17e4445ffe30d926</URIString>\n        <point1 x=\"1927.5944\" y=\"83.99512\"/>\n        <point2 x=\"1966.363\" y=\"191.8711\"/>\n        <ID1 xsi:type=\"node\">145</ID1>\n        <ID2 xsi:type=\"node\">137</ID2>\n        <ctrlPoint0 x=\"1961.4167\" y=\"140.0076\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"152\" label=\"request_revision\" layerID=\"1\"\n        created=\"1433612001033\" x=\"2186.5234\" y=\"248.16272\"\n        width=\"120.53174\" height=\"47.582825\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d229c0a8012b17e4445f6d767d7e</URIString>\n        <point1 x=\"2306.5552\" y=\"248.66272\"/>\n        <point2 x=\"2187.0234\" y=\"295.24554\"/>\n        <ID1 xsi:type=\"node\">144</ID1>\n        <ID2 xsi:type=\"node\">140</ID2>\n        <ctrlPoint0 x=\"2247.692\" y=\"293.06592\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"153\" label=\"DREV\" layerID=\"1\" created=\"1433612001033\"\n        x=\"2106.942\" y=\"405.89917\" width=\"77.5\" height=\"77.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FFC63B</fillColor>\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d22ac0a8012b17e4445f8ac663b8</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"154\" label=\"task.depends_on[*].request_revision\" layerID=\"1\"\n        created=\"1433612001033\" x=\"2181.1453\" y=\"262.4668\"\n        width=\"190.66113\" height=\"172.44257\" strokeWidth=\"3.0\"\n        strokeStyle=\"4\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#EA2218</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d22ac0a8012b17e4445f5c71943d</URIString>\n        <point1 x=\"2334.5308\" y=\"263.9668\"/>\n        <point2 x=\"2182.6453\" y=\"433.40936\"/>\n        <ID1 xsi:type=\"node\">144</ID1>\n        <ID2 xsi:type=\"node\">153</ID2>\n        <ctrlPoint0 x=\"2325.025\" y=\"391.06586\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"155\" label=\"task.depends_on[*].approve\" layerID=\"1\"\n        created=\"1433612001033\" x=\"1961.9539\" y=\"311.20795\"\n        width=\"152.9978\" height=\"126.148224\" strokeWidth=\"3.0\"\n        strokeStyle=\"4\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#EA2218</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d22bc0a8012b17e4445f2a8d786b</URIString>\n        <point1 x=\"2107.974\" y=\"435.85617\"/>\n        <point2 x=\"2113.4517\" y=\"312.70795\"/>\n        <ID1 xsi:type=\"node\">153</ID1>\n        <ID2 xsi:type=\"node\">140</ID2>\n        <ctrlPoint0 x=\"1932.1948\" y=\"396.0425\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"156\" label=\"task.depends_on[*].request_revision\" layerID=\"1\"\n        created=\"1433612001033\" x=\"1968.5037\" y=\"261.4546\" width=\"160.0\"\n        height=\"158.71179\" strokeWidth=\"3.0\" strokeStyle=\"4\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#EA2218</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d22cc0a8012b17e4445fd590992f</URIString>\n        <point1 x=\"1989.7692\" y=\"262.9546\"/>\n        <point2 x=\"2117.0535\" y=\"418.66638\"/>\n        <ID1 xsi:type=\"node\">137</ID1>\n        <ID2 xsi:type=\"node\">153</ID2>\n        <ctrlPoint0 x=\"2043.5958\" y=\"352.66174\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"157\" label=\"create_time_log\" layerID=\"1\"\n        created=\"1433612001033\" x=\"2109.0598\" y=\"333.9453\" width=\"76.0\"\n        height=\"72.52344\" strokeWidth=\"1.0\" autoSized=\"false\"\n        controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d22dc0a8012b17e4445f23a0e668</URIString>\n        <point1 x=\"2147.7192\" y=\"334.4453\"/>\n        <point2 x=\"2146.4004\" y=\"405.96875\"/>\n        <ID1 xsi:type=\"node\">140</ID1>\n        <ID2 xsi:type=\"node\">153</ID2>\n    </child>\n    <child ID=\"158\" label=\"create_time_log\" layerID=\"1\"\n        created=\"1433612001033\" x=\"1846.4009\" y=\"263.23257\"\n        width=\"114.953125\" height=\"92.27725\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d22dc0a8012b17e4445f3be48af4</URIString>\n        <point1 x=\"1960.854\" y=\"267.313\"/>\n        <point2 x=\"1951.025\" y=\"263.73257\"/>\n        <ID1 xsi:type=\"node\">137</ID1>\n        <ctrlPoint0 x=\"1933.8345\" y=\"377.52335\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"1787.2749\" y=\"376.15442\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"159\" label=\"create_time_log\" layerID=\"1\"\n        created=\"1433612001033\" x=\"2170.6958\" y=\"453.95404\"\n        width=\"100.20093\" height=\"51.36853\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d22ec0a8012b17e4445fe8d0268c</URIString>\n        <point1 x=\"2182.9373\" y=\"454.45404\"/>\n        <point2 x=\"2171.1958\" y=\"471.3284\"/>\n        <ID1 xsi:type=\"node\">153</ID1>\n        <ctrlPoint0 x=\"2306.273\" y=\"487.75015\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"2196.7407\" y=\"533.84924\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"160\" label=\"task.depends_on[*].request_revision\" layerID=\"1\"\n        created=\"1433612001033\" x=\"1573.1741\" y=\"111.183105\"\n        width=\"160.0\" height=\"64.960526\" strokeWidth=\"3.0\"\n        strokeStyle=\"4\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#EA2218</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d22fc0a8012b17e4445f07f35e93</URIString>\n        <point1 x=\"1728.2172\" y=\"174.64363\"/>\n        <point2 x=\"1615.529\" y=\"112.683105\"/>\n        <ID1 xsi:type=\"node\">135</ID1>\n        <ID2 xsi:type=\"node\">134</ID2>\n        <ctrlPoint0 x=\"1634.4751\" y=\"162.85635\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"161\" label=\"now > start\" layerID=\"1\"\n        created=\"1433612001033\" x=\"1806.6188\" y=\"194.12686\"\n        width=\"125.67444\" height=\"31.027649\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"1\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d230c0a8012b17e4445fcce5a2e1</URIString>\n        <point1 x=\"1931.7932\" y=\"224.65451\"/>\n        <point2 x=\"1807.1188\" y=\"194.62686\"/>\n        <ID1 xsi:type=\"node\">137</ID1>\n        <ID2 xsi:type=\"node\">135</ID2>\n        <ctrlPoint0 x=\"1857.5957\" y=\"214.25882\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"162\" label=\"resume\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1777.9348\" y=\"347.74854\" width=\"330.14697\" height=\"111.14679\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d231c0a8012b17e4445f0569c66e</URIString>\n        <point1 x=\"1778.4348\" y=\"348.24854\"/>\n        <point2 x=\"2107.5818\" y=\"450.7038\"/>\n        <ID1 xsi:type=\"node\">147</ID1>\n        <ID2 xsi:type=\"node\">153</ID2>\n        <ctrlPoint0 x=\"1883.529\" y=\"487.7696\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"163\" label=\"hold\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1772.3145\" y=\"350.42432\" width=\"337.1704\" height=\"129.22067\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"1\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d231c0a8012b17e4445f44b3f111</URIString>\n        <point1 x=\"2108.9849\" y=\"456.17737\"/>\n        <point2 x=\"1772.8145\" y=\"350.92432\"/>\n        <ID1 xsi:type=\"node\">153</ID1>\n        <ID2 xsi:type=\"node\">147</ID2>\n        <ctrlPoint0 x=\"1867.8501\" y=\"533.5504\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"164\" label=\"resume\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1940.9238\" y=\"8.787188\" width=\"483.17017\" height=\"436.11703\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d232c0a8012b17e4445f46acc020</URIString>\n        <point1 x=\"1941.4238\" y=\"35.69861\"/>\n        <point2 x=\"2184.4414\" y=\"444.40424\"/>\n        <ID1 xsi:type=\"node\">145</ID1>\n        <ID2 xsi:type=\"node\">153</ID2>\n        <ctrlPoint0 x=\"2320.5405\" y=\"-117.46817\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"2673.861\" y=\"444.4683\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"165\" label=\"stop\" layerID=\"1\" created=\"1433612001033\"\n        x=\"1939.4261\" y=\"-7.727159\" width=\"512.50916\" height=\"456.37927\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#404040</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/0447d233c0a8012b17e4445fb3acd833</URIString>\n        <point1 x=\"2184.19\" y=\"446.8817\"/>\n        <point2 x=\"1939.9261\" y=\"33.007996\"/>\n        <ID1 xsi:type=\"node\">153</ID1>\n        <ID2 xsi:type=\"node\">145</ID2>\n        <ctrlPoint0 x=\"2720.9224\" y=\"481.49304\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"2350.9414\" y=\"-174.01553\" xsi:type=\"point\"/>\n    </child>\n    <layer ID=\"1\" label=\"Layer 1\" created=\"1388347735329\" x=\"0.0\"\n        y=\"0.0\" width=\"1.4E-45\" height=\"1.4E-45\" strokeWidth=\"0.0\" autoSized=\"false\">\n        <URIString>http://vue.tufts.edu/rdf/resource/400bcd5c39fad37c2b0b7e543d00aadd</URIString>\n    </layer>\n    <userZoom>0.1872035782673108</userZoom>\n    <userOrigin x=\"141.14682\" y=\"-174.72716\"/>\n    <presentationBackground>#202020</presentationBackground>\n    <PathwayList currentPathway=\"0\" revealerIndex=\"-1\">\n        <pathway ID=\"0\" label=\"Untitled Pathway\" created=\"1388347735313\"\n            x=\"0.0\" y=\"0.0\" width=\"1.4E-45\" height=\"1.4E-45\"\n            strokeWidth=\"0.0\" autoSized=\"false\" currentIndex=\"0\" open=\"true\">\n            <strokeColor>#B3993333</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/400bcd5d39fad37c2b0b7e54a8dd4d0e</URIString>\n            <masterSlide ID=\"2\" created=\"1388347735349\" x=\"0.0\" y=\"0.0\"\n                width=\"800.0\" height=\"600.0\" locked=\"true\"\n                strokeWidth=\"0.0\" autoSized=\"false\">\n                <fillColor>#000000</fillColor>\n                <strokeColor>#404040</strokeColor>\n                <textColor>#000000</textColor>\n                <font>SansSerif-plain-14</font>\n                <URIString>http://vue.tufts.edu/rdf/resource/400bcd5e39fad37c2b0b7e5496c8827d</URIString>\n                <titleStyle ID=\"3\" label=\"Header\"\n                    created=\"1388347735385\" x=\"335.5\" y=\"174.5\"\n                    width=\"129.0\" height=\"50.0\" strokeWidth=\"0.0\"\n                    autoSized=\"true\" isStyle=\"true\" xsi:type=\"node\">\n                    <strokeColor>#404040</strokeColor>\n                    <textColor>#FFFFFF</textColor>\n                    <font>Gill Sans-plain-36</font>\n                    <URIString>http://vue.tufts.edu/rdf/resource/400bcd5f39fad37c2b0b7e5405c26a3d</URIString>\n                    <shape xsi:type=\"rectangle\"/>\n                </titleStyle>\n                <textStyle ID=\"4\" label=\"Slide Text\"\n                    created=\"1388347735387\" x=\"346.5\" y=\"283.0\"\n                    width=\"108.0\" height=\"34.0\" strokeWidth=\"0.0\"\n                    autoSized=\"true\" isStyle=\"true\" xsi:type=\"node\">\n                    <strokeColor>#404040</strokeColor>\n                    <textColor>#FFFFFF</textColor>\n                    <font>Gill Sans-plain-22</font>\n                    <URIString>http://vue.tufts.edu/rdf/resource/400bcd6039fad37c2b0b7e54838aa09c</URIString>\n                    <shape xsi:type=\"rectangle\"/>\n                </textStyle>\n                <linkStyle ID=\"5\" label=\"Links\" created=\"1388347735388\"\n                    x=\"373.5\" y=\"385.0\" width=\"53.0\" height=\"30.0\"\n                    strokeWidth=\"0.0\" autoSized=\"true\" isStyle=\"true\" xsi:type=\"node\">\n                    <strokeColor>#404040</strokeColor>\n                    <textColor>#B3BFE3</textColor>\n                    <font>Gill Sans-plain-18</font>\n                    <URIString>http://vue.tufts.edu/rdf/resource/400bcd6139fad37c2b0b7e542860fb86</URIString>\n                    <shape xsi:type=\"rectangle\"/>\n                </linkStyle>\n            </masterSlide>\n        </pathway>\n    </PathwayList>\n    <date>2013-12-29</date>\n    <modelVersion>6</modelVersion>\n    <saveLocation>/home/eoyilmaz/Documents/development/stalker/stalker/docs/source/_static/images</saveLocation>\n    <saveFile>/home/eoyilmaz/Documents/development/stalker/stalker/docs/source/_static/images/Task_Status_Workflow.vue</saveFile>\n</LW-MAP>\n"
  },
  {
    "path": "docs/source/_static/images/stalker_design.vue",
    "content": "<!-- Tufts VUE 3.3.0 concept-map (stalker_design.vue) 2016-12-24 -->\n<!-- Tufts VUE: http://vue.tufts.edu/ -->\n<!-- Do Not Remove: VUE mapping @version(1.1) jar:file:/C:/Program%20Files%20(x86)/VUE/VUE.jar!/tufts/vue/resources/lw_mapping_1_1.xml -->\n<!-- Do Not Remove: Saved date Sat Dec 24 23:14:02 EET 2016 by eoyilmaz on platform Windows 8 6.2 in JVM 1.7.0_21-b11 -->\n<!-- Do Not Remove: Saving version @(#)VUE: built October 8 2015 at 1658 by tomadm on Linux 2.6.32-504.23.4.el6.x86_64 i386 JVM 1.7.0_21-b11(bits=32) -->\n<?xml version=\"1.0\" encoding=\"US-ASCII\"?>\n<LW-MAP xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:noNamespaceSchemaLocation=\"none\" ID=\"0\"\n    label=\"stalker_design.vue\" created=\"0\" x=\"0.0\" y=\"0.0\"\n    width=\"12530.668\" height=\"3696.0002\" strokeWidth=\"0.0\" autoSized=\"false\">\n    <resource referenceCreated=\"1482614042559\" size=\"542692\"\n        spec=\"C:\\Users\\eoyilmaz\\Documents\\development\\stalker\\stalker\\docs\\source\\_static\\images\\stalker_design.vue\"\n        type=\"1\" xsi:type=\"URLResource\">\n        <title>stalker_design.vue</title>\n        <property key=\"File\" value=\"C:\\Users\\eoyilmaz\\Documents\\development\\stalker\\stalker\\docs\\source\\_static\\images\\stalker_design.vue\"/>\n    </resource>\n    <fillColor>#FFFFFF</fillColor>\n    <strokeColor>#404040</strokeColor>\n    <textColor>#000000</textColor>\n    <font>SansSerif-plain-14</font>\n    <URIString>http://vue.tufts.edu/rdf/resource/5d104b14c00007d601b277f03c449819</URIString>\n    <child ID=\"4225\" label=\"Budget\" layerID=\"1\" created=\"1471422397174\"\n        x=\"9346.564\" y=\"2293.409\" width=\"160.5\" height=\"165.2019\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/97ae2d14c0a82a4a019efdbdfb2a0477</URIString>\n        <child ID=\"4232\" label=\"ProjectMixin\" created=\"1471422481990\"\n            x=\"34.0\" y=\"23.0\" width=\"82.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d14c0a82a4a019efdbde18ed0a7</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4248\" label=\"DAGMixin\" created=\"1471423050069\"\n            x=\"34.0\" y=\"43.25\" width=\"65.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d14c0a82a4a019efdbd0fb6ad9c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4226\" label=\"entries\" created=\"1471422397175\"\n            x=\"34.0\" y=\"63.5\" width=\"49.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d15c0a82a4a019efdbdad4b79ff</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4227\" label=\"custom_template | STRING\"\n            created=\"1471422397175\" x=\"34.0\" y=\"83.75\" width=\"161.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d15c0a82a4a019efdbd06132a49</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4228\" created=\"1471422397175\" x=\"34.0\" y=\"104.0\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d15c0a82a4a019efdbd9fd9c70a</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4229\" created=\"1471422397176\" x=\"34.0\" y=\"126.201965\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d15c0a82a4a019efdbd6059f1d9</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4230\" created=\"1471422397176\" x=\"34.0\" y=\"144.20193\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d15c0a82a4a019efdbd669ca722</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3382\" label=\"Message\" layerID=\"1\" created=\"1295883012242\"\n        x=\"9785.448\" y=\"1508.0773\" width=\"199.5\" height=\"226.24997\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#D4FF00</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b8a673297f0001017e6ab533f3ced4f1</URIString>\n        <child ID=\"3467\" label=\"StatusMixin\" created=\"1296050031611\"\n            x=\"34.0\" y=\"23.0\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/c2bbcec17f0001013611c755dc3f00fa</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3383\" label=\"from --> created_by\"\n            created=\"1295883012242\" x=\"34.0\" y=\"43.25\" width=\"123.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8a673297f0001017e6ab533fe3db766</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3384\" label=\"to | MANY | USER\"\n            created=\"1295883012242\" x=\"34.0\" y=\"63.5\" width=\"105.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#DAA9FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8a673297f0001017e6ab533c72f66e7</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3385\" label=\"subject | STRING\"\n            created=\"1295883012242\" x=\"34.0\" y=\"83.75\" width=\"105.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8a673297f0001017e6ab533ddf68e57</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3386\" label=\"body | STRING\" created=\"1295883012243\"\n            x=\"34.0\" y=\"104.0\" width=\"91.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8a6732a7f0001017e6ab533785e3d58</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3387\" label=\"in_reply_to | INTEGER | MESSAGE.id\"\n            created=\"1295883012243\" x=\"34.0\" y=\"124.25\" width=\"212.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8a6732a7f0001017e6ab5335ce88fa0</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3388\" label=\"replies | MANY | MESSAGE\"\n            created=\"1295883012243\" x=\"34.0\" y=\"144.5\" width=\"156.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#DAA9FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8a6732a7f0001017e6ab533df4fafbf</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3390\" label=\"attachments | MANY | SIMPLEENTTIY\"\n            created=\"1295883012243\" x=\"34.0\" y=\"164.75\" width=\"213.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8a6732a7f0001017e6ab5337661773f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3391\" created=\"1295883012243\" x=\"34.0\" y=\"185.0\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8a6732a7f0001017e6ab533aa4e4f71</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4052\" created=\"1358149001263\" x=\"34.0\" y=\"205.25\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#30D643</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/37fd2d78c0a8000435fa83793037fe30</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1088\" label=\"Asset\" layerID=\"1\" created=\"1272403764881\"\n        x=\"8932.898\" y=\"2475.277\" width=\"178.5\" height=\"142.99995\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/4137afd47f00010135ea5711d86d98a9</URIString>\n        <child ID=\"4016\" label=\"CodeMixin\" created=\"1358081793429\"\n            x=\"34.0\" y=\"23.0\" width=\"70.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/33fc5650c0a8000435fa8379675e6ac8</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1512\" label=\"shots | MANY | SHOT\"\n            created=\"1277558085545\" x=\"34.0\" y=\"43.25\" width=\"125.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/746640447f0001014a6b2ab7fc9c5221</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1137\" label=\"sequences | shots -> sequence\"\n            created=\"1272404571518\" x=\"34.0\" y=\"63.5\" width=\"185.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4140ccdd7f00010135ea57116e5577a9</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2499\" created=\"1295294796978\" x=\"34.0\" y=\"83.75\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b7657f0001015ee819a8aafadfdf</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"3787\" created=\"1310511119720\" x=\"34.0\" y=\"104.0\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/208db1db7f000101264d107e3c78088c</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4039\" created=\"1358111943627\" x=\"34.0\" y=\"121.99998\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aec5c0a8000435fa837926646ede</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1131\" layerID=\"1\" created=\"1272404424409\" x=\"8775.111\"\n        y=\"751.1875\" width=\"1115.1855\" height=\"757.40625\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/4137afe67f00010135ea57113e9099fb</URIString>\n        <point1 x=\"8775.611\" y=\"751.6875\"/>\n        <point2 x=\"9889.797\" y=\"1508.0938\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">3382</ID2>\n        <ctrlPoint0 x=\"8769.293\" y=\"1480.0895\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9894.276\" y=\"1397.921\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1145\" label=\"Link\" layerID=\"1\" created=\"1272404675030\"\n        x=\"9597.522\" y=\"828.225\" width=\"168.75\" height=\"185.4519\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/4140cd037f00010135ea57112130f8c4</URIString>\n        <child ID=\"1147\" label=\"full_path | UNICODE\"\n            created=\"1272404693670\" x=\"34.0\" y=\"23.0\" width=\"123.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4140cd037f00010135ea57117ab47786</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3989\" label=\"original_filename | UNICODE\"\n            created=\"1352553321865\" x=\"34.0\" y=\"43.25\" width=\"172.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/ec507e437f00010143bf3d773ab1cc57</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4141\" label=\"path\" created=\"1369384313981\" x=\"34.0\"\n            y=\"63.5\" width=\"36.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acab167b6a9fda5a40fe82ce930ac9</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4156\" label=\"filename\" created=\"1369385659119\"\n            x=\"34.0\" y=\"83.75\" width=\"60.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5bfbb467b6a9fda5a40fe82ac8cdeb8</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4157\" label=\"extension\" created=\"1369385672521\"\n            x=\"34.0\" y=\"104.0\" width=\"67.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5bfbb477b6a9fda5a40fe824a2cb1f4</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4143\" created=\"1369384327440\" x=\"34.0\" y=\"124.25\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acab0d7b6a9fda5a40fe82688b31f3</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"3788\" created=\"1310511178848\" x=\"34.0\" y=\"146.45197\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/208db1ed7f000101264d107ef3bb38ae</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4044\" created=\"1358112017696\" x=\"34.0\" y=\"164.45193\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#30D643</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3624ae36c0a8000435fa837938f1c1e7</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1157\" label=\"Playlist\" layerID=\"1\"\n        created=\"1272404832018\" x=\"9051.126\" y=\"1514.2256\" width=\"123.0\"\n        height=\"66.5\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/4140cd0a7f00010135ea571155c75028</URIString>\n        <child ID=\"1160\" label=\"files | MANY | LINK\"\n            created=\"1272404845966\" x=\"34.0\" y=\"23.0\" width=\"111.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4140cd0b7f00010135ea5711602370dd</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2491\" created=\"1295294705784\" x=\"34.0\" y=\"43.25\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b7687f0001015ee819a82b2a236f</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"1166\" label=\"Project\" layerID=\"1\" created=\"1272404937838\"\n        x=\"8377.885\" y=\"1858.0773\" width=\"201.75\" height=\"437.74994\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/4140cd0d7f00010135ea5711f07153c7</URIString>\n        <child ID=\"3461\" label=\"StatusMixin\" created=\"1296050001344\"\n            x=\"34.0\" y=\"23.0\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/c2bbced07f0001013611c75577191d5f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3486\" label=\"ScheduleMixin\" created=\"1297430760778\"\n            x=\"34.0\" y=\"43.25\" width=\"94.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/14e83dd27f0001017490649e82b3b87a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4013\" label=\"CodeMixin\" created=\"1358081773550\"\n            x=\"34.0\" y=\"63.5\" width=\"70.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/33fc5655c0a8000435fa8379368c5321</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2221\" label=\"lead | ONE | USER\"\n            created=\"1291042857476\" x=\"34.0\" y=\"83.75\" width=\"113.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/98294d057f000101247aa859e3c11b58</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1799\" label=\"image_format | ONE | IMAGEFORMAT\"\n            created=\"1279573849214\" x=\"34.0\" y=\"104.0\" width=\"216.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/ec8bebda7f0001010147e4f04ea33efd</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1423\" label=\"fps | FLOAT\" created=\"1277505694849\"\n            x=\"34.0\" y=\"124.25\" width=\"74.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7146b48d7f0001010a530461cc109033</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2007\" label=\"structure | ONE | STRUCTURE\"\n            created=\"1289689925944\" x=\"34.0\" y=\"144.5\" width=\"178.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/47830e6e7f0001015a3184d85f950ed5</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2195\" label=\"repository | ONE | REPOSITORY\"\n            created=\"1290434059609\" x=\"34.0\" y=\"164.75\" width=\"186.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/73dd535d7f0001010b45ad3dcc59d99c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2011\" label=\"is_stereoscopic | BOOL\"\n            created=\"1290254126593\" x=\"34.0\" y=\"185.0\" width=\"141.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/6924820d7f00010168376cc3eaa57d6b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1962\" label=\"display_width | FLOAT\"\n            created=\"1289153600481\" x=\"34.0\" y=\"205.25\" width=\"132.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/278b9a9a7f000101775978e75d5e32b6</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4059\" label=\"working_hours | dictionary\"\n            created=\"1363733530407\" x=\"34.0\" y=\"225.5\" width=\"154.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/8c9f23e0eead3b3354aea17a122ae7af</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4063\" label=\"tasks | MANY | TASK\"\n            created=\"1363733707277\" x=\"34.0\" y=\"245.75\" width=\"121.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/8c9f23e1eead3b3354aea17a64edbd9c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3812\" label=\"tickets | MANY | TICKET\"\n            created=\"1315471223904\" x=\"34.0\" y=\"266.0\" width=\"138.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/48326e217f0001017a8ff303fe1e74b3</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1171\" label=\"users | MANY | USER\"\n            created=\"1272404979390\" x=\"34.0\" y=\"286.25\" width=\"127.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4140cd107f00010135ea57114a508f5b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1506\" label=\"sequences | MANY | SEQUENCE\"\n            created=\"1277546655591\" x=\"34.0\" y=\"306.5\" width=\"191.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/73b722e97f000101783f472b90b90716</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1520\" label=\"assets | MANY | ASSET\"\n            created=\"1277849783149\" x=\"34.0\" y=\"326.75\" width=\"137.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/85ca6d117f0001010cdfa65c929e6a1b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3997\"\n            label=\"project_tasks -> resources (for&#xa;all TaskableEntities.tasks)\"\n            created=\"1357765406294\" x=\"34.0\" y=\"347.0\" width=\"179.0\"\n            height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/2120bb40c0a80004173b812d2e40c79a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2496\" created=\"1295294753204\" x=\"34.0\" y=\"378.5\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b76a7f0001015ee819a8fe024cea</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"3786\" created=\"1310511115547\" x=\"34.0\" y=\"398.75\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/208db2207f000101264d107e85978e7a</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4036\" created=\"1358111937208\" x=\"34.0\" y=\"416.74997\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aec9c0a8000435fa83792ea2bbbd</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1176\" label=\"Sequence\" layerID=\"1\"\n        created=\"1272405154343\" x=\"8520.342\" y=\"2475.277\" width=\"133.5\"\n        height=\"145.24997\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/4144a3f27f00010135ea57117ebd1aac</URIString>\n        <child ID=\"4014\" label=\"CodeMixin\" created=\"1358081784556\"\n            x=\"34.0\" y=\"23.0\" width=\"70.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/33fc565ac0a8000435fa8379e65a22be</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1182\" label=\"shots | MANY | SHOT\"\n            created=\"1272405273586\" x=\"34.0\" y=\"43.25\" width=\"125.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4144a3f47f00010135ea57118b03d3ed</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2008\" label=\"offline | LINK\" created=\"1289702947670\"\n            x=\"34.0\" y=\"63.5\" width=\"80.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/484975a57f0001015a3184d81211a0d2</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2010\" label=\"stoaryboard | LINK\"\n            created=\"1289703052278\" x=\"34.0\" y=\"83.75\" width=\"112.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/484b00917f0001015a3184d88131b25d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2497\" created=\"1295294780293\" x=\"34.0\" y=\"104.0\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b76b7f0001015ee819a858e7cc58</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4037\" created=\"1358111939638\" x=\"34.0\" y=\"124.25\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aecbc0a8000435fa837918244a2e</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1185\" label=\"Shot\" layerID=\"1\" created=\"1272405338854\"\n        x=\"8701.075\" y=\"2475.277\" width=\"201.75\" height=\"286.99997\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/414f1a427f00010135ea57117bdc53d1</URIString>\n        <child ID=\"4015\" label=\"CodeMixin\" created=\"1358081789140\"\n            x=\"34.0\" y=\"23.0\" width=\"70.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/33fc565dc0a8000435fa83794c989a62</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1519\" label=\"sequences | MANY | SEQUENCE\"\n            created=\"1277849616279\" x=\"34.0\" y=\"43.25\" width=\"191.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/85ca6d1d7f0001010cdfa65c7007b57b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4139\" label=\"scenes | MANY | SCENES\"\n            created=\"1368620381286\" x=\"34.0\" y=\"63.5\" width=\"152.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a8223ca7a62df9af4916575120e6ee34</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1188\" label=\"cut_in | INTEGER\"\n            created=\"1272405364822\" x=\"34.0\" y=\"83.75\" width=\"106.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/414f1a427f00010135ea5711fda03f80</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1190\" label=\"cut_out | INTEGER\"\n            created=\"1272405378378\" x=\"34.0\" y=\"104.0\" width=\"113.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/414f1a437f00010135ea57113c08fcf5</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1187\" label=\"cut_duration | INTEGER\"\n            created=\"1272405350634\" x=\"34.0\" y=\"124.25\" width=\"141.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/414f1a427f00010135ea57111b10b68c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4083\" label=\"image_format | ONE | IMAGEFORMAT\"\n            created=\"1365064238300\" x=\"34.0\" y=\"144.5\" width=\"216.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d42b5a304cc66935691a928b2c7f7d9d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3828\" label=\"elements | MANY | FILE\"\n            created=\"1336475842369\" x=\"34.0\" y=\"164.75\" width=\"138.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/2c2b7b2c7f00010118154bb12694d02a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2009\" label=\"online | LINK\" created=\"1289702971599\"\n            x=\"34.0\" y=\"185.0\" width=\"81.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4849e7047f0001015a3184d87c51e040</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1960\" label=\"inter_axial_distance | FLOAT\"\n            created=\"1289153571666\" x=\"34.0\" y=\"205.25\" width=\"167.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/278b9ab27f000101775978e70bad0084</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1961\" label=\"convergence_distance | FLOAT\"\n            created=\"1289153587186\" x=\"34.0\" y=\"225.5\" width=\"181.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/278b9ab37f000101775978e7a0658db2</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2502\" created=\"1295294828167\" x=\"34.0\" y=\"245.75\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b76d7f0001015ee819a88a8002c7</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4051\" created=\"1358118709200\" x=\"34.0\" y=\"266.0\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#30D643</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/362ef8b9c0a8000435fa8379dcc6daaa</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1205\" label=\"Task\" layerID=\"1\" created=\"1272406042962\"\n        x=\"8630.298\" y=\"1858.0773\" width=\"251.25\" height=\"367.99997\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/4156061a7f00010135ea5711dcb8f75c</URIString>\n        <child ID=\"3464\" label=\"StatusMixin\" created=\"1296050015627\"\n            x=\"34.0\" y=\"23.0\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/c2bbcee47f0001013611c755c3131536</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4275\" label=\"DateRangeMixin\" created=\"1471423858087\"\n            x=\"34.0\" y=\"43.25\" width=\"103.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97b7c5abc0a82a4a010817798adf5656</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3560\" label=\"ReferenceMixin\" created=\"1298330420443\"\n            x=\"34.0\" y=\"63.5\" width=\"100.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4a8686e97f000101101873a641c3e7c7</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4200\" label=\"ScheduleMixin\" created=\"1459165867490\"\n            x=\"34.0\" y=\"83.75\" width=\"94.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/bd12292ac0a82a4a01152d0efe940b4e</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4246\" label=\"DAGMixin\" created=\"1471423013290\"\n            x=\"34.0\" y=\"104.0\" width=\"65.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2c9ec0a82a4a019efdbd325202e5</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1795\" label=\"priority | INTEGER\"\n            created=\"1279319036406\" x=\"34.0\" y=\"124.25\" width=\"109.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/dd5bc86d7f0001010c4cdf7ed67689ed</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1206\" label=\"resources | MANY | USER\"\n            created=\"1272406057074\" x=\"34.0\" y=\"144.5\" width=\"151.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4156061a7f00010135ea57115fceb210</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4086\" label=\"bid | STRING\" created=\"1365379231725\"\n            x=\"34.0\" y=\"164.75\" width=\"82.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/e8a41d0c648c6197658d749a0994b723</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1394\" label=\"depends_on | MANY | TASK\"\n            created=\"1277500447230\" x=\"34.0\" y=\"185.0\" width=\"140.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/70f609097f0001010a5304612bb962ad</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1213\" label=\"milestone | BOOL\"\n            created=\"1272406111786\" x=\"34.0\" y=\"205.25\" width=\"108.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4156061c7f00010135ea5711df3cc9d5</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1219\" label=\"time_logs | MANY | TIMELOG\"\n            created=\"1272406178764\" x=\"34.0\" y=\"225.5\" width=\"168.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4156061d7f00010135ea5711f78c3ffb</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1482\" label=\"versions | MANY | VERSION\"\n            created=\"1277543180124\" x=\"34.0\" y=\"245.75\" width=\"161.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/738287e27f000101783f472bbfb64909</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4062\" label=\"project | ONE | PROJECT\"\n            created=\"1363733633376\" x=\"34.0\" y=\"266.0\" width=\"148.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/8c9f23eceead3b3354aea17a20109083</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1525\"\n            label=\"published_versions | version.published == True\"\n            created=\"1277850285557\" x=\"34.0\" y=\"286.25\" width=\"277.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/85d09fcc7f0001010cdfa65c9b0d5367</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1526\"\n            label=\"last_published_version | published_versions[-1]\"\n            created=\"1277850333863\" x=\"34.0\" y=\"306.5\" width=\"282.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/85d0f0437f0001010cdfa65c3ab63ddc</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2501\" created=\"1295294816788\" x=\"34.0\" y=\"326.75\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b76f7f0001015ee819a8bb925774</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4040\" created=\"1358111953850\" x=\"34.0\" y=\"347.0\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aed0c0a8000435fa83790c3a64d8</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1220\" layerID=\"1\" created=\"1272406209511\" x=\"8757.033\"\n        y=\"751.1875\" width=\"18.544922\" height=\"1107.4375\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/4156061d7f00010135ea5711c2172d7e</URIString>\n        <point1 x=\"8775.078\" y=\"751.6875\"/>\n        <point2 x=\"8757.533\" y=\"1858.125\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1205</ID2>\n        <ctrlPoint0 x=\"8757.643\" y=\"1831.0906\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8757.919\" y=\"1814.0806\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1227\" label=\"TimeLog\" layerID=\"1\" created=\"1272406409370\"\n        x=\"9203.517\" y=\"1514.2256\" width=\"143.25\" height=\"124.99998\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/4156061e7f00010135ea571114c0ee72</URIString>\n        <child ID=\"3814\" label=\"ScheduleMixin\" created=\"1316463635820\"\n            x=\"34.0\" y=\"23.0\" width=\"94.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/835f6ce87f000101123acf342d8e5a89</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1231\" label=\"task | ONE | TASK\"\n            created=\"1272406430066\" x=\"34.0\" y=\"43.25\" width=\"108.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4156061f7f00010135ea57112dad9a44</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1232\" label=\"resource | ONE | USER\"\n            created=\"1272406435518\" x=\"34.0\" y=\"63.5\" width=\"138.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4156061f7f00010135ea57110e5bbdd2</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2493\" created=\"1295294718645\" x=\"34.0\" y=\"83.75\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b7707f0001015ee819a8d0c7631c</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4050\" created=\"1358118036346\" x=\"34.0\" y=\"104.0\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#30D643</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3624ae42c0a8000435fa83796660f2f7</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1235\" label=\"User\" layerID=\"1\" created=\"1272406484302\"\n        x=\"8087.4805\" y=\"1348.2256\" width=\"206.25\" height=\"415.2499\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/415b1bc47f00010135ea571102f905e6</URIString>\n        <child ID=\"4072\" label=\"ACLMixin\" created=\"1365050633635\"\n            x=\"34.0\" y=\"23.0\" width=\"64.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d35be5214cc66935691a928b1dbc39ce</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3958\" label=\"StatusMixin\" created=\"1337252633904\"\n            x=\"34.0\" y=\"43.25\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5f5269207f0001011df0758683afba34</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1237\" label=\"email | UNICODE\" created=\"1272406498598\"\n            x=\"34.0\" y=\"63.5\" width=\"107.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/415b1bc57f00010135ea5711bb1452df</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1243\" label=\"login | UNICODE\" created=\"1272406543618\"\n            x=\"34.0\" y=\"83.75\" width=\"103.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/415b1bc67f00010135ea5711cc5235aa</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1241\" label=\"departments | MANY | DEPARTMENT\"\n            created=\"1272406526891\" x=\"34.0\" y=\"104.0\" width=\"212.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/415b1bc57f00010135ea5711df108926</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1245\" label=\"password | UNICODE\"\n            created=\"1272406576770\" x=\"34.0\" y=\"124.25\" width=\"131.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/415b1bc67f00010135ea57118ffe5686</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1246\" label=\"groups | MANY | GROUPS\"\n            created=\"1272406583326\" x=\"34.0\" y=\"144.5\" width=\"152.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/415b1bc77f00010135ea57113656d812</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1442\" label=\"tasks | MANY | TASK\"\n            created=\"1277507144415\" x=\"34.0\" y=\"164.75\" width=\"121.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/715c39c47f0001010a53046192f39ef8</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2370\" label=\"last_login | DATETIME\"\n            created=\"1294054995724\" x=\"34.0\" y=\"185.0\" width=\"132.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4bb089d87f000101283b96bbba818d63</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4158\" label=\"vacations | MANY | VACATION\"\n            created=\"1382708754314\" x=\"34.0\" y=\"205.25\" width=\"171.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/efe00692c8211d50048a3743a2254d81</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1530\" label=\"sequences_lead | MANY | SEQUENCE\"\n            created=\"1277850633268\" x=\"34.0\" y=\"225.5\" width=\"222.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#DAA9FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/85d5b47c7f0001010cdfa65ca49c356e</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2222\" label=\"projects_lead| MANY | PROJECT\"\n            created=\"1291125082795\" x=\"34.0\" y=\"245.75\" width=\"189.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#DAA9FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9d0d965c7f0001015d73b35d3456f432</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3381\" label=\"messages | MANY | MESSAGE\"\n            created=\"1295882058734\" x=\"34.0\" y=\"266.0\" width=\"178.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#DAA9FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8973e797f0001017e6ab533ca951c0f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1528\" label=\"projects | tasks -> project\"\n            created=\"1277850527948\" x=\"34.0\" y=\"286.25\" width=\"156.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/85d3daed7f0001010cdfa65c1ca9c5be</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2220\"\n            label=\"assets | tasks -> owner&#xa;query(Asset).filter(User.assets)\"\n            created=\"1291042704348\" x=\"34.0\" y=\"306.5\" width=\"190.0\"\n            height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/982531327f000101247aa859a673b56a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2485\" created=\"1295294628854\" x=\"34.0\" y=\"338.0\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/959391d87f0001015ee819a88b8f9436</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4034\" created=\"1358111917529\" x=\"34.0\" y=\"358.25\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aed4c0a8000435fa83798d86a388</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4223\" created=\"1468653377365\" x=\"34.0\" y=\"376.24997\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#33A8F5</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f2902e11c0a8000f011bc52f7510551a</URIString>\n            <shape xsi:type=\"chevron\"/>\n        </child>\n        <child ID=\"4224\" created=\"1468653377365\" x=\"34.0\" y=\"394.24994\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f2902e11c0a8000f011bc52fda3eaaf8</URIString>\n            <shape xsi:type=\"rhombus\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1258\" label=\"ShootTake\" layerID=\"1\"\n        created=\"1272406937150\" x=\"9259.6045\" y=\"1858.0773\"\n        width=\"138.0\" height=\"228.5\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/416412927f00010135ea5711ee89e7ec</URIString>\n        <child ID=\"1262\" label=\"first_frame\" created=\"1272406965702\"\n            x=\"34.0\" y=\"23.0\" width=\"75.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/416412937f00010135ea5711e1bf0da0</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1263\" label=\"frames_aspect_ratio\"\n            created=\"1272406971602\" x=\"34.0\" y=\"43.25\" width=\"131.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/416412937f00010135ea5711025e4908</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1264\" label=\"frames_have_slate\"\n            created=\"1272406979937\" x=\"34.0\" y=\"63.5\" width=\"121.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/416412937f00010135ea571114f2487c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1265\" label=\"last_frame\" created=\"1272406987082\"\n            x=\"34.0\" y=\"83.75\" width=\"73.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/416412947f00010135ea5711ba3432c6</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1266\" label=\"link (entity)\" created=\"1272406995742\"\n            x=\"34.0\" y=\"104.0\" width=\"73.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/416412947f00010135ea57119c4a005f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1267\" label=\"movie_aspect_ratio\"\n            created=\"1272407002554\" x=\"34.0\" y=\"124.25\" width=\"124.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/416412947f00010135ea571193baeb10</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1268\" label=\"movie_has_slate\" created=\"1272407012830\"\n            x=\"34.0\" y=\"144.5\" width=\"108.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/416412947f00010135ea57111edef472</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1269\" label=\"path_to_frames\" created=\"1272407024178\"\n            x=\"34.0\" y=\"164.75\" width=\"102.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/416412947f00010135ea571180cf8f37</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1275\" label=\"uploaded_movie\" created=\"1272407065510\"\n            x=\"34.0\" y=\"185.0\" width=\"104.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/416412967f00010135ea5711e7b676e0</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2506\" created=\"1295294864385\" x=\"34.0\" y=\"205.25\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b7747f0001015ee819a8fb4d3c74</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"1279\" layerID=\"1\" created=\"1272407119615\" x=\"8774.577\"\n        y=\"751.1875\" width=\"550.34766\" height=\"1107.4062\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/416412967f00010135ea57116b7f484f</URIString>\n        <point1 x=\"8775.077\" y=\"751.6875\"/>\n        <point2 x=\"9324.425\" y=\"1858.0938\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1258</ID2>\n        <ctrlPoint0 x=\"8757.623\" y=\"1831.3555\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9321.81\" y=\"1786.6119\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1295\" label=\"Tag\" layerID=\"1\" created=\"1272407402727\"\n        x=\"8619.469\" y=\"609.8772\" width=\"62.5\" height=\"102.2019\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/41652daa7f00010135ea57110fc3ae1b</URIString>\n        <child ID=\"4150\" created=\"1369384396735\" x=\"34.0\" y=\"23.0\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acabf37b6a9fda5a40fe82fa141e70</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4025\" created=\"1358111854271\" x=\"34.0\" y=\"45.201965\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aed6c0a8000435fa8379a22ba050</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4215\" created=\"1468653297748\" x=\"34.0\" y=\"63.201942\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#33A8F5</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbadc0a8000f011bc52f90de7713</URIString>\n            <shape xsi:type=\"chevron\"/>\n        </child>\n        <child ID=\"4216\" created=\"1468653301243\" x=\"34.0\" y=\"81.20192\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbadc0a8000f011bc52f19c47bcb</URIString>\n            <shape xsi:type=\"rhombus\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1299\" label=\"Event\" layerID=\"1\" created=\"1272407465751\"\n        x=\"10222.512\" y=\"828.225\" width=\"158.25\" height=\"167.75\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/41652dab7f00010135ea57113765e4bc</URIString>\n        <child ID=\"1300\" label=\"attribute_name | UNICODE\"\n            created=\"1272407733084\" x=\"34.0\" y=\"23.0\" width=\"158.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/417945b37f00010135ea5711f7eb2636</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1302\" label=\"event_type | INTEGER\"\n            created=\"1272407752888\" x=\"34.0\" y=\"43.25\" width=\"130.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/417945b47f00010135ea5711afaf9d5a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1304\" label=\"entity | ONE | ENTITY\"\n            created=\"1272407763884\" x=\"34.0\" y=\"63.5\" width=\"123.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/417945b47f00010135ea57110de734c6</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1308\" label=\"user | ONE | USER\"\n            created=\"1272407788218\" x=\"34.0\" y=\"83.75\" width=\"110.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/417945b57f00010135ea5711d9b9080c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1309\" label=\"date_created | DATETIME\"\n            created=\"1272407797912\" x=\"34.0\" y=\"104.0\" width=\"153.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/417945b57f00010135ea5711545ebf12</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1305\" label=\"meta_data | UNICODE\"\n            created=\"1272407772990\" x=\"34.0\" y=\"124.25\" width=\"132.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/417945b47f00010135ea57118bfc42a4</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2510\" created=\"1295294901044\" x=\"34.0\" y=\"144.5\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b7767f0001015ee819a8317b3c9c</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"1329\" label=\"Department\" layerID=\"1\"\n        created=\"1273399280163\" x=\"8326.049\" y=\"1348.2256\" width=\"135.0\"\n        height=\"124.99998\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7c845ab97f00010136a8a19945559ad8</URIString>\n        <child ID=\"4071\" label=\"CodeMixin\" created=\"1364825474109\"\n            x=\"34.0\" y=\"23.0\" width=\"70.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/c5f034e54cc66935691a928b23df921d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1331\" label=\"users | MANY | USER\"\n            created=\"1273399329020\" x=\"34.0\" y=\"43.25\" width=\"127.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7c845ab97f00010136a8a19938a35367</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2223\" label=\"lead | ONE | USER\"\n            created=\"1291842810434\" x=\"34.0\" y=\"63.5\" width=\"113.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/c7d533fd7f00010136536500f161e613</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2489\" created=\"1295294666491\" x=\"34.0\" y=\"83.75\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/959428897f0001015ee819a8be414bd8</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4033\" created=\"1358111904397\" x=\"34.0\" y=\"104.0\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aed8c0a8000435fa83795d4655ff</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1355\" label=\"StatusList\" layerID=\"1\"\n        created=\"1274041570267\" x=\"8298.414\" y=\"828.2019\" width=\"156.0\"\n        height=\"106.70194\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a33b57e77f0001016609e5f078560790</URIString>\n        <child ID=\"3825\" label=\"TargetEntityTypeMixin\"\n            created=\"1317078064232\" x=\"34.0\" y=\"23.0\" width=\"136.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a800f7887f00010130ce553671097af3</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1395\" label=\"statuses | MANY | STATUS\"\n            created=\"1277500691762\" x=\"34.0\" y=\"43.25\" width=\"155.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/70f9fdd47f0001010a5304617b221190</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4153\" created=\"1369384420632\" x=\"34.0\" y=\"63.5\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acac0d7b6a9fda5a40fe82045cd555</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4027\" created=\"1358111867437\" x=\"34.0\" y=\"85.701965\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aed9c0a8000435fa837963fcd72d</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1362\" label=\"Version\" layerID=\"1\" created=\"1275345214941\"\n        x=\"8911.798\" y=\"1858.0773\" width=\"234.0\" height=\"228.49997\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f0822ba57f000101666558bbfaa1423c</URIString>\n        <child ID=\"4247\" label=\"DAGMixin\" created=\"1471423035442\"\n            x=\"34.0\" y=\"23.0\" width=\"65.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2cb3c0a82a4a019efdbdc1bcd62a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1365\"\n            label=\"variant_name | STRING, DEFAULT=&quot;Main&quot;\"\n            created=\"1275345214943\" x=\"34.0\" y=\"43.25\" width=\"228.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f0822ba97f000101666558bbc49201a2</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1452\" label=\"version_number | INTEGER\"\n            created=\"1277507797831\" x=\"34.0\" y=\"63.5\" width=\"163.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/716648fb7f0001010a530461d3a9c89a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3729\" label=\"is_published | BOOL\"\n            created=\"1309373289745\" x=\"34.0\" y=\"83.75\" width=\"125.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/dcc1257d7f0001011f758c30f2b26443</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3993\" label=\"inputs | MANY | LINK\"\n            created=\"1352652805978\" x=\"34.0\" y=\"104.0\" width=\"122.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f07316197f00010143bf3d77fa79a157</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1952\" label=\"outputs | MANY | LINK\"\n            created=\"1288424286766\" x=\"34.0\" y=\"124.25\" width=\"129.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/fc128cad7f00010120d48785781bf5c0</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2106\"\n            label=\"template |&#xa;version_of.project.structure.templates&#xa;.target_entity_type[task.part_of.entity_type]\"\n            created=\"1290418741395\" x=\"34.0\" y=\"144.5\" width=\"259.0\"\n            height=\"53.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72f3a55b7f0001010b45ad3d8fd4b444</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2504\" created=\"1295294852115\" x=\"34.0\" y=\"187.25\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b7787f0001015ee819a8802e3a4c</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4046\" created=\"1358112036428\" x=\"34.0\" y=\"207.5\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#30D643</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3624ae50c0a8000435fa8379c88ae3ca</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1367\" layerID=\"1\" created=\"1275345268184\" x=\"9035.223\"\n        y=\"1013.17773\" width=\"596.4971\" height=\"845.4004\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f0822baa7f000101666558bbc8e3f079</URIString>\n        <point1 x=\"9631.22\" y=\"1013.67773\"/>\n        <point2 x=\"9035.723\" y=\"1858.0781\"/>\n        <ID1 xsi:type=\"node\">1145</ID1>\n        <ID2 xsi:type=\"node\">1362</ID2>\n        <ctrlPoint0 x=\"9546.949\" y=\"1167.8689\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9043.442\" y=\"1730.703\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1372\" layerID=\"1\" created=\"1277498507442\" x=\"8193.134\"\n        y=\"751.21094\" width=\"575.3457\" height=\"597.53906\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/70dc3fa77f0001010a5304616bf72ea2</URIString>\n        <point1 x=\"8767.9795\" y=\"751.71094\"/>\n        <point2 x=\"8193.634\" y=\"1348.25\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1235</ID2>\n        <ctrlPoint0 x=\"8702.578\" y=\"1317.4082\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8195.002\" y=\"1254.4216\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1373\" layerID=\"1\" created=\"1277498564390\" x=\"8388.514\"\n        y=\"751.1875\" width=\"389.1543\" height=\"597.53125\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/70dc3fa97f0001010a530461bed23383</URIString>\n        <point1 x=\"8777.168\" y=\"751.6875\"/>\n        <point2 x=\"8389.014\" y=\"1348.2188\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1329</ID2>\n        <ctrlPoint0 x=\"8784.788\" y=\"1330.6824\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8379.2705\" y=\"1213.946\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1396\" label=\"Status\" layerID=\"1\" created=\"1277500787817\"\n        x=\"8165.1694\" y=\"828.2019\" width=\"92.25\" height=\"120.49993\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/70fbee177f0001010a53046131252ebc</URIString>\n        <child ID=\"4053\" label=\"CodeMixin\" created=\"1358233205184\"\n            x=\"34.0\" y=\"23.0\" width=\"70.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3d020999c0a800044f9e91d0cec913ac</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2474\" created=\"1295294299280\" x=\"34.0\" y=\"43.25\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/958f53697f0001015ee819a87bfe14d0</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4028\" created=\"1358111869751\" x=\"34.0\" y=\"63.5\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aedcc0a8000435fa83792e20f08b</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4221\" created=\"1468653326129\" x=\"34.0\" y=\"81.49998\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#33A8F5</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbb1c0a8000f011bc52fdce9b087</URIString>\n            <shape xsi:type=\"chevron\"/>\n        </child>\n        <child ID=\"4222\" created=\"1468653326129\" x=\"34.0\" y=\"99.499954\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbb1c0a8000f011bc52fdc0896fc</URIString>\n            <shape xsi:type=\"rhombus\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1402\" label=\"Groups\" layerID=\"1\" created=\"1277500921470\"\n        x=\"7935.752\" y=\"1347.4773\" width=\"135.0\" height=\"104.74998\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/70ff4c457f0001010a5304618b714b63</URIString>\n        <child ID=\"4073\" label=\"ACLMixin\" created=\"1365050640218\"\n            x=\"34.0\" y=\"23.0\" width=\"64.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d35be5474cc66935691a928b9ebf2073</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1503\" label=\"users | MANY | USER\"\n            created=\"1277544525089\" x=\"34.0\" y=\"43.25\" width=\"127.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7396a2bf7f000101783f472b0c288dca</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2488\" created=\"1295294645319\" x=\"34.0\" y=\"63.5\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9594288d7f0001015ee819a808d4eae0</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4035\" created=\"1358111921466\" x=\"34.0\" y=\"83.75\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aeddc0a8000435fa83793c037b2a</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1444\" label=\"Playblast / Flipbook\" layerID=\"1\"\n        created=\"1277507592356\" x=\"9377.27\" y=\"1514.2256\" width=\"147.0\"\n        height=\"86.75\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/716433b77f0001010a530461ea2689a9</URIString>\n        <child ID=\"1450\" label=\"of | ONE | ASSETBASE\"\n            created=\"1277507592357\" x=\"34.0\" y=\"23.0\" width=\"133.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/716433b87f0001010a530461ae077f28</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1938\" label=\"file | ONE | FILE\"\n            created=\"1288269754490\" x=\"34.0\" y=\"43.25\" width=\"93.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f2dcabc17f0001010d9438de492b2f36</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2494\" created=\"1295294732353\" x=\"34.0\" y=\"63.5\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b77c7f0001015ee819a88d3af5f8</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"1465\" label=\"ImageFormat\" layerID=\"1\"\n        created=\"1277508730708\" x=\"8500.845\" y=\"829.0019\" width=\"147.0\"\n        height=\"167.45193\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7175e3647f0001010a5304618afb3c6d</URIString>\n        <child ID=\"1468\" label=\"width | INTEGER\" created=\"1277508730709\"\n            x=\"34.0\" y=\"23.0\" width=\"102.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7175e3647f0001010a530461c2444b8a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1469\" label=\"height | INTEGER\"\n            created=\"1277508787050\" x=\"34.0\" y=\"43.25\" width=\"107.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7175e3647f0001010a530461a525aa4a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1471\" label=\"device_aspect | FLOAT\"\n            created=\"1277508818841\" x=\"34.0\" y=\"63.5\" width=\"136.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7175e3657f0001010a530461d423e0c0</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1470\" label=\"pixel_aspect | FLOAT\"\n            created=\"1277508794355\" x=\"34.0\" y=\"83.75\" width=\"126.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7175e3647f0001010a530461ed5f93c3</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1797\" label=\"print_resolution | FLOAT\"\n            created=\"1279319466446\" x=\"34.0\" y=\"104.0\" width=\"143.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/dd623f737f0001010c4cdf7e1538b03a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4152\" created=\"1369384415644\" x=\"34.0\" y=\"124.25\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acac407b6a9fda5a40fe8240c9c0bf</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4026\" created=\"1358111863766\" x=\"34.0\" y=\"146.45197\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aedec0a8000435fa8379c28ae15c</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1475\" layerID=\"1\" created=\"1277542415873\" x=\"8778.163\"\n        y=\"751.1875\" width=\"671.7285\" height=\"763.53125\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7376e9ec7f000101783f472b76efe287</URIString>\n        <point1 x=\"8778.663\" y=\"751.6875\"/>\n        <point2 x=\"9449.392\" y=\"1514.2188\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1444</ID2>\n        <ctrlPoint0 x=\"8800.1\" y=\"1379.9562\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9446.507\" y=\"1423.3969\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1555\"\n        label=\"STALKER OBJECT MODEL&#xa;INHERITANCE DIAGRAM v1.5.0\"\n        layerID=\"1\" created=\"1279119389330\" x=\"8543.104\" y=\"-107.323044\"\n        width=\"501.333\" height=\"102.66669\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#B5B995</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-18</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/d175941d7f0001014ed1a34a819236b1</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"1817\" label=\"Implemented classes\" layerID=\"1\"\n        created=\"1282128100173\" x=\"9222.637\" y=\"-57.51773\" width=\"208.0\"\n        height=\"17.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/84ca7c997f00010138aea9817270f61e</URIString>\n        <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Implemented classes\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Implemented classes</label>\n    </child>\n    <child ID=\"1879\" label=\"Implemented attributes\" layerID=\"1\"\n        created=\"1288167062202\" x=\"9222.637\" y=\"98.84404\" width=\"301.0\"\n        height=\"17.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ecbe81757f0001012cc821ef070ba546</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Implemented attributes\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Implemented attributes</label>\n    </child>\n    <child ID=\"1887\" label=\"Entity\" layerID=\"1\" created=\"1288257108731\"\n        x=\"8709.4795\" y=\"609.0019\" width=\"133.5\" height=\"142.7019\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f21c2a7a7f0001010d9438de5c6ad321</URIString>\n        <child ID=\"1906\" label=\"tags | MANY | TAG\"\n            created=\"1288257253087\" x=\"34.0\" y=\"23.0\" width=\"108.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f220e0137f0001010d9438de0e9e63e0</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2439\" label=\"notes | MANY | NOTE\"\n            created=\"1294846706622\" x=\"34.0\" y=\"43.25\" width=\"125.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7ae32d587f0001017115e16621f44fe5</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4149\" created=\"1369384391227\" x=\"34.0\" y=\"63.5\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acac4f7b6a9fda5a40fe82e49025e5</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4042\" created=\"1358111975811\" x=\"34.0\" y=\"85.701965\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#30D643</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aee0c0a8000435fa837959e2d05e</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4217\" created=\"1468653304659\" x=\"34.0\" y=\"103.70194\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#33A8F5</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbb4c0a8000f011bc52ff34ef0a9</URIString>\n            <shape xsi:type=\"chevron\"/>\n        </child>\n        <child ID=\"4218\" created=\"1468653307926\" x=\"34.0\" y=\"121.70192\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbb4c0a8000f011bc52f65bedc44</URIString>\n            <shape xsi:type=\"rhombus\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1901\" label=\"SimpleEntity\" layerID=\"1\"\n        created=\"1288257253086\" x=\"8702.232\" y=\"141.87709\" width=\"202.5\"\n        height=\"345.20187\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f220e0127f0001010d9438de61473dd2</URIString>\n        <child ID=\"2263\" label=\"name | UNICODE\" created=\"1292744446733\"\n            x=\"34.0\" y=\"23.0\" width=\"108.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/ff73db697f0001015481be8a5451779a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2264\" label=\"description | UNICODE\"\n            created=\"1292744446733\" x=\"34.0\" y=\"43.25\" width=\"137.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/ff73db697f0001015481be8af558ee50</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1888\" label=\"created_by | ONE | USER\"\n            created=\"1288257108731\" x=\"34.0\" y=\"63.5\" width=\"149.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f21c2a7a7f0001010d9438de2db62acf</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1889\" label=\"updated_by | ONE | USER\"\n            created=\"1288257108731\" x=\"34.0\" y=\"83.75\" width=\"153.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f21c2a7a7f0001010d9438de885e42f6</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1890\" label=\"date_created | DATETIME\"\n            created=\"1288257108732\" x=\"34.0\" y=\"104.0\" width=\"150.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f21c2a7a7f0001010d9438de584fbe77</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1891\" label=\"date_updated | DATETIME\"\n            created=\"1288257108732\" x=\"34.0\" y=\"124.25\" width=\"154.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f21c2a7a7f0001010d9438dead15905c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2458\" label=\"nice_name | UNICODE\"\n            created=\"1294925510351\" x=\"34.0\" y=\"144.5\" width=\"138.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7f93720e7f0001017115e1664dc590db</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"1128\" label=\"thumbnail | ONE | LINK\"\n            created=\"1272404328842\" x=\"34.0\" y=\"164.75\" width=\"137.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/4137afd27f00010135ea5711ec68f281</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3632\" label=\"type | ONE | TYPE\"\n            created=\"1306191718656\" x=\"34.0\" y=\"185.0\" width=\"107.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/1f190a3a7f000101578cef1800e852f8</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3674\" label=\"__stalker_version__ | STRING\"\n            created=\"1307851983782\" x=\"34.0\" y=\"205.25\" width=\"177.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/820dc20f7f0001015d3258773963db05</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3478\" label=\"generic_data | MANY | SIMPLEENTITY\"\n            created=\"1296077941615\" x=\"34.0\" y=\"225.5\" width=\"217.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/c44466c67f0001013611c755f271acd4</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4272\" label=\"generic_text | STRING\"\n            created=\"1471423657470\" x=\"34.0\" y=\"245.75\" width=\"131.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97afe790c0a82a4a0108177921807cf8</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4151\" created=\"1369384405691\" x=\"34.0\" y=\"266.0\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acac627b6a9fda5a40fe822a456e6a</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4021\" created=\"1358111834001\" x=\"34.0\" y=\"288.20197\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#30D643</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aee2c0a8000435fa8379eb933d7b</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4211\" created=\"1468653281990\" x=\"34.0\" y=\"306.20193\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#33A8F5</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbb5c0a8000f011bc52f94cd3e52</URIString>\n            <shape xsi:type=\"chevron\"/>\n        </child>\n        <child ID=\"4212\" created=\"1468653285082\" x=\"34.0\" y=\"324.2019\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbb5c0a8000f011bc52f9eb42d35</URIString>\n            <shape xsi:type=\"rhombus\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"1912\" layerID=\"1\" created=\"1288257599993\" x=\"8214.804\"\n        y=\"751.25\" width=\"562.0869\" height=\"77.44531\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f22534227f0001010d9438dee0d42673</URIString>\n        <point1 x=\"8776.391\" y=\"751.75\"/>\n        <point2 x=\"8215.304\" y=\"828.1953\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1396</ID2>\n        <ctrlPoint0 x=\"8776.48\" y=\"791.8681\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8219.904\" y=\"759.04425\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1913\" layerID=\"1\" created=\"1288257601892\" x=\"8382.308\"\n        y=\"750.5\" width=\"394.51367\" height=\"78.203125\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f22534227f0001010d9438dea8c2b1fc</URIString>\n        <point1 x=\"8776.321\" y=\"751.0\"/>\n        <point2 x=\"8382.808\" y=\"828.2031\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1355</ID2>\n        <ctrlPoint0 x=\"8776.375\" y=\"792.0636\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8391.725\" y=\"753.8007\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1915\" layerID=\"1\" created=\"1288257624543\" x=\"8573.61\"\n        y=\"751.5\" width=\"203.1582\" height=\"78.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f22534227f0001010d9438de64236ba0</URIString>\n        <point1 x=\"8776.269\" y=\"752.0\"/>\n        <point2 x=\"8574.11\" y=\"829.0\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1465</ID2>\n        <ctrlPoint0 x=\"8776.29\" y=\"792.0413\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8573.89\" y=\"750.2044\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1918\" layerID=\"1\" created=\"1288257690703\" x=\"8775.942\"\n        y=\"751.25\" width=\"1525.5938\" height=\"77.5\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f22534237f0001010d9438dede87b33c</URIString>\n        <point1 x=\"8776.442\" y=\"751.75\"/>\n        <point2 x=\"10301.036\" y=\"828.25\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1299</ID2>\n        <ctrlPoint0 x=\"8776.561\" y=\"791.71063\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"10300.243\" y=\"717.47723\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1919\" layerID=\"1\" created=\"1288257693210\" x=\"8776.023\"\n        y=\"751.25\" width=\"906.33496\" height=\"77.25\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f22534237f0001010d9438de69c8feac</URIString>\n        <point1 x=\"8776.523\" y=\"751.75\"/>\n        <point2 x=\"9681.858\" y=\"828.0\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1145</ID2>\n        <ctrlPoint0 x=\"8776.689\" y=\"792.14606\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9681.819\" y=\"734.10425\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1933\" layerID=\"1\" created=\"1288268055042\" x=\"8776.892\"\n        y=\"751.1875\" width=\"496.18262\" height=\"763.53125\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f2c6c0257f0001010d9438de6add753c</URIString>\n        <point1 x=\"8777.392\" y=\"751.6875\"/>\n        <point2 x=\"9272.574\" y=\"1514.2188\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1227</ID2>\n        <ctrlPoint0 x=\"8786.786\" y=\"1327.7544\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9268.255\" y=\"1409.1014\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1935\" layerID=\"1\" created=\"1288268059310\" x=\"8776.339\"\n        y=\"751.1875\" width=\"336.83008\" height=\"763.3125\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f2c6c0257f0001010d9438de4ef74d59</URIString>\n        <point1 x=\"8776.839\" y=\"751.6875\"/>\n        <point2 x=\"9112.669\" y=\"1514.0\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1157</ID2>\n        <ctrlPoint0 x=\"8781.626\" y=\"1311.9343\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9112.833\" y=\"1388.2299\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1940\" label=\"Package\" layerID=\"1\" created=\"1288424104620\"\n        x=\"9427.72\" y=\"1858.0773\" width=\"162.0\" height=\"66.5\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/fc1178ae7f00010120d48785675aa223</URIString>\n        <child ID=\"1945\" label=\"asset | MANY | ASSETBASE\"\n            created=\"1288424104621\" x=\"34.0\" y=\"23.0\" width=\"163.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/fc1178af7f00010120d48785088a5d16</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2507\" created=\"1295294868654\" x=\"34.0\" y=\"43.25\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b7857f0001015ee819a81ccbfc97</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"1950\" layerID=\"1\" created=\"1288424120074\" x=\"8774.397\"\n        y=\"751.21875\" width=\"732.05273\" height=\"1107.3594\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/fc1178b07f00010120d4878508d5a5da</URIString>\n        <point1 x=\"8774.897\" y=\"751.71875\"/>\n        <point2 x=\"9505.95\" y=\"1858.0781\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1940</ID2>\n        <ctrlPoint0 x=\"8754.7\" y=\"1834.1465\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9500.001\" y=\"1786.6385\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1957\" layerID=\"1\" created=\"1288799345809\" x=\"8775.977\"\n        y=\"751.25\" width=\"524.1719\" height=\"77.46875\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/126dc1f67f0001014196c169c59591bf</URIString>\n        <point1 x=\"8776.477\" y=\"751.75\"/>\n        <point2 x=\"9299.648\" y=\"828.21875\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">2180</ID2>\n        <ctrlPoint0 x=\"8776.615\" y=\"792.01715\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9303.951\" y=\"766.1328\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1995\" label=\"Delivery\" layerID=\"1\"\n        created=\"1289688586892\" x=\"9632.252\" y=\"1508.0773\" width=\"123.0\"\n        height=\"86.75\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/476ff9f37f0001015a3184d866b3d9f0</URIString>\n        <child ID=\"3466\" label=\"StatusMixin\" created=\"1296050026720\"\n            x=\"34.0\" y=\"23.0\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/c2bbcf617f0001013611c75512d46e2b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2001\" label=\"files | MANY | LINK\"\n            created=\"1289688586898\" x=\"34.0\" y=\"43.25\" width=\"111.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/476ff9f47f0001015a3184d89f755940</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2508\" created=\"1295294872623\" x=\"34.0\" y=\"63.5\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9597b7867f0001015ee819a85ff2e993</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2005\" layerID=\"1\" created=\"1289688647878\" x=\"8775.092\"\n        y=\"751.1875\" width=\"921.4961\" height=\"757.3906\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/476ff9f57f0001015a3184d86becdbe1</URIString>\n        <point1 x=\"8775.592\" y=\"751.6875\"/>\n        <point2 x=\"9696.088\" y=\"1508.0781\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1995</ID2>\n        <ctrlPoint0 x=\"8769.07\" y=\"1480.4794\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9701.445\" y=\"1408.6132\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"2012\" label=\"Structure\" layerID=\"1\"\n        created=\"1290373811327\" x=\"8976.72\" y=\"828.225\" width=\"216.0\"\n        height=\"124.70192\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/704adc107f0001012ed4f13aa93265ac</URIString>\n        <child ID=\"2017\" label=\"templates | MANY | FILENAMETEMPLATE\"\n            created=\"1290373958896\" x=\"34.0\" y=\"23.0\" width=\"235.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/704adc117f0001012ed4f13ae0ef0e20</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3630\" label=\"custom_template | STRING\"\n            created=\"1306190987002\" x=\"34.0\" y=\"43.25\" width=\"161.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/1f0d0cc37f000101578cef18dc05580c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4147\" created=\"1369384371710\" x=\"34.0\" y=\"63.5\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acac8a7b6a9fda5a40fe827c2c1502</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"3790\" created=\"1310511196916\" x=\"34.0\" y=\"85.701965\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/208df7367f000101264d107e7f8218a8</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4030\" created=\"1358111879155\" x=\"34.0\" y=\"103.70194\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aee6c0a8000435fa8379abbe7bb8</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"2016\" layerID=\"1\" created=\"1290373841415\" x=\"8775.822\"\n        y=\"751.5\" width=\"309.38184\" height=\"77.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/704adc147f0001012ed4f13a8b835d41</URIString>\n        <point1 x=\"8776.322\" y=\"752.0\"/>\n        <point2 x=\"9084.704\" y=\"828.0\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">2012</ID2>\n        <ctrlPoint0 x=\"8776.375\" y=\"791.803\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9084.689\" y=\"766.74225\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"2026\" label=\"PROBLEM\" layerID=\"1\" created=\"1290415153137\"\n        x=\"12753.83\" y=\"580.21045\" width=\"215.0\" height=\"92.0\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FCDBD9</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c7681c7f0001010b45ad3d65da5015</URIString>\n        <child ID=\"2025\"\n            label=\"Where to save the templates, or more clearly, where to save the data that holds which file of which pipeline_step should saved where...\"\n            created=\"1290379453440\" x=\"5.0\" y=\"23.0\" width=\"205.0\"\n            height=\"63.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/709d95a57f0001012ed4f13a4667f382</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Where to save the templates, or more clearly, where to save the data \n      that holds which file of which pipeline_step should saved where...\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>Where to save the templates, or more clearly, where to save the data that holds which file of which pipeline_step should saved where...</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2027\" label=\"Hold in AssetType\" layerID=\"1\"\n        created=\"1290415471563\" x=\"12585.413\" y=\"825.87695\"\n        width=\"160.0\" height=\"107.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c7681e7f0001010b45ad3dbd6e27e5</URIString>\n        <child ID=\"2029\"\n            label=\"Asset Type holds a lot of things, so may be the asset type should hold the data that shows where to save a specific type\"\n            created=\"1290415523885\" x=\"5.0\" y=\"23.0\" width=\"150.0\"\n            height=\"78.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72c7681f7f0001010b45ad3d77bc2d11</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Asset Type holds a lot of things, so may be the asset type should hold \n      the\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      data that shows where to save a specific type\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>Asset Type holds a lot of things, so may be the asset type should hold the data that shows where to save a specific type</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2028\" label=\"SOLUTION 1\" layerID=\"1\"\n        created=\"1290415471574\" x=\"12706.315\" y=\"671.71094\"\n        width=\"119.916016\" height=\"154.66602\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c768217f0001010b45ad3d27ead190</URIString>\n        <point1 x=\"12825.731\" y=\"672.21094\"/>\n        <point2 x=\"12706.815\" y=\"825.87695\"/>\n        <ID1 xsi:type=\"node\">2026</ID1>\n        <ID2 xsi:type=\"node\">2027</ID2>\n    </child>\n    <child ID=\"2031\" label=\"limits flexibility\" layerID=\"1\"\n        created=\"1290415567637\" x=\"12585.413\" y=\"995.37695\"\n        width=\"160.0\" height=\"107.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c768227f0001010b45ad3de419a2bb</URIString>\n        <child ID=\"2033\"\n            label=\"This leads a weird setup where an object has to be saved to the same &#xa;      place in every project, this limits the flexibility we aim\"\n            created=\"1290415575363\" x=\"5.0\" y=\"23.0\" width=\"150.0\"\n            height=\"78.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72c768237f0001010b45ad3ddd5305d2</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      This leads a weird setup where an object has to be saved to the same \n      place in every project, this limits the flexibility we aim\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>This leads a weird setup where an object has to be saved to the same \n      place in every project, this limits the flexibility we aim</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2032\" layerID=\"1\" created=\"1290415567645\" x=\"12664.913\"\n        y=\"932.37695\" width=\"1.0\" height=\"63.5\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c768247f0001010b45ad3dedb39d1c</URIString>\n        <point1 x=\"12665.413\" y=\"932.87695\"/>\n        <point2 x=\"12665.413\" y=\"995.37695\"/>\n        <ID1 xsi:type=\"node\">2027</ID1>\n        <ID2 xsi:type=\"node\">2031</ID2>\n    </child>\n    <child ID=\"2035\" label=\"Hold in Project\" layerID=\"1\"\n        created=\"1290415693380\" x=\"12901.163\" y=\"835.37695\"\n        width=\"153.0\" height=\"77.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c768257f0001010b45ad3d2cd5572c</URIString>\n        <child ID=\"2037\"\n            label=\"hold the data in project node, so any project can have different setups\"\n            created=\"1290415702905\" x=\"5.0\" y=\"23.0\" width=\"143.0\"\n            height=\"48.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72c768267f0001010b45ad3d7b74ae69</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      hold the data in project node, so any project can have different setups\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>hold the data in project node, so any project can have different setups</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2036\" label=\"SOLUTION 2\" layerID=\"1\"\n        created=\"1290415693397\" x=\"12882.4375\" y=\"671.71094\"\n        width=\"77.6416\" height=\"164.16602\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c768287f0001010b45ad3ddc9a3886</URIString>\n        <point1 x=\"12882.9375\" y=\"672.21094\"/>\n        <point2 x=\"12959.579\" y=\"835.37695\"/>\n        <ID1 xsi:type=\"node\">2026</ID1>\n        <ID2 xsi:type=\"node\">2035</ID2>\n    </child>\n    <child ID=\"2038\" label=\"special settings for every node\" layerID=\"1\"\n        created=\"1290415727634\" x=\"12884.163\" y=\"971.37695\"\n        width=\"187.0\" height=\"152.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c768297f0001010b45ad3d2ac3c4b6</URIString>\n        <child ID=\"2040\"\n            label=\"this needs to have an entry for every asset type, so if I'm going to &#xa;      hold a template for assets I need to specify that this template is for assets, and if I want to have a template for shots, I need to specify that this template is for shots\"\n            created=\"1290415748194\" x=\"5.0\" y=\"23.0\" width=\"177.0\"\n            height=\"123.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72c768297f0001010b45ad3d14b50bfc</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      this needs to have an entry for every asset type, so if I'm going to \n      hold a template for assets I need to specify that this template is for \n      assets, and if I want to have a template for shots, I need to specify \n      that this template is for shots\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>this needs to have an entry for every asset type, so if I'm going to \n      hold a template for assets I need to specify that this template is for assets, and if I want to have a template for shots, I need to specify that this template is for shots</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2039\" layerID=\"1\" created=\"1290415727642\" x=\"12977.163\"\n        y=\"911.87695\" width=\"1.0\" height=\"60.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c7682b7f0001010b45ad3d805a8614</URIString>\n        <point1 x=\"12977.663\" y=\"912.37695\"/>\n        <point2 x=\"12977.663\" y=\"971.37695\"/>\n        <ID1 xsi:type=\"node\">2035</ID1>\n        <ID2 xsi:type=\"node\">2038</ID2>\n    </child>\n    <child ID=\"2043\" label=\"REASONABLE\" layerID=\"1\"\n        created=\"1290415848293\" x=\"12933.163\" y=\"1184.377\" width=\"89.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#30D643</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c7682c7f0001010b45ad3d801617fc</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2044\" layerID=\"1\" created=\"1290415848302\" x=\"12977.163\"\n        y=\"1122.877\" width=\"1.0\" height=\"62.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c7682d7f0001010b45ad3dc2a36449</URIString>\n        <point1 x=\"12977.663\" y=\"1123.377\"/>\n        <point2 x=\"12977.663\" y=\"1184.377\"/>\n        <ID1 xsi:type=\"node\">2038</ID1>\n        <ID2 xsi:type=\"node\">2043</ID2>\n    </child>\n    <child ID=\"2045\" label=\"BAD\" layerID=\"1\" created=\"1290415882271\"\n        x=\"12627.413\" y=\"1189.377\" width=\"76.0\" height=\"27.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c8b39c7f0001010b45ad3d3de72c20</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2046\" layerID=\"1\" created=\"1290415882275\" x=\"12664.913\"\n        y=\"1101.877\" width=\"1.0\" height=\"88.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c8b39d7f0001010b45ad3dbf4d4a87</URIString>\n        <point1 x=\"12665.413\" y=\"1102.377\"/>\n        <point2 x=\"12665.413\" y=\"1189.377\"/>\n        <ID1 xsi:type=\"node\">2031</ID1>\n        <ID2 xsi:type=\"node\">2045</ID2>\n    </child>\n    <child ID=\"2048\" label=\"how to connect\" layerID=\"1\"\n        created=\"1290415915729\" x=\"12897.663\" y=\"1259.377\" width=\"160.0\"\n        height=\"60.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c8b39e7f0001010b45ad3dce9e9452</URIString>\n        <child ID=\"2050\" label=\"how to connect the data, in this case\"\n            created=\"1290415921232\" x=\"5.0\" y=\"23.0\" width=\"150.0\"\n            height=\"31.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72c8b39e7f0001010b45ad3d443056e8</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 11; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 11; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 11; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      &lt;font style=\"font-size:12;\"&gt;how to connect the data, in this case&lt;/font&gt;\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>how to connect the data, in this case</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2049\" layerID=\"1\" created=\"1290415915733\" x=\"12977.163\"\n        y=\"1206.877\" width=\"1.0\" height=\"53.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72c8b3a07f0001010b45ad3d861cccb5</URIString>\n        <point1 x=\"12977.663\" y=\"1207.377\"/>\n        <point2 x=\"12977.663\" y=\"1259.377\"/>\n        <ID1 xsi:type=\"node\">2043</ID1>\n        <ID2 xsi:type=\"node\">2048</ID2>\n    </child>\n    <child ID=\"2051\" label=\"AssetType\" layerID=\"1\"\n        created=\"1290415957287\" x=\"12786.163\" y=\"1403.377\" width=\"169.0\"\n        height=\"92.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72ccceac7f0001010b45ad3deca8e4e1</URIString>\n        <child ID=\"2054\"\n            label=\"so for every assetType given to the system, the user can specify the &#xa;      default location to save\"\n            created=\"1290415993540\" x=\"5.0\" y=\"23.0\" width=\"159.0\"\n            height=\"63.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72cccead7f0001010b45ad3d050b14ca</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      so for every assetType given to the system, the user can specify the \n      default location to save\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>so for every assetType given to the system, the user can specify the \n      default location to save</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2052\" layerID=\"1\" created=\"1290415957292\" x=\"12900.926\"\n        y=\"1318.877\" width=\"57.174805\" height=\"85.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72ccceaf7f0001010b45ad3d72cbe1eb</URIString>\n        <point1 x=\"12957.601\" y=\"1319.377\"/>\n        <point2 x=\"12901.426\" y=\"1403.377\"/>\n        <ID1 xsi:type=\"node\">2048</ID1>\n        <ID2 xsi:type=\"node\">2051</ID2>\n    </child>\n    <child ID=\"2057\" label=\"PipelineStep\" layerID=\"1\"\n        created=\"1290416080796\" x=\"13016.663\" y=\"1396.377\" width=\"160.0\"\n        height=\"158.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72ccceb07f0001010b45ad3d6a47dcac</URIString>\n        <child ID=\"2059\"\n            label=\"the files are produced in pipeline steps, for a &quot;Character&quot; assetType, &#xa;      the &quot;Modeling&quot; pipelne step produces the file, so we need three data to hold, one the assetType, second the pipeline step, and third the template string\"\n            created=\"1290416089913\" x=\"5.0\" y=\"23.0\" width=\"150.0\"\n            height=\"129.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72ccceb07f0001010b45ad3d80acd543</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 11; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 11; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 11; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      the files are produced in pipeline steps, for a &amp;quot;Character&amp;quot; assetType, \n      the &amp;quot;Modeling&amp;quot; pipelne step produces the file, so we need three data to \n      hold, one the assetType, second the pipeline step, and third the \n      template string\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>the files are produced in pipeline steps, for a \"Character\" assetType, \n      the \"Modeling\" pipelne step produces the file, so we need three data to hold, one the assetType, second the pipeline step, and third the template string</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2058\" layerID=\"1\" created=\"1290416080803\" x=\"12996.357\"\n        y=\"1318.877\" width=\"50.262695\" height=\"78.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72ccceb27f0001010b45ad3d93aaab40</URIString>\n        <point1 x=\"12996.856\" y=\"1319.377\"/>\n        <point2 x=\"13046.119\" y=\"1396.377\"/>\n        <ID1 xsi:type=\"node\">2048</ID1>\n        <ID2 xsi:type=\"node\">2057</ID2>\n    </child>\n    <child ID=\"2060\" label=\"Idea 2\" layerID=\"1\" created=\"1290416289541\"\n        x=\"12923.163\" y=\"1621.377\" width=\"160.0\" height=\"107.0\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FC938D</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72d4e8727f0001010b45ad3d6534da51</URIString>\n        <child ID=\"2062\"\n            label=\"Create a new composite foreign key, that holds a key to one asset type &#xa;      object and a pipeline step, and has a string template\"\n            created=\"1290416301932\" x=\"5.0\" y=\"23.0\" width=\"150.0\"\n            height=\"78.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72d4e8727f0001010b45ad3d7e0392c9</URIString>\n            <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Create a new composite foreign key, that holds a key to one asset type \n      object and a pipeline step, and has a string template\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>Create a new composite foreign key, that holds a key to one asset type \n      object and a pipeline step, and has a string template</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2061\" layerID=\"1\" created=\"1290416289545\" x=\"13027.737\"\n        y=\"1553.877\" width=\"32.40039\" height=\"68.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72d4e8737f0001010b45ad3d19b9fc7e</URIString>\n        <point1 x=\"13059.638\" y=\"1554.377\"/>\n        <point2 x=\"13028.237\" y=\"1621.377\"/>\n        <ID1 xsi:type=\"node\">2057</ID1>\n        <ID2 xsi:type=\"node\">2060</ID2>\n    </child>\n    <child ID=\"2063\" label=\"Structure\" layerID=\"1\"\n        created=\"1290416676242\" x=\"12942.163\" y=\"1783.377\" width=\"124.0\"\n        height=\"62.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#30D643</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72d4e8737f0001010b45ad3d683e5230</URIString>\n        <child ID=\"2065\" label=\"this can work with the structure system\"\n            created=\"1290416682757\" x=\"5.0\" y=\"23.0\" width=\"114.0\"\n            height=\"33.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72d4e8737f0001010b45ad3db5912744</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      this can work with the structure system\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>this can work with the structure system</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2064\" layerID=\"1\" created=\"1290416676246\" x=\"13003.047\"\n        y=\"1727.875\" width=\"1.3945312\" height=\"56.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72d4e8747f0001010b45ad3d2421dadd</URIString>\n        <point1 x=\"13003.547\" y=\"1728.375\"/>\n        <point2 x=\"13003.941\" y=\"1783.375\"/>\n        <ID1 xsi:type=\"node\">2060</ID1>\n        <ID2 xsi:type=\"node\">2063</ID2>\n    </child>\n    <child ID=\"2066\" layerID=\"1\" created=\"1290416717375\" x=\"12897.192\"\n        y=\"1494.877\" width=\"75.03516\" height=\"127.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72d4e8747f0001010b45ad3dfa561263</URIString>\n        <point1 x=\"12897.692\" y=\"1495.377\"/>\n        <point2 x=\"12971.728\" y=\"1621.377\"/>\n        <ID1 xsi:type=\"node\">2051</ID1>\n        <ID2 xsi:type=\"node\">2060</ID2>\n    </child>\n    <child ID=\"2067\" label=\"idea 1\" layerID=\"1\" created=\"1290416821416\"\n        x=\"12698.252\" y=\"1588.377\" width=\"160.0\" height=\"62.0\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FC938D</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72d6ca677f0001010b45ad3d78d6eb10</URIString>\n        <child ID=\"2069\"\n            label=\"use assetType side by side with a template\"\n            created=\"1290416826833\" x=\"5.0\" y=\"23.0\" width=\"150.0\"\n            height=\"33.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72d6ca687f0001010b45ad3d900a2d2c</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 11; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 11; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 11; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      &lt;font style=\"font-size:12;\"&gt;use assetType side by side with a template&lt;/font&gt;\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>use assetType side by side with a template</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2068\" layerID=\"1\" created=\"1290416821420\" x=\"12794.604\"\n        y=\"1494.877\" width=\"51.554688\" height=\"94.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72d6ca687f0001010b45ad3dde441d00</URIString>\n        <point1 x=\"12845.658\" y=\"1495.377\"/>\n        <point2 x=\"12795.104\" y=\"1588.377\"/>\n        <ID1 xsi:type=\"node\">2051</ID1>\n        <ID2 xsi:type=\"node\">2067</ID2>\n    </child>\n    <child ID=\"2075\" label=\"BIGGER QUESTION\" layerID=\"1\"\n        created=\"1290417771427\" x=\"13785.086\" y=\"569.37695\"\n        width=\"160.0\" height=\"92.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FCDBD9</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72e63ba77f0001010b45ad3d2724344a</URIString>\n        <child ID=\"2077\"\n            label=\"Should I have a template for any entity those have a connection with the &#xa;      file system\"\n            created=\"1290417822261\" x=\"5.0\" y=\"23.0\" width=\"150.0\"\n            height=\"63.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/72e63ba87f0001010b45ad3dc2870b40</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Should I have a template for any entity those have a connection with the \n      file system\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>Should I have a template for any entity those have a connection with the \n      file system</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2080\" label=\"For Example?\" layerID=\"1\"\n        created=\"1290417984688\" x=\"13820.586\" y=\"716.37695\" width=\"88.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a57f0001010b45ad3d8a1153d0</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2081\" layerID=\"1\" created=\"1290417984692\" x=\"13864.135\"\n        y=\"661.0\" width=\"1.2451172\" height=\"56.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a57f0001010b45ad3d490f63de</URIString>\n        <point1 x=\"13864.881\" y=\"661.5\"/>\n        <point2 x=\"13864.636\" y=\"716.5\"/>\n        <ID1 xsi:type=\"node\">2075</ID1>\n        <ID2 xsi:type=\"node\">2080</ID2>\n    </child>\n    <child ID=\"2082\" label=\"where to save the a version of a task ?\"\n        layerID=\"1\" created=\"1290418001363\" x=\"13641.586\" y=\"794.37695\"\n        width=\"247.0\" height=\"51.5\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a67f0001010b45ad3d49b4e257</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2083\" layerID=\"1\" created=\"1290418001367\" x=\"13792.359\"\n        y=\"738.87695\" width=\"60.322266\" height=\"56.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a67f0001010b45ad3ddebb1a70</URIString>\n        <point1 x=\"13852.182\" y=\"739.37695\"/>\n        <point2 x=\"13792.859\" y=\"794.37695\"/>\n        <ID1 xsi:type=\"node\">2080</ID1>\n        <ID2 xsi:type=\"node\">2082</ID2>\n    </child>\n    <child ID=\"2084\" label=\"does it need to have a template ?\"\n        layerID=\"1\" created=\"1290418016558\" x=\"13653.086\" y=\"905.0198\"\n        width=\"220.0\" height=\"55.5\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a67f0001010b45ad3d95cb489f</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2085\" layerID=\"1\" created=\"1290418016563\" x=\"13763.078\"\n        y=\"845.375\" width=\"2.0498047\" height=\"60.125\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a67f0001010b45ad3dc1ae5313</URIString>\n        <point1 x=\"13764.629\" y=\"845.875\"/>\n        <point2 x=\"13763.579\" y=\"905.0\"/>\n        <ID1 xsi:type=\"node\">2082</ID1>\n        <ID2 xsi:type=\"node\">2084</ID2>\n    </child>\n    <child ID=\"2086\" label=\"where&#xa;to create&#xa;a sequence folder\"\n        layerID=\"1\" created=\"1290418067315\" x=\"13985.086\" y=\"798.37695\"\n        width=\"110.0\" height=\"53.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a77f0001010b45ad3d3f31f2fc</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2087\" layerID=\"1\" created=\"1290418067319\" x=\"13884.893\"\n        y=\"738.87695\" width=\"108.53711\" height=\"60.436523\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a77f0001010b45ad3d06e536cd</URIString>\n        <point1 x=\"13885.393\" y=\"739.37695\"/>\n        <point2 x=\"13992.93\" y=\"798.8135\"/>\n        <ID1 xsi:type=\"node\">2080</ID1>\n        <ID2 xsi:type=\"node\">2086</ID2>\n    </child>\n    <child ID=\"2088\"\n        label=\"sequences are&#xa;dummy folders like projects\"\n        layerID=\"1\" created=\"1290418090858\" x=\"13952.586\" y=\"892.62695\"\n        width=\"190.0\" height=\"58.5\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a77f0001010b45ad3d2df9b014</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2089\" layerID=\"1\" created=\"1290418090865\" x=\"14041.635\"\n        y=\"850.875\" width=\"4.1904297\" height=\"42.25\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a87f0001010b45ad3d2466129e</URIString>\n        <point1 x=\"14042.135\" y=\"851.375\"/>\n        <point2 x=\"14045.325\" y=\"892.625\"/>\n        <ID1 xsi:type=\"node\">2086</ID1>\n        <ID2 xsi:type=\"node\">2088</ID2>\n    </child>\n    <child ID=\"2091\"\n        label=\"thus they are hidden in the&#xa;template code\"\n        layerID=\"1\" created=\"1290418155937\" x=\"13958.836\" y=\"992.37695\"\n        width=\"183.0\" height=\"65.5\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a87f0001010b45ad3d3c38709f</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2092\" layerID=\"1\" created=\"1290418155942\" x=\"14047.865\"\n        y=\"950.625\" width=\"2.0986328\" height=\"42.25\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a87f0001010b45ad3d908c11e0</URIString>\n        <point1 x=\"14048.364\" y=\"951.125\"/>\n        <point2 x=\"14049.463\" y=\"992.375\"/>\n        <ID1 xsi:type=\"node\">2088</ID1>\n        <ID2 xsi:type=\"node\">2091</ID2>\n    </child>\n    <child ID=\"2095\"\n        label=\"in fact it has, the whole idea&#xa;behind project.template is to&#xa;define the place to save a&#xa;version of a pipeline step\"\n        layerID=\"1\" created=\"1290418232979\" x=\"13675.086\" y=\"1019.6626\"\n        width=\"166.0\" height=\"68.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a97f0001010b45ad3d6fed6405</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2096\" layerID=\"1\" created=\"1290418232984\" x=\"13758.993\"\n        y=\"960.03125\" width=\"3.4453125\" height=\"60.125\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a97f0001010b45ad3d34de9e5c</URIString>\n        <point1 x=\"13761.938\" y=\"960.53125\"/>\n        <point2 x=\"13759.493\" y=\"1019.65625\"/>\n        <ID1 xsi:type=\"node\">2084</ID1>\n        <ID2 xsi:type=\"node\">2095</ID2>\n    </child>\n    <child ID=\"2100\"\n        label=\"how to pass the data to be used&#xa;inside templates\"\n        layerID=\"1\" created=\"1290418291978\" x=\"13665.086\" y=\"1146.8054\"\n        width=\"186.0\" height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a97f0001010b45ad3d6728aea3</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2101\" layerID=\"1\" created=\"1290418291983\" x=\"13757.586\"\n        y=\"1087.1626\" width=\"1.0\" height=\"60.142822\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113a97f0001010b45ad3d3911b327</URIString>\n        <point1 x=\"13758.086\" y=\"1087.6626\"/>\n        <point2 x=\"13758.086\" y=\"1146.8054\"/>\n        <ID1 xsi:type=\"node\">2095</ID1>\n        <ID2 xsi:type=\"node\">2100</ID2>\n    </child>\n    <child ID=\"2102\"\n        label=\"templates are jinja2 template, they&#xa;need variables, so how can I pass the&#xa;neccesary data to the version\"\n        layerID=\"1\" created=\"1290418304858\" x=\"13649.086\" y=\"1243.9482\"\n        width=\"218.0\" height=\"53.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113aa7f0001010b45ad3d9af06b7f</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2103\" layerID=\"1\" created=\"1290418304863\" x=\"13757.586\"\n        y=\"1184.3054\" width=\"1.0\" height=\"60.142822\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113aa7f0001010b45ad3dd1e49c4e</URIString>\n        <point1 x=\"13758.086\" y=\"1184.8054\"/>\n        <point2 x=\"13758.086\" y=\"1243.9482\"/>\n        <ID1 xsi:type=\"node\">2100</ID1>\n        <ID2 xsi:type=\"node\">2102</ID2>\n    </child>\n    <child ID=\"2104\"\n        label=\"the templates should&#xa;be using the version itself,&#xa;so a template for a version&#xa;should be like:&#xa;&#xa;{{version.project.name}}/ASSETS/{{version.asset.code}}/&#xa;{{version.asset_type.pipeline_step.code}}/&#xa;{{version.asset.code}}_{{version.asset.sub_name}}&#xa;&#xa;where the desired template should look like&#xa;&#xa;{{project.name}}/ASSET/{{asset.code}}/{{pipeline_step.code}}...&#xa;&#xa;which is more clear then the previous one\"\n        layerID=\"1\" created=\"1290418353149\" x=\"13581.586\" y=\"1356.0911\"\n        width=\"353.0\" height=\"218.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113aa7f0001010b45ad3d3e1507b7</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2105\" layerID=\"1\" created=\"1290418353153\" x=\"13757.586\"\n        y=\"1296.4482\" width=\"1.0\" height=\"60.142822\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72f113ab7f0001010b45ad3d1cbe64b2</URIString>\n        <point1 x=\"13758.086\" y=\"1296.9482\"/>\n        <point2 x=\"13758.086\" y=\"1356.0911\"/>\n        <ID1 xsi:type=\"node\">2102</ID1>\n        <ID2 xsi:type=\"node\">2104</ID2>\n    </child>\n    <child ID=\"2109\"\n        label=\"the template can be given these variables:&#xa;&#xa;project&#xa;sequence&#xa;asset&#xa;assetType&#xa;task&#xa;version&#xa;&#xa;so a user can by default use this template&#xa;variables\"\n        layerID=\"1\" created=\"1290419014194\" x=\"13637.586\" y=\"1633.2339\"\n        width=\"241.0\" height=\"173.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72fbe81f7f0001010b45ad3d3a6d4e41</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2110\" layerID=\"1\" created=\"1290419014198\" x=\"13757.586\"\n        y=\"1573.5911\" width=\"1.0\" height=\"60.142822\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72fbe8207f0001010b45ad3d2b6e222b</URIString>\n        <point1 x=\"13758.086\" y=\"1574.0911\"/>\n        <point2 x=\"13758.086\" y=\"1633.2339\"/>\n        <ID1 xsi:type=\"node\">2104</ID1>\n        <ID2 xsi:type=\"node\">2109</ID2>\n    </child>\n    <child ID=\"2111\"\n        label=\"so the answer to the question is&#xa;&#xa;NO&#xa;&#xa;we don't have to have a template&#xa;for any entity\"\n        layerID=\"1\" created=\"1290419270418\" x=\"13664.086\" y=\"1865.3767\"\n        width=\"188.0\" height=\"98.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72fbe8207f0001010b45ad3df22cfe6c</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2112\" layerID=\"1\" created=\"1290419270423\" x=\"13757.586\"\n        y=\"1805.7339\" width=\"1.0\" height=\"60.142822\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/72fbe8207f0001010b45ad3d28033eea</URIString>\n        <point1 x=\"13758.086\" y=\"1806.2339\"/>\n        <point2 x=\"13758.086\" y=\"1865.3767\"/>\n        <ID1 xsi:type=\"node\">2109</ID1>\n        <ID2 xsi:type=\"node\">2111</ID2>\n    </child>\n    <child ID=\"2142\" label=\"external references\" layerID=\"1\"\n        created=\"1290428386234\" x=\"15312.611\" y=\"561.544\" width=\"188.0\"\n        height=\"79.0\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FCDBD9</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7389b2a27f0001010b45ad3d774a4589</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2143\"\n        label=\"the template of a reference&#xa;can be saved inside project\"\n        layerID=\"1\" created=\"1290428397581\" x=\"15206.822\" y=\"938.24414\"\n        width=\"161.0\" height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#C1F780</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7389b2a27f0001010b45ad3d4fa061ad</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2144\" layerID=\"1\" created=\"1290428397586\" x=\"15300.024\"\n        y=\"880.34375\" width=\"40.884766\" height=\"58.40039\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7389b2a27f0001010b45ad3d045938fe</URIString>\n        <point1 x=\"15340.409\" y=\"880.84375\"/>\n        <point2 x=\"15300.524\" y=\"938.24414\"/>\n        <ID1 xsi:type=\"node\">2167</ID1>\n        <ID2 xsi:type=\"node\">2143</ID2>\n    </child>\n    <child ID=\"2145\"\n        label=\"need to have another data column&#xa;then the templates column,&#xa;because a template is like:\"\n        layerID=\"1\" created=\"1290428439578\" x=\"15174.822\" y=\"1001.24365\"\n        width=\"225.0\" height=\"171.875\" strokeWidth=\"1.0\"\n        autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7389b2a37f0001010b45ad3d2eb1871e</URIString>\n        <child ID=\"2149\" label=\"Template\" created=\"1290428510175\"\n            x=\"34.0\" y=\"53.0\" width=\"236.0\" height=\"150.5\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#ECFFD4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7389b2a37f0001010b45ad3d2ffed3ed</URIString>\n            <child ID=\"2150\" label=\"asset_type | ONE | ASSETTYPE\"\n                created=\"1290428510175\" x=\"34.0\" y=\"23.0\" width=\"183.0\"\n                height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n                <fillColor>#FDE888</fillColor>\n                <strokeColor>#776D6D</strokeColor>\n                <textColor>#000000</textColor>\n                <font>Arial-plain-12</font>\n                <URIString>http://vue.tufts.edu/rdf/resource/7389b2a37f0001010b45ad3da34391da</URIString>\n                <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n            </child>\n            <child ID=\"2151\" label=\"pipeline_step | ONE | PIPELENSTEP\"\n                created=\"1290428510176\" x=\"34.0\" y=\"42.5\" width=\"212.0\"\n                height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n                <fillColor>#FDE888</fillColor>\n                <strokeColor>#776D6D</strokeColor>\n                <textColor>#000000</textColor>\n                <font>Arial-plain-12</font>\n                <URIString>http://vue.tufts.edu/rdf/resource/7389b2a37f0001010b45ad3d5d01bd82</URIString>\n                <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n            </child>\n            <child ID=\"2152\" label=\"template_code | UNICODE\"\n                created=\"1290428510176\" x=\"34.0\" y=\"62.0\" width=\"158.0\"\n                height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n                <fillColor>#FDE888</fillColor>\n                <strokeColor>#776D6D</strokeColor>\n                <textColor>#000000</textColor>\n                <font>Arial-plain-12</font>\n                <URIString>http://vue.tufts.edu/rdf/resource/7389b2a47f0001010b45ad3dcbb17974</URIString>\n                <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n            </child>\n            <child ID=\"2153\"\n                label=\"Examples: {{projects.root}}/{{project.name}} /SEQUENCES/{{sequence.name}} /SHOTS/{{shot.name}}\"\n                created=\"1290428510176\" x=\"34.0\" y=\"81.5\" width=\"197.0\"\n                height=\"63.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n                <strokeColor>#404040</strokeColor>\n                <textColor>#000000</textColor>\n                <font>SansSerif-plain-14</font>\n                <URIString>http://vue.tufts.edu/rdf/resource/7389b2a47f0001010b45ad3d91817f7f</URIString>\n                <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Examples:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      {{projects.root}}/{{project.name}}\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      /SEQUENCES/{{sequence.name}}\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      /SHOTS/{{shot.name}}\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n                <label>Examples: {{projects.root}}/{{project.name}} /SEQUENCES/{{sequence.name}} /SHOTS/{{shot.name}}</label>\n            </child>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2146\" layerID=\"1\" created=\"1290428439582\" x=\"15286.822\"\n        y=\"975.74414\" width=\"1.0\" height=\"25.999512\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7389b2a57f0001010b45ad3de520dce3</URIString>\n        <point1 x=\"15287.322\" y=\"976.24414\"/>\n        <point2 x=\"15287.322\" y=\"1001.24365\"/>\n        <ID1 xsi:type=\"node\">2143</ID1>\n        <ID2 xsi:type=\"node\">2145</ID2>\n    </child>\n    <child ID=\"2147\" label=\"too much specialization\" layerID=\"1\"\n        created=\"1290428484654\" x=\"15218.6\" y=\"1259.4553\" width=\"145.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7389b2a57f0001010b45ad3dda5f0f57</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2154\" label=\"Repository\" layerID=\"1\"\n        created=\"1290429229074\" x=\"7954.5513\" y=\"828.2019\"\n        width=\"156.75\" height=\"178.99997\" strokeWidth=\"1.0\"\n        autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7394cf4e7f0001010b45ad3d4505f265</URIString>\n        <child ID=\"2192\" label=\"linux_path | UNICODE\"\n            created=\"1290433909826\" x=\"34.0\" y=\"23.0\" width=\"132.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/73db9f4c7f0001010b45ad3de656431d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2193\" label=\"macos_path | UNICODE\"\n            created=\"1290433929378\" x=\"34.0\" y=\"43.25\" width=\"126.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/73db9f4c7f0001010b45ad3dbced640d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2194\" label=\"windows_path | UNICODE\"\n            created=\"1290433937695\" x=\"34.0\" y=\"63.5\" width=\"156.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/73db9f4c7f0001010b45ad3d456aaae7</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2197\"\n            label=\"path -> one of the path&#xa;above, defined&#xa;according to the&#xa;current os\"\n            created=\"1290435168262\" x=\"34.0\" y=\"83.75\" width=\"137.0\"\n            height=\"68.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/73ee6ed37f0001010b45ad3d6434d46f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2475\" created=\"1295294303904\" x=\"34.0\" y=\"137.75\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/958f53917f0001015ee819a8ab2b371f</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4029\" created=\"1358111873136\" x=\"34.0\" y=\"158.0\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aef7c0a8000435fa837957f37360</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"2157\" layerID=\"1\" created=\"1290429260559\" x=\"8044.364\"\n        y=\"751.0\" width=\"732.4839\" height=\"77.703125\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7394cf4e7f0001010b45ad3d9f393b4e</URIString>\n        <point1 x=\"8776.348\" y=\"751.5\"/>\n        <point2 x=\"8044.864\" y=\"828.2031\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">2154</ID2>\n        <ctrlPoint0 x=\"8776.415\" y=\"791.9108\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8054.59\" y=\"755.27936\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"2160\"\n        label=\"I want to be able to move the&#xa;localy selected files in to the&#xa;project\"\n        layerID=\"1\" created=\"1290429671872\" x=\"15270.611\" y=\"684.44336\"\n        width=\"166.0\" height=\"53.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/739c551d7f0001010b45ad3d8cc96286</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2161\" layerID=\"1\" created=\"1290429671877\" x=\"15365.891\"\n        y=\"640.0449\" width=\"22.170898\" height=\"44.898438\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/739c551d7f0001010b45ad3d7e48d57e</URIString>\n        <point1 x=\"15387.5625\" y=\"640.5449\"/>\n        <point2 x=\"15366.392\" y=\"684.44336\"/>\n        <ID1 xsi:type=\"node\">2142</ID1>\n        <ID2 xsi:type=\"node\">2160</ID2>\n    </child>\n    <child ID=\"2162\"\n        label=\"so I need templates&#xa;to tell me where to&#xa;put the reference files\"\n        layerID=\"1\" created=\"1290429695965\" x=\"15289.111\" y=\"762.84326\"\n        width=\"129.0\" height=\"53.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/739c551d7f0001010b45ad3d5a0a2c93</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2163\" layerID=\"1\" created=\"1290429695971\" x=\"15353.111\"\n        y=\"736.94336\" width=\"1.0\" height=\"26.399902\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/739c551e7f0001010b45ad3d485244a7</URIString>\n        <point1 x=\"15353.611\" y=\"737.44336\"/>\n        <point2 x=\"15353.611\" y=\"762.84326\"/>\n        <ID1 xsi:type=\"node\">2160</ID1>\n        <ID2 xsi:type=\"node\">2162</ID2>\n    </child>\n    <child ID=\"2167\" label=\"where to save&#xa;the template information\"\n        layerID=\"1\" created=\"1290429724283\" x=\"15281.611\" y=\"842.84326\"\n        width=\"144.0\" height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/739c551e7f0001010b45ad3d4388adf9</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2168\" layerID=\"1\" created=\"1290429724287\" x=\"15353.111\"\n        y=\"815.34326\" width=\"1.0\" height=\"28.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/739c551e7f0001010b45ad3d829e5aec</URIString>\n        <point1 x=\"15353.611\" y=\"815.84326\"/>\n        <point2 x=\"15353.611\" y=\"842.84326\"/>\n        <ID1 xsi:type=\"node\">2162</ID1>\n        <ID2 xsi:type=\"node\">2167</ID2>\n    </child>\n    <child ID=\"2169\" label=\"add another reference_templates column\"\n        layerID=\"1\" created=\"1290429902656\" x=\"15172.3\" y=\"1192.8125\"\n        width=\"238.0\" height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/739e1da67f0001010b45ad3dba5f91ba</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2175\" layerID=\"1\" created=\"1290429913998\" x=\"15289.74\"\n        y=\"1172.625\" width=\"1.6689453\" height=\"20.6875\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/739e1da67f0001010b45ad3d81c3b80d</URIString>\n        <point1 x=\"15290.24\" y=\"1173.125\"/>\n        <point2 x=\"15290.909\" y=\"1192.8125\"/>\n        <ID1 xsi:type=\"node\">2145</ID1>\n        <ID2 xsi:type=\"node\">2169</ID2>\n    </child>\n    <child ID=\"2176\" layerID=\"1\" created=\"1290429916033\" x=\"15290.635\"\n        y=\"1215.5\" width=\"1.1298828\" height=\"44.5\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/739e1da77f0001010b45ad3ddeae5d00</URIString>\n        <point1 x=\"15291.265\" y=\"1216.0\"/>\n        <point2 x=\"15291.135\" y=\"1259.5\"/>\n        <ID1 xsi:type=\"node\">2169</ID1>\n        <ID2 xsi:type=\"node\">2147</ID2>\n    </child>\n    <child ID=\"2177\"\n        label=\"template should be like this:&#xa;{{repository.path}}/{{project.code}}/REFS/{{entity.code}}/{{file.id}}_{{file.name}}\"\n        layerID=\"1\" created=\"1290430092371\" x=\"15522.985\" y=\"702.04297\"\n        width=\"428.0\" height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/73a89ec47f0001010b45ad3d3340d8ba</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2178\" layerID=\"1\" created=\"1290430092376\" x=\"15499.074\"\n        y=\"634.31006\" width=\"186.10254\" height=\"68.23291\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/73a89ec57f0001010b45ad3d5be80605</URIString>\n        <point1 x=\"15499.573\" y=\"634.81006\"/>\n        <point2 x=\"15684.676\" y=\"702.04297\"/>\n        <ID1 xsi:type=\"node\">2142</ID1>\n        <ID2 xsi:type=\"node\">2177</ID2>\n    </child>\n    <child ID=\"2180\" label=\"FilenameTemplate\" layerID=\"1\"\n        created=\"1290430525012\" x=\"9223.047\" y=\"828.225\" width=\"143.0\"\n        height=\"147.20193\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/73a89ec67f0001010b45ad3d49fb4e30</URIString>\n        <child ID=\"3826\" label=\"TargetEntityTypeMixin\"\n            created=\"1317078076240\" x=\"34.0\" y=\"23.0\" width=\"136.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a800f9087f00010130ce553694f16644</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2332\" label=\"path | STRING\" created=\"1293710778089\"\n            x=\"34.0\" y=\"43.25\" width=\"89.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/372d244c7f00010150882100fb860d9b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2331\" label=\"filename | STRING\"\n            created=\"1293710767892\" x=\"34.0\" y=\"63.5\" width=\"113.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/372d244c7f000101508821007f113662</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3628\" label=\"output_path | STRING\"\n            created=\"1306190609523\" x=\"34.0\" y=\"83.75\" width=\"130.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/1f0762537f000101578cef187c1d8dbe</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4146\" created=\"1369384364857\" x=\"34.0\" y=\"104.0\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acad2e7b6a9fda5a40fe8247f8b756</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4031\" created=\"1358111882603\" x=\"34.0\" y=\"126.201965\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8aefac0a8000435fa837971185c26</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"2198\" label=\"BUT!!!&#xa;REASONABLE ENOUGH\" layerID=\"1\"\n        created=\"1290435474198\" x=\"15221.573\" y=\"1332.5925\"\n        width=\"140.0\" height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#30D643</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/73f32c657f0001010b45ad3de4631d2a</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2199\" layerID=\"1\" created=\"1290435474203\" x=\"15290.668\"\n        y=\"1282.0\" width=\"1.2939453\" height=\"51.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/73f32c657f0001010b45ad3d5690194d</URIString>\n        <point1 x=\"15291.168\" y=\"1282.5\"/>\n        <point2 x=\"15291.462\" y=\"1332.5\"/>\n        <ID1 xsi:type=\"node\">2147</ID1>\n        <ID2 xsi:type=\"node\">2198</ID2>\n    </child>\n    <child ID=\"2203\" label=\"Back -references\" layerID=\"1\"\n        created=\"1290436660061\" x=\"9222.637\" y=\"130.2057\" width=\"301.0\"\n        height=\"17.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/740838427f0001010b45ad3d5e0a73f3</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Back -references\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Back -references</label>\n    </child>\n    <child ID=\"2205\"\n        label=\"Secondary attributes which are derived from current attributes and are not persistet in the database\"\n        layerID=\"1\" created=\"1290436660061\" x=\"9222.637\" y=\"196.23404\"\n        width=\"300.0\" height=\"32.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/740838437f0001010b45ad3d8a55f990</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Secondary attributes which are derived from current attributes and are \n      not persistet in the database\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Secondary attributes which are derived from current attributes and are not persistet in the database</label>\n    </child>\n    <child ID=\"2208\" label=\"Nodes those are not going to be implemented\"\n        layerID=\"1\" created=\"1290436767093\" x=\"9222.637\" y=\"230.26205\"\n        width=\"301.0\" height=\"24.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/740838437f0001010b45ad3d5ec22072</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Nodes those are not going to be implemented\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Nodes those are not going to be implemented</label>\n    </child>\n    <child ID=\"1813\" layerID=\"1\" created=\"1282128089956\" x=\"9187.644\"\n        y=\"-57.51773\" width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/84ca7c987f00010138aea98153c19261</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"1818\" layerID=\"1\" created=\"1282128771399\" x=\"9187.644\"\n        y=\"98.84404\" width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#F2AE45</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/84d4ca357f00010138aea981c2362867</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"2202\" layerID=\"1\" created=\"1290436660061\" x=\"9187.644\"\n        y=\"130.2057\" width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#DAA9FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/740838427f0001010b45ad3d613481d3</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"2204\" layerID=\"1\" created=\"1290436660061\" x=\"9187.644\"\n        y=\"196.23404\" width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#83CEFF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/740838437f0001010b45ad3d6fcb50ca</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"2209\" layerID=\"1\" created=\"1290436833398\" x=\"9187.644\"\n        y=\"230.26205\" width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/740838447f0001010b45ad3df4e42c98</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"2214\" label=\"STILL COOKING IDEAS\" layerID=\"1\"\n        created=\"1290437542596\" x=\"10976.716\" y=\"-34.489746\"\n        width=\"305.667\" height=\"102.66669\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#B5B995</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-18</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7412d2a47f0001010b45ad3d0fe8838d</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2224\" label=\"Mapper\" layerID=\"1\" created=\"1292274937082\"\n        x=\"14691.515\" y=\"-86.656494\" width=\"80.0\" height=\"44.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce5597f00010177026e406428bc52</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2225\" label=\"Tables\" layerID=\"1\" created=\"1292274943148\"\n        x=\"14707.515\" y=\"-178.6565\" width=\"77.0\" height=\"38.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce55a7f00010177026e405b6f7397</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2227\" label=\"Python&#xa;Object Model\" layerID=\"1\"\n        created=\"1292274951125\" x=\"14586.515\" y=\"-173.6565\" width=\"82.0\"\n        height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce55b7f00010177026e40538d6e0a</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2228\" layerID=\"1\" created=\"1292274951919\" x=\"14648.971\"\n        y=\"-136.15625\" width=\"57.62207\" height=\"50.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce55c7f00010177026e404ac18180</URIString>\n        <point1 x=\"14649.471\" y=\"-135.65625\"/>\n        <point2 x=\"14706.093\" y=\"-86.65625\"/>\n        <ID1 xsi:type=\"node\">2227</ID1>\n        <ID2 xsi:type=\"node\">2224</ID2>\n    </child>\n    <child ID=\"2229\" label=\"SessionMaker\" layerID=\"1\"\n        created=\"1292274975500\" x=\"14420.515\" y=\"-221.6565\"\n        width=\"118.0\" height=\"47.0\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce55d7f00010177026e40f5873189</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2230\" label=\"engine\" layerID=\"1\" created=\"1292274983370\"\n        x=\"14580.715\" y=\"-380.65637\" width=\"77.0\" height=\"39.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce55e7f00010177026e4085954d8d</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2231\" layerID=\"1\" created=\"1292275116284\" x=\"14499.155\"\n        y=\"-342.15625\" width=\"103.8457\" height=\"121.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce55f7f00010177026e40305f270b</URIString>\n        <point1 x=\"14602.501\" y=\"-341.65625\"/>\n        <point2 x=\"14499.655\" y=\"-221.65625\"/>\n        <ID1 xsi:type=\"node\">2230</ID1>\n        <ID2 xsi:type=\"node\">2229</ID2>\n    </child>\n    <child ID=\"2232\" label=\"session\" layerID=\"1\" created=\"1292275126025\"\n        x=\"14545.015\" y=\"-20.156494\" width=\"84.0\" height=\"50.5\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce5617f00010177026e40f6ef7cde</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2233\" layerID=\"1\" created=\"1292275126033\" x=\"14491.443\"\n        y=\"-175.15625\" width=\"82.7168\" height=\"155.5\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce5627f00010177026e400796cc3f</URIString>\n        <point1 x=\"14491.943\" y=\"-174.65625\"/>\n        <point2 x=\"14573.66\" y=\"-20.15625\"/>\n        <ID1 xsi:type=\"node\">2229</ID1>\n        <ID2 xsi:type=\"node\">2232</ID2>\n    </child>\n    <child ID=\"2234\" label=\"MetaData\" layerID=\"1\"\n        created=\"1292275149153\" x=\"14711.515\" y=\"-275.6565\" width=\"91.0\"\n        height=\"43.0\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce5637f00010177026e4015495ed2</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2236\" label=\"Database\" layerID=\"1\"\n        created=\"1292275223829\" x=\"14578.715\" y=\"-481.65637\"\n        width=\"93.0\" height=\"45.0\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce5647f00010177026e406e92ee08</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2237\" label=\"engine(database)\" layerID=\"1\"\n        created=\"1292275229366\" x=\"14574.123\" y=\"-437.15625\"\n        width=\"96.0\" height=\"57.0\" strokeWidth=\"1.0\" autoSized=\"false\"\n        controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce5657f00010177026e40a5e1ab26</URIString>\n        <point1 x=\"14623.837\" y=\"-436.65625\"/>\n        <point2 x=\"14620.408\" y=\"-380.65625\"/>\n        <ID1 xsi:type=\"node\">2236</ID1>\n        <ID2 xsi:type=\"node\">2230</ID2>\n    </child>\n    <child ID=\"2238\" layerID=\"1\" created=\"1292275242605\" x=\"14747.727\"\n        y=\"-233.16406\" width=\"7.286133\" height=\"55.007812\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce5667f00010177026e40a2f1c46f</URIString>\n        <point1 x=\"14754.513\" y=\"-232.66406\"/>\n        <point2 x=\"14748.227\" y=\"-178.65625\"/>\n        <ID1 xsi:type=\"node\">2234</ID1>\n        <ID2 xsi:type=\"node\">2225</ID2>\n    </child>\n    <child ID=\"2226\" layerID=\"1\" created=\"1292274944072\" x=\"14734.373\"\n        y=\"-141.15625\" width=\"9.2421875\" height=\"55.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce5677f00010177026e40fab7b7de</URIString>\n        <point1 x=\"14743.115\" y=\"-140.65625\"/>\n        <point2 x=\"14734.873\" y=\"-86.65625\"/>\n        <ID1 xsi:type=\"node\">2225</ID1>\n        <ID2 xsi:type=\"node\">2224</ID2>\n    </child>\n    <child ID=\"2242\" label=\"metadata.create_all(engine)\" layerID=\"1\"\n        created=\"1292275291662\" x=\"14610.327\" y=\"-342.15625\"\n        width=\"153.0\" height=\"67.0\" strokeWidth=\"1.0\" autoSized=\"false\"\n        controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e19ce5687f00010177026e40d871ebc7</URIString>\n        <point1 x=\"14644.328\" y=\"-341.65625\"/>\n        <point2 x=\"14729.326\" y=\"-275.65625\"/>\n        <ID1 xsi:type=\"node\">2230</ID1>\n        <ID2 xsi:type=\"node\">2234</ID2>\n    </child>\n    <child ID=\"2249\" label=\"add\" layerID=\"1\" created=\"1292275365435\"\n        x=\"14592.916\" y=\"-136.15625\" width=\"30.28125\" height=\"116.5\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e1a292207f00010177026e40616be0a9</URIString>\n        <point1 x=\"14622.697\" y=\"-135.65625\"/>\n        <point2 x=\"14593.416\" y=\"-20.15625\"/>\n        <ID1 xsi:type=\"node\">2227</ID1>\n        <ID2 xsi:type=\"node\">2232</ID2>\n    </child>\n    <child ID=\"2250\" label=\"Persisting\" layerID=\"1\"\n        created=\"1292275372356\" x=\"14305.607\" y=\"-457.34113\"\n        width=\"273.60742\" height=\"480.51657\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e1a292217f00010177026e4079dd75c1</URIString>\n        <point1 x=\"14545.015\" y=\"16.992676\"/>\n        <point2 x=\"14578.715\" y=\"-456.84113\"/>\n        <ID1 xsi:type=\"node\">2232</ID1>\n        <ID2 xsi:type=\"node\">2236</ID2>\n        <ctrlPoint0 x=\"14313.736\" y=\"82.51904\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"14202.64\" y=\"-438.11633\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"2254\" label=\"config&#xa;(environment&#xa;variables)\"\n        layerID=\"1\" created=\"1292275586156\" x=\"14574.215\" y=\"-642.6564\"\n        width=\"83.0\" height=\"53.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e1a292227f00010177026e40f20f40e8</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2255\" label=\"database_adress&#xa;&amp;&#xa;user_name\"\n        layerID=\"1\" created=\"1292275586704\" x=\"14573.086\" y=\"-590.15625\"\n        width=\"95.0\" height=\"109.0\" strokeWidth=\"1.0\" autoSized=\"false\"\n        controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e1a292227f00010177026e404f21e562</URIString>\n        <point1 x=\"14617.318\" y=\"-589.65625\"/>\n        <point2 x=\"14623.853\" y=\"-481.65625\"/>\n        <ID1 xsi:type=\"node\">2254</ID1>\n        <ID2 xsi:type=\"node\">2236</ID2>\n    </child>\n    <child ID=\"2257\" label=\"SQLALCHEMY DB SETUP\" layerID=\"1\"\n        created=\"1292277139230\" x=\"14451.715\" y=\"-773.65643\"\n        width=\"328.0\" height=\"87.0\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#B5B995</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-18</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/e1b958057f00010177026e4065f7dcfe</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2267\" layerID=\"1\" created=\"1292744499011\" x=\"8649.359\"\n        y=\"486.5625\" width=\"157.43555\" height=\"123.78125\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ff73db6b7f0001015481be8a2537d4ab</URIString>\n        <point1 x=\"8806.295\" y=\"487.0625\"/>\n        <point2 x=\"8649.859\" y=\"609.84375\"/>\n        <ID1 xsi:type=\"node\">1901</ID1>\n        <ID2 xsi:type=\"node\">1295</ID2>\n        <ctrlPoint0 x=\"8807.627\" y=\"568.82007\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8648.785\" y=\"545.93036\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1900\" layerID=\"1\" created=\"1288257142624\" x=\"8779.806\"\n        y=\"486.5625\" width=\"27.305664\" height=\"122.9375\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f21c2a7b7f0001010d9438ded95282fa</URIString>\n        <point1 x=\"8806.611\" y=\"487.0625\"/>\n        <point2 x=\"8780.306\" y=\"609.0\"/>\n        <ID1 xsi:type=\"node\">1901</ID1>\n        <ID2 xsi:type=\"node\">1887</ID2>\n        <ctrlPoint0 x=\"8807.837\" y=\"554.70557\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8783.59\" y=\"551.4998\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"2287\" label=\"Hold in Structure\" layerID=\"1\"\n        created=\"1293638447932\" x=\"13135.107\" y=\"844.37695\"\n        width=\"241.0\" height=\"173.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#30D643</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/32deba3f7f0001015088210050d1939f</URIString>\n        <child ID=\"2288\"\n            label=\"Generalize the structure system and add: folder_templates asset_templates reference_templates to hold templates for every one of them. This leads us a complete structure object where one can see the whole picture in one object...\"\n            created=\"1293638447932\" x=\"5.0\" y=\"23.0\" width=\"231.0\"\n            height=\"144.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/32deba3f7f000101508821006c2d59f4</URIString>\n            <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Generalize the structure system and add:\n    &lt;/p&gt;\n    &lt;ul color=\"#000000\"&gt;\n      &lt;li style=\"color: #000000\" color=\"#000000\"&gt;\n        folder_templates\n      &lt;/li&gt;\n      &lt;li style=\"color: #000000\" color=\"#000000\"&gt;\n        asset_templates\n      &lt;/li&gt;\n      &lt;li style=\"color: #000000\" color=\"#000000\"&gt;\n        reference_templates\n      &lt;/li&gt;\n    &lt;/ul&gt;\n    &lt;p color=\"#000000\"&gt;\n      \n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      to hold templates for every one of them. This leads us a complete \n      structure object where one can see the whole picture in one object...\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>Generalize the structure system and add: folder_templates asset_templates reference_templates to hold templates for every one of them. This leads us a complete structure object where one can see the whole picture in one object...</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2289\" label=\"SOLUTION 3&#xa;THE BEST SOLUTION\"\n        layerID=\"1\" created=\"1293638451191\" x=\"12919.361\" y=\"670.71094\"\n        width=\"226.10352\" height=\"175.39746\" strokeWidth=\"3.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/32deba407f00010150882100a056b737</URIString>\n        <point1 x=\"12920.861\" y=\"672.21094\"/>\n        <point2 x=\"13143.965\" y=\"844.6084\"/>\n        <ID1 xsi:type=\"node\">2026</ID1>\n        <ID2 xsi:type=\"node\">2287</ID2>\n    </child>\n    <child ID=\"2319\" label=\"version\" layerID=\"1\" created=\"1293706572638\"\n        x=\"12419.274\" y=\"1489.2103\" width=\"93.0\" height=\"29.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/370c82157f00010150882100318ba225</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2321\" label=\"version.task\" layerID=\"1\"\n        created=\"1293706577144\" x=\"12430.139\" y=\"1517.6875\" width=\"69.0\"\n        height=\"82.1875\" strokeWidth=\"1.0\" autoSized=\"false\"\n        controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/370c82167f00010150882100fbc03f47</URIString>\n        <point1 x=\"12465.476\" y=\"1518.1875\"/>\n        <point2 x=\"12463.803\" y=\"1599.375\"/>\n        <ID1 xsi:type=\"node\">2319</ID1>\n        <ID2 xsi:type=\"node\">2322</ID2>\n    </child>\n    <child ID=\"2322\" label=\"task\" layerID=\"1\" created=\"1293706587561\"\n        x=\"12419.774\" y=\"1599.377\" width=\"87.5\" height=\"27.0\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/370c82167f00010150882100c967ecd4</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2324\" label=\"pipelineStep\" layerID=\"1\"\n        created=\"1293706642673\" x=\"12315.108\" y=\"1716.377\" width=\"80.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/370c82167f00010150882100be8598dc</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2325\" label=\"task.pipeline_step\" layerID=\"1\"\n        created=\"1293706642678\" x=\"12357.374\" y=\"1625.877\" width=\"102.0\"\n        height=\"91.0\" strokeWidth=\"1.0\" autoSized=\"false\"\n        controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/370c82167f000101508821004c57c340</URIString>\n        <point1 x=\"12450.798\" y=\"1626.377\"/>\n        <point2 x=\"12365.95\" y=\"1716.377\"/>\n        <ID1 xsi:type=\"node\">2322</ID1>\n        <ID2 xsi:type=\"node\">2324</ID2>\n    </child>\n    <child ID=\"2328\" label=\"assetBase\" layerID=\"1\"\n        created=\"1293706765117\" x=\"12559.774\" y=\"1733.7102\" width=\"71.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/370c82167f000101508821000ce84590</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2329\" label=\"parent_asset\" layerID=\"1\"\n        created=\"1293706765122\" x=\"12476.465\" y=\"1625.877\"\n        width=\"107.86035\" height=\"108.33301\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/370c82167f000101508821004cd5726c</URIString>\n        <point1 x=\"12476.965\" y=\"1626.377\"/>\n        <point2 x=\"12583.825\" y=\"1733.71\"/>\n        <ID1 xsi:type=\"node\">2322</ID1>\n        <ID2 xsi:type=\"node\">2328</ID2>\n    </child>\n    <child ID=\"2333\" label=\"AssetTemplate\" layerID=\"1\"\n        created=\"1293717259136\" x=\"12360.107\" y=\"-412.16458\"\n        width=\"177.0\" height=\"46.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/378f410b7f00010150882100d07ef254</URIString>\n        <child ID=\"2369\" label=\"asset_type | ONE | ASSETTYPE\"\n            created=\"1293721259644\" x=\"34.0\" y=\"23.0\" width=\"183.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/37cc22ee7f00010150882100864e6ffb</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2358\" label=\"ReferenceTemplate\" layerID=\"1\"\n        created=\"1293717927562\" x=\"12338.357\" y=\"-487.16458\"\n        width=\"220.5\" height=\"46.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/379966d07f000101508821000b2bd7fd</URIString>\n        <child ID=\"2359\" label=\"reference_type | ONE | REFERENCETYPE\"\n            created=\"1293717927562\" x=\"34.0\" y=\"23.0\" width=\"241.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/379966d07f0001015088210028a2ff79</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2374\"\n        label=\"Implemented classes with persistancy (Classic SOM)\"\n        layerID=\"1\" created=\"1294147396477\" x=\"9222.637\" y=\"-21.742615\"\n        width=\"332.0\" height=\"21.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/51327de87f0001017517e53b4e0b6faa</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Implemented classes with persistancy (Classic SOM)\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Implemented classes with persistancy (Classic SOM)</label>\n    </child>\n    <child ID=\"2375\" layerID=\"1\" created=\"1294147396477\" x=\"9187.644\"\n        y=\"-26.542633\" width=\"11.0\" height=\"22.0\" strokeWidth=\"1.0\"\n        autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/51327de87f0001017517e53b06dcb343</URIString>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"2379\" label=\"FileSequence\" layerID=\"1\"\n        created=\"1294150177342\" x=\"12312.344\" y=\"-219.42288\"\n        width=\"115.0\" height=\"66.5\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5161bffb7f0001017517e53b94128c41</URIString>\n        <child ID=\"2380\" label=\"start | INTEGER\" created=\"1294150177342\"\n            x=\"34.0\" y=\"23.0\" width=\"97.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5161bffb7f0001017517e53b3481e15b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2381\" label=\"end | INTEGER\" created=\"1294150177342\"\n            x=\"34.0\" y=\"43.25\" width=\"94.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5161bffc7f0001017517e53b06eb0375</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2382\" label=\"File\" layerID=\"1\" created=\"1294150177342\"\n        x=\"12154.38\" y=\"-219.04764\" width=\"128.25\" height=\"46.25\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5161bffc7f0001017517e53b0a74a25b</URIString>\n        <child ID=\"2383\" label=\"file_size | INTEGER\"\n            created=\"1294150177342\" x=\"34.0\" y=\"23.0\" width=\"118.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5161bffc7f0001017517e53bfcda26b7</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2384\" layerID=\"1\" created=\"1294150177342\" x=\"12309.803\"\n        y=\"-399.32327\" width=\"61.476562\" height=\"180.38577\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5161bffc7f0001017517e53b1d3dd4c2</URIString>\n        <point1 x=\"12310.303\" y=\"-398.82327\"/>\n        <point2 x=\"12370.779\" y=\"-219.4375\"/>\n        <ID2 xsi:type=\"node\">2379</ID2>\n        <ctrlPoint0 x=\"12315.923\" y=\"-233.81949\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"12372.488\" y=\"-280.19986\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"2385\" layerID=\"1\" created=\"1294150177342\" x=\"12218.27\"\n        y=\"-399.32327\" width=\"92.475586\" height=\"180.82327\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5161bffc7f0001017517e53b0c1d6699</URIString>\n        <point1 x=\"12310.245\" y=\"-398.82327\"/>\n        <point2 x=\"12218.77\" y=\"-219.0\"/>\n        <ID2 xsi:type=\"node\">2382</ID2>\n        <ctrlPoint0 x=\"12315.6\" y=\"-233.2283\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"12219.3955\" y=\"-273.93698\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"2386\" label=\"Folder\" layerID=\"1\" created=\"1294150177342\"\n        x=\"12078.92\" y=\"-219.04764\" width=\"46.0\" height=\"23.0\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5161bffd7f0001017517e53b80768083</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2387\" layerID=\"1\" created=\"1294150177342\" x=\"12101.456\"\n        y=\"-399.32327\" width=\"209.29492\" height=\"180.82327\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5161bffd7f0001017517e53b94d526a0</URIString>\n        <point1 x=\"12310.251\" y=\"-398.82327\"/>\n        <point2 x=\"12101.956\" y=\"-219.0\"/>\n        <ID2 xsi:type=\"node\">2386</ID2>\n        <ctrlPoint0 x=\"12315.551\" y=\"-235.86832\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"12102.15\" y=\"-280.1417\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"2388\" label=\"Web\" layerID=\"1\" created=\"1294150177342\"\n        x=\"12457.045\" y=\"-219.42288\" width=\"107.25\" height=\"46.25\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5161bffd7f0001017517e53b87414322</URIString>\n        <child ID=\"2389\" label=\"url | UNICODE\" created=\"1294150177343\"\n            x=\"34.0\" y=\"23.0\" width=\"90.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5161bffd7f0001017517e53bf8dd37fb</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2390\" layerID=\"1\" created=\"1294150177342\" x=\"12309.773\"\n        y=\"-399.32327\" width=\"202.05957\" height=\"180.38577\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5161bffd7f0001017517e53b5ed7b8f6</URIString>\n        <point1 x=\"12310.273\" y=\"-398.82327\"/>\n        <point2 x=\"12511.333\" y=\"-219.4375\"/>\n        <ID2 xsi:type=\"node\">2388</ID2>\n        <ctrlPoint0 x=\"12315.568\" y=\"-239.22952\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"12513.098\" y=\"-280.98056\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"2397\" label=\"Type\" layerID=\"1\" created=\"1294154217183\"\n        x=\"9415.732\" y=\"828.225\" width=\"141.75\" height=\"106.70194\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/519d2c637f0001017517e53b1a4782a4</URIString>\n        <child ID=\"3827\" label=\"TargetEntityTypeMixin\"\n            created=\"1317078622396\" x=\"34.0\" y=\"23.0\" width=\"136.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a800f9bb7f00010130ce55362b9cdc24</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4054\" label=\"CodeMixin\" created=\"1358233231637\"\n            x=\"34.0\" y=\"43.25\" width=\"70.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3d029f0cc0a800044f9e91d03a4720d1</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4145\" created=\"1369384356043\" x=\"34.0\" y=\"63.5\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acad957b6a9fda5a40fe820dca0155</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4032\" created=\"1358111888146\" x=\"34.0\" y=\"85.701965\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8af06c0a8000435fa83795f0c1c20</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"2363\"\n        label=\"Reference Template Example 1: ReferenceType = Image {{repository.path}}/{{project.code}}/REFS/{{reference.type.name}}/{{reference.link.name}}\"\n        layerID=\"1\" created=\"1293717968929\" x=\"12136.389\" y=\"2687.0854\"\n        width=\"518.0\" height=\"48.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/379ae6b47f00010150882100c2142256</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Reference Template Example 1:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      ReferenceType = Image\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      {{repository.path}}/{{project.code}}/REFS/{{reference.type.name}}/{{reference.link.name}}\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Reference Template Example 1: ReferenceType = Image {{repository.path}}/{{project.code}}/REFS/{{reference.type.name}}/{{reference.link.name}}</label>\n    </child>\n    <child ID=\"2338\"\n        label=\"Asset Version Template Example 1: AssetType.name = Shot {{repository.path}}/{{project.code}}/SEQUENCES/{{sequence.name}}/SHOTS/{{shot.code}}/{{pipeline_step.code}}/{{shot.code}}_{{take.name}}_{{version.version_number}}\"\n        layerID=\"1\" created=\"1293717259137\" x=\"12136.389\" y=\"2575.0854\"\n        width=\"984.0\" height=\"48.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/378f410b7f0001015088210053e34b34</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Asset Version Template Example 1:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      AssetType.name = Shot\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      {{repository.path}}/{{project.code}}/SEQUENCES/{{sequence.name}}/SHOTS/{{shot.code}}/{{pipeline_step.code}}/{{shot.code}}_{{take.name}}_{{version.version_number}}\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Asset Version Template Example 1: AssetType.name = Shot {{repository.path}}/{{project.code}}/SEQUENCES/{{sequence.name}}/SHOTS/{{shot.code}}/{{pipeline_step.code}}/{{shot.code}}_{{take.name}}_{{version.version_number}}</label>\n    </child>\n    <child ID=\"2408\" label=\"TRASHED IDEAS\" layerID=\"1\"\n        created=\"1294155278342\" x=\"12147.773\" y=\"-633.4564\"\n        width=\"305.667\" height=\"102.66669\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#B5B995</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-18</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/51aaed357f0001017517e53b20cfc539</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2409\"\n        label=\"Asset Version Template Example 2: AssetType.name = Character {{repository.path}}/{{project.code}}/ASSETS/{{asset_type.name}}/{{pipeline_step.code}}/{{asset.name}}_{{take.name}}_{{asset_type.name}}_{{version.version_number}}\"\n        layerID=\"1\" created=\"1294155909846\" x=\"12136.389\" y=\"2631.0435\"\n        width=\"977.0\" height=\"48.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/51b7d1837f0001017517e53b679d9674</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Asset Version Template Example 2:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      AssetType.name = Character\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      {{repository.path}}/{{project.code}}/ASSETS/{{asset_type.name}}/{{pipeline_step.code}}/{{asset.name}}_{{take.name}}_{{asset_type.name}}_{{version.version_number}}\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Asset Version Template Example 2: AssetType.name = Character {{repository.path}}/{{project.code}}/ASSETS/{{asset_type.name}}/{{pipeline_step.code}}/{{asset.name}}_{{take.name}}_{{asset_type.name}}_{{version.version_number}}</label>\n    </child>\n    <child ID=\"2413\"\n        label=\"Reference Template Example 2: ReferenceType = Web (the same with the Image example) {{repository.path}}/{{project.code}}/REFS/{{reference.type.name}}/{{reference.link.name}}\"\n        layerID=\"1\" created=\"1294156830278\" x=\"12136.389\" y=\"2741.0435\"\n        width=\"518.0\" height=\"48.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/51c2b1ad7f0001017517e53b94d0c271</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Reference Template Example 2:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      ReferenceType = Web (the same with the Image example)\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      {{repository.path}}/{{project.code}}/REFS/{{reference.type.name}}/{{reference.link.name}}\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Reference Template Example 2: ReferenceType = Web (the same with the Image example) {{repository.path}}/{{project.code}}/REFS/{{reference.type.name}}/{{reference.link.name}}</label>\n    </child>\n    <child ID=\"2440\" label=\"Note\" layerID=\"1\" created=\"1294846767149\"\n        x=\"8891.292\" y=\"607.6561\" width=\"126.75\" height=\"122.4519\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7ae32d837f0001017115e1660c06a911</URIString>\n        <child ID=\"2441\" label=\"content | UNICODE\"\n            created=\"1294846767149\" x=\"34.0\" y=\"23.0\" width=\"116.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7ae32d837f0001017115e1666b17d87e</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4148\" created=\"1369384385601\" x=\"34.0\" y=\"43.25\"\n            width=\"30.333334\" height=\"25.602621\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/d5acada37b6a9fda5a40fe82eb524c4b</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4043\" created=\"1358111978077\" x=\"34.0\" y=\"65.451965\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#30D643</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35c8af09c0a8000435fa8379afc3b443</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4219\" created=\"1468653311663\" x=\"34.0\" y=\"83.45194\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#33A8F5</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbcfc0a8000f011bc52f32888ef6</URIString>\n            <shape xsi:type=\"chevron\"/>\n        </child>\n        <child ID=\"4220\" created=\"1468653314809\" x=\"34.0\" y=\"101.45192\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbcfc0a8000f011bc52f462ab2eb</URIString>\n            <shape xsi:type=\"rhombus\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"2442\" layerID=\"1\" created=\"1294846767149\" x=\"8805.921\"\n        y=\"486.59375\" width=\"149.67773\" height=\"121.40625\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7ae32d837f0001017115e16676d33287</URIString>\n        <point1 x=\"8806.421\" y=\"487.09375\"/>\n        <point2 x=\"8955.099\" y=\"607.5\"/>\n        <ID1 xsi:type=\"node\">1901</ID1>\n        <ID2 xsi:type=\"node\">2440</ID2>\n        <ctrlPoint0 x=\"8807.702\" y=\"562.3818\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8955.573\" y=\"540.1338\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"1507\"\n        label=\"PipelineStep Examples: design-DESIGN model-MODEL rig-RIG fur-FUR shading-SHADE previs-PREVIS match move-MM animation-ANIM fx-FX cloth sim-CLOTHSIM layout-LAYOUT lighting-LIGHT compositing-COMP\"\n        layerID=\"1\" created=\"1277557969560\" x=\"11740.921\" y=\"2513.225\"\n        width=\"114.0\" height=\"243.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7464cb477f0001014a6b2ab74e33abce</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      PipelineStep\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Examples:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      design-DESIGN\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      model-MODEL\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      rig-RIG\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      fur-FUR\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      shading-SHADE\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      previs-PREVIS\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      match move-MM\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      animation-ANIM\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      fx-FX\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      cloth sim-CLOTHSIM\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      layout-LAYOUT\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      lighting-LIGHT\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      compositing-COMP\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>PipelineStep Examples: design-DESIGN model-MODEL rig-RIG fur-FUR shading-SHADE previs-PREVIS match move-MM animation-ANIM fx-FX cloth sim-CLOTHSIM layout-LAYOUT lighting-LIGHT compositing-COMP</label>\n    </child>\n    <child ID=\"1508\"\n        label=\"AssetType Examples: Character FuryCharacter Vehicle Prop Environment Shot\"\n        layerID=\"1\" created=\"1277558023775\" x=\"11934.202\" y=\"2578.475\"\n        width=\"115.0\" height=\"123.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7464cb547f0001014a6b2ab78c34774b</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      AssetType\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Examples:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Character\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      FuryCharacter\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Vehicle\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Prop\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Environment\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Shot\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>AssetType Examples: Character FuryCharacter Vehicle Prop Environment Shot</label>\n    </child>\n    <child ID=\"2014\"\n        label=\"Examples: ASSETS SEQUENCES SEQUENCES\\EDIT_MOVIE ...\"\n        layerID=\"1\" created=\"1290373811339\" x=\"11725.721\" y=\"2419.475\"\n        width=\"156.0\" height=\"78.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/704adc127f0001012ed4f13a3db45dfe</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Examples:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      ASSETS\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      SEQUENCES\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      SEQUENCES\\EDIT_MOVIE\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      ...\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Examples: ASSETS SEQUENCES SEQUENCES\\EDIT_MOVIE ...</label>\n    </child>\n    <child ID=\"2305\"\n        label=\"LinkType Examples: Image ImageSequence Video Text Web ...\"\n        layerID=\"1\" created=\"1293639905702\" x=\"12177.091\" y=\"2275.8252\"\n        width=\"111.0\" height=\"108.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/32f3ce3d7f000101508821003b9a78db</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      LinkType Examples:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Image\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      ImageSequence\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Video\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Text\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Web\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      ...\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>LinkType Examples: Image ImageSequence Video Text Web ...</label>\n    </child>\n    <child ID=\"2403\"\n        label=\"ProjectType Examples: Commercial Movie Still ...\"\n        layerID=\"1\" created=\"1294154324356\" x=\"12322.508\" y=\"2271.425\"\n        width=\"128.0\" height=\"78.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/519d2c647f0001017517e53b19274f95</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      ProjectType Examples:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Commercial\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Movie\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Still\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      ...\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>ProjectType Examples: Commercial Movie Still ...</label>\n    </child>\n    <child ID=\"2406\"\n        label=\"UserType Examples: SuperUser Admin Normal ...\"\n        layerID=\"1\" created=\"1294154384750\" x=\"12621.506\" y=\"2361.425\"\n        width=\"120.0\" height=\"78.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <fillColor>#FCDBD9</fillColor>\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/519d2c647f0001017517e53b912c4954</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      UserType Examples:\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      SuperUser\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Admin\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Normal\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      ...\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>UserType Examples: SuperUser Admin Normal ...</label>\n    </child>\n    <child ID=\"2462\" layerID=\"1\" created=\"1295291873435\" x=\"9187.644\"\n        y=\"255.62396\" width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/9589d5c77f0001015ee819a888178704</URIString>\n        <shape xsi:type=\"triangle\"/>\n    </child>\n    <child ID=\"2463\" label=\"has __eq__, __ne__\" layerID=\"1\"\n        created=\"1295291882607\" x=\"9222.637\" y=\"257.62396\" width=\"302.0\"\n        height=\"18.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/9589d5c87f0001015ee819a8969060b6</URIString>\n        <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      has __eq__, __ne__\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>has __eq__, __ne__</label>\n    </child>\n    <child ID=\"2468\" layerID=\"1\" created=\"1295294154121\" x=\"9187.644\"\n        y=\"287.62393\" width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/958e29dc7f0001015ee819a82f59b16a</URIString>\n        <shape xsi:type=\"triangle\"/>\n    </child>\n    <child ID=\"2469\" label=\"doesn't have __eq__, __ne__\" layerID=\"1\"\n        created=\"1295294165800\" x=\"9222.637\" y=\"290.62393\" width=\"302.0\"\n        height=\"18.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/958e29dc7f0001015ee819a836cff506</URIString>\n        <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      doesn't have __eq__, __ne__\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>doesn't have __eq__, __ne__</label>\n    </child>\n    <child ID=\"2516\" layerID=\"1\" created=\"1295345430716\" x=\"9820.951\"\n        y=\"1733.8281\" width=\"30.24414\" height=\"96.88281\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/989c41f37f0001015ee819a8aa4c7a01</URIString>\n        <point1 x=\"9850.695\" y=\"1734.3281\"/>\n        <point2 x=\"9821.451\" y=\"1830.2109\"/>\n        <ID1 xsi:type=\"node\">3382</ID1>\n        <ID2 xsi:type=\"node\">2517</ID2>\n    </child>\n    <child ID=\"2517\" label=\"Statuses\" layerID=\"1\"\n        created=\"1295345447286\" x=\"9741.384\" y=\"1830.21\" width=\"127.5\"\n        height=\"107.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#83CEFF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/989c41f37f0001015ee819a84c18ca80</URIString>\n        <child ID=\"2519\" label=\"NotRead - NR\" created=\"1295345464908\"\n            x=\"34.0\" y=\"23.0\" width=\"85.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/989c41f47f0001015ee819a869e77b53</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2520\" label=\"Read - R\" created=\"1295345495895\"\n            x=\"34.0\" y=\"43.25\" width=\"58.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/989c41f47f0001015ee819a82998ee58</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"2521\" label=\"Replied - REP\" created=\"1295345503438\"\n            x=\"34.0\" y=\"63.5\" width=\"86.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/989c41f47f0001015ee819a8dac3e9ea</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3901\" label=\"Forwarded - FORW\"\n            created=\"1336640474414\" x=\"34.0\" y=\"83.75\" width=\"117.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fce5c77f000101663ffd147c8b9e26</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2536\" label=\"PROBLEM\" layerID=\"1\" created=\"1295363437120\"\n        x=\"14496.209\" y=\"565.01025\" width=\"215.0\" height=\"76.0\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FCDBD9</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647627f00010103d66c396d43c345</URIString>\n        <child ID=\"2537\"\n            label=\"the Status and StatusList objects doesn't carry any usage information &#xa;      other than the name\"\n            created=\"1295363437121\" x=\"5.0\" y=\"23.0\" width=\"205.0\"\n            height=\"47.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/99b647627f00010103d66c39e7b3a569</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      the Status and StatusList objects doesn't carry any usage information \n      other than the name\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>the Status and StatusList objects doesn't carry any usage information \n      other than the name</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2540\" label=\"Ex1: Project Status\" layerID=\"1\"\n        created=\"1295363507887\" x=\"14517.209\" y=\"751.5659\" width=\"173.0\"\n        height=\"151.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647637f00010103d66c3913f91d93</URIString>\n        <child ID=\"2535\"\n            label=\"A project StatusList may have these status objects * WaitingToStart * In Progress * Stopped * Completed\"\n            created=\"1295363362392\" x=\"5.0\" y=\"23.0\" width=\"163.0\"\n            height=\"122.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/99b647637f00010103d66c39798b7d69</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      A project StatusList may have these status objects\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      \n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      * WaitingToStart\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      * In Progress\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      * Stopped\n    &lt;/p&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      * Completed\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>A project StatusList may have these status objects * WaitingToStart * In Progress * Stopped * Completed</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2541\" layerID=\"1\" created=\"1295363507901\" x=\"14603.209\"\n        y=\"707.2881\" width=\"1.0\" height=\"44.777832\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647647f00010103d66c39554a3237</URIString>\n        <point1 x=\"14603.709\" y=\"707.7881\"/>\n        <point2 x=\"14603.709\" y=\"751.5659\"/>\n        <ID1 xsi:type=\"node\">2542</ID1>\n        <ID2 xsi:type=\"node\">2540</ID2>\n    </child>\n    <child ID=\"2542\" label=\"For Example?\" layerID=\"1\"\n        created=\"1295363518289\" x=\"14559.709\" y=\"684.7881\" width=\"88.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647647f00010103d66c3971f65221</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2544\" layerID=\"1\" created=\"1295363523767\" x=\"14603.209\"\n        y=\"640.51025\" width=\"1.0\" height=\"44.777832\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647647f00010103d66c39b6f57f91</URIString>\n        <point1 x=\"14603.709\" y=\"641.01025\"/>\n        <point2 x=\"14603.709\" y=\"684.7881\"/>\n        <ID1 xsi:type=\"node\">2536</ID1>\n        <ID2 xsi:type=\"node\">2542</ID2>\n    </child>\n    <child ID=\"2545\"\n        label=\"When somebody creates a new project&#xa;he/she also needs to connect the&#xa;suitable StatusList to the created&#xa;project&#xa;&#xa;which is awkward\"\n        layerID=\"1\" created=\"1295363602206\" x=\"14482.0625\" y=\"946.34375\"\n        width=\"252.0\" height=\"114.166504\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647657f00010103d66c391ab244f1</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2546\" layerID=\"1\" created=\"1295363602216\" x=\"14605.073\"\n        y=\"902.0625\" width=\"2.0800781\" height=\"44.75\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647657f00010103d66c39a31ed307</URIString>\n        <point1 x=\"14605.573\" y=\"902.5625\"/>\n        <point2 x=\"14606.653\" y=\"946.3125\"/>\n        <ID1 xsi:type=\"node\">2540</ID1>\n        <ID2 xsi:type=\"node\">2545</ID2>\n    </child>\n    <child ID=\"2547\"\n        label=\"There could be a configuration&#xa;(StatusListConfig)&#xa;object storing the config for&#xa;an entity_type and a corresponding StatusLists\"\n        layerID=\"1\" created=\"1295363665880\" x=\"14325.361\" y=\"1155.4437\"\n        width=\"295.0\" height=\"95.06653\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647657f00010103d66c3962c652da</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2548\" label=\"SOLUTION 1\" layerID=\"1\"\n        created=\"1295363665890\" x=\"14504.567\" y=\"1060.0098\"\n        width=\"65.32031\" height=\"95.93359\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647657f00010103d66c39f018504b</URIString>\n        <point1 x=\"14569.388\" y=\"1060.5098\"/>\n        <point2 x=\"14505.067\" y=\"1155.4434\"/>\n        <ID1 xsi:type=\"node\">2545</ID1>\n        <ID2 xsi:type=\"node\">2547</ID2>\n    </child>\n    <child ID=\"2550\"\n        label=\"entity_type is not stored in objects,&#xa;it is introduced with SQLAlchemy&#xa;inheritance mapping\"\n        layerID=\"1\" created=\"1295363894917\" x=\"14355.109\" y=\"1290.302\"\n        width=\"227.83008\" height=\"72.916626\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647667f00010103d66c3963aa548b</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2551\" layerID=\"1\" created=\"1295363894931\" x=\"14469.653\"\n        y=\"1250.0\" width=\"2.234375\" height=\"40.8125\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647667f00010103d66c39cc52cbaf</URIString>\n        <point1 x=\"14471.388\" y=\"1250.5\"/>\n        <point2 x=\"14470.153\" y=\"1290.3125\"/>\n        <ID1 xsi:type=\"node\">2547</ID1>\n        <ID2 xsi:type=\"node\">2550</ID2>\n    </child>\n    <child ID=\"2552\"\n        label=\"adding the entity_type as a&#xa;class attribute and trying to&#xa;store it makes things complex\"\n        layerID=\"1\" created=\"1295363983600\" x=\"14364.109\" y=\"1403.0103\"\n        width=\"209.83008\" height=\"74.166626\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647667f00010103d66c39bd63898b</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"2553\" layerID=\"1\" created=\"1295363983606\" x=\"14468.524\"\n        y=\"1362.7188\" width=\"1.0\" height=\"40.791626\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/99b647677f00010103d66c39516f6584</URIString>\n        <point1 x=\"14469.024\" y=\"1363.2186\"/>\n        <point2 x=\"14469.024\" y=\"1403.0103\"/>\n        <ID1 xsi:type=\"node\">2550</ID1>\n        <ID2 xsi:type=\"node\">2552</ID2>\n    </child>\n    <child ID=\"3268\" label=\"statusList_statuses\" layerID=\"1\"\n        created=\"1295449567282\" x=\"8027.6895\" y=\"1093.0936\"\n        width=\"191.4\" height=\"96.9\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/9ed04b217f000101584edf4a532914e6</URIString>\n        <child ID=\"3269\" label=\"status_id | INTEGER\"\n            created=\"1295449567283\" x=\"49.199997\" y=\"39.2\" width=\"124.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9ed04b227f000101584edf4adc4f16e1</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3270\" label=\"status_list_id | INTEGER\"\n            created=\"1295449567284\" x=\"49.199997\" y=\"59.45\"\n            width=\"147.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9ed04b227f000101584edf4ac287ddd0</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3271\" layerID=\"1\" created=\"1295449631420\" x=\"8149.727\"\n        y=\"925.28906\" width=\"28.620605\" height=\"168.30469\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/9ed0d73a7f000101584edf4aec9c5bc9</URIString>\n        <point1 x=\"8165.1694\" y=\"925.78906\"/>\n        <point2 x=\"8156.662\" y=\"1093.0938\"/>\n        <ID1 xsi:type=\"node\">1396</ID1>\n        <ID2 xsi:type=\"node\">3268</ID2>\n        <ctrlPoint0 x=\"8112.1006\" y=\"968.7471\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8221.418\" y=\"998.7998\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3272\" layerID=\"1\" created=\"1295449633485\" x=\"8184.4805\"\n        y=\"917.54395\" width=\"114.43359\" height=\"186.24512\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/9ed0d73a7f000101584edf4a0d1e47f0</URIString>\n        <point1 x=\"8298.414\" y=\"918.04395\"/>\n        <point2 x=\"8184.9805\" y=\"1103.2891\"/>\n        <ID1 xsi:type=\"node\">1355</ID1>\n        <ID2 xsi:type=\"node\">3268</ID2>\n        <ctrlPoint0 x=\"8195.113\" y=\"966.3718\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8284.184\" y=\"1041.6741\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3277\" label=\"SimpleInfoMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"6918.8574\" y=\"886.5633\"\n        width=\"143.25\" height=\"107.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad487f000101584edf4a94fcff70</URIString>\n        <child ID=\"3278\" label=\"name | UNICODE\" created=\"1295454217799\"\n            x=\"34.0\" y=\"23.0\" width=\"108.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad487f000101584edf4ae72ab318</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3279\" label=\"description | UNICODE\"\n            created=\"1295454217799\" x=\"34.0\" y=\"43.25\" width=\"137.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad497f000101584edf4ad849b8ad</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3280\" label=\"nice_name | UNICODE\"\n            created=\"1295454217800\" x=\"34.0\" y=\"63.5\" width=\"138.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad497f000101584edf4a3bff7252</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3281\" label=\"code | UNICODE\" created=\"1295454217800\"\n            x=\"34.0\" y=\"83.75\" width=\"103.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad497f000101584edf4ab7894019</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3282\" label=\"AuditInfoMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"7092.4014\" y=\"886.5633\"\n        width=\"155.25\" height=\"107.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad497f000101584edf4acff2ae90</URIString>\n        <child ID=\"3283\" label=\"created_by | ONE | USER\"\n            created=\"1295454217800\" x=\"34.0\" y=\"23.0\" width=\"149.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad497f000101584edf4a1105aa98</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3284\" label=\"updated_by | ONE | USER\"\n            created=\"1295454217800\" x=\"34.0\" y=\"43.25\" width=\"153.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad4a7f000101584edf4aeaffa9cd</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3285\" label=\"date_created | DATETIME\"\n            created=\"1295454217801\" x=\"34.0\" y=\"63.5\" width=\"150.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad4a7f000101584edf4a3be63e9c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3286\" label=\"date_updated | DATETIME\"\n            created=\"1295454217801\" x=\"34.0\" y=\"83.75\" width=\"154.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad4a7f000101584edf4a6202ee85</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3287\" label=\"NoteMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"7428.9893\" y=\"886.5633\" width=\"135.0\"\n        height=\"46.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4a7f000101584edf4a8c4e4827</URIString>\n        <child ID=\"3288\" label=\"Notes | MANY | NOTE\"\n            created=\"1295454217801\" x=\"34.0\" y=\"23.0\" width=\"127.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad4a7f000101584edf4a026ab232</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3289\" label=\"StatusMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"6804.215\" y=\"1381.2302\" width=\"177.0\"\n        height=\"66.5\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4a7f000101584edf4a8be2ca88</URIString>\n        <child ID=\"3290\" label=\"status | INTEGER\"\n            created=\"1295454217802\" x=\"34.0\" y=\"23.0\" width=\"107.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad4b7f000101584edf4a57f6e798</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3291\" label=\"status_list | ONE | STATUSLIST\"\n            created=\"1295454217802\" x=\"34.0\" y=\"43.25\" width=\"183.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad4b7f000101584edf4abdcb2014</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3294\" label=\"LinkMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"6932.709\" y=\"1066.5635\"\n        width=\"125.25\" height=\"46.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4b7f000101584edf4a9c160278</URIString>\n        <child ID=\"3295\" label=\"links | MANY | LINK\"\n            created=\"1295454217803\" x=\"34.0\" y=\"23.0\" width=\"114.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad4b7f000101584edf4a8f7bf7ed</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3296\" label=\"TagMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"7277.9453\" y=\"886.5633\"\n        width=\"120.75\" height=\"46.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4c7f000101584edf4aa59d18f5</URIString>\n        <child ID=\"3297\" label=\"tags | MANY | TAG\"\n            created=\"1295454217803\" x=\"34.0\" y=\"23.0\" width=\"108.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad4c7f000101584edf4a9920537a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3298\" label=\"SimpleInfoMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"6939.9824\" y=\"858.62384\"\n        width=\"101.0\" height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4c7f000101584edf4a2e38037c</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3299\" label=\"AuditInfoMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"7124.5264\" y=\"858.62384\" width=\"91.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4c7f000101584edf4a31e3a548</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3300\" label=\"TagMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"7307.3203\" y=\"858.62384\" width=\"62.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4c7f000101584edf4a5dc9cb23</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3301\" label=\"NoteMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"7462.9893\" y=\"858.62384\" width=\"67.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4c7f000101584edf4a6a5f9529</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3302\" label=\"StatusMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"6853.715\" y=\"1353.2908\" width=\"78.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4d7f000101584edf4a28f3414f</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3304\" label=\"LinkMixin\" layerID=\"1\"\n        created=\"1295454217799\" x=\"6962.834\" y=\"1038.624\" width=\"65.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4d7f000101584edf4a684a1372</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3305\" label=\"ReferenceMixin\" layerID=\"1\"\n        created=\"1295454231670\" x=\"7011.083\" y=\"1381.2302\"\n        width=\"192.75\" height=\"46.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4d7f000101584edf4a0f42ffd2</URIString>\n        <child ID=\"3306\" label=\"references | MANY | SIMPLEENTITY\"\n            created=\"1295454231670\" x=\"34.0\" y=\"23.0\" width=\"204.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a090ad4d7f000101584edf4ae3d8db49</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3307\" label=\"ReferenceMixin\" layerID=\"1\"\n        created=\"1295454231670\" x=\"7030.833\" y=\"1353.2908\" width=\"100.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a090ad4d7f000101584edf4a6e4c0543</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3308\" label=\"entity_tags\" layerID=\"1\"\n        created=\"1295513081494\" x=\"8329.315\" y=\"629.09357\" width=\"129.0\"\n        height=\"85.5\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a3e3fcb37f000101584edf4a6c183c36</URIString>\n        <child ID=\"3309\" label=\"entity_id\" created=\"1295513081494\"\n            x=\"43.5\" y=\"33.5\" width=\"56.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a3e3fcb37f000101584edf4ab9417f26</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3310\" label=\"tag_id\" created=\"1295513081494\" x=\"43.5\"\n            y=\"53.75\" width=\"45.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a3e3fcb37f000101584edf4af1431523</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3311\" layerID=\"1\" created=\"1295513085191\" x=\"8453.757\"\n        y=\"639.91956\" width=\"166.21191\" height=\"38.878357\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a3e3fcb47f000101584edf4a6af13a2b</URIString>\n        <point1 x=\"8619.469\" y=\"650.3679\"/>\n        <point2 x=\"8454.257\" y=\"677.8034\"/>\n        <ID1 xsi:type=\"node\">1295</ID1>\n        <ID2 xsi:type=\"node\">3308</ID2>\n        <ctrlPoint0 x=\"8515.571\" y=\"615.0924\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8527.499\" y=\"685.0255\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3312\" layerID=\"1\" created=\"1295513087786\" x=\"8444.074\"\n        y=\"691.51855\" width=\"265.90527\" height=\"28.437012\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a3e3fcb47f000101584edf4aefd4d6f5</URIString>\n        <point1 x=\"8709.4795\" y=\"699.2102\"/>\n        <point2 x=\"8444.574\" y=\"692.01855\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">3308</ID2>\n        <ctrlPoint0 x=\"8567.119\" y=\"739.42786\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8490.824\" y=\"710.40155\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3327\" label=\"Secondary tables and columns\" layerID=\"1\"\n        created=\"1295615853370\" x=\"9222.637\" y=\"162.42393\" width=\"268.0\"\n        height=\"24.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a8ba8fd57f000101584edf4a096af17f</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Secondary tables and columns\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Secondary tables and columns</label>\n    </child>\n    <child ID=\"3328\" layerID=\"1\" created=\"1295615853370\" x=\"9187.644\"\n        y=\"162.42393\" width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a8ba8fd57f000101584edf4a3546060b</URIString>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3345\" label=\"Ticket\" layerID=\"1\" created=\"1295880821526\"\n        x=\"9781.032\" y=\"1113.4767\" width=\"177.0\" height=\"185.74997\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b88461ff7f0001017e6ab533d4503f36</URIString>\n        <child ID=\"3471\" label=\"StatusMixin\" created=\"1296054703214\"\n            x=\"34.0\" y=\"23.0\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/c2e1996b7f0001013611c755eb7ed3ec</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3929\" label=\"summary | STRING\"\n            created=\"1336649323755\" x=\"34.0\" y=\"43.25\" width=\"117.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21ec47f000101663ffd1411756b19</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3872\" label=\"links| MANY | SIMPLEENTITY\"\n            created=\"1336638928135\" x=\"34.0\" y=\"63.5\" width=\"168.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8c717f000101663ffd14fa7d5a3c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3427\" label=\"related_tickets | MANY | TICKET\"\n            created=\"1295885927221\" x=\"34.0\" y=\"83.75\" width=\"183.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8d52ad37f0001017e6ab533649e6f1a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3429\" label=\"priority | ENUM\" created=\"1295885972789\"\n            x=\"34.0\" y=\"104.0\" width=\"91.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8d52ad37f0001017e6ab5334b256314</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3930\" label=\"comments | MANY | NOTE\"\n            created=\"1336649467413\" x=\"34.0\" y=\"124.25\" width=\"153.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21ec57f000101663ffd14e6bf6992</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3891\" label=\"reported_by --> created_by\"\n            created=\"1336639168963\" x=\"34.0\" y=\"144.5\" width=\"165.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8c767f000101663ffd141afe5d40</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4047\" created=\"1358112045645\" x=\"34.0\" y=\"164.75\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#30D643</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3624ae91c0a8000435fa8379e0b02f97</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3372\" label=\"message_attachments\" layerID=\"1\"\n        created=\"1295881713679\" x=\"9989.352\" y=\"1781.927\" width=\"213.6\"\n        height=\"101.1\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b892c8087f0001017e6ab53319adbe2b</URIString>\n        <child ID=\"3373\" label=\"message_id | INTEGER\"\n            created=\"1295881713679\" x=\"51.300003\" y=\"41.3\" width=\"143.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b892c8087f0001017e6ab53369965690</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3374\" label=\"simpleEntity_id | INTEGER\"\n            created=\"1295881713680\" x=\"51.300003\" y=\"61.55\"\n            width=\"157.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b892c8097f0001017e6ab5339330d0c8</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3377\" label=\"entity_types\" layerID=\"1\"\n        created=\"1295881843057\" x=\"8390.583\" y=\"348.39355\"\n        width=\"163.75\" height=\"99.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b89461017f0001017e6ab533c77dd91b</URIString>\n        <child ID=\"3378\" label=\"id | INTEGER\" created=\"1295881843057\"\n            x=\"50.25\" y=\"40.25\" width=\"83.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b89461027f0001017e6ab533d3d5b3b9</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3379\" label=\"entity_type | STRING\"\n            created=\"1295881843057\" x=\"50.25\" y=\"60.5\" width=\"122.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b89461027f0001017e6ab533421c6a11</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3393\" label=\"Dailies\" layerID=\"1\" created=\"1295883108630\"\n        x=\"10014.451\" y=\"1508.0773\" width=\"150.0\" height=\"86.75\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b8a7d59e7f0001017e6ab5334de98edf</URIString>\n        <child ID=\"3917\" label=\"ScheduleMixin\" created=\"1336641056893\"\n            x=\"34.0\" y=\"23.0\" width=\"94.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/360402fc7f000101663ffd14d8ec2158</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3468\" label=\"StatusMixin\" created=\"1296050036295\"\n            x=\"34.0\" y=\"43.25\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/c2bbd0437f0001013611c755eeb1c974</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3394\" label=\"playlist | ONE | PLAYLIST\"\n            created=\"1295883108630\" x=\"34.0\" y=\"63.5\" width=\"147.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8a7d59e7f0001017e6ab5339706b727</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3396\" layerID=\"1\" created=\"1295883153950\" x=\"9876.9375\"\n        y=\"1733.8281\" width=\"126.6875\" height=\"117.22119\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b8a7d59e7f0001017e6ab5334b036143</URIString>\n        <point1 x=\"9877.964\" y=\"1734.3281\"/>\n        <point2 x=\"10003.125\" y=\"1846.9181\"/>\n        <ID1 xsi:type=\"node\">3382</ID1>\n        <ID2 xsi:type=\"node\">3372</ID2>\n        <ctrlPoint0 x=\"9871.733\" y=\"1831.7656\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9902.772\" y=\"1862.4963\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3397\" layerID=\"1\" created=\"1295883197344\" x=\"8775.108\"\n        y=\"751.1875\" width=\"1314.5088\" height=\"757.4375\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b8a8a4607f0001017e6ab533617f8ad0</URIString>\n        <point1 x=\"8775.608\" y=\"751.6875\"/>\n        <point2 x=\"10089.117\" y=\"1508.125\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">3393</ID2>\n        <ctrlPoint0 x=\"8769.253\" y=\"1481.3043\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"10088.245\" y=\"1394.849\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3453\" layerID=\"1\" created=\"1295909615611\" x=\"8807.853\"\n        y=\"486.59375\" width=\"2027.4814\" height=\"1297.0859\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ba3bf5b37f0001017e6ab5338b9ff6a2</URIString>\n        <point1 x=\"10155.936\" y=\"1783.1797\"/>\n        <point2 x=\"8808.353\" y=\"487.09375\"/>\n        <ID1 xsi:type=\"node\">3372</ID1>\n        <ID2 xsi:type=\"node\">1901</ID2>\n        <ctrlPoint0 x=\"12256.546\" y=\"51.03253\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8812.2295\" y=\"624.50507\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3476\" label=\"GENERAL DESIGN QUESTIONS\" layerID=\"1\"\n        created=\"1296055192405\" x=\"13538.116\" y=\"311.34357\"\n        width=\"328.0\" height=\"87.0\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#B5B995</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-18</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/c2e990f77f0001013611c75558b3d989</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3480\" label=\"ScheduleMixin\" layerID=\"1\"\n        created=\"1297409383496\" x=\"7228.116\" y=\"1381.2302\" width=\"255.0\"\n        height=\"188.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/13a3d6fc7f0001017490649e5a37d165</URIString>\n        <child ID=\"4203\" label=\"schedule_timing | FLOAT\"\n            created=\"1459165978619\" x=\"34.0\" y=\"23.0\" width=\"149.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/bd122965c0a82a4a01152d0eff7dbcbe</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4202\"\n            label=\"schedule_unit | STRING:{min, h, d, w, m, y}\"\n            created=\"1459165946055\" x=\"34.0\" y=\"43.25\" width=\"244.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/bd122965c0a82a4a01152d0e8ff34d55</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4201\"\n            label=\"schedule_model | STRING:{effort, duration, length}\"\n            created=\"1459165895160\" x=\"34.0\" y=\"63.5\" width=\"287.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/bd122965c0a82a4a01152d0ee270ee7f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4265\"\n            label=\"schedule_constraint | STRING: {onstart, onend}\"\n            created=\"1471423327881\" x=\"34.0\" y=\"83.75\" width=\"270.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2cf5c0a82a4a019efdbd112abd0a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4090\" label=\"timing_resolution | TIMEDELTA\"\n            created=\"1365407705378\" x=\"34.0\" y=\"104.0\" width=\"181.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/e8a539b8648c6197658d749ac2c528ef</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4266\" label=\"least_meaningful_time_unit\"\n            created=\"1471423396444\" x=\"34.0\" y=\"124.25\" width=\"169.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2cf5c0a82a4a019efdbdb69fc72f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4091\" label=\"to_seconds\" created=\"1365407725309\"\n            x=\"34.0\" y=\"144.5\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/e8a539b8648c6197658d749a4719b74d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4267\" label=\"schedule_seconds\"\n            created=\"1471423416618\" x=\"34.0\" y=\"164.75\" width=\"119.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2cf6c0a82a4a019efdbd9323b8c2</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3482\" label=\"ScheduleMixin\" layerID=\"1\"\n        created=\"1297409383496\" x=\"7250.866\" y=\"1353.2908\" width=\"94.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/13a3d6fc7f0001017490649eaefc5577</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3493\" label=\"Hold it in StatusList.target_entity_type\"\n        layerID=\"1\" created=\"1297950196089\" x=\"14708.116\" y=\"1155.4437\"\n        width=\"313.0\" height=\"92.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#30D643</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33e78f547f0001016203171e285ee458</URIString>\n        <child ID=\"3495\"\n            label=\"Just add another parameter called &quot;taget_entity_type&quot; to the &#xa;      StatusLits object, and use it when assigning to other objects to check if it is compatible with the StatusList\"\n            created=\"1297950490161\" x=\"5.0\" y=\"23.0\" width=\"303.0\"\n            height=\"63.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/33e78f547f0001016203171e3badff02</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"text-align: center; color: #000000\" color=\"#000000\"&gt;\n      Just add another parameter called &amp;quot;taget_entity_type&amp;quot; to the StatusLits \n      object,\n    &lt;/p&gt;\n    &lt;p style=\"text-align: center; color: #000000\" color=\"#000000\"&gt;\n      and use it when assigning to other objects to check if it is compatible \n      with the StatusList\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>Just add another parameter called \"taget_entity_type\" to the \n      StatusLits object, and use it when assigning to other objects to check if it is compatible with the StatusList</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3494\" label=\"SOLUTION 2&#xa;BEST SOLUTION\" layerID=\"1\"\n        created=\"1297950196100\" x=\"14681.521\" y=\"1060.0098\"\n        width=\"123.99707\" height=\"95.93359\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33e78f557f0001016203171e4e51f849</URIString>\n        <point1 x=\"14682.0205\" y=\"1060.5098\"/>\n        <point2 x=\"14805.018\" y=\"1155.4434\"/>\n        <ID1 xsi:type=\"node\">2545</ID1>\n        <ID2 xsi:type=\"node\">3493</ID2>\n    </child>\n    <child ID=\"3501\" label=\"Tasks And TaskTypes\" layerID=\"1\"\n        created=\"1297950697096\" x=\"16212.05\" y=\"575.9103\"\n        width=\"257.33398\" height=\"84.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FCDBD9</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33e78f557f0001016203171ee3b835b9</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3523\"\n        label=\"An Asset could have an AssetType&#xa;but it shouldn't limit the availale&#xa;PipelineSteps it should be just&#xa;a template for initialization&#xa;or reuse\"\n        layerID=\"1\" created=\"1297951962490\" x=\"16241.717\" y=\"751.34357\"\n        width=\"198.0\" height=\"83.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb187f0001016203171e914499e4</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3524\" layerID=\"1\" created=\"1297951962497\" x=\"16340.217\"\n        y=\"659.4103\" width=\"1.0\" height=\"92.43329\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb187f0001016203171eb92d4790</URIString>\n        <point1 x=\"16340.717\" y=\"659.9103\"/>\n        <point2 x=\"16340.717\" y=\"751.34357\"/>\n        <ID1 xsi:type=\"node\">3501</ID1>\n        <ID2 xsi:type=\"node\">3523</ID2>\n    </child>\n    <child ID=\"3525\"\n        label=\"it should be possible&#xa;to add a new step thus a new&#xa;task to only one Asset without&#xa;creating a new AssetType&#xa;or new step\"\n        layerID=\"1\" created=\"1297952019051\" x=\"16254.717\" y=\"1044.3435\"\n        width=\"172.0\" height=\"83.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb257f0001016203171eda0f1c6e</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3526\" layerID=\"1\" created=\"1297952019059\" x=\"16340.217\"\n        y=\"991.8435\" width=\"1.0\" height=\"52.99994\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb257f0001016203171e7277c051</URIString>\n        <point1 x=\"16340.717\" y=\"992.34357\"/>\n        <point2 x=\"16340.717\" y=\"1044.3435\"/>\n        <ID1 xsi:type=\"node\">3527</ID1>\n        <ID2 xsi:type=\"node\">3525</ID2>\n    </child>\n    <child ID=\"3527\"\n        label=\"whenever the&#xa;asset is created with the&#xa;type is set, the list of tasks&#xa;also should be initialized,&#xa;with the predefined list&#xa;of tasks\"\n        layerID=\"1\" created=\"1297952051105\" x=\"16263.717\" y=\"894.34357\"\n        width=\"154.0\" height=\"98.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb257f0001016203171ef4f135a1</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3528\" layerID=\"1\" created=\"1297952051113\" x=\"16340.217\"\n        y=\"833.84357\" width=\"1.0\" height=\"61.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb257f0001016203171ec0f120f3</URIString>\n        <point1 x=\"16340.717\" y=\"834.34357\"/>\n        <point2 x=\"16340.717\" y=\"894.34357\"/>\n        <ID1 xsi:type=\"node\">3523</ID1>\n        <ID2 xsi:type=\"node\">3527</ID2>\n    </child>\n    <child ID=\"3529\"\n        label=\"TaskTemplates --> list of TaskTypes&#xa;Character --> [Model, Shading etc.]\"\n        layerID=\"1\" created=\"1297952087374\" x=\"16499.46\" y=\"924.34357\"\n        width=\"210.0\" height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb267f0001016203171e0dce531a</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3530\" layerID=\"1\" created=\"1297952087388\" x=\"16417.219\"\n        y=\"942.84357\" width=\"82.74414\" height=\"1.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb267f0001016203171e019e7b69</URIString>\n        <point1 x=\"16417.717\" y=\"943.34357\"/>\n        <point2 x=\"16499.46\" y=\"943.34357\"/>\n        <ID1 xsi:type=\"node\">3527</ID1>\n        <ID2 xsi:type=\"node\">3529</ID2>\n    </child>\n    <child ID=\"3531\" label=\"AssetBase.tasks --> list of Tasks\"\n        layerID=\"1\" created=\"1297952165157\" x=\"16500.71\" y=\"1073.5935\"\n        width=\"191.0\" height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb267f0001016203171ed809bcbd</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3532\" layerID=\"1\" created=\"1297952165165\" x=\"16426.219\"\n        y=\"1084.8738\" width=\"74.99414\" height=\"1.2172852\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb267f0001016203171ed634beb3</URIString>\n        <point1 x=\"16426.717\" y=\"1085.5911\"/>\n        <point2 x=\"16500.71\" y=\"1085.3738\"/>\n        <ID1 xsi:type=\"node\">3525</ID1>\n        <ID2 xsi:type=\"node\">3531</ID2>\n    </child>\n    <child ID=\"3533\"\n        label=\"Tasks then can have different types like:&#xa;Model&#xa;Animation&#xa;Lighting&#xa;Shading&#xa;etc.\"\n        layerID=\"1\" created=\"1297952179634\" x=\"16226.717\" y=\"1182.3435\"\n        width=\"228.0\" height=\"98.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb277f0001016203171e4fc43b50</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3534\" layerID=\"1\" created=\"1297952179642\" x=\"16340.217\"\n        y=\"1126.8435\" width=\"1.0\" height=\"56.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb277f0001016203171ed315cddc</URIString>\n        <point1 x=\"16340.717\" y=\"1127.3435\"/>\n        <point2 x=\"16340.717\" y=\"1182.3435\"/>\n        <ID1 xsi:type=\"node\">3525</ID1>\n        <ID2 xsi:type=\"node\">3533</ID2>\n    </child>\n    <child ID=\"3536\" label=\"Task.type --> TaskTypes\" layerID=\"1\"\n        created=\"1297952222946\" x=\"16527.21\" y=\"1217.0935\" width=\"172.0\"\n        height=\"28.5\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb277f0001016203171e807ae3dd</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3537\" layerID=\"1\" created=\"1297952222954\" x=\"16454.219\"\n        y=\"1230.8435\" width=\"73.49414\" height=\"1.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fbcb277f0001016203171e88a8737b</URIString>\n        <point1 x=\"16454.717\" y=\"1231.3435\"/>\n        <point2 x=\"16527.21\" y=\"1231.3435\"/>\n        <ID1 xsi:type=\"node\">3533</ID1>\n        <ID2 xsi:type=\"node\">3536</ID2>\n    </child>\n    <child ID=\"3538\" label=\"AssetTypes --> TaskTemplates\" layerID=\"1\"\n        created=\"1297952773356\" x=\"16513.46\" y=\"986.34357\" width=\"182.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/340fb3a07f0001016203171e0b84f385</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3539\" layerID=\"1\" created=\"1297952773364\" x=\"16603.96\"\n        y=\"961.84357\" width=\"1.0\" height=\"25.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/340fb3a17f0001016203171ee96598cc</URIString>\n        <point1 x=\"16604.46\" y=\"962.34357\"/>\n        <point2 x=\"16604.46\" y=\"986.34357\"/>\n        <ID1 xsi:type=\"node\">3529</ID1>\n        <ID2 xsi:type=\"node\">3538</ID2>\n    </child>\n    <child ID=\"3540\" label=\"AssetType\" layerID=\"1\"\n        created=\"1297952795403\" x=\"16783.367\" y=\"994.84357\" width=\"68.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/340fb3a17f0001016203171efaeeb79b</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3541\" label=\"TaskTemplate\" layerID=\"1\"\n        created=\"1297952801933\" x=\"16772.367\" y=\"1079.3435\" width=\"90.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/340fb3a17f0001016203171e5fdad64e</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3542\" layerID=\"1\" created=\"1297952801941\" x=\"16816.867\"\n        y=\"1017.3435\" width=\"1.0\" height=\"62.49994\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/340fb3a17f0001016203171e87530ff3</URIString>\n        <point1 x=\"16817.367\" y=\"1017.84357\"/>\n        <point2 x=\"16817.367\" y=\"1079.3435\"/>\n        <ID1 xsi:type=\"node\">3540</ID1>\n        <ID2 xsi:type=\"node\">3541</ID2>\n    </child>\n    <child ID=\"3543\" label=\"TaskTypes\" layerID=\"1\"\n        created=\"1297952806342\" x=\"16781.867\" y=\"1149.3435\" width=\"71.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/340fb3a17f0001016203171e05062db5</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3544\" layerID=\"1\" created=\"1297952806349\" x=\"16816.867\"\n        y=\"1101.8435\" width=\"1.0\" height=\"48.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/340fb3a27f0001016203171ee4b4bb51</URIString>\n        <point1 x=\"16817.367\" y=\"1102.3435\"/>\n        <point2 x=\"16817.367\" y=\"1149.3435\"/>\n        <ID1 xsi:type=\"node\">3541</ID1>\n        <ID2 xsi:type=\"node\">3543</ID2>\n    </child>\n    <child ID=\"3562\" label=\"Non-implemented attributes\" layerID=\"1\"\n        created=\"1298540076103\" x=\"9222.637\" y=\"68.42398\" width=\"300.0\"\n        height=\"33.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/570552607f0001016f0e090494a33c8f</URIString>\n        <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Non-implemented attributes\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Non-implemented attributes</label>\n    </child>\n    <child ID=\"3563\" layerID=\"1\" created=\"1298540076103\" x=\"9187.644\"\n        y=\"68.42398\" width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/570552617f0001016f0e09045f703f7f</URIString>\n        <shape xsi:type=\"ellipse\"/>\n    </child>\n    <child ID=\"3565\" label=\"EnvironmentVariables\" layerID=\"1\"\n        created=\"1299605590562\" x=\"10929.242\" y=\"110.968506\"\n        width=\"164.0\" height=\"46.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/9a259fb57f0001010772fbca6a52863b</URIString>\n        <child ID=\"3569\" label=\"stalker repository_path\"\n            created=\"1299605618563\" x=\"34.0\" y=\"23.0\" width=\"136.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9a259fb67f0001010772fbcafae74713</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3570\" label=\"POSSIBLE MIXINS\" layerID=\"1\"\n        created=\"1305333600438\" x=\"6978.449\" y=\"684.5101\"\n        width=\"501.333\" height=\"102.66669\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#B5B995</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-18</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ebf7261b7f0001012e9d478bae99d2a2</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3571\" label=\"AVAILABLE MIXINS\" layerID=\"1\"\n        created=\"1305333615676\" x=\"6940.4497\" y=\"1220.51\"\n        width=\"501.333\" height=\"102.66669\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#B5B995</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-18</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ebf7261b7f0001012e9d478b95c14742</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3605\" layerID=\"1\" created=\"1305784780169\" x=\"8776.104\"\n        y=\"751.25\" width=\"714.76465\" height=\"77.46875\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/06dc9a487f000101049ada8600a11492</URIString>\n        <point1 x=\"8776.604\" y=\"751.75\"/>\n        <point2 x=\"9490.368\" y=\"828.21875\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">2397</ID2>\n        <ctrlPoint0 x=\"8776.836\" y=\"795.92163\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9495.639\" y=\"753.4334\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3615\"\n        label=\"The duty of TypeEntities and the derived classes&#xa;are not clear enough?\"\n        layerID=\"1\" created=\"1306189397240\" x=\"17124.117\" y=\"572.84357\"\n        width=\"349.0\" height=\"96.0\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FCDBD9</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/1ef854967f000101578cef183082fb96</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3620\"\n        label=\"Enhance the design&#xa;by changign the TypeEntity&#xa;to Type and adding two attributes&#xa;like the name of the type (like Character)&#xa;and the target entity_type (like Asset)\"\n        layerID=\"1\" created=\"1306189451069\" x=\"17114.617\" y=\"753.34357\"\n        width=\"230.0\" height=\"83.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/1ef854967f000101578cef1825e67a00</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3621\" layerID=\"1\" created=\"1306189451076\" x=\"17245.574\"\n        y=\"668.34375\" width=\"34.507812\" height=\"85.5\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/1ef854967f000101578cef184d47f17e</URIString>\n        <point1 x=\"17279.582\" y=\"668.84375\"/>\n        <point2 x=\"17246.074\" y=\"753.34375\"/>\n        <ID1 xsi:type=\"node\">3615</ID1>\n        <ID2 xsi:type=\"node\">3620</ID2>\n    </child>\n    <child ID=\"3622\"\n        label=\"Liked it very much,&#xa;what could be the&#xa;implications?\"\n        layerID=\"1\" created=\"1306189507844\" x=\"17174.117\" y=\"878.34357\"\n        width=\"111.0\" height=\"53.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/1ef854967f000101578cef183f94113a</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3623\" layerID=\"1\" created=\"1306189507862\" x=\"17229.117\"\n        y=\"835.84357\" width=\"1.0\" height=\"43.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/1ef854977f000101578cef18f1f525b9</URIString>\n        <point1 x=\"17229.617\" y=\"836.34357\"/>\n        <point2 x=\"17229.617\" y=\"878.34357\"/>\n        <ID1 xsi:type=\"node\">3620</ID1>\n        <ID2 xsi:type=\"node\">3622</ID2>\n    </child>\n    <child ID=\"3624\"\n        label=\"how would you relate&#xa;them with StructureTemplates\"\n        layerID=\"1\" created=\"1306189536026\" x=\"17142.117\" y=\"961.3435\"\n        width=\"175.0\" height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/1ef854977f000101578cef1889e1b257</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3625\" layerID=\"1\" created=\"1306189536033\" x=\"17229.117\"\n        y=\"930.8435\" width=\"1.0\" height=\"30.999939\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/1ef854977f000101578cef18b1a0a07f</URIString>\n        <point1 x=\"17229.617\" y=\"931.34357\"/>\n        <point2 x=\"17229.617\" y=\"961.3435\"/>\n        <ID1 xsi:type=\"node\">3622</ID1>\n        <ID2 xsi:type=\"node\">3624</ID2>\n    </child>\n    <child ID=\"3626\"\n        label=\"StructureTemplates can also&#xa;be defined with entity_types&#xa;so one can create a&#xa;StructureTemplate with a specific&#xa;template code for Asset entity_types\"\n        layerID=\"1\" created=\"1306189558421\" x=\"17126.117\" y=\"1040.3435\"\n        width=\"207.0\" height=\"83.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/1ef854977f000101578cef18dd82dd5a</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3627\" layerID=\"1\" created=\"1306189558428\" x=\"17229.117\"\n        y=\"998.8435\" width=\"1.0\" height=\"42.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/1ef854977f000101578cef18a1f46511</URIString>\n        <point1 x=\"17229.617\" y=\"999.3435\"/>\n        <point2 x=\"17229.617\" y=\"1040.3435\"/>\n        <ID1 xsi:type=\"node\">3624</ID1>\n        <ID2 xsi:type=\"node\">3626</ID2>\n    </child>\n    <child ID=\"3634\" label=\"POSSIBLE DATABASE SCENARIOS\" layerID=\"1\"\n        created=\"1306708027747\" x=\"5165.7827\" y=\"-24.82308\"\n        width=\"512.0\" height=\"138.66667\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#B5B995</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-18</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a157f0001010892949f5a269b82</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3637\" label=\"Single Database\" layerID=\"1\"\n        created=\"1306708073182\" x=\"5057.783\" y=\"219.17693\"\n        width=\"213.3335\" height=\"85.33333\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FCDBD9</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a167f0001010892949fd04bdf89</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3638\" label=\"Per Project Database\" layerID=\"1\"\n        created=\"1306708082193\" x=\"5574.449\" y=\"220.51028\"\n        width=\"206.33398\" height=\"85.33334\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#FCDBD9</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a167f0001010892949f8dd61baf</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3639\"\n        label=\"single SQLite3 database&#xa;file on project root\"\n        layerID=\"1\" created=\"1306708117853\" x=\"5604.7827\" y=\"370.01025\"\n        width=\"149.0\" height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFD8C</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a227f0001010892949f64b52421</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3640\" layerID=\"1\" created=\"1306708117868\" x=\"5677.681\"\n        y=\"305.34375\" width=\"1.8496094\" height=\"65.15625\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a227f0001010892949ff8a4b051</URIString>\n        <point1 x=\"5678.181\" y=\"305.84375\"/>\n        <point2 x=\"5679.031\" y=\"370.0\"/>\n        <ID1 xsi:type=\"node\">3638</ID1>\n        <ID2 xsi:type=\"node\">3639</ID2>\n    </child>\n    <child ID=\"3641\" label=\"easy to backup/archive\" layerID=\"1\"\n        created=\"1306708144463\" x=\"5498.449\" y=\"474.34357\" width=\"170.0\"\n        height=\"44.5\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a237f0001010892949f22be8b46</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3642\" layerID=\"1\" created=\"1306708144493\" x=\"5602.769\"\n        y=\"407.51025\" width=\"60.088867\" height=\"67.333496\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a237f0001010892949fe0834314</URIString>\n        <point1 x=\"5662.358\" y=\"408.01025\"/>\n        <point2 x=\"5603.269\" y=\"474.34375\"/>\n        <ID1 xsi:type=\"node\">3639</ID1>\n        <ID2 xsi:type=\"node\">3641</ID2>\n    </child>\n    <child ID=\"3643\"\n        label=\"single databaes on a&#xa;dedicated database&#xa;server\"\n        layerID=\"1\" created=\"1306708158266\" x=\"5094.449\" y=\"351.34357\"\n        width=\"128.0\" height=\"53.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFD8C</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a247f0001010892949f1625e7bd</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3644\" layerID=\"1\" created=\"1306708158290\" x=\"5159.3203\"\n        y=\"304.0078\" width=\"3.4228516\" height=\"47.835938\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a247f0001010892949fd1aa4890</URIString>\n        <point1 x=\"5162.243\" y=\"304.5078\"/>\n        <point2 x=\"5159.8203\" y=\"351.34375\"/>\n        <ID1 xsi:type=\"node\">3637</ID1>\n        <ID2 xsi:type=\"node\">3643</ID2>\n    </child>\n    <child ID=\"3645\" label=\"fast\" layerID=\"1\" created=\"1306708173021\"\n        x=\"4943.449\" y=\"447.34357\" width=\"68.0\" height=\"32.5\"\n        strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a257f0001010892949f7cb2b9f1</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3646\" layerID=\"1\" created=\"1306708173028\" x=\"5006.801\"\n        y=\"403.56958\" width=\"96.79053\" height=\"46.381348\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a257f0001010892949f67f3b25a</URIString>\n        <point1 x=\"5103.092\" y=\"404.06958\"/>\n        <point2 x=\"5007.3013\" y=\"449.45093\"/>\n        <ID1 xsi:type=\"node\">3643</ID1>\n        <ID2 xsi:type=\"node\">3645</ID2>\n    </child>\n    <child ID=\"3647\" label=\"error prone\" layerID=\"1\"\n        created=\"1306708179566\" x=\"5019.449\" y=\"522.34357\" width=\"76.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a267f0001010892949fcba370c0</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3648\" layerID=\"1\" created=\"1306708179577\" x=\"5064.3945\"\n        y=\"403.84424\" width=\"77.39746\" height=\"118.99951\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a267f0001010892949f5c46788f</URIString>\n        <point1 x=\"5141.292\" y=\"404.34424\"/>\n        <point2 x=\"5064.8945\" y=\"522.34375\"/>\n        <ID1 xsi:type=\"node\">3643</ID1>\n        <ID2 xsi:type=\"node\">3647</ID2>\n    </child>\n    <child ID=\"3649\" label=\"database lives with&#xa;the studio itself\"\n        layerID=\"1\" created=\"1306708184176\" x=\"5121.449\" y=\"517.34357\"\n        width=\"120.0\" height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a277f0001010892949f8996f92c</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3650\" layerID=\"1\" created=\"1306708184185\" x=\"5161.795\"\n        y=\"403.84375\" width=\"17.396973\" height=\"114.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a277f0001010892949f2fb8309b</URIString>\n        <point1 x=\"5162.295\" y=\"404.34375\"/>\n        <point2 x=\"5178.692\" y=\"517.34375\"/>\n        <ID1 xsi:type=\"node\">3643</ID1>\n        <ID2 xsi:type=\"node\">3649</ID2>\n    </child>\n    <child ID=\"3651\" label=\"hard to&#xa;backup/archive\" layerID=\"1\"\n        created=\"1306708203854\" x=\"5253.449\" y=\"472.34357\" width=\"98.0\"\n        height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a287f0001010892949f44fa5d31</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3652\" layerID=\"1\" created=\"1306708203862\" x=\"5191.5703\"\n        y=\"403.8435\" width=\"87.27344\" height=\"69.000244\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a287f0001010892949fa01e4444</URIString>\n        <point1 x=\"5192.0703\" y=\"404.3435\"/>\n        <point2 x=\"5278.3438\" y=\"472.34375\"/>\n        <ID1 xsi:type=\"node\">3643</ID1>\n        <ID2 xsi:type=\"node\">3651</ID2>\n    </child>\n    <child ID=\"3653\" label=\"easy to manage\" layerID=\"1\"\n        created=\"1306708256008\" x=\"5711.449\" y=\"512.34357\" width=\"101.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a297f0001010892949ff4d93d34</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3654\" layerID=\"1\" created=\"1306708256016\" x=\"5690.4316\"\n        y=\"407.50977\" width=\"64.967285\" height=\"105.333984\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a297f0001010892949fa023f701</URIString>\n        <point1 x=\"5690.9316\" y=\"408.00977\"/>\n        <point2 x=\"5754.899\" y=\"512.34375\"/>\n        <ID1 xsi:type=\"node\">3639</ID1>\n        <ID2 xsi:type=\"node\">3653</ID2>\n    </child>\n    <child ID=\"3655\" label=\"more open to&#xa;new features\" layerID=\"1\"\n        created=\"1306708263231\" x=\"5832.449\" y=\"446.34357\" width=\"86.0\"\n        height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a2a7f0001010892949f44b54c39</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3656\" layerID=\"1\" created=\"1306708263250\" x=\"5727.6104\"\n        y=\"407.51025\" width=\"108.01221\" height=\"42.641113\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a2a7f0001010892949fb8a36c74</URIString>\n        <point1 x=\"5728.11\" y=\"408.01025\"/>\n        <point2 x=\"5835.122\" y=\"449.65137\"/>\n        <ID1 xsi:type=\"node\">3639</ID1>\n        <ID2 xsi:type=\"node\">3655</ID2>\n    </child>\n    <child ID=\"3657\"\n        label=\"needs a second&#xa;database for the&#xa;persistent data&#xa;like users,&#xa;departments&#xa;etc.\"\n        layerID=\"1\" created=\"1306708290786\" x=\"5846.449\" y=\"301.34357\"\n        width=\"104.0\" height=\"98.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#9DDB53</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a2a7f0001010892949febaf0c39</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3658\" layerID=\"1\" created=\"1306708290794\" x=\"5752.4297\"\n        y=\"359.0177\" width=\"94.52002\" height=\"17.49945\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3de32a2b7f0001010892949f2e76a112</URIString>\n        <point1 x=\"5752.929\" y=\"376.01715\"/>\n        <point2 x=\"5846.449\" y=\"359.5177\"/>\n        <ID1 xsi:type=\"node\">3639</ID1>\n        <ID2 xsi:type=\"node\">3657</ID2>\n    </child>\n    <child ID=\"3659\" layerID=\"1\" created=\"1307711459168\" x=\"8001.266\"\n        y=\"751.2031\" width=\"763.2661\" height=\"596.7656\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/79af23367f0001017997f53852f91300</URIString>\n        <point1 x=\"8764.032\" y=\"751.7031\"/>\n        <point2 x=\"8001.766\" y=\"1347.4688\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1402</ID2>\n        <ctrlPoint0 x=\"8666.721\" y=\"1320.9504\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"7998.3296\" y=\"1226.3097\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3669\" label=\"group_acls\" layerID=\"1\"\n        created=\"1307720099855\" x=\"7704.449\" y=\"1276.6936\"\n        width=\"148.95\" height=\"96.2\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7a3132fe7f0001017997f538a8ecadd7</URIString>\n        <child ID=\"3670\" label=\"gid | INTEGER\" created=\"1307720099856\"\n            x=\"48.85\" y=\"38.85\" width=\"90.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7a3132ff7f0001017997f538ca751ab1</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3982\" label=\"acl_id | INTEGER\"\n            created=\"1337334133016\" x=\"48.85\" y=\"59.1\" width=\"106.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5f58e5f87f0001011df07586553401a4</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3672\" layerID=\"1\" created=\"1307720106243\" x=\"7847.4253\"\n        y=\"1316.4622\" width=\"88.82666\" height=\"68.62268\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7a31df207f0001017997f538762e76d7</URIString>\n        <point1 x=\"7847.9253\" y=\"1316.9622\"/>\n        <point2 x=\"7935.752\" y=\"1384.5848\"/>\n        <ID1 xsi:type=\"node\">3669</ID1>\n        <ID2 xsi:type=\"node\">1402</ID2>\n        <ctrlPoint0 x=\"7887.2637\" y=\"1312.4973\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"7869.869\" y=\"1369.6831\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3673\" layerID=\"1\" created=\"1307720113696\" x=\"7833.7964\"\n        y=\"1351.625\" width=\"102.45557\" height=\"63.256104\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/7a31df207f0001017997f538fcd5851b</URIString>\n        <point1 x=\"7834.2964\" y=\"1352.125\"/>\n        <point2 x=\"7935.752\" y=\"1411.9473\"/>\n        <ID1 xsi:type=\"node\">3669</ID1>\n        <ID2 xsi:type=\"node\">1402</ID2>\n        <ctrlPoint0 x=\"7869.869\" y=\"1369.6831\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"7852.475\" y=\"1426.8691\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3689\" label=\"TaskDependRelation\" layerID=\"1\"\n        created=\"1307887603557\" x=\"11112.949\" y=\"110.968506\"\n        width=\"180.0\" height=\"139.25\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/842e31877f0001015d3258770684f65b</URIString>\n        <child ID=\"3690\" label=\"task | ONE | TASK\"\n            created=\"1307887603557\" x=\"34.0\" y=\"23.0\" width=\"108.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/842e31877f0001015d32587797aa8b4f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3691\" label=\"depends_on | MANY | TASK\"\n            created=\"1307887603558\" x=\"34.0\" y=\"43.25\" width=\"140.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/842e31877f0001015d32587701245b1b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3692\" label=\"type | ENUM\" created=\"1307887603558\"\n            x=\"34.0\" y=\"63.5\" width=\"77.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/842e31887f0001015d325877a276acf6</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3693\" label=\"lag | TIMEDELTA\" created=\"1307887603558\"\n            x=\"34.0\" y=\"83.75\" width=\"102.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/842e31887f0001015d3258776ba432ce</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3694\" created=\"1307887603558\" x=\"34.0\" y=\"104.0\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/842e31887f0001015d325877eaa6dd71</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3695\"\n        label=\"type Enums:&#xa;0: start_start&#xa;1: start_end&#xa;2: end_to_end\"\n        layerID=\"1\" created=\"1307887603557\" x=\"11157.449\" y=\"262.5935\"\n        width=\"91.0\" height=\"68.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#83CEFF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/842e31887f0001015d325877f78ac9f5</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3696\" layerID=\"1\" created=\"1307887603557\" x=\"11202.449\"\n        y=\"249.7185\" width=\"1.0\" height=\"13.375\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/842e31887f0001015d3258774eee4aba</URIString>\n        <point1 x=\"11202.949\" y=\"250.2185\"/>\n        <point2 x=\"11202.949\" y=\"262.5935\"/>\n        <ID1 xsi:type=\"node\">3689</ID1>\n        <ID2 xsi:type=\"node\">3695</ID2>\n    </child>\n    <child ID=\"3707\" label=\"ResourceData\" layerID=\"1\"\n        created=\"1308436257183\" x=\"11313.156\" y=\"110.968506\"\n        width=\"158.25\" height=\"86.75\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a4e147e67f0001015c382de564be1104</URIString>\n        <child ID=\"3708\" label=\"allocate | MANY | USER\"\n            created=\"1308436257184\" x=\"34.0\" y=\"23.0\" width=\"138.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a4e147e77f0001015c382de55503d112</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3709\" label=\"alternatives | MANY | USER\"\n            created=\"1308436257184\" x=\"34.0\" y=\"43.25\" width=\"158.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a4e147e87f0001015c382de59322ab1e</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3710\" label=\"load_condition | ENUM\"\n            created=\"1308436257184\" x=\"34.0\" y=\"63.5\" width=\"136.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a4e147e87f0001015c382de54a61be40</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3718\"\n        label=\"Turn the status&#xa;&quot;Archived - ARCH&quot; for&#xa;archived projects.\"\n        layerID=\"1\" created=\"1309124113512\" x=\"8041.093\" y=\"2868.3438\"\n        width=\"134.0\" height=\"53.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#83CEFF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/cde1332d7f0001011f758c30b7215c18</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3719\" layerID=\"1\" created=\"1309124113529\" x=\"8119.6025\"\n        y=\"2295.3281\" width=\"260.4629\" height=\"573.5156\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/cde1332e7f0001011f758c30a99bec06</URIString>\n        <point1 x=\"8379.565\" y=\"2295.8281\"/>\n        <point2 x=\"8120.1025\" y=\"2868.3438\"/>\n        <ID1 xsi:type=\"node\">1166</ID1>\n        <ID2 xsi:type=\"node\">3718</ID2>\n    </child>\n    <child ID=\"3720\" label=\"maya file&#xa;(ma)\" layerID=\"1\"\n        created=\"1309208518501\" x=\"7526.949\" y=\"2236.8435\" width=\"60.0\"\n        height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/d2e9e3e57f0001011f758c305fb99f9c</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3721\" label=\"can have other&#xa;ma files\" layerID=\"1\"\n        created=\"1309208528127\" x=\"7394.949\" y=\"2335.8435\" width=\"91.0\"\n        height=\"38.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/d2e9e3e57f0001011f758c30c1da54d4</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3723\"\n        label=\"can have photoshop&#xa;illustrasions&#xa;(Asset)\"\n        layerID=\"1\" created=\"1309208538064\" x=\"7495.949\" y=\"2351.8435\"\n        width=\"122.0\" height=\"53.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/d2e9e3e57f0001011f758c309827a705</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3725\"\n        label=\"can be exported&#xa;to other formats&#xa;like&#xa;fbx, obj\"\n        layerID=\"1\" created=\"1309208570727\" x=\"7634.449\" y=\"2310.3435\"\n        width=\"98.0\" height=\"68.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/d2e9e3e57f0001011f758c30cd7fa2ed</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3726\" layerID=\"1\" created=\"1309208570736\" x=\"7581.4316\"\n        y=\"2272.822\" width=\"57.290527\" height=\"40.38086\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/d2e9e3e67f0001011f758c30307e5d6e</URIString>\n        <point1 x=\"7581.932\" y=\"2273.322\"/>\n        <point2 x=\"7638.2227\" y=\"2312.703\"/>\n        <ID1 xsi:type=\"node\">3720</ID1>\n        <ID2 xsi:type=\"node\">3725</ID2>\n    </child>\n    <child ID=\"3727\" layerID=\"1\" created=\"1309208576692\" x=\"7556.449\"\n        y=\"2274.3435\" width=\"1.0\" height=\"78.0\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/d2e9e3e67f0001011f758c30a9bc47e0</URIString>\n        <point1 x=\"7556.949\" y=\"2274.8435\"/>\n        <point2 x=\"7556.949\" y=\"2351.8435\"/>\n        <ID1 xsi:type=\"node\">3720</ID1>\n        <ID2 xsi:type=\"node\">3723</ID2>\n    </child>\n    <child ID=\"3728\" layerID=\"1\" created=\"1309208578201\" x=\"7462.3076\"\n        y=\"2273.9585\" width=\"73.23633\" height=\"62.385254\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/d2e9e3e67f0001011f758c30f0fdbd62</URIString>\n        <point1 x=\"7535.044\" y=\"2274.4585\"/>\n        <point2 x=\"7462.8076\" y=\"2335.8438\"/>\n        <ID1 xsi:type=\"node\">3720</ID1>\n        <ID2 xsi:type=\"node\">3721</ID2>\n    </child>\n    <child ID=\"3733\" label=\"TaskMixin\" layerID=\"1\"\n        created=\"1309652883304\" x=\"6843.949\" y=\"2276.8435\" width=\"93.0\"\n        height=\"66.5\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ed9b76207f0001014f9078ed4e765585</URIString>\n        <child ID=\"3735\" label=\"project\" created=\"1309652905862\"\n            x=\"34.0\" y=\"23.0\" width=\"48.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/ed9b76207f0001014f9078ed1d81a9f5</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3734\" label=\"tasks\" created=\"1309652897029\" x=\"34.0\"\n            y=\"43.25\" width=\"41.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/ed9b76217f0001014f9078ed56d6b831</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3736\" label=\"Task\" layerID=\"1\" created=\"1309652914965\"\n        x=\"7079.949\" y=\"2282.5935\" width=\"78.0\" height=\"46.25\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ed9b76217f0001014f9078ede683b13e</URIString>\n        <child ID=\"3737\" label=\"task_of\" created=\"1309652914966\"\n            x=\"34.0\" y=\"23.0\" width=\"51.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/ed9b76217f0001014f9078ed28a8095a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3739\" label=\"update\" layerID=\"1\" created=\"1309652940549\"\n        x=\"6908.1416\" y=\"2285.199\" width=\"210.33057\" height=\"46.20288\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ed9b76217f0001014f9078edc617eb94</URIString>\n        <point1 x=\"7117.972\" y=\"2306.5593\"/>\n        <point2 x=\"6908.6416\" y=\"2330.1287\"/>\n        <ID1 xsi:type=\"node\">3737</ID1>\n        <ID2 xsi:type=\"node\">3734</ID2>\n        <ctrlPoint0 x=\"6996.421\" y=\"2244.9138\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"7021.6943\" y=\"2340.5369\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3740\" label=\"update\" layerID=\"1\" created=\"1309652952811\"\n        x=\"6967.7314\" y=\"2322.3438\" width=\"166.38525\" height=\"124.60376\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ed9b76227f0001014f9078ede397f58b</URIString>\n        <point1 x=\"6968.2314\" y=\"2443.6016\"/>\n        <point2 x=\"7133.6167\" y=\"2322.8438\"/>\n        <ID1 xsi:type=\"node\">3741</ID1>\n        <ID2 xsi:type=\"node\">3737</ID2>\n        <ctrlPoint0 x=\"7021.1436\" y=\"2455.0156\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"7140.914\" y=\"2438.8425\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3741\" label=\"ValidatedList\" layerID=\"1\"\n        created=\"1309652986125\" x=\"6888.949\" y=\"2423.8435\" width=\"82.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FDE888</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ed9b76227f0001014f9078ed41e7d3ff</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3742\" layerID=\"1\" created=\"1309652986128\" x=\"6890.7695\"\n        y=\"2336.8438\" width=\"35.59912\" height=\"87.5\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/ed9b76227f0001014f9078ed447ef19a</URIString>\n        <point1 x=\"6892.318\" y=\"2337.3438\"/>\n        <point2 x=\"6925.8687\" y=\"2423.8438\"/>\n        <ID1 xsi:type=\"node\">3734</ID1>\n        <ID2 xsi:type=\"node\">3741</ID2>\n        <ctrlPoint0 x=\"6886.4326\" y=\"2387.7788\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"6906.3545\" y=\"2368.8518\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3745\" layerID=\"1\" created=\"1309657617797\" x=\"9187.644\"\n        y=\"35.02394\" width=\"11.0\" height=\"22.0\" strokeWidth=\"1.0\"\n        autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/edafea3d7f0001014f9078ed989a19b5</URIString>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3746\" label=\"Declarative SOM (Completed with Tests)\"\n        layerID=\"1\" created=\"1309657617797\" x=\"9222.637\" y=\"37.52394\"\n        width=\"300.0\" height=\"18.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/edafea3d7f0001014f9078edca304b53</URIString>\n        <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Declarative SOM (Completed with Tests)\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Declarative SOM (Completed with Tests)</label>\n    </child>\n    <child ID=\"3784\" layerID=\"1\" created=\"1310511051896\" x=\"9189.595\"\n        y=\"323.87396\" width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#AF55F4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/208db6a77f000101264d107e132cc774</URIString>\n        <shape xsi:type=\"diamond\"/>\n    </child>\n    <child ID=\"3785\" label=\"strictly_typed\" layerID=\"1\"\n        created=\"1310511081221\" x=\"9222.637\" y=\"325.37393\" width=\"300.0\"\n        height=\"17.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/208db6a77f000101264d107ea800a149</URIString>\n        <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      strictly_typed\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>strictly_typed</label>\n    </child>\n    <child ID=\"3793\" label=\"ProjectMixin\" layerID=\"1\"\n        created=\"1313059262614\" x=\"7503.245\" y=\"1381.2302\"\n        width=\"150.75\" height=\"46.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b8701a037f00010133854518f97ed189</URIString>\n        <child ID=\"3794\" label=\"project | ONE | PROJECT\"\n            created=\"1313059262615\" x=\"34.0\" y=\"23.0\" width=\"148.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b8701a047f000101338545189002cbe7</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3797\" label=\"ProjectMixin\" layerID=\"1\"\n        created=\"1313059299166\" x=\"7529.449\" y=\"1353.2908\" width=\"82.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b8701a047f00010133854518c82767d8</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3804\" layerID=\"1\" created=\"1315128486050\" x=\"9187.644\"\n        y=\"4.4239693\" width=\"11.0\" height=\"22.0\" strokeWidth=\"1.0\"\n        autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#D4FF00</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33c552027f0001017e6d32099d199fb8</URIString>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3805\"\n        label=\"Declarative SOM (Started or Partially moved)\" layerID=\"1\"\n        created=\"1315128486050\" x=\"9222.637\" y=\"6.9239655\" width=\"300.0\"\n        height=\"18.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33c552037f0001017e6d320901980c9c</URIString>\n        <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Declarative SOM (Started or Partially moved)\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Declarative SOM (Started or Partially moved)</label>\n    </child>\n    <child ID=\"3807\" layerID=\"1\" created=\"1315218792592\" x=\"8477.472\"\n        y=\"751.21094\" width=\"314.4541\" height=\"1107.5391\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3ed254137f0001017e6d320990e92d47</URIString>\n        <point1 x=\"8781.481\" y=\"751.71094\"/>\n        <point2 x=\"8477.972\" y=\"1858.25\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">1166</ID2>\n        <ctrlPoint0 x=\"8857.064\" y=\"1778.6975\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8477.463\" y=\"1716.7916\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3808\" layerID=\"1\" created=\"1315218794336\" x=\"8588.786\"\n        y=\"2225.625\" width=\"169.15723\" height=\"250.15625\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3ed254137f0001017e6d3209f4c83f0b</URIString>\n        <point1 x=\"8757.443\" y=\"2226.125\"/>\n        <point2 x=\"8589.286\" y=\"2475.2812\"/>\n        <ID1 xsi:type=\"node\">1205</ID1>\n        <ID2 xsi:type=\"node\">1176</ID2>\n        <ctrlPoint0 x=\"8759.088\" y=\"2424.922\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8592.151\" y=\"2380.4355\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3809\" layerID=\"1\" created=\"1315218796042\" x=\"8755.995\"\n        y=\"2225.75\" width=\"39.566406\" height=\"250.03125\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3ed254137f0001017e6d32091356ed17</URIString>\n        <point1 x=\"8756.495\" y=\"2226.25\"/>\n        <point2 x=\"8795.062\" y=\"2475.2812\"/>\n        <ID1 xsi:type=\"node\">1205</ID1>\n        <ID2 xsi:type=\"node\">1185</ID2>\n        <ctrlPoint0 x=\"8757.103\" y=\"2421.8352\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8789.982\" y=\"2369.506\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3810\" layerID=\"1\" created=\"1315218797063\" x=\"8757.566\"\n        y=\"2225.5625\" width=\"251.97852\" height=\"250.21484\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3ed254147f0001017e6d32092876fa0e</URIString>\n        <point1 x=\"8758.066\" y=\"2226.0625\"/>\n        <point2 x=\"9009.045\" y=\"2475.2773\"/>\n        <ID1 xsi:type=\"node\">1205</ID1>\n        <ID2 xsi:type=\"node\">1088</ID2>\n        <ctrlPoint0 x=\"8760.34\" y=\"2421.2244\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8998.261\" y=\"2416.4392\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3815\" label=\"Notifications\" layerID=\"1\"\n        created=\"1316728436014\" x=\"9564.924\" y=\"1249.1434\" width=\"165.0\"\n        height=\"86.75\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/9323191a7f0001010820c09b268bea70</URIString>\n        <child ID=\"3817\" label=\"user | ONE | USER\"\n            created=\"1316728436017\" x=\"34.0\" y=\"23.0\" width=\"114.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9323191c7f0001010820c09b48ba2648</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3818\" label=\"entity | ONE | SIMPLEENTITY\"\n            created=\"1316728436018\" x=\"34.0\" y=\"43.25\" width=\"167.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9323191c7f0001010820c09b3fdbd157</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3821\" label=\"action | ENUM\" created=\"1316728511464\"\n            x=\"34.0\" y=\"63.5\" width=\"88.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/9323191d7f0001010820c09b3d11ba97</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3820\" layerID=\"1\" created=\"1316728444997\" x=\"8776.768\"\n        y=\"751.1875\" width=\"858.1592\" height=\"498.45508\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/9323191e7f0001010820c09b0d9f2466</URIString>\n        <point1 x=\"8777.268\" y=\"751.6875\"/>\n        <point2 x=\"9634.427\" y=\"1249.1426\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">3815</ID2>\n        <ctrlPoint0 x=\"8785.653\" y=\"1327.8733\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9598.088\" y=\"1127.8678\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3822\" label=\"TargetEntityTypeMixin\" layerID=\"1\"\n        created=\"1316972313110\" x=\"6829.116\" y=\"1680.6133\" width=\"164.0\"\n        height=\"46.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a1ac43f57f0001010820c09b897a0e17</URIString>\n        <child ID=\"3823\" label=\"target_entity_type | STRING\"\n            created=\"1316972313111\" x=\"34.0\" y=\"23.0\" width=\"160.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a1ac43f67f0001010820c09ba9d40681</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3824\" label=\"TargetEntityTypeMixin\" layerID=\"1\"\n        created=\"1316972313110\" x=\"6843.116\" y=\"1652.6738\" width=\"136.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a1ac43f77f0001010820c09b3959f466</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3834\" label=\"Ticket\" layerID=\"1\" created=\"1336638487865\"\n        x=\"11373.065\" y=\"428.9685\" width=\"177.0\" height=\"167.75\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d207f000101663ffd1486c76cc8</URIString>\n        <child ID=\"3835\" label=\"StatusMixin\" created=\"1336638487866\"\n            x=\"34.0\" y=\"23.0\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d217f000101663ffd14e3cb886c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3836\" label=\"project | ONE | PROJECT\"\n            created=\"1336638487866\" x=\"34.0\" y=\"43.25\" width=\"148.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d237f000101663ffd14a0656966</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3837\" label=\"assigned_to | ONE | USER\"\n            created=\"1336638487867\" x=\"34.0\" y=\"63.5\" width=\"158.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d247f000101663ffd1482254dd5</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3838\" label=\"releases | MANY | RELEASE\"\n            created=\"1336638487867\" x=\"34.0\" y=\"83.75\" width=\"165.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d267f000101663ffd1401ed83d8</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3839\" label=\"related_tickets | MANY | TICKET\"\n            created=\"1336638487867\" x=\"34.0\" y=\"104.0\" width=\"183.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d267f000101663ffd140a50984d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3840\" label=\"revisions | MANY | REVISION\"\n            created=\"1336638487868\" x=\"34.0\" y=\"124.25\" width=\"167.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d277f000101663ffd14c571ff64</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3841\" label=\"priority | INTEGER\"\n            created=\"1336638487868\" x=\"34.0\" y=\"144.5\" width=\"109.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d287f000101663ffd140a12fddf</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3842\" label=\"Release\" layerID=\"1\" created=\"1336638487865\"\n        x=\"11142.399\" y=\"428.9685\" width=\"150.75\" height=\"86.75\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d297f000101663ffd14556b3698</URIString>\n        <child ID=\"3843\" label=\"StatusMixin\" created=\"1336638487869\"\n            x=\"34.0\" y=\"23.0\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d2a7f000101663ffd14e671d93e</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3844\" label=\"project | ONE | PROJECT\"\n            created=\"1336638487869\" x=\"34.0\" y=\"43.25\" width=\"148.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d2b7f000101663ffd14fa90b1e9</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3845\" label=\"tickets | MANY | TICKET\"\n            created=\"1336638487870\" x=\"34.0\" y=\"63.5\" width=\"138.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d2d7f000101663ffd144c7d1f8a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3846\" label=\"Revision\" layerID=\"1\"\n        created=\"1336638487865\" x=\"11596.315\" y=\"428.9685\"\n        width=\"198.75\" height=\"107.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#ECFFD4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d2e7f000101663ffd1499fddc91</URIString>\n        <child ID=\"3847\" label=\"StatusMixin\" created=\"1336638487870\"\n            x=\"34.0\" y=\"23.0\" width=\"78.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d2f7f000101663ffd1440611e1b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3848\" label=\"project | ONE | PROJECT\"\n            created=\"1336638487871\" x=\"34.0\" y=\"43.25\" width=\"148.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d307f000101663ffd14fe9a5dcc</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3849\" label=\"tickets | MANY | TICKET\"\n            created=\"1336638487871\" x=\"34.0\" y=\"63.5\" width=\"138.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d317f000101663ffd14fbffc76b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3850\" label=\"related_revisions | MANY | REVISION\"\n            created=\"1336638487872\" x=\"34.0\" y=\"83.75\" width=\"212.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FDE888</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d317f000101663ffd149a6733df</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3851\" label=\"revision_revisions\" layerID=\"1\"\n        created=\"1336638487865\" x=\"11854.948\" y=\"537.2517\"\n        width=\"214.55\" height=\"107.8\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d337f000101663ffd141a200f3e</URIString>\n        <child ID=\"3852\" label=\"source_revision_id | INTEGER\"\n            created=\"1336638487872\" x=\"54.65\" y=\"44.65\" width=\"178.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d337f000101663ffd14d4a75b5e</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3853\" label=\"target_revision_id | INTEGER\"\n            created=\"1336638487873\" x=\"54.65\" y=\"64.9\" width=\"171.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d347f000101663ffd146f3c629b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3854\" layerID=\"1\" created=\"1336638487865\" x=\"11794.565\"\n        y=\"483.01587\" width=\"122.3418\" height=\"54.736084\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d367f000101663ffd14d3f7c399</URIString>\n        <point1 x=\"11795.065\" y=\"483.51587\"/>\n        <point2 x=\"11916.407\" y=\"537.25195\"/>\n        <ID1 xsi:type=\"node\">3846</ID1>\n        <ID2 xsi:type=\"node\">3851</ID2>\n        <ctrlPoint0 x=\"11872.342\" y=\"484.33032\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"11879.569\" y=\"493.91528\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3855\" label=\"revision_tickets\" layerID=\"1\"\n        created=\"1336638487865\" x=\"11662.016\" y=\"668.5852\" width=\"174.3\"\n        height=\"101.3\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d377f000101663ffd140888ec46</URIString>\n        <child ID=\"3856\" label=\"revision_id | INTEGER\"\n            created=\"1336638487874\" x=\"51.4\" y=\"41.4\" width=\"133.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d397f000101663ffd14395cbf10</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3857\" label=\"ticket_id | INTEGER\"\n            created=\"1336638487874\" x=\"51.4\" y=\"61.65\" width=\"118.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d3a7f000101663ffd141c8fb707</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3858\" layerID=\"1\" created=\"1336638487865\" x=\"11757.042\"\n        y=\"535.46875\" width=\"113.18359\" height=\"39.73584\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d3b7f000101663ffd142dce9439</URIString>\n        <point1 x=\"11757.542\" y=\"535.96875\"/>\n        <point2 x=\"11869.726\" y=\"574.7046\"/>\n        <ID1 xsi:type=\"node\">3846</ID1>\n        <ID2 xsi:type=\"node\">3851</ID2>\n        <ctrlPoint0 x=\"11791.079\" y=\"564.9773\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"11804.223\" y=\"563.0574\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3859\" layerID=\"1\" created=\"1336638487865\" x=\"11700.391\"\n        y=\"535.46875\" width=\"21.248047\" height=\"133.61719\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d3c7f000101663ffd148f53f000</URIString>\n        <point1 x=\"11702.083\" y=\"535.96875\"/>\n        <point2 x=\"11721.139\" y=\"668.58594\"/>\n        <ID1 xsi:type=\"node\">3846</ID1>\n        <ID2 xsi:type=\"node\">3855</ID2>\n        <ctrlPoint0 x=\"11711.211\" y=\"612.3513\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"11683.425\" y=\"600.42896\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3860\" layerID=\"1\" created=\"1336638487865\" x=\"11549.565\"\n        y=\"552.9888\" width=\"135.16895\" height=\"138.14209\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d3c7f000101663ffd14e4057a6e</URIString>\n        <point1 x=\"11550.065\" y=\"553.4888\"/>\n        <point2 x=\"11684.234\" y=\"690.63086\"/>\n        <ID1 xsi:type=\"node\">3834</ID1>\n        <ID2 xsi:type=\"node\">3855</ID2>\n        <ctrlPoint0 x=\"11660.66\" y=\"604.282\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"11616.4\" y=\"660.7468\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3861\" label=\"ticket_tickets\" layerID=\"1\"\n        created=\"1336638487865\" x=\"11390.865\" y=\"715.41846\"\n        width=\"200.8\" height=\"105.3\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d3d7f000101663ffd146e1c7fd0</URIString>\n        <child ID=\"3862\" label=\"source_ticket_id | INTEGER\"\n            created=\"1336638487876\" x=\"53.4\" y=\"43.4\" width=\"163.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d3e7f000101663ffd1434ff22cf</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3863\" label=\"target_ticket_id | INTEGER\"\n            created=\"1336638487877\" x=\"53.4\" y=\"63.65\" width=\"156.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d3f7f000101663ffd14ab17b17b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3864\" layerID=\"1\" created=\"1336638487865\" x=\"11445.79\"\n        y=\"596.21875\" width=\"33.62207\" height=\"119.69922\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d3f7f000101663ffd14de61dc8e</URIString>\n        <point1 x=\"11448.3\" y=\"596.71875\"/>\n        <point2 x=\"11478.912\" y=\"715.41797\"/>\n        <ID1 xsi:type=\"node\">3834</ID1>\n        <ID2 xsi:type=\"node\">3861</ID2>\n        <ctrlPoint0 x=\"11438.881\" y=\"656.27026\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"11463.782\" y=\"650.93774\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3865\" layerID=\"1\" created=\"1336638487865\" x=\"11500.055\"\n        y=\"596.21875\" width=\"23.222656\" height=\"119.69922\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d407f000101663ffd14dc0170fa</URIString>\n        <point1 x=\"11507.829\" y=\"596.71875\"/>\n        <point2 x=\"11500.555\" y=\"715.41797\"/>\n        <ID1 xsi:type=\"node\">3834</ID1>\n        <ID2 xsi:type=\"node\">3861</ID2>\n        <ctrlPoint0 x=\"11540.185\" y=\"655.3779\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"11512.05\" y=\"650.25964\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3866\" label=\"release_tickets\" layerID=\"1\"\n        created=\"1336638487865\" x=\"11143.207\" y=\"643.9021\"\n        width=\"173.25\" height=\"101.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d417f000101663ffd14c9ff9af0</URIString>\n        <child ID=\"3867\" label=\"release_id | INTEGER\"\n            created=\"1336638487878\" x=\"51.25\" y=\"41.25\" width=\"132.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d427f000101663ffd144441696d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3868\" label=\"ticket_id | INTEGER\"\n            created=\"1336638487879\" x=\"51.25\" y=\"61.5\" width=\"118.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d427f000101663ffd14b37b7d18</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3869\" layerID=\"1\" created=\"1336638487865\" x=\"11206.082\"\n        y=\"515.21875\" width=\"36.143555\" height=\"129.1836\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d447f000101663ffd14034c742d</URIString>\n        <point1 x=\"11237.18\" y=\"515.71875\"/>\n        <point2 x=\"11211.908\" y=\"643.90234\"/>\n        <ID1 xsi:type=\"node\">3842</ID1>\n        <ID2 xsi:type=\"node\">3866</ID2>\n        <ctrlPoint0 x=\"11260.461\" y=\"567.75757\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"11186.25\" y=\"571.6116\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3879\" label=\"Ticket Statuses (StatusList)\" layerID=\"1\"\n        created=\"1336638980471\" x=\"10008.482\" y=\"938.01013\"\n        width=\"197.0\" height=\"127.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#83CEFF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d467f000101663ffd14b3fb0bee</URIString>\n        <child ID=\"3890\" label=\"New\" created=\"1336639100878\" x=\"34.0\"\n            y=\"23.0\" width=\"36.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d467f000101663ffd14eb6f869b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3881\" label=\"ReOpened\" created=\"1336638989263\"\n            x=\"34.0\" y=\"43.25\" width=\"70.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d477f000101663ffd143040c34c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3884\" label=\"Assigned\" created=\"1336639009832\"\n            x=\"34.0\" y=\"63.5\" width=\"64.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d477f000101663ffd14ebf5331e</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3886\" label=\"Accepted\" created=\"1336639027396\"\n            x=\"34.0\" y=\"83.75\" width=\"65.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d497f000101663ffd145356577c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3928\" label=\"Closed\" created=\"1336649098209\" x=\"34.0\"\n            y=\"104.0\" width=\"50.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3680755a7f000101663ffd145d8f6033</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3880\" layerID=\"1\" created=\"1336638980480\" x=\"9957.532\"\n        y=\"1064.7598\" width=\"76.15234\" height=\"65.79199\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d4a7f000101663ffd14b7708354</URIString>\n        <point1 x=\"9958.032\" y=\"1130.0518\"/>\n        <point2 x=\"10033.185\" y=\"1065.2598\"/>\n        <ID1 xsi:type=\"node\">3345</ID1>\n        <ID2 xsi:type=\"node\">3879</ID2>\n    </child>\n    <child ID=\"3893\" label=\"Ticket Types (Type)\" layerID=\"1\"\n        created=\"1336639200821\" x=\"10062.615\" y=\"1091.0936\"\n        width=\"149.0\" height=\"66.5\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#8AEE95</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d4b7f000101663ffd141146400f</URIString>\n        <child ID=\"3894\" label=\"Defect\" created=\"1336639200821\" x=\"34.0\"\n            y=\"23.0\" width=\"48.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#8AEE95</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d4b7f000101663ffd142adf3f72</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3895\" label=\"Enhancement\" created=\"1336639200822\"\n            x=\"34.0\" y=\"43.25\" width=\"89.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#8AEE95</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/35fa8d4c7f000101663ffd14b437adda</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3900\" layerID=\"1\" created=\"1336639408389\" x=\"9957.533\"\n        y=\"1146.6763\" width=\"105.58301\" height=\"33.05249\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d4d7f000101663ffd144d4a9602</URIString>\n        <point1 x=\"9958.032\" y=\"1179.2288\"/>\n        <point2 x=\"10062.615\" y=\"1147.1763\"/>\n        <ID1 xsi:type=\"node\">3345</ID1>\n        <ID2 xsi:type=\"node\">3893</ID2>\n    </child>\n    <child ID=\"3910\"\n        label=\"It is not possible&#xa;to store the users&#xa;in one database&#xa;and projects in another&#xa;because users will&#xa;be attached to another&#xa;session while projects&#xa;to another\"\n        layerID=\"1\" created=\"1336640812078\" x=\"5848.949\" y=\"45.843506\"\n        width=\"266.0\" height=\"239.0\" strokeWidth=\"1.0\" autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-18</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3604033f7f000101663ffd145b469f06</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3912\" layerID=\"1\" created=\"1336640893817\" x=\"5780.1895\"\n        y=\"207.59863\" width=\"69.260254\" height=\"22.943604\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/360403407f000101663ffd1440a0efe3</URIString>\n        <point1 x=\"5780.689\" y=\"230.04224\"/>\n        <point2 x=\"5848.949\" y=\"208.09863\"/>\n        <ID1 xsi:type=\"node\">3638</ID1>\n        <ID2 xsi:type=\"node\">3910</ID2>\n    </child>\n    <child ID=\"3918\" layerID=\"1\" created=\"1336644686386\" x=\"8774.722\"\n        y=\"751.1875\" width=\"1095.9902\" height=\"362.8125\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/363d47ef7f000101663ffd147aedb6bd</URIString>\n        <point1 x=\"8775.222\" y=\"751.6875\"/>\n        <point2 x=\"9870.212\" y=\"1113.5\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">3345</ID2>\n        <ctrlPoint0 x=\"8767.172\" y=\"1321.302\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9871.733\" y=\"905.60504\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3921\" label=\"Ticket Priorities\" layerID=\"1\"\n        created=\"1336648644751\" x=\"10066.114\" y=\"1189.2603\"\n        width=\"129.0\" height=\"127.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#E4E6D2</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3678e9817f000101663ffd147e48304b</URIString>\n        <child ID=\"3927\" label=\"0 : trival\" created=\"1336648704475\"\n            x=\"34.0\" y=\"23.0\" width=\"55.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#E4E6D2</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3678e9837f000101663ffd145a0469c0</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3926\" label=\"1 : minor\" created=\"1336648697359\"\n            x=\"34.0\" y=\"43.25\" width=\"60.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#E4E6D2</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3678e9827f000101663ffd144fb9e0f6</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3925\" label=\"2 : major\" created=\"1336648690011\"\n            x=\"34.0\" y=\"63.5\" width=\"60.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#E4E6D2</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3678e9827f000101663ffd14e8afaa2a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3923\" label=\"3 : critical\" created=\"1336648644752\"\n            x=\"34.0\" y=\"83.75\" width=\"66.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#E4E6D2</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3678e9827f000101663ffd144ceb272f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3922\" label=\"4 : blocker\" created=\"1336648644752\"\n            x=\"34.0\" y=\"104.0\" width=\"70.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#E4E6D2</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3678e9817f000101663ffd149b7e6993</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3924\" layerID=\"1\" created=\"1336648648313\" x=\"9957.532\"\n        y=\"1221.6255\" width=\"109.08203\" height=\"20.263794\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/3678e9837f000101663ffd14af5fb80b</URIString>\n        <point1 x=\"9958.032\" y=\"1222.1254\"/>\n        <point2 x=\"10066.114\" y=\"1241.3892\"/>\n        <ID1 xsi:type=\"node\">3345</ID1>\n        <ID2 xsi:type=\"node\">3921</ID2>\n    </child>\n    <child ID=\"3933\" label=\"TicketLog\" layerID=\"1\"\n        created=\"1336649990599\" x=\"10230.449\" y=\"1163.9685\"\n        width=\"133.5\" height=\"84.49998\" strokeWidth=\"1.0\"\n        autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/36a21ef87f000101663ffd1411b60466</URIString>\n        <child ID=\"3935\" label=\"ticket | ONE | TICKET\"\n            created=\"1336649990599\" x=\"34.0\" y=\"23.0\" width=\"125.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21ef87f000101663ffd143ee68814</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3936\" label=\"action | ENUM\" created=\"1336649990600\"\n            x=\"34.0\" y=\"43.25\" width=\"88.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21ef87f000101663ffd141c2db82d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4048\" created=\"1358112050934\" x=\"34.0\" y=\"63.5\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#30D643</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/3624aeb3c0a8000435fa8379e276ea76</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3941\" label=\"TicketLog_Operations\" layerID=\"1\"\n        created=\"1336650037564\" x=\"10470.449\" y=\"1186.2185\"\n        width=\"283.0\" height=\"128.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#E4E6D2</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/36a21ef97f000101663ffd1446677ed4</URIString>\n        <child ID=\"3942\" label=\"0 : accept\" created=\"1336650037565\"\n            x=\"34.0\" y=\"23.0\" width=\"66.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#E4E6D2</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21ef97f000101663ffd147086af45</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3943\" label=\"1: reassign\" created=\"1336650037565\"\n            x=\"34.0\" y=\"43.25\" width=\"74.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#E4E6D2</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21ef97f000101663ffd14f97322da</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3944\" label=\"2: resolve\" created=\"1336650037565\"\n            x=\"34.0\" y=\"63.5\" width=\"66.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#E4E6D2</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21ef97f000101663ffd14c8ebee7d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3945\" label=\"3 : reopen\" created=\"1336650037566\"\n            x=\"34.0\" y=\"83.75\" width=\"67.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#E4E6D2</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21efa7f000101663ffd145b9140d4</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3947\"\n            label=\"http://trac.edgewall.org/wiki/TracWorkflow\"\n            created=\"1336650124388\" x=\"34.0\" y=\"104.0\" width=\"244.0\"\n            height=\"18.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n            <strokeColor>#404040</strokeColor>\n            <textColor>#000000</textColor>\n            <font>SansSerif-plain-14</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21efa7f000101663ffd14cef1077e</URIString>\n            <richText>&lt;html&gt;\n  &lt;head style=\"color: #000000\" color=\"#000000\"&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      http://trac.edgewall.org/wiki/TracWorkflow\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n            <label>http://trac.edgewall.org/wiki/TracWorkflow</label>\n        </child>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"3948\" layerID=\"1\" created=\"1336650146175\" x=\"10363.449\"\n        y=\"1215.0498\" width=\"107.5\" height=\"15.887939\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/36a21efb7f000101663ffd1453cee0ae</URIString>\n        <point1 x=\"10363.949\" y=\"1215.5498\"/>\n        <point2 x=\"10470.449\" y=\"1230.4377\"/>\n        <ID1 xsi:type=\"node\">3933</ID1>\n        <ID2 xsi:type=\"node\">3941</ID2>\n    </child>\n    <child ID=\"3950\" layerID=\"1\" created=\"1336650182867\" x=\"8808.297\"\n        y=\"486.59375\" width=\"1637.0703\" height=\"677.875\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/36a21efb7f000101663ffd1490b7da7d</URIString>\n        <point1 x=\"8808.797\" y=\"487.09375\"/>\n        <point2 x=\"10330.912\" y=\"1163.9688\"/>\n        <ID1 xsi:type=\"node\">1901</ID1>\n        <ID2 xsi:type=\"node\">3933</ID2>\n        <ctrlPoint0 x=\"8813.76\" y=\"648.2809\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"10954.907\" y=\"381.96698\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3952\" label=\"ticket_related_tickets\" layerID=\"1\"\n        created=\"1336650487013\" x=\"9869.449\" y=\"1344.0935\" width=\"204.8\"\n        height=\"99.3\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/36a21efb7f000101663ffd14e8a3c2c0</URIString>\n        <child ID=\"3953\" label=\"ticket_id\" created=\"1336650487013\"\n            x=\"50.4\" y=\"40.4\" width=\"56.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21efb7f000101663ffd14a24b7fb3</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3954\" label=\"related_ticket_id\"\n            created=\"1336650487014\" x=\"50.4\" y=\"60.65\" width=\"101.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/36a21efc7f000101663ffd14dc43d8ba</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3955\" layerID=\"1\" created=\"1336650498189\" x=\"9839.641\"\n        y=\"1298.7266\" width=\"96.32422\" height=\"45.867188\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/36a21efc7f000101663ffd14e1a098c4</URIString>\n        <point1 x=\"9842.318\" y=\"1299.2266\"/>\n        <point2 x=\"9935.465\" y=\"1344.0938\"/>\n        <ID1 xsi:type=\"node\">3345</ID1>\n        <ID2 xsi:type=\"node\">3952</ID2>\n        <ctrlPoint0 x=\"9826.693\" y=\"1352.5485\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9894.164\" y=\"1287.735\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3956\" layerID=\"1\" created=\"1336650521319\" x=\"9957.532\"\n        y=\"1277.769\" width=\"59.503906\" height=\"66.82471\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"1\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/36a21efc7f000101663ffd14b48e38a5</URIString>\n        <point1 x=\"9958.032\" y=\"1278.269\"/>\n        <point2 x=\"9999.215\" y=\"1344.0938\"/>\n        <ID1 xsi:type=\"node\">3345</ID1>\n        <ID2 xsi:type=\"node\">3952</ID2>\n        <ctrlPoint0 x=\"9989.436\" y=\"1303.7885\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"10045.057\" y=\"1260.9203\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3959\" label=\"Base (declarative)\" layerID=\"1\"\n        created=\"1337333910132\" x=\"8749.698\" y=\"46.818554\" width=\"114.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5f526b557f0001011df075868f9d004d</URIString>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3973\" layerID=\"1\" created=\"1337333931692\" x=\"8805.149\"\n        y=\"69.3125\" width=\"1.9042969\" height=\"73.0625\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5f526b567f0001011df0758616c29082</URIString>\n        <point1 x=\"8806.554\" y=\"69.8125\"/>\n        <point2 x=\"8805.649\" y=\"141.875\"/>\n        <ID1 xsi:type=\"node\">3959</ID1>\n        <ID2 xsi:type=\"node\">1901</ID2>\n    </child>\n    <child ID=\"3974\" label=\"Permission\" layerID=\"1\"\n        created=\"1337333960700\" x=\"8149.949\" y=\"155.94356\"\n        width=\"140.25\" height=\"66.5\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5f526b567f0001011df0758654c54329</URIString>\n        <child ID=\"3976\" label=\"action | STRING\" created=\"1337333969654\"\n            x=\"34.0\" y=\"23.0\" width=\"98.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5f526b577f0001011df07586a16bd21d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3977\" label=\"class_name | STRING\"\n            created=\"1337333981798\" x=\"34.0\" y=\"43.25\" width=\"134.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5f526b587f0001011df07586479df966</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3975\" layerID=\"1\" created=\"1337333966973\" x=\"8289.699\"\n        y=\"69.3186\" width=\"465.95215\" height=\"104.730225\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5f526b597f0001011df07586e10ce581</URIString>\n        <point1 x=\"8755.151\" y=\"69.8186\"/>\n        <point2 x=\"8290.199\" y=\"173.54883\"/>\n        <ID1 xsi:type=\"node\">3959</ID1>\n        <ID2 xsi:type=\"node\">3974</ID2>\n    </child>\n    <child ID=\"3978\" label=\"ACL\" layerID=\"1\" created=\"1337333994306\"\n        x=\"8356.699\" y=\"193.39357\" width=\"176.25\" height=\"66.5\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5f526b5a7f0001011df075861aec2f3a</URIString>\n        <child ID=\"3979\" label=\"allowance | STRING\"\n            created=\"1337333994306\" x=\"34.0\" y=\"23.0\" width=\"121.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5f526b5b7f0001011df07586b2951812</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3980\" label=\"permission | ONE | Permission\"\n            created=\"1337333994306\" x=\"34.0\" y=\"43.25\" width=\"182.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5f526b5c7f0001011df07586e0220ae9</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"3981\" layerID=\"1\" created=\"1337334052415\" x=\"8515.807\"\n        y=\"69.3186\" width=\"266.66797\" height=\"124.57495\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5f526b5c7f0001011df07586103a1a93</URIString>\n        <point1 x=\"8781.975\" y=\"69.8186\"/>\n        <point2 x=\"8516.307\" y=\"193.39355\"/>\n        <ID1 xsi:type=\"node\">3959</ID1>\n        <ID2 xsi:type=\"node\">3978</ID2>\n    </child>\n    <child ID=\"3983\" label=\"user_acls\" layerID=\"1\"\n        created=\"1337334373492\" x=\"7759.6743\" y=\"1468.1436\"\n        width=\"148.95\" height=\"96.2\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#F4E5FF</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5f58e6257f0001011df0758673c76d34</URIString>\n        <child ID=\"3984\" label=\"uid | INTEGER\" created=\"1337334373492\"\n            x=\"48.85\" y=\"38.85\" width=\"90.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5f58e6267f0001011df07586c5448109</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"3985\" label=\"acl_id | INTEGER\"\n            created=\"1337334373492\" x=\"48.85\" y=\"59.1\" width=\"106.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F4E5FF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/5f58e6267f0001011df07586a9836027</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"hexagon\"/>\n    </child>\n    <child ID=\"3986\" layerID=\"1\" created=\"1337334391368\" x=\"7903.3477\"\n        y=\"1508.908\" width=\"184.63281\" height=\"49.174072\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5f58e6277f0001011df07586777f0913</URIString>\n        <point1 x=\"7903.8477\" y=\"1509.408\"/>\n        <point2 x=\"8087.4805\" y=\"1557.582\"/>\n        <ID1 xsi:type=\"node\">3983</ID1>\n        <ID2 xsi:type=\"node\">1235</ID2>\n        <ctrlPoint0 x=\"7973.189\" y=\"1502.6073\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"7955.7944\" y=\"1559.7931\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"3987\" layerID=\"1\" created=\"1337334391368\" x=\"7892.0205\"\n        y=\"1538.7823\" width=\"195.95996\" height=\"56.41028\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/5f58e6277f0001011df07586feb3e052</URIString>\n        <point1 x=\"7892.5205\" y=\"1539.2823\"/>\n        <point2 x=\"8087.4805\" y=\"1581.8247\"/>\n        <ID1 xsi:type=\"node\">3983</ID1>\n        <ID2 xsi:type=\"node\">1235</ID2>\n        <ctrlPoint0 x=\"7952.594\" y=\"1562.9932\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"7935.2\" y=\"1620.1792\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"4009\" label=\"CodeMixin\" layerID=\"1\"\n        created=\"1358078140071\" x=\"7051.2344\" y=\"1680.6133\"\n        width=\"117.0\" height=\"46.25\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fb2067c0a8000435fa8379ff5e5c4b</URIString>\n        <child ID=\"4008\" label=\"code | UNICODE\" created=\"1358078127303\"\n            x=\"34.0\" y=\"23.0\" width=\"103.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/33fb2068c0a8000435fa8379dcbb914c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4012\" label=\"CodeMixin\" layerID=\"1\"\n        created=\"1358078140071\" x=\"7074.7344\" y=\"1652.6738\" width=\"70.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/33fb2068c0a8000435fa8379b118788f</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"4017\" layerID=\"1\" created=\"1358111659745\" x=\"9189.595\"\n        y=\"352.52405\" width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#30D643</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35c8af4ac0a8000435fa8379819ac38b</URIString>\n        <shape xsi:type=\"diamond\"/>\n    </child>\n    <child ID=\"4018\" label=\"__auto_name__ True\" layerID=\"1\"\n        created=\"1358111659745\" x=\"9222.637\" y=\"354.02402\" width=\"300.0\"\n        height=\"10.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35c8af4ac0a8000435fa83793900fa23</URIString>\n        <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      __auto_name__ True\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>__auto_name__ True</label>\n    </child>\n    <child ID=\"4019\" layerID=\"1\" created=\"1358111748691\" x=\"9189.595\"\n        y=\"382.92407\" width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#EA2218</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35c8af4bc0a8000435fa837996a96163</URIString>\n        <shape xsi:type=\"diamond\"/>\n    </child>\n    <child ID=\"4020\" label=\"strictly_named\" layerID=\"1\"\n        created=\"1358111748691\" x=\"9222.637\" y=\"384.42404\" width=\"300.0\"\n        height=\"10.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35c8af4bc0a8000435fa83797a0999cc</URIString>\n        <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      __auto_name__ False\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>strictly_named</label>\n    </child>\n    <child ID=\"3870\" layerID=\"1\" created=\"1336638487865\" x=\"11292.734\"\n        y=\"560.5132\" width=\"80.831055\" height=\"104.398926\"\n        strokeWidth=\"1.0\" strokeStyle=\"4\" autoSized=\"false\"\n        controlCount=\"2\" arrowState=\"3\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/35fa8d457f000101663ffd14cd1b4218</URIString>\n        <point1 x=\"11373.065\" y=\"561.0132\"/>\n        <point2 x=\"11293.234\" y=\"664.4121\"/>\n        <ID1 xsi:type=\"node\">3834</ID1>\n        <ID2 xsi:type=\"node\">3866</ID2>\n        <ctrlPoint0 x=\"11310.913\" y=\"594.84155\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"11345.901\" y=\"639.49976\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"4067\" label=\"ACLMixin\" layerID=\"1\"\n        created=\"1364475580000\" x=\"7231.366\" y=\"1680.6133\" width=\"186.0\"\n        height=\"66.5\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b118a18d4a976c7d56f44fb9c5dca92c</URIString>\n        <child ID=\"4068\" label=\"__acl__ | List of Tuples\"\n            created=\"1364475580002\" x=\"34.0\" y=\"23.0\" width=\"141.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b118a18e4a976c7d56f44fb9e3c74e42</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4070\" label=\"permissions | MANY | Permission\"\n            created=\"1364475736939\" x=\"34.0\" y=\"43.25\" width=\"195.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/b118a18e4a976c7d56f44fb94262c610</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4069\" label=\"ACLMixin\" layerID=\"1\"\n        created=\"1364475580000\" x=\"7292.366\" y=\"1652.6738\" width=\"64.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/b118a18f4a976c7d56f44fb9bbe83150</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"4092\" label=\"Studio\" layerID=\"1\" created=\"1366071020306\"\n        x=\"8491.448\" y=\"1348.2256\" width=\"204.0\" height=\"266.74997\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/103310bf1d61c1f706111f5d817e32c0</URIString>\n        <child ID=\"4093\" label=\"ScheduleMixin\" created=\"1366071020308\"\n            x=\"34.0\" y=\"23.0\" width=\"94.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c01d61c1f706111f5d68aee756</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4121\" label=\"WorkingHoursMixin\"\n            created=\"1366071463871\" x=\"34.0\" y=\"43.25\" width=\"123.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/10349fb61d61c1f706111f5da867a161</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4106\" label=\"scheduler | SchedulerBase\"\n            created=\"1366071262667\" x=\"34.0\" y=\"63.5\" width=\"160.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c41d61c1f706111f5de62954c5</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4122\" label=\"now | datetime.datetime\"\n            created=\"1366071497033\" x=\"34.0\" y=\"83.75\" width=\"142.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/1035292f1d61c1f706111f5d98bf7781</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4099\" label=\"projects | Project.query.all()\"\n            created=\"1366071047342\" x=\"34.0\" y=\"104.0\" width=\"167.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c11d61c1f706111f5dbb37702f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4104\" label=\"users | User.query.all()\"\n            created=\"1366071226855\" x=\"34.0\" y=\"124.25\" width=\"139.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c21d61c1f706111f5d381d3a8c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4098\" label=\"departments | Department.query.all()\"\n            created=\"1366071046441\" x=\"34.0\" y=\"144.5\" width=\"219.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c21d61c1f706111f5d370ce97a</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4102\" label=\"active_project | Project.query...\"\n            created=\"1366071192280\" x=\"34.0\" y=\"164.75\" width=\"186.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c21d61c1f706111f5d668f6479</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4103\" label=\"inactive_project | Project.query...\"\n            created=\"1366071219401\" x=\"34.0\" y=\"185.0\" width=\"196.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c31d61c1f706111f5ddfaa481b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4105\" label=\"schedule()\" created=\"1366071245456\"\n            x=\"34.0\" y=\"205.25\" width=\"71.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c31d61c1f706111f5df5b70542</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4096\" created=\"1366071020312\" x=\"34.0\" y=\"225.5\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c41d61c1f706111f5d83a80721</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4097\" created=\"1366071020313\" x=\"34.0\" y=\"245.75\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c41d61c1f706111f5d1302ab7d</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4101\" layerID=\"1\" created=\"1366071133748\" x=\"8583.315\"\n        y=\"751.1875\" width=\"194.16797\" height=\"597.53906\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/103310c51d61c1f706111f5d43894860</URIString>\n        <point1 x=\"8776.983\" y=\"751.6875\"/>\n        <point2 x=\"8583.815\" y=\"1348.2266\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">4092</ID2>\n        <ctrlPoint0 x=\"8783.341\" y=\"1353.5111\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8575.693\" y=\"1235.7643\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"4107\" label=\"SchedulerBase\" layerID=\"1\"\n        created=\"1366071283725\" x=\"8140.0244\" y=\"274.64355\"\n        width=\"126.0\" height=\"66.5\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/103310c51d61c1f706111f5dadc12882</URIString>\n        <child ID=\"4109\" label=\"studio\" created=\"1366071283725\" x=\"34.0\"\n            y=\"23.0\" width=\"45.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c51d61c1f706111f5d1c0e0a66</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4110\" label=\"schedule()\" created=\"1366071297632\"\n            x=\"34.0\" y=\"43.25\" width=\"71.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c61d61c1f706111f5dc481d994</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4111\" label=\"TaskJugglerScheduler\" layerID=\"1\"\n        created=\"1366071325565\" x=\"8118.3496\" y=\"405.84357\"\n        width=\"168.0\" height=\"107.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/103310c61d61c1f706111f5dd4376894</URIString>\n        <child ID=\"4112\" label=\"tjp_file\" created=\"1366071325566\"\n            x=\"34.0\" y=\"23.0\" width=\"47.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c61d61c1f706111f5d2faedd65</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4115\" label=\"csv_file\" created=\"1366071355618\"\n            x=\"34.0\" y=\"43.25\" width=\"52.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c71d61c1f706111f5dfad5796d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4113\" label=\"studio\" created=\"1366071325566\" x=\"34.0\"\n            y=\"63.5\" width=\"46.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c71d61c1f706111f5d1df97965</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4116\" label=\"schedule()\" created=\"1366071369168\"\n            x=\"34.0\" y=\"83.75\" width=\"71.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/103310c71d61c1f706111f5d1fa3fa02</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4114\" layerID=\"1\" created=\"1366071328767\" x=\"8202.088\"\n        y=\"340.625\" width=\"1.2880859\" height=\"65.625\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"0\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/103310c81d61c1f706111f5d46f498e5</URIString>\n        <point1 x=\"8202.876\" y=\"341.125\"/>\n        <point2 x=\"8202.588\" y=\"405.75\"/>\n        <ID1 xsi:type=\"node\">4107</ID1>\n        <ID2 xsi:type=\"node\">4111</ID2>\n    </child>\n    <child ID=\"4117\" label=\"WorkingHoursMixin\" layerID=\"1\"\n        created=\"1366071433048\" x=\"7451.5493\" y=\"1680.6133\"\n        width=\"182.25\" height=\"107.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/10349fba1d61c1f706111f5de58b0d92</URIString>\n        <child ID=\"4119\" label=\"working_hours | PICKLE\"\n            created=\"1366071433049\" x=\"34.0\" y=\"23.0\" width=\"145.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/10349fba1d61c1f706111f5dd49712cc</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4123\" label=\"daily_working_hours | PICKLE\"\n            created=\"1366071553456\" x=\"34.0\" y=\"43.25\" width=\"177.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/10367d161d61c1f706111f5d5f945c46</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4124\" label=\"weekly_working_hours | FLOAT\"\n            created=\"1366071569203\" x=\"34.0\" y=\"63.5\" width=\"190.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/10367d161d61c1f706111f5df4afa051</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4125\" label=\"yearly_working_hours | FLOAT\"\n            created=\"1366071584881\" x=\"34.0\" y=\"83.75\" width=\"177.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/10367d161d61c1f706111f5df47d61a6</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4120\" label=\"WorkingHoursMixin\" layerID=\"1\"\n        created=\"1366071433048\" x=\"7481.1743\" y=\"1652.6738\"\n        width=\"123.0\" height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/10349fba1d61c1f706111f5d52b2e31e</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"4127\" label=\"Scene\" layerID=\"1\" created=\"1368620198891\"\n        x=\"8188.949\" y=\"1858.0773\" width=\"133.5\" height=\"124.99998\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a8223faca62df9af491657517acecb2b</URIString>\n        <child ID=\"4137\" label=\"ProjectMixin\" created=\"1368620291689\"\n            x=\"34.0\" y=\"23.0\" width=\"82.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a8223faca62df9af491657512cf9eb8b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4128\" label=\"CodeMixin\" created=\"1368620198893\"\n            x=\"34.0\" y=\"43.25\" width=\"70.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a8223fada62df9af491657510ea3a2ed</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4129\" label=\"shots | MANY | SHOT\"\n            created=\"1368620198895\" x=\"34.0\" y=\"63.5\" width=\"125.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a8223fada62df9af49165751d2e649e2</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4134\" created=\"1368620198904\" x=\"34.0\" y=\"83.75\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a8223faea62df9af49165751a44b4527</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4135\" created=\"1368620198906\" x=\"34.0\" y=\"104.0\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/a8223faea62df9af49165751cba15620</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4136\" layerID=\"1\" created=\"1368620229200\" x=\"8261.573\"\n        y=\"751.2031\" width=\"595.2168\" height=\"1107.3828\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/a8223fafa62df9af49165751e9b56c77</URIString>\n        <point1 x=\"8792.848\" y=\"751.7031\"/>\n        <point2 x=\"8262.073\" y=\"1858.0859\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">4127</ID2>\n        <ctrlPoint0 x=\"9082.37\" y=\"1994.8195\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8288.798\" y=\"1596.0646\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"4159\" label=\"Vacation\" layerID=\"1\"\n        created=\"1382708793616\" x=\"8428.048\" y=\"498.0102\" width=\"125.25\"\n        height=\"140.74994\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/efe0076bc8211d50048a37431a1c1633</URIString>\n        <child ID=\"4160\" label=\"ScheduleMixin\" created=\"1382708793617\"\n            x=\"34.0\" y=\"23.0\" width=\"94.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#FF66CC</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/efe0076bc8211d50048a3743cecdca2f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4161\" label=\"user | ONE | USER\"\n            created=\"1382708793617\" x=\"34.0\" y=\"43.25\" width=\"114.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/efe0076cc8211d50048a374306e29274</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4163\" created=\"1382708793617\" x=\"34.0\" y=\"63.5\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#FEFB03</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/efe0076cc8211d50048a3743133d5bd2</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4164\" created=\"1382708793617\" x=\"34.0\" y=\"83.75\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/efe0076cc8211d50048a374340c4fe92</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <child ID=\"4213\" created=\"1468653290985\" x=\"34.0\" y=\"101.74998\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#33A8F5</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbffc0a8000f011bc52fd9c4fed2</URIString>\n            <shape xsi:type=\"chevron\"/>\n        </child>\n        <child ID=\"4214\" created=\"1468653294154\" x=\"34.0\" y=\"119.749954\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#AF55F4</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/f28ffbffc0a8000f011bc52f22a80a94</URIString>\n            <shape xsi:type=\"rhombus\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4166\" layerID=\"1\" created=\"1382708864226\" x=\"8496.098\"\n        y=\"470.6831\" width=\"312.2793\" height=\"44.147827\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"2\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/efe0076cc8211d50048a37434fdaa095</URIString>\n        <point1 x=\"8807.877\" y=\"487.09375\"/>\n        <point2 x=\"8496.598\" y=\"498.0\"/>\n        <ID1 xsi:type=\"node\">1901</ID1>\n        <ID2 xsi:type=\"node\">4159</ID2>\n        <ctrlPoint0 x=\"8810.178\" y=\"577.50446\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"8504.1455\" y=\"408.33646\" xsi:type=\"point\"/>\n    </child>\n    <child ID=\"4170\" label=\"Media\" layerID=\"1\" created=\"1397835424148\"\n        x=\"9336.949\" y=\"1027.2249\" width=\"158.25\" height=\"127.25\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/757fa42fc0a8004336c48f760e6c52fc</URIString>\n        <child ID=\"4178\" label=\"width | INTEGER\" created=\"1397835476427\"\n            x=\"34.0\" y=\"23.0\" width=\"102.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/757fa42fc0a8004336c48f762d6b6b6d</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4179\" label=\"height | INTEGER\"\n            created=\"1397835493677\" x=\"34.0\" y=\"43.25\" width=\"107.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/757fa430c0a8004336c48f7684388152</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4183\" label=\"duration | FLOAT\"\n            created=\"1397835571109\" x=\"34.0\" y=\"63.5\" width=\"102.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/757fa431c0a8004336c48f76121a87ec</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4181\" label=\"bitrate | INTEGER\"\n            created=\"1397835532870\" x=\"34.0\" y=\"83.75\" width=\"107.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/757fa431c0a8004336c48f76ea9e7e28</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4186\" label=\"other_sizes | MANY | Media\"\n            created=\"1397835683545\" x=\"34.0\" y=\"104.0\" width=\"158.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/7581cfb5c0a8004336c48f7613cb7500</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4171\" layerID=\"1\" created=\"1397835424158\" x=\"9494.699\"\n        y=\"974.3784\" width=\"103.32324\" height=\"66.399414\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/757fa430c0a8004336c48f76e74e4757</URIString>\n        <point1 x=\"9597.522\" y=\"974.8784\"/>\n        <point2 x=\"9495.199\" y=\"1040.2778\"/>\n        <ID1 xsi:type=\"node\">1145</ID1>\n        <ID2 xsi:type=\"node\">4170</ID2>\n    </child>\n    <child ID=\"4191\" label=\"Roles\" layerID=\"1\" created=\"1459162812123\"\n        x=\"8715.449\" y=\"1348.2256\" width=\"71.0\" height=\"64.24998\"\n        strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/bce315cdc0a82a4a01152d0e90f705d8</URIString>\n        <child ID=\"4194\" created=\"1459162812126\" x=\"34.0\" y=\"23.0\"\n            width=\"29.0\" height=\"23.0\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/bce315cdc0a82a4a01152d0ebdecdd7d</URIString>\n            <shape xsi:type=\"triangle\"/>\n        </child>\n        <child ID=\"4195\" created=\"1459162812127\" x=\"34.0\" y=\"43.25\"\n            width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n            autoSized=\"false\" xsi:type=\"node\">\n            <fillColor>#EA2218</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/bce315cec0a82a4a01152d0e48b05e0b</URIString>\n            <shape xsi:type=\"diamond\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4196\" layerID=\"1\" created=\"1459162843527\" x=\"8751.61\"\n        y=\"751.1875\" width=\"22.542969\" height=\"597.5156\"\n        strokeWidth=\"1.0\" autoSized=\"false\" controlCount=\"0\"\n        arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/bce315cec0a82a4a01152d0e6dc206c0</URIString>\n        <point1 x=\"8773.653\" y=\"751.6875\"/>\n        <point2 x=\"8752.11\" y=\"1348.2031\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">4191</ID2>\n    </child>\n    <child ID=\"4204\" layerID=\"1\" created=\"1468652962732\" x=\"9189.595\"\n        y=\"418.3753\" width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#33A8F5</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f28ffc00c0a8000f011bc52f338782cf</URIString>\n        <shape xsi:type=\"chevron\"/>\n    </child>\n    <child ID=\"4205\"\n        label=\"Implmented as RESTFul Service with unit tests\"\n        layerID=\"1\" created=\"1468652962732\" x=\"9222.637\" y=\"419.87527\"\n        width=\"298.0\" height=\"14.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f28ffc00c0a8000f011bc52f7f3ff996</URIString>\n        <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Implmented as RESTFul Service with unit tests\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Implmented as RESTFul Service with unit tests</label>\n    </child>\n    <child ID=\"4206\" layerID=\"1\" created=\"1468653091361\" x=\"9187.595\"\n        y=\"452.82346\" width=\"29.0\" height=\"19.99997\" strokeWidth=\"1.0\"\n        autoSized=\"false\" xsi:type=\"node\">\n        <fillColor>#AF55F4</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-plain-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f28ffc01c0a8000f011bc52f168f3afb</URIString>\n        <shape xsi:type=\"rhombus\"/>\n    </child>\n    <child ID=\"4207\"\n        label=\"Implemented as RESTFul Service with functional tests\"\n        layerID=\"1\" created=\"1468653091361\" x=\"9222.637\" y=\"454.32343\"\n        width=\"297.0\" height=\"17.0\" strokeWidth=\"0.0\" autoSized=\"false\" xsi:type=\"text\">\n        <strokeColor>#404040</strokeColor>\n        <textColor>#000000</textColor>\n        <font>SansSerif-plain-14</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/f28ffc01c0a8000f011bc52fd0608bf1</URIString>\n        <richText>&lt;html&gt;\n  &lt;head&gt;\n    &lt;style type=\"text/css\"&gt;\n      &lt;!--\n        body { font-family: Arial; margin-bottom: 0px; color: #000000; margin-top: 0px; font-size: 12; margin-right: 0px; margin-left: 0px }\n        ol { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n        p { margin-bottom: 0; color: #000000; margin-top: 0; margin-right: 0; margin-left: 0 }\n        ul { font-family: Arial; list-style-position: outside; margin-top: 6; font-size: 12; margin-left: 30; vertical-align: middle }\n      --&gt;\n    &lt;/style&gt;\n    \n  &lt;/head&gt;\n  &lt;body&gt;\n    &lt;p style=\"color: #000000\" color=\"#000000\"&gt;\n      Implemented as RESTFul Service with functional tests\n    &lt;/p&gt;\n  &lt;/body&gt;\n&lt;/html&gt;\n</richText>\n        <label>Implemented as RESTFul Service with functional tests</label>\n    </child>\n    <child ID=\"4233\" label=\"DAGMixin\" layerID=\"1\"\n        created=\"1471422518912\" x=\"6824.949\" y=\"1870.0214\"\n        width=\"173.25\" height=\"167.75\" strokeWidth=\"1.0\"\n        autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/97ae2d15c0a82a4a019efdbd4499f5e0</URIString>\n        <child ID=\"4234\" label=\"parent | {CLASS}\"\n            created=\"1471422518913\" x=\"34.0\" y=\"23.0\" width=\"102.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d15c0a82a4a019efdbdd74739f1</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4236\" label=\"children | {CLASS}\"\n            created=\"1471422664669\" x=\"34.0\" y=\"43.25\" width=\"111.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d15c0a82a4a019efdbdee3e5fcf</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4237\" label=\"is_root | BOOL\" created=\"1471422705938\"\n            x=\"34.0\" y=\"63.5\" width=\"93.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d15c0a82a4a019efdbd580043fc</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4238\" label=\"is_container | BOOL\"\n            created=\"1471422717295\" x=\"34.0\" y=\"83.75\" width=\"124.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d16c0a82a4a019efdbdb898570c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4239\" label=\"is_leaf | BOOL\" created=\"1471422756262\"\n            x=\"34.0\" y=\"104.0\" width=\"91.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d16c0a82a4a019efdbd49bb9763</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4240\" label=\"parents | [{CLASS}]\"\n            created=\"1471422769228\" x=\"34.0\" y=\"124.25\" width=\"121.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d16c0a82a4a019efdbdb9a3c36c</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4241\" label=\"walk_hierarchy | GENERATOR\"\n            created=\"1471422788467\" x=\"34.0\" y=\"144.5\" width=\"178.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d16c0a82a4a019efdbd21ac06fb</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4235\" label=\"DAGMixin\" layerID=\"1\"\n        created=\"1471422518912\" x=\"6879.074\" y=\"1842.0819\" width=\"65.0\"\n        height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/97ae2d16c0a82a4a019efdbdcdc4ddd0</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"4252\" label=\"DateRangeMixin\" layerID=\"1\"\n        created=\"1471423301805\" x=\"7051.2344\" y=\"1870.0214\"\n        width=\"183.75\" height=\"208.25\" strokeWidth=\"1.0\"\n        autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FEFB03</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/97ae2d16c0a82a4a019efdbd8801c064</URIString>\n        <child ID=\"4253\" label=\"start | DATETIME\"\n            created=\"1471423301806\" x=\"34.0\" y=\"23.0\" width=\"102.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d16c0a82a4a019efdbda9b85014</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4254\" label=\"end | DATETIME\" created=\"1471423301806\"\n            x=\"34.0\" y=\"43.25\" width=\"99.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d16c0a82a4a019efdbdd0362e2b</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4255\" label=\"duration | TIMEDELTA\"\n            created=\"1471423301806\" x=\"34.0\" y=\"63.5\" width=\"130.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d16c0a82a4a019efdbd7680eb26</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4256\" label=\"computed_start | DATETIME\"\n            created=\"1471423301806\" x=\"34.0\" y=\"83.75\" width=\"164.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d17c0a82a4a019efdbd0eab4c71</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4257\" label=\"computed_end | DATETIME\"\n            created=\"1471423301807\" x=\"34.0\" y=\"104.0\" width=\"161.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d17c0a82a4a019efdbdf8cdb703</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4258\" label=\"computed_duration | TIMEDELTA\"\n            created=\"1471423301807\" x=\"34.0\" y=\"124.25\" width=\"192.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#F2AE45</fillColor>\n            <strokeColor>#7F7F7F</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-plain-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d17c0a82a4a019efdbd85fb5607</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4263\" label=\"round_time\" created=\"1471423301809\"\n            x=\"34.0\" y=\"144.5\" width=\"76.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d17c0a82a4a019efdbd5c065e28</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4269\" label=\"total_seconds\" created=\"1471423499525\"\n            x=\"34.0\" y=\"164.75\" width=\"92.0\" height=\"23.0\"\n            strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d17c0a82a4a019efdbd9812cb2e</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <child ID=\"4271\" label=\"computed_total_seconds\"\n            created=\"1471423513144\" x=\"34.0\" y=\"185.0\" width=\"156.0\"\n            height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n            <fillColor>#83CEFF</fillColor>\n            <strokeColor>#776D6D</strokeColor>\n            <textColor>#000000</textColor>\n            <font>Arial-bold-12</font>\n            <URIString>http://vue.tufts.edu/rdf/resource/97ae2d17c0a82a4a019efdbde484861f</URIString>\n            <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n        </child>\n        <shape xsi:type=\"rectangle\"/>\n    </child>\n    <child ID=\"4264\" label=\"DateRangeMixin\" layerID=\"1\"\n        created=\"1471423301805\" x=\"7091.6094\" y=\"1842.0819\"\n        width=\"103.0\" height=\"23.0\" strokeWidth=\"1.0\" autoSized=\"true\" xsi:type=\"node\">\n        <fillColor>#FF66CC</fillColor>\n        <strokeColor>#776D6D</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-12</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/97ae2d17c0a82a4a019efdbde98bd4d5</URIString>\n        <shape arcwidth=\"20.0\" archeight=\"20.0\" xsi:type=\"roundRect\"/>\n    </child>\n    <child ID=\"4273\" layerID=\"1\" created=\"1471423799077\" x=\"8775.2705\"\n        y=\"751.25\" width=\"654.4365\" height=\"1542.6562\" strokeWidth=\"1.0\"\n        autoSized=\"false\" controlCount=\"2\" arrowState=\"2\" xsi:type=\"link\">\n        <strokeColor>#000000</strokeColor>\n        <textColor>#000000</textColor>\n        <font>Arial-bold-11</font>\n        <URIString>http://vue.tufts.edu/rdf/resource/97b7c5fac0a82a4a01081779ad8219f2</URIString>\n        <point1 x=\"8775.7705\" y=\"751.75\"/>\n        <point2 x=\"9429.207\" y=\"2293.4062\"/>\n        <ID1 xsi:type=\"node\">1887</ID1>\n        <ID2 xsi:type=\"node\">4225</ID2>\n        <ctrlPoint0 x=\"8768.16\" y=\"1934.3668\" xsi:type=\"point\"/>\n        <ctrlPoint1 x=\"9433.276\" y=\"2152.837\" xsi:type=\"point\"/>\n    </child>\n    <layer ID=\"1\" label=\"Layer 1\" created=\"0\" x=\"0.0\" y=\"0.0\"\n        width=\"1.4E-45\" height=\"1.4E-45\" strokeWidth=\"0.0\" autoSized=\"false\">\n        <URIString>http://vue.tufts.edu/rdf/resource/5d104b32c00007d601b277f026b72230</URIString>\n    </layer>\n    <userZoom>1.2251885318196525</userZoom>\n    <userOrigin x=\"6041.144\" y=\"-963.3883\"/>\n    <presentationBackground>#202020</presentationBackground>\n    <PathwayList currentPathway=\"-1\" revealerIndex=\"-1\"/>\n    <date>2009-05-20</date>\n    <modelVersion>6</modelVersion>\n    <saveLocation>C:\\Users\\eoyilmaz\\Documents\\development\\stalker\\stalker\\docs\\source\\_static\\images</saveLocation>\n    <saveFile>C:\\Users\\eoyilmaz\\Documents\\development\\stalker\\stalker\\docs\\source\\_static\\images\\stalker_design.vue</saveFile>\n</LW-MAP>\n"
  },
  {
    "path": "docs/source/_templates/autosummary/base.rst",
    "content": "{{ fullname }}\n{{ underline }}\n\n.. currentmodule:: {{ module }}\n\n.. auto{{ objtype }}:: {{ objname }}\n"
  },
  {
    "path": "docs/source/_templates/autosummary/class.rst",
    "content": "{{ fullname }}\n{{ underline }}\n\n.. inheritance-diagram::\n     {{ fullname }}\n     :parts: 1\n\n.. currentmodule:: {{ module }}\n\n.. autoclass:: {{ objname }}\n   :show-inheritance:\n   :inherited-members:\n\n   {% block methods %}\n   .. automethod:: __init__\n\n   {% if methods %}\n   .. rubric:: Methods\n\n   .. autosummary::\n   {% for item in methods %}\n      ~{{ name }}.{{ item }}\n   {%- endfor %}\n   {% endif %}\n   {% endblock %}\n\n   {% block attributes %}\n   {% if attributes %}\n   .. rubric:: Attributes\n\n   .. autosummary::\n   {% for item in attributes %}\n      ~{{ name }}.{{ item }}\n   {%- endfor %}\n   {% endif %}\n   {% endblock %}\n"
  },
  {
    "path": "docs/source/_templates/autosummary/module.rst",
    "content": "{{ fullname }}\n{{ underline }}\n\n.. automodule:: {{ fullname }}\n\n   {% block functions %}\n   {% if functions %}\n   .. rubric:: Functions\n\n   .. autosummary::\n   {% for item in functions %}\n      {{ item }}\n   {%- endfor %}\n   {% endif %}\n   {% endblock %}\n\n   {% block classes %}\n   {% if classes %}\n   .. rubric:: Classes\n\n   .. autosummary::\n   {% for item in classes %}\n      {{ item }}\n   {%- endfor %}\n   {% endif %}\n   {% endblock %}\n\n   {% block exceptions %}\n   {% if exceptions %}\n   .. rubric:: Exceptions\n\n   .. autosummary::\n   {% for item in exceptions %}\n      {{ item }}\n   {%- endfor %}\n   {% endif %}\n   {% endblock %}\n"
  },
  {
    "path": "docs/source/about.rst",
    "content": ".. about_toplevel:\n\n.. include:: source/../../../README.rst"
  },
  {
    "path": "docs/source/changelog.rst",
    "content": ".. _changelog_toplevel:\n\n.. include:: source/../../../CHANGELOG.rst"
  },
  {
    "path": "docs/source/conf.py",
    "content": "# -*- coding: utf-8 -*-\n#\n# Stalker documentation build configuration file, created by\n# sphinx-quickstart on Tue Jul 26 20:41:01 2016.\n#\n# This file is execfile()d with the current directory set to its\n# containing dir.\n#\n# Note that not all possible configuration values are present in this\n# autogenerated file.\n#\n# All configuration values have a default; values that are commented out\n# serve to show the default.\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\n\nimport os\nimport sys\n\n# sys.path.insert(0, os.path.abspath('.'))\n\n# -- General configuration ------------------------------------------------\n\n# If your documentation needs a minimal Sphinx version, state it here.\n#\n# needs_sphinx = '1.0'\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.intersphinx\",\n    \"sphinx.ext.autosummary\",\n    \"sphinx.ext.todo\",\n    \"sphinx.ext.coverage\",\n    \"sphinx.ext.viewcode\",\n    \"sphinx.ext.githubpages\",\n    \"sphinx.ext.graphviz\",\n    \"sphinx.ext.inheritance_diagram\",\n    # 'sphinx.ext.imgmath',\n    \"sphinx.ext.mathjax\",\n    \"sphinx.ext.ifconfig\",\n]\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = [\"_templates\"]\n\n# The suffix(es) of source filenames.\n# You can specify multiple suffix as a list of string:\n#\nsource_suffix = ['.rst', '.md']\n# source_suffix = \".rst\"\n\n# The encoding of source files.\n#\n# source_encoding = 'utf-8-sig'\n\n# The master toctree document.\nmaster_doc = \"index\"\n\n# General information about the project.\nproject = \"Stalker\"\ncopyright = \"2009-2024, Erkan Ozgur Yilmaz\"\nauthor = \"Erkan Ozgur Yilmaz\"\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\n\n# find stalkers path\ndirName = os.path.dirname(__file__)\nmodulePath = os.path.sep.join(dirName.split(os.path.sep)[:-2])\nsys.path.append(modulePath)\nimport stalker\n\nversion = stalker.__version__\n# The full version, including alpha/beta/rc tags.\nrelease = stalker.__version__\n\n# The language for content autogenerated by Sphinx. Refer to documentation\n# for a list of supported languages.\n#\n# This is also used if you do content translation via gettext catalogs.\n# Usually you set \"language\" from the command line for these cases.\nlanguage = 'en'\n\n# There are two options for replacing |today|: either, you set today to some\n# non-false value, then it is used:\n#\n# today = ''\n#\n# Else, today_fmt is used as the format for a strftime call.\n#\n# today_fmt = '%B %d, %Y'\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This patterns also effect to html_static_path and html_extra_path\nexclude_patterns = [\"build\", \"templates\"]\n\n# The reST default role (used for this markup: `text`) to use for all\n# documents.\n#\n# default_role = None\n\n# If true, '()' will be appended to :func: etc. cross-reference text.\n#\n# add_function_parentheses = True\n\n# If true, the current module name will be prepended to all description\n# unit titles (such as .. function::).\n#\n# add_module_names = True\n\n# If true, sectionauthor and moduleauthor directives will be shown in the\n# output. They are ignored by default.\n#\n# show_authors = False\n\n# The name of the Pygments (syntax highlighting) style to use.\npygments_style = \"sphinx\"\n\n# A list of ignored prefixes for module index sorting.\n# modindex_common_prefix = []\n\n# If true, keep warnings as \"system message\" paragraphs in the built documents.\n# keep_warnings = False\n\n# If true, `todo` and `todoList` produce output, else they produce nothing.\ntodo_include_todos = True\n\n\n# -- Options for HTML output ----------------------------------------------\n\n# The theme to use for HTML and HTML Help pages.  See the documentation for\n# a list of builtin themes.\n# html_theme = 'default'\n# html_theme = 'scrolls'\n# html_theme = 'agogo'\n# html_theme = 'sphinxdoc'\nhtml_theme = \"furo\"\n# html_theme = 'alabaster'\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further.  For a list of options available for each theme, see the\n# documentation.\n#\n# html_theme_options = {}\n\n# Add any paths that contain custom themes here, relative to this directory.\n# html_theme_path = []\n\n# The name for this set of Sphinx documents.\n# \"<project> v<release> documentation\" by default.\n#\n# html_title = None\n\n# A shorter title for the navigation bar.  Default is the same as html_title.\n#\n# html_short_title = None\n\n# The name of an image file (relative to this directory) to place at the top\n# of the sidebar.\n#\n# html_logo = None\n\n# The name of an image file (relative to this directory) to use as a favicon of\n# the docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32\n# pixels large.\n#\n# html_favicon = None\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = [\"_static\"]\n\n# Add any extra paths that contain custom files (such as robots.txt or\n# .htaccess) here, relative to this directory. These files are copied\n# directly to the root of the documentation.\n#\n# html_extra_path = []\n\n# If not None, a 'Last updated on:' timestamp is inserted at every page\n# bottom, using the given strftime format.\n# The empty string is equivalent to '%b %d, %Y'.\n#\n# html_last_updated_fmt = None\n\n# If true, SmartyPants will be used to convert quotes and dashes to\n# typographically correct entities.\n#\n# html_use_smartypants = True\n\n# Custom sidebar templates, maps document names to template names.\n#\n# html_sidebars = {}\n\n# Additional templates that should be rendered to pages, maps page names to\n# template names.\n#\n# html_additional_pages = {}\n\n# If false, no module index is generated.\n#\n# html_domain_indices = True\n\n# If false, no index is generated.\n#\n# html_use_index = True\n\n# If true, the index is split into individual pages for each letter.\n#\n# html_split_index = False\n\n# If true, links to the reST sources are added to the pages.\n#\n# html_show_sourcelink = True\n\n# If true, \"Created using Sphinx\" is shown in the HTML footer. Default is True.\n#\n# html_show_sphinx = True\n\n# If true, \"(C) Copyright ...\" is shown in the HTML footer. Default is True.\n#\n# html_show_copyright = True\n\n# If true, an OpenSearch description file will be output, and all pages will\n# contain a <link> tag referring to it.  The value of this option must be the\n# base URL from which the finished HTML is served.\n#\n# html_use_opensearch = ''\n\n# This is the file name suffix for HTML files (e.g. \".xhtml\").\n# html_file_suffix = None\n\n# Language to be used for generating the HTML full-text search index.\n# Sphinx supports the following languages:\n#   'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'\n#   'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'\n#\n# html_search_language = 'en'\n\n# A dictionary with options for the search language support, empty by default.\n# 'ja' uses this config value.\n# 'zh' user can custom change `jieba` dictionary path.\n#\n# html_search_options = {'type': 'default'}\n\n# The name of a javascript file (relative to the configuration directory) that\n# implements a search results scorer. If empty, the default will be used.\n#\n# html_search_scorer = 'scorer.js'\n\n# Output file base name for HTML help builder.\nhtmlhelp_basename = \"Stalkerdoc\"\n\n\n# -- Options for LaTeX output ---------------------------------------------\n\nlatex_elements = {\n    # The paper size ('letterpaper' or 'a4paper').\n    #\n    \"papersize\": \"a4paper\",\n    # The font size ('10pt', '11pt' or '12pt').\n    #\n    # 'pointsize': '10pt',\n    # Additional stuff for the LaTeX preamble.\n    #\n    # 'preamble': '',\n    # Latex figure (float) alignment\n    #\n    # 'figure_align': 'htbp',\n}\n\n# Grouping the document tree into LaTeX files. List of tuples\n# (source start file, target name, title,\n#  author, documentclass [howto, manual, or own class]).\nlatex_documents = [\n    (\n        \"contents\",\n        \"Stalker.tex\",\n        \"Stalker Documentation\",\n        \"Erkan Ozgur Yilmaz\",\n        \"manual\",\n    ),\n]\n\n# The name of an image file (relative to this directory) to place at the top of\n# the title page.\n#\n# latex_logo = None\n\n# For \"manual\" documents, if this is true, then toplevel headings are parts,\n# not chapters.\n#\n# latex_use_parts = False\n\n# If true, show page references after internal links.\n#\n# latex_show_pagerefs = False\n\n# If true, show URL addresses after external links.\n#\n# latex_show_urls = False\n\n# Additional stuff for the LaTeX preamble.\n#\n# latex_preamble = '\\setcounter{tocdepth}{3}'\n\n# Documents to append as an appendix to all manuals.\n#\n# latex_appendices = []\n\n# It false, will not define \\strong, \\code, \titleref, \\crossref ... but only\n# \\sphinxstrong, ..., \\sphinxtitleref, ... To help avoid clash with user added\n# packages.\n#\n# latex_keep_old_macro_names = True\n\n# If false, no module index is generated.\n#\n# latex_domain_indices = True\n\n\n# -- Options for manual page output ---------------------------------------\n\n# One entry per manual page. List of tuples\n# (source start file, name, description, authors, manual section).\nman_pages = [(master_doc, \"stalker\", \"Stalker Documentation\", [author], 1)]\n\n# If true, show URL addresses after external links.\n#\n# man_show_urls = False\n\n\n# -- Options for Texinfo output -------------------------------------------\n\n# Grouping the document tree into Texinfo files. List of tuples\n# (source start file, target name, title, author,\n#  dir menu entry, description, category)\ntexinfo_documents = [\n    (\n        master_doc,\n        \"Stalker\",\n        \"Stalker Documentation\",\n        author,\n        \"Stalker\",\n        \"One line description of project.\",\n        \"Miscellaneous\",\n    ),\n]\n\n# Documents to append as an appendix to all manuals.\n#\n# texinfo_appendices = []\n\n# If false, no module index is generated.\n#\n# texinfo_domain_indices = True\n\n# How to display URL addresses: 'footnote', 'no', or 'inline'.\n#\n# texinfo_show_urls = 'footnote'\n\n# If true, do not generate a @detailmenu in the \"Top\" node's menu.\n#\n# texinfo_no_detailmenu = False\n\n\n# -- Options for Epub output ----------------------------------------------\n\n# Bibliographic Dublin Core info.\nepub_title = \"Stalker\"\nepub_author = \"Erkan Ozgur Yilmaz\"\nepub_publisher = \"Erkan Ozgur Yilmaz\"\nepub_copyright = \"2014, Erkan Ozgur Yilmaz\"\n\n# The basename for the epub file. It defaults to the project name.\n# epub_basename = u'Stalker'\n\n# The HTML theme for the epub output. Since the default themes are not optimized\n# for small screen space, using the same theme for HTML and epub output is\n# usually not wise. This defaults to 'epub', a theme designed to save visual\n# space.\n# epub_theme = 'epub'\n\n# The language of the text. It defaults to the language option\n# or en if the language is not set.\n# epub_language = ''\n\n# The scheme of the identifier. Typical schemes are ISBN or URL.\n# epub_scheme = ''\n\n# The unique identifier of the text. This can be a ISBN number\n# or the project homepage.\n# epub_identifier = ''\n\n# A unique identification for the text.\n# epub_uid = ''\n\n# A tuple containing the cover image and cover page html template filenames.\n# epub_cover = ()\n\n# A sequence of (type, uri, title) tuples for the guide element of content.opf.\n# epub_guide = ()\n\n# HTML files that should be inserted before the pages created by sphinx.\n# The format is a list of tuples containing the path and title.\n# epub_pre_files = []\n\n# HTML files shat should be inserted after the pages created by sphinx.\n# The format is a list of tuples containing the path and title.\n# epub_post_files = []\n\n# A list of files that should not be packed into the epub file.\n# epub_exclude_files = []\n\n# The depth of the table of contents in toc.ncx.\n# epub_tocdepth = 3\n\n# Allow duplicate toc entries.\n# epub_tocdup = True\n\n# Choose between 'default' and 'includehidden'.\n# epub_tocscope = 'default'\n\n# Fix unsupported image types using the PIL.\n# epub_fix_images = False\n\n# Scale large images.\n# epub_max_image_width = 0\n\n# How to display URL addresses: 'footnote', 'no', or 'inline'.\n# epub_show_urls = 'inline'\n\n# If false, no index is generated.\n# epub_use_index = True\n\n\n# Example configuration for intersphinx: refer to the Python standard library.\nintersphinx_mapping = {\n    \"python\": (\"https://docs.python.org/\", None),\n}\n\nautosummary_generate = True\nautodoc_member_order = \"bysource\"\n\n\ndef setup(app):\n    app.add_object_type(\n        \"confval\",\n        \"confval\",\n        objname=\"configuration value\",\n        indextemplate=\"pair: %s; configuration value\",\n    )\n\n    # # this next two lines are for Sphinx 1.2 to work\n    # import sqlalchemy.ext.declarative.api\n    # from stalker.db.declarative import Base\n\n    # sqlalchemy.ext.declarative.api.Base = Base\n"
  },
  {
    "path": "docs/source/configure.rst",
    "content": ".. _configuration_toplevel:\n\n.. _configuring_stalker:\n\nConfiguring Stalker\n===================\n\nTo configure Stalker and make it fit to your Studios need you should use the\n``config.py`` file as mentioned in next sections.\n\nconfig.py File\n--------------\n\nStalker uses the ``config.py`` to let one to customize the system config.\n\nThe ``config.py`` file is searched in a couple of places through the system:\n    \n  * under \"~/.strc/\" directory (not yet)\n  * under \"$STALKER_PATH\"\n\nThe first path is a folder in the users home dir. The second one is a path\ndefined by the ``STALKER_PATH`` environment variable.\n\nDefining the ``config.py`` by using the environment variable gives the most\ncustomizable and consistent setup through the studio. You can set\n``STALKER_PATH`` to a shared folder in your fileserver where all the users can\naccess.\n\nBecause, ``config.py`` is a regular Python code which is executed by\nStalker, you can do anything you were doing in a normal Python\nscript. This is very handy (also dangerous!) if you have another source of\ninformation which is reachable by a Python script.\n\nIf there is no ``STALKER_PATH`` variable in your current environment or it is\nnot showing an existing path or there is no ``config.py`` file the system will\nuse the system defaults.\n\nConfig Variables\n----------------\n\nVariables which can be set in ``config.py`` are as follows:\n\n.. confval:: actions\n\n   Actions for authorization system. These are used to create ACLs. Stalker\n   uses `CRUDL`_ system. Default value is::\n\n     actions = ['Create', 'Read', 'Update', 'Delete', 'List'] #CRUDL\n\n   .. _CRUDL: http://en.wikipedia.org/wiki/Create,_read,_update_and_delete\n\n.. confval:: auto_create_admin\n\n   Tells Stalker to create an admin by default. Default value is::\n\n     auto_create_admin = True\n\n.. confval:: admin_name\n\n   The default admin user name. Default value is::\n\n     admin_name = 'admin'\n\n.. confval:: admin_login\n\n   The default admin login. Default value is::\n\n     admin_login = 'admin'\n\n.. confval:: admin_password\n\n   The default admin password. Default value is::\n\n     admin_password = 'admin'\n\n.. confval:: admin_email\n\n   The default email for admin user. Default value is::\n\n     admin_email = 'admin@admin.com'\n\n.. confval:: admin_department_name\n\n   The default department name for admin. Default value is::\n\n     admin_department_name = 'admins'\n\n.. confval:: admin_group_name\n\n   The default admin permission group name. Default value is::\n\n     admin_group_name = 'admins'\n\n.. confval:: database_engine_settings\n\n   A dictionary of config values. The default value is::\n\n     database_engine_settings = {\n         \"sqlalchemy.url\": \"sqlite:///:memory:\",\n         \"sqlalchemy.echo\": False,\n     }\n\n.. confval:: database_session_settings\n   \n   This value is not used.\n\n.. confval:: local_storage_path\n\n   The local storage path for Stalker.\n\n     local_storage_path = os.path.expanduser('~/.strc')   \n\n.. confval:: local_session_data_file_name\n\n   The per user or local session file name. It is used for storing logged in\n   user info. The default value is::\n\n     local_session_data_file_name = 'local_session_data'\n\n.. confval:: server_side_storage_path\n\n   Storage for uploaded files. This used by `Stalker Pyramid`_ and shows the\n   server side storage path. Will be moved to Stalker Pyramid in later\n   versions. Not used by Stalker by default. Default value is::\n\n     server_side_storage_path = os.path.expanduser('~/Stalker_Storage')\n\n   .. _`Stalker Pyramid`: https://pypi.python.org/pypi/stalker_pyramid\n\n.. confval:: key\n\n   The default keyword which is going to be used in password scrambling.\n   Default value is::\n\n     key = \"stalker_default_key\"\n\n.. confval:: version_variant_name\n\n   The default variant name for :class:`~stalker.models.version.Version`\n   instances. Default value is::\n\n     version_variant_name = \"Main\"\n\n.. confval:: status_bg_color\n\n   Default background color for :class:`~stalker.models.status.Status`\n   instances. Default value is::\n\n     status_bg_color = 0xffffff\n\n.. confval:: status_fg_color\n\n   Default foreground color for :class:`~stalker.models.status.Status`\n   instances. Default value is::\n\n     status_fg_color = 0x000000\n \n.. confval:: ticket_label\n\n   Default ticket label. Used by :class:`~stalker.models.ticket.Ticket` when\n   generating a ticket name. Default value is::\n\n     ticket_label = \"Ticket\"\n\n.. confval:: ticket_status_order\n\n   Defines the ticket statuses and the order of them. Default value is::\n\n     ticket_status_order = [\n         'new', 'accepted', 'assigned', 'reopened', 'closed'\n     ]\n\n.. confval:: ticket_resolutions\n\n   Defines the default ticket resolutions. Default value is::\n\n     ticket_resolutions = [\n         'fixed', 'invalid', 'wontfix', 'duplicate', 'worksforme', 'cantfix'\n     ]\n\n.. confval:: ticket_workflow\n\n   Defines the default ticket workflow. It is a dictionary of actions. Shows\n   the new status per action. Default value is::\n\n     ticket_workflow = {\n         'resolve' : {\n             'new': {\n                 'new_status': 'closed',\n                 'action': 'set_resolution'\n             },\n             'accepted': {\n                 'new_status': 'closed',\n                 'action': 'set_resolution'\n             },\n             'assigned': {\n                 'new_status': 'closed',\n                 'action': 'set_resolution'\n             },\n             'reopened': {\n                 'new_status': 'closed',\n                 'action': 'set_resolution'\n             },\n         },\n         'accept' : {\n             'new': {\n                 'new_status': 'accepted',\n                 'action': 'set_owner'\n             },\n             'accepted': {\n                 'new_status': 'accepted',\n                 'action': 'set_owner'\n             },\n             'assigned': {\n                 'new_status': 'accepted',\n                 'action': 'set_owner'\n             },\n             'reopened': {\n                 'new_status': 'accepted',\n                 'action': 'set_owner'\n             },\n         },\n         'reassign': {\n             'new': {\n                 'new_status': 'assigned',\n                 'action': 'set_owner'\n             },\n             'accepted': {\n                 'new_status': 'assigned',\n                 'action': 'set_owner'\n             },\n             'assigned': {\n                 'new_status': 'assigned',\n                 'action': 'set_owner'\n             },\n             'reopened': {\n                 'new_status': 'assigned',\n                 'action': 'set_owner'\n             },\n         },\n         'reopen': {\n             'closed': {\n                 'new_status': 'reopened',\n                 'action': 'del_resolution'\n             }\n         }\n     }\n\n.. confval:: timing_resolution\n\n   Defines the default timing resolution for classes which are mixed with\n   :class:`~stalker.models.mixins.DateRangeMixin`\\ . Stalker uses the\n   TaskJuggler default timing resolution which is 1 hour::\n\n     timing_resolution = datetime.timedelta(hours=1)\n\n.. confval:: task_duration\n\n   Defines the default task duration. If only a start or end value is entered\n   for a :class:`~stalker.models.task.Task` then Stalker calculates the other\n   value by adding or subtracting the default task duration value from it.\n   Default value is 1 hour::\n\n     task_duration = datetime.timedelta(hours=1)\n\n\n.. confval:: task_priority\n\n   Defines the default task priority. This is used by TaskJuggler to prioritize\n   tasks. Should be a number between 0 and 1000. Default value is 500::\n\n     task_priority = 500\n\n.. confval:: working_hours\n\n   Defines the default weekly working hours per week day. Stalker uses the\n   TaskJuggler default value of 9am to 6pm. The values entered are minutes from\n   midnight, and it is a list of lists of two integers. Each list of two\n   integers shows a working hour interval. Default value is::\n\n     working_hours = {\n       'mon': [[540, 1080]], # 9:00 - 18:00\n       'tue': [[540, 1080]], # 9:00 - 18:00\n       'wed': [[540, 1080]], # 9:00 - 18:00\n       'thu': [[540, 1080]], # 9:00 - 18:00\n       'fri': [[540, 1080]], # 9:00 - 18:00\n       'sat': [], # saturday off\n       'sun': [], # sunday off\n     }\n\n.. confval:: daily_working_hours\n\n   Defines the default daily working hour. This is strongly related with the\n   ``working_hours``, ``weekly_working_hours``, ``weekly_working_days`` and\n   ``yearly_working_days`` settings and shows a mean value of daily working\n   hour. Default value is 9::\n\n     daily_working_hours = 9\n\n.. confval:: weekly_working_hours\n\n   Defines the default weekly working hour. This is strongly related with the\n   ``working_hours``, ``daily_working_hours``, ``weekly_working_days`` and\n   ``yearly_working_days`` settings. Default value is 45::\n\n     weekly_working_hours = 45\n\n.. confval:: weekly_working_days\n\n   Defines the default weekly working days. This is strongly related with the\n   ``working_hours``, ``daily_working_hours``, ``weekly_working_hours`` and\n   ``yearly_working_days`` settings. Default value is 5::\n\n     weekly_working_days = 5\n\n.. confval:: yearly_working_days\n\n   Defines the default yearly working days. This is strongly related with the\n   ``working_hours``, ``daily_working_hours``, ``weekly_working_hours`` and\n   ``weekly_working_days`` settings. Default value is 260.714 which equals\n   ``weekly_working_days`` * 52.1428::\n\n     yearly_working_days = 260.714\n\n.. confval:: day_order\n\n   Defines the order of the week days. Default value uses European system::\n\n     day_order = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']\n\n.. confval:: datetime_units\n\n   Defines the date and time units. The order should match the\n   ``datetime_unit_names`` setting. Default value is::\n\n     datetime_units = ['min', 'h', 'd', 'w', 'm', 'y']\n\n.. confval:: datetime_unit_names\n\n   Defines the names of date and time units. The order should match the\n   ``datetime_units`` setting. Default value is::\n\n     datetime_unit_names = ['minute', 'hour', 'day', 'week', 'month', 'year']\n\n.. confval:: datetime_units_to_timedelta_kwargs\n\n   Defines the conversion ratios of each date and time unit. Default value is::\n\n     datetime_units_to_timedelta_kwargs = {\n         'min': {'name': 'minutes', 'multiplier': 1},\n         'h'  : {'name': 'hours'  , 'multiplier': 1},\n         'd'  : {'name': 'days'   , 'multiplier': 1},\n         'w'  : {'name': 'weeks'  , 'multiplier': 1},\n         'm'  : {'name': 'days'   , 'multiplier': 30},\n         'y'  : {'name': 'days'   , 'multiplier': 365}\n     }\n\n.. confval:: task_schedule_models\n\n   Defines the default schedule models. These are highly related with\n   TaskJuggler, so anything entered here should exist in TaskJuggler. Default\n   value is::\n\n     task_schedule_models = ['effort', 'length', 'duration']\n\n.. confval:: task_schedule_constraints\n\n   Defines the default schedule constraints. The order also defines a binary\n   number corresponding to each value (00: none, 01: start, 10:end, 11:both)\n   and used in defining which side of a Task is constrained to a date. Also\n   used by TaskJuggler to constrain the start or end or both dates of a task to\n   a certain date. Also a Task with schedule_constraint is set to 2 (both) is\n   considered a **duration** task even if its schedule_model is set to\n   **effort** or **length**. Default value is::\n\n     task_schedule_constraints = ['none', 'start', 'end', 'both']\n\n.. confval:: tjp_working_hours_template\n\n   Defines a Jinja2 template for converting\n   :class:`~stalker.models.studio.WorkingHours` instances to a TaskJuggler\n   compatible string. By default Stalker converts a WorkingHours instance to a\n   ``workinghours`` statement in TaskJuggler. Default value is::\n\n     tjp_working_hours_template = \"\"\"{% macro wh(wh, day) -%}\n     {%- if wh[day]|length %}    workinghours {{day}} {% for part in wh[day] -%}\n             {%- if loop.index != 1%}, {% endif -%}\n             {{\"%02d\"|format(part[0]//60)}}:{{\"%02d\"|format(part[0]%60)}} - {{\"%02d\"|format(part[1]//60)}}:{{\"%02d\"|format(part[1]%60)}}\n             {%- endfor -%}\n     {%- else %}    workinghours {{day}} off\n     {%- endif -%}\n     {%- endmacro -%}\n     {{wh(workinghours, 'mon')}}\n     {{wh(workinghours, 'tue')}}\n     {{wh(workinghours, 'wed')}}\n     {{wh(workinghours, 'thu')}}\n     {{wh(workinghours, 'fri')}}\n     {{wh(workinghours, 'sat')}}\n     {{wh(workinghours, 'sun')}}\"\"\"\n\n.. confval:: tjp_studio_template\n\n   Defines a Jinja2 template for converting a\n   :class:`~stalker.models.studio.Studio` instance to a TaskJuggler compatible\n   string. By default Stalker converts a Studio instance to a ``project``\n   statement in TaskJuggler. Default value is::\n\n     tjp_studio_template = \"\"\"project {{ studio.tjp_id }} \"{{ studio.name }}\" {{ studio.start.date() }} - {{ studio.end.date() }} {\n         timingresolution {{ '%i'|format((studio.timing_resolution.days * 86400 + studio.timing_resolution.seconds)//60|int) }}min\n         now {{ studio.now.strftime('%Y-%m-%d-%H:%M') }}\n         dailyworkinghours {{ studio.daily_working_hours }}\n         weekstartsmonday\n     {{ studio.working_hours.to_tjp }}\n         timeformat \"%Y-%m-%d\"\n         scenario plan \"Plan\"\n         trackingscenario plan\n     }\n     \"\"\"\n\n.. confval:: tjp_project_template\n\n   Defines a Jinja2 template for converting a\n   :class:`~stalker.models.project.Project` instance to a TaskJuggler\n   compatible string. By default Stalker converts a Project instance to a\n   ``task`` statement in TaskJuggler. Default value is::\n\n     tjp_project_template = \"\"\"task {{project.tjp_id}} \"{{project.name}}\" {\n         {% for task in project.root_tasks %}\n             {{task.to_tjp}}\n         {% endfor %}\n     }\n     \"\"\"\n\n.. confval:: tjp_task_template\n\n   Defines a Jinja2 template for converting a\n   :class:`~stalker.models.task.Task` instance to a TaskJuggler compatible\n   string. By default Stalker converts a Task to a ``task`` statement in\n   TaskJuggler. Default value is::\n\n     tjp_task_template = \"\"\"task {{task.tjp_id}} \"{{task.name}}\" {\n     {% if task.priority != 500 -%}priority {{task.priority}}{%- endif %}\n     {%- if task.depends_on %}\n         depends_on {% for depends_on in task.depends_on %}\n         {%- if loop.index != 1 %}, {% endif %}{{depends_on.tjp_abs_id}}\n     {%- endfor -%}\n     {%- endif -%}\n     {%- if task.is_container -%}\n         {%- for child_task in task.children %}\n             {{ child_task.to_tjp }}\n         {%- endfor %}\n     {%- else %}\n         {% if task.resources|length -%}\n         {% if task.schedule_constraint %}\n             {%- if task.schedule_constraint == 1 or task.schedule_constraint == 3 -%}\n                 start {{ task.start.strftime('%Y-%m-%d-%H:%M') }}\n             {%- endif %}\n             {%- if task.schedule_constraint == 2 or task.schedule_constraint == 3 %}\n                 end {{ task.end.strftime('%Y-%m-%d-%H:%M') }}\n             {%- endif -%}\n         {% endif %}\n         {{task.schedule_model}} {{task.schedule_timing}}{{task.schedule_unit}}\n         allocate {% for resource in task.resources -%}\n             {%-if loop.index != 1 %}, {% endif %}{{resource.tjp_id}}{% endfor %}\n         {%- endif -%}\n         {% for time_log in task.time_logs %}\n         booking {{time_log.resource.tjp_id}} {{time_log.start.strftime('%Y-%m-%d-%H:%M:%S')}} +{{'%i'|format(time_log.duration.days*24 + time_log.duration.seconds/3600)}}h { overtime 2 }\n         {%- endfor -%}\n     {% endif %}\n     }\n     \"\"\"\n\n.. confval:: tjp_department_template\n\n   Defines a Jinja2 template for converting a\n   :class:`~stalker.models.department.Department` instance to a TaskJuggler\n   compatible string. By default Stalker converts a Department to a\n   ``resource`` statement in TaskJuggler. Default value is::\n\n     tjp_department_template = '''resource {{department.tjp_id}} \"{{department.name}}\" {\n     {%- for resource in department.users %}\n         {{resource.to_tjp}}\n     {%- endfor %}\n     }'''\n\n.. confval:: tjp_vacation_template\n\n   Defines a Jinja2 template for converting a\n   :class:`~stalker.models.vacation.Vacation` instance to a TaskJuggler\n   compatible string. By default Stalker converts a Vacation instance to a\n   ``vacation`` statement in TaskJuggler. Default value is::\n\n     tjp_vacation_template = '''vacation {{ vacation.start.strftime('%Y-%m-%d-%H:%M') }}, {{ vacation.end.strftime('%Y-%m-%d-%H:%M') }}'''\n\n.. confval:: tjp_user_template\n\n   Defines a Jinja2 template for converting a\n   :class:`~stalker.models.auth.User` instance to a TaskJuggler ``resource``\n   statement. Default value is::\n\n     tjp_user_template = '''resource {{user.tjp_id}} \"{{user.name}}\"{% if user.vacations %} {\n         {% for vacation in user.vacations -%}\n             {{vacation.to_tjp}}\n         {% endfor -%}\n     }{% endif %}'''\n\n.. confval:: tjp_main_template\n\n   Defines a Jinja2 template for converting all the information coming from\n   Stalker to a TaskJuggler compatible ``tjp`` file. Default value is::\n\n     tjp_main_template = \"\"\"# Generated By Stalker v{{stalker.__version__}}\n     {{studio.to_tjp}}\n     \n     # resources\n     resource resources \"Resources\" {\n     {%- for user in studio.users %}\n         {{user.to_tjp}}\n     {%- endfor %}\n     }\n     \n     # tasks\n     {% for project in studio.active_projects %}\n         {{project.to_tjp}}\n     {% endfor %}\n     \n     # reports\n     taskreport breakdown \"{{csv_file_full_path}}\"{\n         formats csv\n         timeformat \"%Y-%m-%d-%H:%M\"\n         columns id, start, end\n     }\n     \"\"\"\n\n.. confval:: tj_command\n\n   Defines the TaskJuggler command. Stalker uses this configuration value to\n   call TaskJugglers ``tj3`` command.\n\n     tj_command = '/usr/local/bin/tj3',\n\n.. confval:: path_template\n\n   Defines a default value for path template for\n   :class:`~stalker.models.template.FilenameTemplate` instances to be used by\n   :class:`~stalker.models.version.Version` instances. This value is not used\n   yet. Default value is::\n\n     path_template = '{{project.code}}/{%- for parent_task in parent_tasks -%}{{parent_task.nice_name}}/{%- endfor -%}'\n\n.. confval:: filename_template\n\n   Defines a default value for filename template for\n   :class:`~stalker.models.template.FilenameTemplate` instances to be used by\n   :class:`~stalker.models.version.Version` instances. This value is not used\n   yet. Default value is::\n\n     filename_template = '{{task.entity_type}}_{{task.id}}_{{version.variant_name}}_v{{\"%03d\"|format(version.version_number)}}'\n\n.. confval:: sequence_format\n\n   Defines the default file sequence format to be used with `PySeq`_. This\n   value is not used yet. Default value is::\n\n     sequence_format = \"%h%p%t %R\"\n\n   Fore details about the format see the `PySeq documentation`_.\n   \n   .. _PySeq: http://rsgalloway.github.com/pyseq/\n   .. _PySeq documentation: http://rsgalloway.github.com/pyseq/\n\n.. confval:: file_size_format\n\n   Defines the default file size format to be used in UI. Default value is::\n\n     file_size_format = \"%.2f MB\"\n\n.. confval:: date_time_format\n\n   Defines the default datetime format to be used in UI and string\n   representations of datetime.datetime instances. Default value is::\n\n     date_time_format = '%Y.%m.%d %H:%M'\n\n.. confval:: resolution_presets\n\n   Defines default resolution presets. This value is not used yet. Default\n   value is::\n\n     resolution_presets = {\n         \"PC Video\": [640, 480, 1.0],\n         \"NTSC\": [720, 486, 0.91],\n         \"NTSC 16:9\": [720, 486, 1.21],\n         \"PAL\": [720, 576, 1.067],\n         \"PAL 16:9\": [720, 576, 1.46],\n         \"HD 720\": [1280, 720, 1.0],\n         \"HD 1080\": [1920, 1080, 1.0],\n         \"1K Super 35\": [1024, 778, 1.0],\n         \"2K Super 35\": [2048, 1556, 1.0],\n         \"4K Super 35\": [4096, 3112, 1.0],\n         \"A4 Portrait\": [2480, 3508, 1.0],\n         \"A4 Landscape\": [3508, 2480, 1.0],\n         \"A3 Portrait\": [3508, 4960, 1.0],\n         \"A3 Landscape\": [4960, 3508, 1.0],\n         \"A2 Portrait\": [4960, 7016, 1.0],\n         \"A2 Landscape\": [7016, 4960, 1.0],\n         \"50x70cm Poster Portrait\": [5905, 8268, 1.0],\n         \"50x70cm Poster Landscape\": [8268, 5905, 1.0],\n         \"70x100cm Poster Portrait\": [8268, 11810, 1.0],\n         \"70x100cm Poster Landscape\": [11810, 8268, 1.0],\n         \"1k Square\": [1024, 1024, 1.0],\n         \"2k Square\": [2048, 2048, 1.0],\n         \"3k Square\": [3072, 3072, 1.0],\n         \"4k Square\": [4096, 4096, 1.0],\n     }\n\n.. confval:: default_resolution_preset\n\n   Defines the default resolution preset fro new Projects. This value is not\n   used yet. Default value is::\n\n     default_resolution_preset = \"HD 1080\"\n\n.. confval:: project_structure\n\n   Defines the default project structure. This value is not used by Stalker.\n   Default value is::\n\n     project_structure = \"\"\"{% for shot in project.shots %}\n             Shots/{{shot.code}}\n             Shots/{{shot.code}}/Plate\n             Shots/{{shot.code}}/Reference\n             Shots/{{shot.code}}/Texture\n         {% endfor %}\n     {% for asset in project.assets%}\n         {% set asset_path = project.full_path + '/Assets/' + asset.type.name + '/' + asset.code %}\n         {{asset_path}}/Texture\n         {{asset_path}}/Reference\n     {% endfor %}\n     \"\"\"\n\n.. confval:: thumbnail_format\n\n   Defines the default thumbnail format. This value is not used by Stalker.\n   Default value is::\n\n     thumbnail_format = \"jpg\"\n\n.. confval:: thumbnail_quality\n\n   Defines the default thumbnail quality. This value is not used by Stalker.\n   Default value is::\n\n     thumbnail_quality = 70\n\n.. confval:: thumbnail_size\n   \n   Defines the defaul thumbnail size. This value is not used by Stalker.\n   Default value is::\n\n     thumbnail_size = [320, 180]\n"
  },
  {
    "path": "docs/source/contents.rst",
    "content": ".. _contents:\n\nTable of Contents\n=================\n\n.. toctree::\n    :maxdepth: 3\n\n    about.rst\n    installation.rst\n    tutorial.rst\n    design.rst\n    configure.rst\n    upgrade_db.rst\n    contribute.rst\n    roadmap.rst\n    changelog.rst\n    todo.rst\n    summary.rst"
  },
  {
    "path": "docs/source/contribute.rst",
    "content": ".. _contribute_toplevel:\n\n=================\nHow To Contribute\n=================\n\nStalker started as an Open Source project with the expectation of\ncontributions. The soul of the open source is to share the knowledge and\ncontribute.\n\nThese are the areas that you can contribute to:\n * Documentation\n * Testing the code\n * Writing the code\n * Creating user interface elements (graphics, icons etc.)\n\nDevelopment Style\n=================\n\nStalker is developed strictly by following `TDD`_ practices. So every\nparticipant should follow TDD methodology. Skipping this steps is highly\nprohibited. Every added code to the trunk should have a corresponding test and\nthe tests should be written before implementing a single line of code.\n\n.. _TDD: http://en.wikipedia.org/wiki/Test-driven_development\n\n`DRY`_ is also another methodology that a participant should follow. So nothing\nshould be repeated. If something needs to be repeated, then it is a good sign\nthat this part needs to be in a special module, class or function.\n\n.. _DRY: http:http://en.wikipedia.org/wiki/Don%27t_repeat_yourself\n\nTesting\n=======\nAs stated above all the code written should have a corresponding test.\n\nAdding new features should start with design sketches. These sketches could be\nplain text files or mind maps or anything that can express the thing in you\nmind. While writing down these sketches, it should be kept in mind that these\nfiles also could be used to generate the documentation of the system. So\nwriting down the sketches as rest files inside the docs is something very\nmeaningful.\n\nThe design should be followed by the tests. And the test should be followed by\nthe implementation, and the implementation should be followed by tests again,\nuntil you are confident about your code and it is rock solid. Then the\nrefactoring phase can start, and because you have enough tests that will keep\nyour code doing a certain thing, you can freely change your code, because you\nknow that you code will do the same thing if it passes all the tests.\n\nThe first tests written should always fail by having::\n\n    self.fail(\"the test is not implemented yet\")\n\nfailures. This is something good to have. This will inform us that the test is\nnot written yet. After blocking all the tests and you are confident about the\ntests are covering all the aspects of your design sketches, you can start\nwriting the tests.\n\nAnother very important note about the tests are the docstrings of the test\nmethods. You should explain what is this test method testing, and what you\nexpect as a result of the test. It \n\nAfter finishing implementing the tests you can start adding the code that will\npass the tests.\n\nThe test framework of Stalker is unitTest and nose to help testing.\n\nThese python modules should be installed to test Stalker properly:\n\n * Nose\n * Coverage\n\nThe coverage of the tests should be kept as close as possible to %100.\n\nThere is a helper script in the root of the project, called *doTests*. This is\na shell script for linux, which runs all the necessary tests and prints the\ntests results and the coverage table.\n\n.. note::\n  \n  From version 0.1.1 the use of Mocker library is discontinued. The tests are\n  done using real objects. It is done in this way cause the design of the\n  objects were changing too quickly, and it started to be a guess work to see\n  which of the tests are effected by this changes. So the Mocker is removed and\n  it will not be used in future releases.\n\nCoding Style\n============\n\nFor the general coding style every participant should strictly follow `PEP 8`_\nrules, and there are some extra rules as listed below:\n \n * Class names should start with an upper-case letter, function and method\n   names should start with lower-case letter::\n   \n     class MyClass(object):\n         \"\"\"the doc string of the class\n         \"\"\"\n         \n         def __init__(self):\n             pass\n         \n         def my_method(self):\n             pass\n \n * There should be 1 spaces before and after functions and class methods::\n   \n     class StatusBase(object):\n         \"\"\"The StatusBase class\n         \"\"\"\n         \n         def __init__(self, name, abbreviation, thumbnail=None):\n             self._name = self._checkName(name)\n         \n         def _checkName(self, name):\n             \"\"\"checks the name attribute\n             \"\"\"\n             \n             if name == \"\" or not isinstance(name, str):\n                 raise(ValueError(\"the name shouldn't be empty and it should \\\n                     be a str\"))\n                 \n                 return name.title()\n   \n * And also there should be 1 spaces before and after a class body::\n   \n     #-*- coding: utf-8 -*-\n     \n     class A(object):\n         pass\n     \n     class B(object):\n         pass\n     \n     pass\n \n * Any lines that may contain a code or comment can not be longer than 79\n   characters, all the longer lines should be cancelled with \"\\\\\" character and\n   should continue properly from the line below::\n   \n     def _checkName(self, name):\n         \"\"\"checks the name attribute\n         \"\"\"\n         \n         if name == \"\" or not isinstance(name, str):\n             raise(ValueError(\"the name shouldn't be empty and it should be a \\\n             str\"))\n         \n         return name.title()\n   \n   This rule is not followed for the first line of the docstrings and in long\n   function or method names (particularly in tests).\n \n * If anything is going to be checked against being None you should do it in\n   this way::\n   \n     if a is None:\n         pass\n \n * Do not add docstrings to __init__ rather use the classes' own docstring.\n * The first line in the docstring should be a brief summary separated from the\n   rest by a blank line.\n\n\nIf you are going to add a new python file (\\*.py), use the following line in\nthe first line::\n  \n  #-*- coding: utf-8 -*-\n\n.. _PEP 8: http://www.python.org/dev/peps/pep-0008/\n\nSCM - Git\n=========\n\nThe choice of SCM is Git. Every developer should be familiar with it. It\nis a good start to go the `Git Web Site`_ and do the tutorial if you\ndon't feel familiar enough with hg.\n\n.. _Git Web Site: https://git-scm.com/\n\nAdding Changes\n==============\n\nStalker is hosted in `GitHub`_.\n\n.. _GitHub: https://github.com/eoyilmaz/stalker\n\nIf you want to do changes in Stalker, the basic pipeline is as follows:\n\n * Fork Stalker from `GitHub`_ project page.\n\n * Clone your own Stalker repository to your own computer.\n\n * Do your addition, run your tests, and be sure that your part doesn't have\n   any errors or failures.\n\n * Commit your changes.\n\n * Before creating a pull request check if your repository is in sync with the\n   upstream GitHub repository (the repository that you've forked Stalker from)\n   by using the tools supplied in your GitHub project page.\n\n * In case there are new changes in upstream, merge them with yours.\n\n * Do the tests again. If there are problems in your part of the code, solve\n   the errors/failures.\n\n * Commit your changes again.\n\n * And push them to your own GitHub repository.\n\n * And in the original `GitHub`_ page create a Pull Request.\n\n"
  },
  {
    "path": "docs/source/design.rst",
    "content": ".. _design_toplevel:\n\n======\nDesign\n======\n\nThis document explores Stalker, an open-source Python library designed for\nproduction asset management.\n\nIntroduction\n============\n\nWhile primarily designed for VFX and animation studios, Stalker's flexible\narchitecture makes it adaptable to various industries.\n\nAn Asset Management (AM) System is responsible for organizing and storing data\ncreated by users, ensuring easy accessibility. A Production Asset Management\n(ProdAM) System extends the functionality of an AMS by managing production\nsteps, tasks, and enabling collaboration among team members.\n\nImplementing an ProdAM System in an animation or VFX studio is crucial for\nmaintaining order and efficiency. The benefits of a well-organized system far\noutweigh the initial setup effort.\n\nMany studios develop their own custom ProdAM solutions. Stalker aims to provide\na solid foundation for these systems, reducing the need for redundant\ndevelopment efforts.\n\nStalker focuses on organizing assets and tasks within projects,\nstreamlining workflows. It goes beyond basic asset management by incorporating\nproduction steps and collaboration tools.\n\nConcepts\n========\n\nThere are a few key design concepts to understand before diving deeper into\nStalker.\n\nEssentially, Stalker serves as the **Model** component in an **MTV**\n(Model-Template-View) architecture. `Stalker Pyramid`_ provides the\n*Template* and *View* components, defining the presentation layer and user\ninterface. Stalker itself focuses on defining the data structures and their\ninteractions.\n\nStalker Object Model (SOM)\n--------------------------\n\nStalker's robust object model, the Stalker Object Model (SOM), provides a\nflexible framework for building production pipelines. SOM is designed to be\nboth usable out-of-the-box and extensible to meet specific studio needs.\n\nLets look at how a studio simply works and try to create our asset management\nconcepts around it.\n\nAn animation of VFX studio's primary goal is to complete a :class:`.Project`.\nThis project involves creating a series of :class:`.Sequences`, each composed\nof individual :class:`.Shot`\\ s. These shots, in turn, often rely on reusable\n:class:`.Asset`\\ s.\n\nTo break down the work into manageable chunks, Projects, Sequences, Shots, and\nAssets are further divided into :class:`.Task`\\ s. These tasks often represent\nspecific pipeline steps like modeling, look development, rigging, animation,\nlighting, and so on.\n\nThese tasks can be assigned to specific :class:`User`\\ s and require a certain\namount of **effort** to complete. This effort is tracked using\n:class:`.TimeLog`\\ s.\n\nAs work progresses on a task, :class:`.Version`\\ s are created to represent\ndifferent iterations or revisions of the output. These versions are linked to\nfiles stored in a :class:`.Repository`.\n\nAll the names those shown in bold fonts are a class in SOM. and there are a\nseries of other classes to accommodate the needs of a :class:`.Studio`.\n\nThe inheritance diagram of the classes in the SOM is shown below:\n\n.. include:: inheritance_diagram.rst\n\nStalker is a highly configurable and open-source system. This flexibility\nallows for various customization options.\n\nThere are two main approaches to extending Stalker:\n\n    1. **Simple Customization:** This involves adding or modifying existing\n       entities like statuses, types, or other predefined elements. The current\n       Stalker design is well-suited for this level of customization. More\n       details can be found in the `How to Customize Stalker`_ section.\n\n    2. **Extending the SOM:** This involves creating new classes and database\n       tables, or modifying existing ones. This approach is more complex but\n       allows for significant customization of Stalker's core functionality.\n       Refer to the `How To Extend SOM`_ section for further guidance.\n\nFeatures\n--------\n\nStalker boasts a robust feature set designed to streamline your production\npipeline:\n\n 1. **Pure Python:** Built entirely on Python 3.8 and above (continuously\n    tested with Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13), utilizing rigorous\n    Test Driven Development (TDD) practices for exceptional code quality (test\n    coverage is 99.7%).\n\n 2. **SQLAlchemy Integration:** Leverages SQLAlchemy for its database backend\n    and Object-Relational Mapping (ORM) capabilities, ensuring efficient data\n    management. Designed PostgreSQL (versions 14 to 17) in mind but not limited\n    to it.\n\n 3. **Jinja2 Templates:** Employs Jinja2 for flexible file and folder naming\n    conventions. For a structured naming scheme it is possible to define\n    templates like:\n\n    {repository.path}/{project.code}/Assets/{asset.type.name}/{asset.code}/{asset.name}_{asset.type.name}_v{version.version_number:03d}.{version.extension}\n\n 5. **Review Workflow:** Stalker incorporates a comprehensive task review\n    workflow and a robust task status management system to ensure efficient and\n    quality production.\n\n 6. **Automated File Placement:** Upload files, folders, and even file\n    sequences as versions. Stalker utilizes the defined templates to\n    automatically determine their placement on the server, promoting\n    organization.\n\n 7. **Fine-Grained Event System:** Gain complete control over the CRUDL\n    (Create, Read, Update, Delete, List) lifecycle. Define custom callbacks to\n    execute before or after specific operations, enabling tailored behavior.\n\n 8. **Embedded Ticketing System:** Streamline issue tracking and project\n    discussions with a built-in ticketing system.\n\n 9. **TaskJuggler Integration:** Integrate with TaskJuggler for enhanced task\n    management capabilities, supporting basic task attributes.\n\n 10. **Predefined Task Statuses:** Manage task progress efficiently with a\n     pre-defined Task Status Workflow, providing a structured approach to\n     tracking task completion stages.\n\nFor usage examples see :ref:`tutorial_toplevel`\\ .\n\nHow To Customize Stalker\n========================\n\nUpcoming! This part will explain the customization of Stalker.\n\nHow To Extend SOM\n=================\n\nUpcoming! This part will explain how to extend Stalker Object Model or SOM.\n\n\n.. _`Stalker Pyramid`: https://pypi.python.org/pypi/stalker_pyramid\n"
  },
  {
    "path": "docs/source/index.rst",
    "content": ".. _index_toplevel:\n\n=====================\nStalker Documentation\n=====================\n\n.. include:: about.rst\n\n.. include:: contents.rst\n\nIndices and tables\n------------------\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`"
  },
  {
    "path": "docs/source/inheritance_diagram.rst",
    "content": ".. _inheritance_diagram_toplevel:\n\nInheritance Diagram\n===================\n\n.. inheritance-diagram::\n   stalker.exceptions.CircularDependencyError\n   stalker.exceptions.DependencyViolationError\n   stalker.exceptions.LoginError\n   stalker.exceptions.OverBookedError\n   stalker.exceptions.StatusError\n   stalker.models.asset.Asset\n   stalker.models.auth.AuthenticationLog\n   stalker.models.auth.Group\n   stalker.models.auth.LocalSession\n   stalker.models.auth.Permission\n   stalker.models.auth.Role\n   stalker.models.auth.User\n   stalker.models.budget.Budget\n   stalker.models.budget.BudgetEntry\n   stalker.models.budget.Good\n   stalker.models.budget.Invoice\n   stalker.models.budget.Payment\n   stalker.models.budget.PriceList\n   stalker.models.department.Department\n   stalker.models.department.DepartmentUser\n   stalker.models.client.Client\n   stalker.models.entity.Entity\n   stalker.models.entity.EntityGroup\n   stalker.models.entity.SimpleEntity\n   stalker.models.file.File\n   stalker.models.format.ImageFormat\n   stalker.models.message.Message\n   stalker.models.mixins.ACLMixin\n   stalker.models.mixins.CodeMixin\n   stalker.models.mixins.DateRangeMixin\n   stalker.models.mixins.ProjectMixin\n   stalker.models.mixins.ReferenceMixin\n   stalker.models.mixins.ScheduleMixin\n   stalker.models.mixins.StatusMixin\n   stalker.models.mixins.TargetEntityTypeMixin\n   stalker.models.mixins.WorkingHoursMixin\n   stalker.models.note.Note\n   stalker.models.project.Project\n   stalker.models.project.ProjectClient\n   stalker.models.project.ProjectRepository\n   stalker.models.project.ProjectUser\n   stalker.models.repository.Repository\n   stalker.models.review.Review\n   stalker.models.review.Daily\n   stalker.models.review.DailyFile\n   stalker.models.scene.Scene\n   stalker.models.schedulers.SchedulerBase\n   stalker.models.schedulers.TaskJugglerScheduler\n   stalker.models.sequence.Sequence\n   stalker.models.shot.Shot\n   stalker.models.status.Status\n   stalker.models.status.StatusList\n   stalker.models.structure.Structure\n   stalker.models.studio.Studio\n   stalker.models.studio.Vacation\n   stalker.models.studio.WorkingHours\n   stalker.models.tag.Tag\n   stalker.models.task.Task\n   stalker.models.task.TaskDependency\n   stalker.models.task.TimeLog\n   stalker.models.template.FilenameTemplate\n   stalker.models.ticket.Ticket\n   stalker.models.ticket.TicketLog\n   stalker.models.type.EntityType\n   stalker.models.type.Type\n   stalker.models.variant.Variant\n   stalker.models.version.Version\n   stalker.models.wiki.Page\n   :parts: 1\n"
  },
  {
    "path": "docs/source/installation.rst",
    "content": ".. _installation_toplevel:\n\n============\nInstallation\n============\n\n\nHow to Install Stalker\n======================\n\n\nThis document will help you install and run Stalker.\n\nInstall Python\n==============\n\nStalker is completely written with Python, so it requires Python. It currently\nworks with Python version 2.6 and 2.7. So you first need to have Python\ninstalled in your system. On Linux and macOS there is a system wide Python\nalready installed. For Windows, you need to download the Python installer\nsuitable for your Windows operating system (32 or 64 bit) from `Python.org`_\n\n.. _Python.org: http://www.python.org/\n\nInstall Stalker\n===============\n\nThe easiest way to install the latest version of Stalker along with all its\ndependencies is to use the `setuptools`. If your system doesn't have setuptools\n(particularly Windows) you need to install `setuptools` by using `ez_setup`\nbootstrap script.\n\nInstalling `setuptools` with `ez_setup`:\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nThese steps are generally needed just for Windows. Linux and macOS users can skip\nthis part.\n\n1. download `ez_setup.py`_\n2. run the following command in the command prompt/shell/terminal::\n  \n    python ez_setup\n  \n  It will install or build the `setuptools` if there are no suitable installer\n  for your operating system.\n\n.. _ez_setup.py: http://peak.telecommunity.com/dist/ez_setup.py\n\nInstalling Stalker (All OSes):\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nAfter installing the `setuptools` you can run the following command::\n\n  easy_install -U stalker\n\nNow you have installed Stalker along with all its dependencies.\n\nChecking the installation of Stalker\n====================================\n\nIf everything went ok you should be able to import and check the version of\nStalker by using the Python prompt like this::\n  \n  >>> import stalker\n  >>> stalker.__version__\n  0.2.21\n\nFor developers\n==============\n\nIt is highly recommended to create a `VirtualEnv` specific for Stalker\ndevelopment. So to setup a virtualenv for Stalker::\n\n  virtualenv --no-site-packages stalker\n\nThen clone the repository (you need git to do that)::\n\n  cd stalker\n  git clone https://github.com/eoyilmaz/stalker.git stalker\n\nAnd then to setup the virtual environment for development::\n\n  cd stalker\n  ../bin/python setup.py develop\n\nThis command should install any dependent package to the virtual environment.\n\n.. _VirtualEnv: https://pypi.python.org/pypi/virtualenv\n\nInstalling a Database\n=====================\n\nStalker uses a database to store all the data. The only database backend that\ndoesn't require any extra installation is SQLite3. You can setup Stalker to run\nwith an SQLite3 database. But it is much suitable to have a dedicated database\nserver in your studio. And it is recommended to use the same kind of database\nbackend both in development and production to reduce any compatibility problems\nand any migration headaches.\n\nAlthough Stalker is mainly tested and developed on SQLite3, the developers of\nStalker are using it in a studio environment where the main database is\nPosgreSQL, and it is the recommended database for any application based on\nStalker. But, testing and using Stalker in any other database is encouraged. \n\nSee the `SQLAlchemy documentation`_ for supported databases.\n\n.. _SQLAlchemy documentation: http://www.sqlalchemy.org/docs/core/engines.html#supported-dbapis\n"
  },
  {
    "path": "docs/source/roadmap.rst",
    "content": ".. _roadmap_toplevel:\n\n===========================\nStalker Development Roadmap\n===========================\n\nThis section describes the direction Stalker is going.\n\nRoadmap Based on Versions\n=========================\n\nBelow you can find the roadmap based on the version\n\n0.1.0:\n------\n\n * A complete working set of models in SOM which are using\n   SQLAlchemy.ext.declarative.\n\n0.2.0:\n------\n * Web interface\n * Complete ProdAM capabilities.\n\n0.3.0:\n------\n * Complete working Event system\n"
  },
  {
    "path": "docs/source/status_and_status_lists.rst",
    "content": ".. _status_and_status_lists_toplevel:\n\nStatuses and Status Lists\n=========================\n\nIn Stalker, classes mixed with :class:`.StatusMixin` needs to be created with a\n*suitable* :class:`.StatusList` instance.\n\nBecause most of the *statusable* classes are going to be using the same\n:class:`.Status`\\ es (ex: **WIP**, **Pending Review**, **Completed** etc.) over\nand over again, it is much efficient to create those Statuses only once and use\nthem multiple times by grouping them in :class:`.StatusList`\\ s.\n\nA *suitable status list* means, the :attr:`.StatusList.target_entity_type` is\nset to the name of that particular class.\n"
  },
  {
    "path": "docs/source/summary.rst",
    "content": ".. _summary_toplevel:\n\nSummary\n=======\n\n.. autosummary::\n   :toctree: generated/\n   :nosignatures:\n   \n   stalker.db\n   stalker.db.setup\n   stalker.exceptions\n   stalker.exceptions.CircularDependencyError\n   stalker.exceptions.LoginError\n   stalker.exceptions.OverBookedError\n   stalker.exceptions.StatusError\n   stalker.models\n   stalker.models.asset.Asset\n   stalker.models.auth.AuthenticationLog\n   stalker.models.auth.Group\n   stalker.models.auth.LocalSession\n   stalker.models.auth.Role\n   stalker.models.auth.Permission\n   stalker.models.auth.User\n   stalker.models.budget.Budget\n   stalker.models.budget.BudgetEntry\n   stalker.models.budget.Good\n   stalker.models.budget.Invoice\n   stalker.models.budget.Payment\n   stalker.models.budget.PriceList\n   stalker.models.department.Department\n   stalker.models.department.DepartmentUser\n   stalker.models.client.Client\n   stalker.models.client.ClientUser\n   stalker.models.entity.Entity\n   stalker.models.entity.EntityGroup\n   stalker.models.entity.SimpleEntity\n   stalker.models.file.File\n   stalker.models.format.ImageFormat\n   stalker.models.message.Message\n   stalker.models.mixins.ACLMixin\n   stalker.models.mixins.CodeMixin\n   stalker.models.mixins.DateRangeMixin\n   stalker.models.mixins.ProjectMixin\n   stalker.models.mixins.ReferenceMixin\n   stalker.models.mixins.ScheduleMixin\n   stalker.models.mixins.StatusMixin\n   stalker.models.mixins.TargetEntityTypeMixin\n   stalker.models.mixins.WorkingHoursMixin\n   stalker.models.note.Note\n   stalker.models.project.Project\n   stalker.models.project.ProjectClient\n   stalker.models.project.ProjectRepository\n   stalker.models.project.ProjectUser\n   stalker.models.repository.Repository\n   stalker.models.review.Review\n   stalker.models.review.Daily\n   stalker.models.review.DailyFile\n   stalker.models.scene.Scene\n   stalker.models.schedulers.SchedulerBase\n   stalker.models.schedulers.TaskJugglerScheduler\n   stalker.models.sequence.Sequence\n   stalker.models.shot.Shot\n   stalker.models.status.Status\n   stalker.models.status.StatusList\n   stalker.models.structure.Structure\n   stalker.models.studio.Studio\n   stalker.models.studio.WorkingHours\n   stalker.models.tag.Tag\n   stalker.models.task.Task\n   stalker.models.task.TaskDependency\n   stalker.models.task.TimeLog\n   stalker.models.template.FilenameTemplate\n   stalker.models.ticket.Ticket\n   stalker.models.ticket.TicketLog\n   stalker.models.type.EntityType\n   stalker.models.type.Type\n   stalker.models.variant.Variant\n   stalker.models.version.Version\n   stalker.models.wiki.Page\n"
  },
  {
    "path": "docs/source/task_review_workflow.rst",
    "content": ".. _task_review_workflow_toplevel:\n\n====================\nTask Review Workflow\n====================\n\nIntroduction\n============\n\nAll tasks in Stalker have a specific purpose and goal. :class:`.Task` resources\nare responsible for completing these tasks, while task responsible ensure their\ncorrect execution. The Task Review Workflow provides a mechanism for reviewing\ntasks withing Stalker.\n\nThe Workflow\n============\n\nA task resource can request a review from the task's responsible at any stage\nof task completion, including for supervisory purposes.\n\nWhen a review request is made, Stalker creates a :class:`.Review` instance\nassociated with the :class:`.Task`. This :class:`.Review` instance tracks the\nreview's status (initially set to `NEW`), any requested revisions (including\ndescription, additional time allowances, and desired task statuses).\n\nSingle Responsible and Resource Scenario\n========================================\n\nConsider a task with a single responsible and a single resource. When the\nresource requests a review, Stalker creates a :class:`.Review` instance\nassigned to the responsible. The responsible then review the task and can:\n\n- **Approve**: If the task is complete, the responsible can approve the review\n  by calling the :meth:`.Review.approve()` method.\n\n- **Request Revision**: If additional work is required, the responsible can\n  request a revision by calling the :meth:`.Review.request_revision()` method.\n  This involves specifying the necessary revisions, additional time.\n\nMultiple Responsible Scenario\n=============================\n\nIf multiple responsible are assigned to a task, a Review instance is created\nfor each of them when a review is requested. The task is considered incomplete\nuntil all responsible have approved the review. If multiple responsible request\nrevisions, the total revision time is added to the task, and the resource\ncontinues working.\n\nDependent Tasks\n===============\n\nWhen a revision is requested for a completed task with dependent tasks, different\nscenarios arise:\n\n**Scenario A: Dependent Tasks Are All in Ready-To-Start (RTS) Status*\n\nIf there are no dependent tasks or none have started (all in `RTS`), the\ndependent tasks are set to `Waiting For Dependency (WFD)` to prevent work until\nthe original task is completed.\n\n**Scenario B: Started or Completed Dependent Tasks**\n\nIf there are dependent tasks and some have started or completed,\ntheir status is updated based on the following table:\n\n  +----------------+--------------+\n  | Initial Status | Final Status |\n  +----------------+--------------+\n  | WFD            | WFD          |\n  +----------------+--------------+\n  | RTS            | WFD          |\n  +----------------+--------------+\n  | WIP            | DREV         |\n  +----------------+--------------+\n  | PREV           | PREV         |\n  +----------------+--------------+\n  | HREV           | DREV         |\n  +----------------+--------------+\n  | DREV           | DREV         |\n  +----------------+--------------+\n  | OH             | OH           |\n  +----------------+--------------+\n  | STOP           | STOP         |\n  +----------------+--------------+\n  | CMPL           | DREV         |\n  +----------------+--------------+ \n\nOnce the revised tasks is approved and set back to `CMPL`, dependent tasks are\nrestored to their original statuses based on their time logs:\n\n  +-----------------+------+------+-----+----+------+\n  |                 | DREV | PREV | WFD | OH | STOP |\n  +-----------------+------+------+-----+----+------+\n  | Has No TimeLogs | RTS  | PREV | RTS | OH | STOP |\n  +-----------------+------+------+-----+----+------+\n  | Has TimeLogs    | WIP  | PREV | WIP | OH | STOP |\n  +-----------------+------+------+-----+----+------+\n\nAs you see the task statuses will be restored to their original statuses except\nfor HREV and CMPL. HREV tasks can not be restored, because even in a normal\nsituation where there are no revision requested for the dependent task,\ncreating a new time log will set its status to WIP, and a CMPL task can not be\nstored to CMPL status because there were revisions to the depending task so\nthere should be some work to be done to update this task, so it is restored as\nWIP.\n\nThe following workflow diagram illustrates the task status transitions, and it\nis a good idea to familiarize yourself with the task statuses used in Stalker.\n\n.. image:: _static/images/Task_Status_Workflow.png\n      :width: 637 px\n      :height: 381 px\n      :align: center\n\nRevision Counter\n================\n\nBoth :class:`.Task` and :class:`.Review` instances have ``review_number``\nattribute. Reviews with the same ``review_number`` belong to the same review\nset. Multiple :class:`.Review` instances with the same\n:attr:`Review.review_number` can exist if they have different reviewers.\n\n- The :attr:`.Task.review_number` starts at 0 for the initial revision and\n  increments with each review requests. So a :class:`.Task` with\n  ``review_number`` is 0 has no reviews yet.\n\n- A newly created :class:`.Review` instance has a ``review_number`` one higher\nthan the :attr:`.Task.review_number` at the time of creation.\n\nTo create revisions effectively, use the :meth:`.Task.request_review()` method.\nThis ensures correct :class:`.Review` instance creation per reviewer and\ncorrect ``review_number`` assignment and will return the newly created\n:class:`.Review` instances as a list. Each responsible should use the\n:meth:`.Review.approve()` or :meth:`.Review.request_revision()` methods to set\nthe appropriate status and additional revision information.\n"
  },
  {
    "path": "docs/source/todo.rst",
    "content": ".. _todo_toplevel:\n\n.. include:: source/../../../TODO.rst\n"
  },
  {
    "path": "docs/source/tutorial/asset_management.rst",
    "content": ".. _tutorial_asset_management_toplevel:\n\nAsset Management\n================\n\nNow that we've created projects, tasks and resources, it's time to manage the\nfiles generated during production.\n\nFile Storage and Repository setup\n---------------------------------\n\nContrary to a Source Code Management (SCM) System where revisions to a file is\nhandled incrementally, Stalker handles file versions all together. Meaning\nthat, all the files (:class:`.File`) that are created for individual versions\n(:class:`.Version`) of a task are individual files stored in a shared location\naccessible to everyone in your studio. This location is called a\n:class:`.Repository` in Stalker.\n\nDefining Repository Paths\n-------------------------\n\nA repository can be a network share or a locally mounted directory. You can\ndefine multiple repositories for different project types or needs. We've\nalready created a repository while creating our first project. But the\nrepository has missing information. Here's how to define paths for a commercial\nproject repository::\n\n.. code-block:: python\n\n    commercial_repo.linux_path   = \"/mnt/M/commercials\"\n    commercial_repo.macos_path   = \"/Volumes/M/commercials\"\n    commercial_repo.windows_path = \"M:/commercials\"\n    # Stalker automatically corrects backslashes (\\) to forward slashes (/)\n\nAnd if you ask for the path to a repository object, it will always return the\ncorrect path according to your operating system:\n\n.. code-block:: python\n\n    print(commercial_repo.path)\n\nYou'll get the appropriate path based on your OS:\n\n* **Windows:** M:/commercials\n* **Linux:** /mnt/M/commercials\n* **macOS:** /Volumes/M/commercials\n\nThis ensures consistent file path handling across different platforms.\n\n.. note::\n\n  Stalker consistently uses forward slashes (/) in path definitions, regardless\n  of the operating system. This applies even if you initially specify paths\n  with backward slashes (\\\\).\n\n\nAssigning Repository to Project\n-------------------------------\n\nConnecting a repository to your project lets Stalker know where project files\nare stored. However, it still needs information about the project's specific\ndirectory structure.\n\nDefining Project Structure\n--------------------------\n\nA :class:`.Structure` object defines the directory hierarchy for your project\nwithin the repository. We create a structure named \"Commercial Projects\nStructure\" and assign it to our project:\n\n.. code-block:: python\n\n    from stalker import Structure\n\n    commercial_project_structure = Structure(\n        name=\"Commercial Projects Structure\"\n    )\n\n    # now assign this structure to our project\n    new_project.structure = commercial_project_structure\n\n.. versionadded:: 0.2.13\n\n   Starting with Stalker version 0.2.13, :class:`.Project` instances can be\n   associated with multiple :class:`.Repository` instances. This allows for\n   more flexible file management, such as storing published version files on a\n   separate server or directing rendered outputs to a different location.\n\n   While the following examples are simplified, future versions will showcase\n   the full potential of multiple repositories.\n\nCreating Filename Templates\n---------------------------\n\nNext we create :class:`.FilenameTemplate` instances. These templates define how\nfilenames and paths will be generated for by :class:`.Version` instances\nassociated with tasks.\n\nHere, we create a :class:`.FilenameTemplate` named \"Task Template for\nCommercials\" that uses `Jinja2`_ syntax for the ``path`` and ``filename``\narguments. The :class:`.Version.generate_path()` method knows how to render\nthese templates to generate a ``pathlib.Path`` object:\n\n.. code-block:: python\n\n    from stalker import FilenameTemplate\n\n    task_template = FilenameTemplate(\n        name='Task Template for Commercials',\n        target_entity_type='Task',\n        path='$REPO{{project.repository.code}}/{{project.code}}/{%- for p in parent_tasks -%}{{p.nice_name}}/{%- endfor -%}',\n        filename='{{version.nice_name}}_r{{\"%02d\"|format(version.revision_number)}}_v{{\"%03d\"|format(version.version_number)}}'\n    )\n\n  # Append the template to the project structure\n  commercial_project_structure.templates.append(task_template)\n\n  # No need to add anything as the project is already in the database\n  DBsession.commit()\n\nExplanation of the Template:\n\n* ``$REPO{{project.repository.code}}``: This references the first repository\n  assigned to the project. Importantly, this uses an environment variable\n  ``$REPO``. Stalker dynamically creates environment variables for each\n  repository upon database connection or creation, simplifying path definitions\n  within templates.\n\n* ``{{project.code}}``: This represent the project code and it is guaranteed to\n  be file system safe.\n\n* ``{%- for p in parent_tasks -%}{{p.nice_name}}/{%- endfor -%}``: This loop\n  iterates over parent tasks, creating subdirectories for each.\n\n* ``{{version.nice_name}}_r{{\"%02d\"|format(version.revision_number)}}_v{{\"%03d\"|format(version.version_number)}}``: This\n  defines the filename format with revision and version numbers padded.\n\nCreating and Managing Versions\n------------------------------\n\nNow, let's create a :class:`.Version`` instance for the \"comp\" task:\n\n.. code-block:: python\n\n    from stalker import Version\n\n    vers1 = Version(task=comp)\n\n    # Generate a path using the template\n    path = vers1.generate_path()\n\n    print(path.parent)  # '$REPO33/FC/SH001/comp'\n    print(path.name)    # 'SH001_comp_r01_v001'\n    print(path)         # '$REPO33/FC/SH001/comp/SH001_comp_r01_v001'\n\n    # Absolute paths with repository root based on your OS\n    # unfortunately the Path object doesn't directly render environment variables\n    print(os.path.expandvars(path.parent))       # '/mnt/M/commercials/FC/SH001/comp'\n    print(os.path.expandvars(path))  # '/mnt/M/commercials/FC/SH001/comp/SH001_comp_r01_v001'\n\n    # Get the revision number (manually incremented)\n    print(vers1.revision_number)      # 1\n\n    # Get version number (automatically incremented)\n    print(vers1.version_number)      # 1\n\n    # commit to database\n    DBsession.commit()\n\nStalker automatically generates a consistent path and filename for the version\nbased on the template.\n\nStalker eliminates the need for those cumbersome and confusing file naming\nconventions like ``Shot1_comp_Final``, ``Shot1_comp_Final_revised``,\n``Shot1_comp_Final_revised_Final``,\n``Shot1_comp_Final_revised_Final_real_final`` ...and the list goes on,\nwe've all experienced the frustration of such naming conventions, haven't we\n😊.. It ensures a consistent and organized file structure, making asset\nmanagement significantly more efficient.\n\nThe :attr:`.Version.is_published` attribute within the :class:`.Version` class\nhelps differentiate between finalized and in-progress versions. Setting\n:attr:`.is_published` to ``True`` flags a version as ready for use or review.\n\n.. code-block:: python\n\n    vers1.is_published = False  # This version is still being worked on\n\nAutomatic Version Numbering\n---------------------------\n\nStalker automatically increments version numbers for each new version created\nfor the same task. This ensures you always have the latest iteration readily\nidentified.\n\n.. code-block:: python\n\n    vers2 = Version(task=comp)\n    print(vers2.version_number)        # Output: 2\n    print(vers2.generate_path().name)  # Output: 'SH001_comp_r01_v002'\n\n    vers3 = Version(task=comp)\n    print(vers3.version_number)        # Output: 3\n    print(vers3.generate_path().name)  # Output: 'SH001_comp_r01_v003'\n\n:attr:`.Version.revision_number` is not updated automatically and left for the\nuser to update:\n\n.. code-block:: python\n\n    vers4 = Version(task=comp, revision_number=2)\n    print(vers4.version_number)        # Output: 4\n    print(vers4.generate_path().name)  # Output: 'SH001_comp_r02_v001'\n\nQuerying Versions\n-----------------\n\nYou can retrieve all versions associated with a specific task using either\nusing the :attr:`.Task.versions` attribute or by doing a database query.\n\n.. code-block:: python\n\n    # using pure Python\n    vers_from_python = comp.versions\n    # [<FC_SH001_comp_r01_v001 (Version)>,\n    #  <FC_SH001_comp_r01_v002 (Version)>,\n    #  <FC_SH001_comp_r01_v003 (Version)>]\n\n    # # Using SQLAlchemy query\n    vers_from_query = Version.query.filter_by(task=comp).all()\n\n    # again returns\n    # [<FC_SH001_comp_r01_v001 (Version)>,\n    #  <FC_SH001_comp_r01_v002 (Version)>,\n    #  <FC_SH001_comp_r01_v003 (Version)>]\n\n    # Both methods return a list of Version objects\n    assert vers_from_python == vers_from_query\n\n.. _Jinja2: http://jinja.pocoo.org/\n\n.. note::\n\n   Files related to :class:`.Version` instances can be created by instantiating\n   the :class:`.File` class. The :attr:`.File.full_path` can be set with the\n   value coming from the :meth:`.Version.generate_path()` method, which by\n   default will contain environment variables for the repository path and\n   Stalker will save the :att:`.File.full_path` attribute value in the database\n   without converting the environment variables so the paths will stay\n   operating system independent.\n\nYou can define default directories within your project structure using custom\ntemplates.\n\n.. code-block:: python\n\n    commercial_project_structure.custom_template = \"\"\"\n    Temp\n    References\n    References/Movies\n    References/Images\n    \"\"\"\n\nWhen executed, this template will generate the following directory structure:\n\n.. code-block:: shell\n\n    Temp\n    References\n        Movies\n        Images\n"
  },
  {
    "path": "docs/source/tutorial/basics.rst",
    "content": ".. _tutorial_basics_toplevel:\n\nBasics\n======\n\nImagine you've just installed Stalker and want to integrate it into your first\nproject. The first step involves connecting to the database to store\ninformation about your studio and projects.\n\nConnecting to the Database\n--------------------------\n\nA helper function is provided for connecting to the default database. Use the\nfollowing command:\n\n.. code-block:: python\n\n    from stalker.db.setup import setup\n    setup({\"sqlalchemy.url\": \"sqlite:///\"})\n\nThis creates an in-memory SQLite3 database, suitable only for testing. For\npractical use, consider a file-based SQLite3 database:\n\n.. code-block:: python\n\n    # Windows\n    setup({\"sqlalchemy.url\": \"sqlite:///C:/studio.db\"})\n\n    # Linux or macOS\n    setup({\"sqlalchemy.url\": \"sqlite:////home/ozgur/studio.db\"})\n\n\nThis command will do the following:\n\n1. **Database Connection:** Creates an `engine`_ to establish the connection.\n2. **Database Creation:** Creates the SQLite3 database file if doesn't exist.\n3. **Session Creation:** Creates a `session`_ instance for interacting with the\n   database.\n4. **Mapping:** Defines how SOM classes `map`_ to database tables (see\n   SQLAlchemy documentation for details).\n\n.. _session: http://www.sqlalchemy.org/docs/orm/session.html\n.. _engine: http://www.sqlalchemy.org/docs/core/engines.html\n.. _map: http://www.sqlalchemy.org/docs/orm/mapper_config.html\n\n.. note::\n\n   While SQLite3 support was officially dropped in Stalker v0.2.18, it's still\n   possible to use SQLite3 databases with Stalker. However, PostgreSQL (versions\n   14 to 17) is the recommended database backend.\n\nDatabase Initialization\n-----------------------\n\nOn your initial connection, use `db.init()` to create essential default data\nfor Stalker to function properly:\n\n.. code-block:: python\n\n    db.init()\n\nThis is a one-time operation; subsequent calls to `db.init()` won't break\nanything, but they're unnecessary.\n\nCreating a Studio\n-----------------\n\nLet's create a :class:`.Studio` object to represent your studio:\n\n.. code-block:: python\n\n    from stalker import Studio\n    my_studio = Studio(\n        name='My Great Studio'\n    )\n\nWe'll explain the concept of :class:`.Studio` later in the tutorial.\n\nCreating a User\n---------------\n\nNow, let's create a :class:`.User` object representing yourself in the\ndatabase:\n\n1. Import the :class:`.User` class:\n\n   .. code-block:: python\n\n        from stalker import User\n\n2. Create the :class:`.User` object:\n\n   .. code-block:: python\n\n        me = User(\n            name=\"Erkan Ozgur Yilmaz\",\n            login=\"eoyilmaz\",\n            email=\"some_email_address@gmail.com\",\n            password=\"secret\",\n            description=\"This is me\"\n        )\n\nThis creates a user object that represents you.\n\nCreating and Assigning a Department\n-----------------------------------\n\n1. Import the :class:`.Department` class:\n\n   .. code-block:: python\n\n        from stalker import Department\n\n2. Create a :class:`.Department` object:\n\n   .. code-block:: python\n\n        tds_department = Department(\n            name=\"TDs\",\n            description=\"This is the TDs department\"\n        )\n\n3. Assign yourself to the department:\n\nThere are two ways to do this:\n\n* Using the :class:`.Department` object:\n\n  .. code-block:: python\n\n    tds_department.users.append(me)\n\n* Using the :class:`.User` object:\n\n  .. code-block:: python\n\n    me.departments.append(tds_department)\n\nBoth methods achieve the same result.\n\nVerifying Department Assignment\n-------------------------------\n\nYou can verify the assignment by printing the :attr:`.User.departments` for\nyour user:\n\n.. code-block:: python\n\n    print(me.departments)\n    # Output: [<TDs (Department)>]\n\nSaving Data to the Database\n---------------------------\n\nSo far, the data hasn't been saved to the database yet. To commit the changes,\nuse the :class:`.DBSession` object:\n\n.. code-block:: python\n\n    from stalker.db.session import DBSession\n\n    DBSession.add(my_studio)\n    DBSession.add(me)\n    DBSession.add(tds_department)\n    DBSession.commit()\n\nRetrieving Data\n---------------\n\nLet's retrieve data from the database. Here, we'll fetch all departments, get\nthe second one (excluding the default `admins` department), and print the name\nof its first member:\n\n.. code-block:: python\n\n    all_departments = Department.query.all()\n    print(all_departments)\n    # Output: [<admins (Department)>, <TDs (Department)>]\n    # \"admins\" department is created by default\n\n    admins = all_departments[0]\n    tds = all_departments[1]\n\n    all_users = tds.users  # Department.users is a \"synonym\" for Department.members\n    #                        they are essentially the same attribute\n\n    print(all_users[0])\n    # Output: <Erkan Ozgur Yilmaz ('eoyilmaz') (User)>\n\nThis retrieves and prints the information.\n"
  },
  {
    "path": "docs/source/tutorial/collaboration.rst",
    "content": ".. _tutorial_collaboration_toplevel:\n\nCollaboration in Stalker\n========================\n\nWhile we've covered the core functionalities of Stalker, effective\ncollaboration is essential in any production pipeline. Stalker provides several\ntools to facilitate communication and knowledge sharing among team members.\n\nNote System:\n\n    You can leave :class:`Note`\\ s on any Stalker :class:`.Entity` (except\n    other :class:`.Note`\\ s and :class:`.Tag`\\ s). This allows you to add\n    comments, reminders, or specific instructions to tasks, assets, versions,\n    and other objects.\n\nMessaging System:\n\n    A direct messaging system (currently under development) will allow you to\n    send private messages to individuals or groups of users.\n\nTicket System:\n\n    Create and track tickets for specific projects to report issues, request\n    features, or discuss project-related matters.\n\nWiki Pages:\n\n    Create and maintain project-specific wiki pages to document procedures,\n    best practices, and other important information.\n"
  },
  {
    "path": "docs/source/tutorial/conclusion.rst",
    "content": ".. _tutorial_toplevel:\n\nConclusion\n==========\n\nIn this tutorial, you have nearly learned a quarter of what Stalker supplies as\na Python library.\n\nStalker provides a robust framework for production asset management, serving\nthe needs of both large and small studios. Its 16-years development history (as\nof 2025) and use in major feature films and countless commercials is a\ntestament to its effectiveness.\n\nWhile Stalker itself lacks a graphical user interface (GUI), its power extends\nbeyond raw code. Here are some additional tools that leverage Stalker's core\nfunctionality:\n\n`Stalker Pyramid`_:\n\n    A web application built using the `Pyramid`_ framework, utilizing Stalker\n    as its database model. This allows for user-friendly web-based interaction\n    with project data.\n\n`Anima Pipeline`_:\n\n    A pipeline library that incorporates Stalker, showcasing how its\n    functionalities can be integrated into a pipeline management system.\n    Notably, Anima demonstrates the creation of Qt UIs using Stalker.\n\nFor a deeper dive into how Stalker interacts with UIs and web applications,\nconsider exploring the repositories of `Stalker Pyramid`_ and\n`Anima Pipeline`_.\n\nBy understanding how Stalker integrates with these tools, you can unlock its\nfull potential for streamlining your production workflows.\n\n.. _Stalker Pyramid: https://www.github.com/eoyilmaz/stalker_pyramid\n.. _Anima Pipeline: https://github.com/eoyilmaz/anima\n.. _Pyramid: https://trypyramid.com/\n"
  },
  {
    "path": "docs/source/tutorial/creating_simple_data.rst",
    "content": ".. _tutorial_creating_simple_data_toplevel:\n\nCreating Simple Data\n====================\n\nLet's imagine you're starting a new commercial project and want to use Stalker.\nThe first step is to create a :class:`.Project` object to store project\ninformation.\n\nProject setup\n-------------\n\n.. versionadded:: 0.2.24.2\n\n   Starting with Stalker v0.2.24.2, you no longer need to manually create\n   :class:`.StatusList` instances for :class:`.Project` objects. The\n   :func:`stalker.db.setup.init()` function will automatically create them\n   during database initialization.\n\n.. note::\n   When the Stalker database is first initialized (with ``db.setup.init()``), a\n   set of default :class:`.Status` instances for :class:`.Task`,\n   :class:`.Asset`, :class:`.Shot`, :class:`.Sequence`, :class:`.Ticket` and\n   :class:`.Variant` classes are created, along with their respective\n   :class:`.StatusList` instances.\n\nCreating a Repository\n---------------------\n\nTo create a project, we first need to create a :class:`.Repository`.\nThe Repository (or Repo) is a directory on your file server, where project\nfiles are stored and accessible to all workstations and render farm computers:\n\n.. code-block:: python\n\n    from stalker import Repository\n\n    commercial_repo = Repository(\n        name=\"Commercial Repository\",\n        code=\"CR\"\n    )\n\n.. versionadded:: 0.2.24\n\n   Starting with Stalker version 0.2.24 :class:`.Repository` instances have a\n   :attr:`stalker.models.repository.Repository.code` attribute to help\n   generate universal paths across operating systems and Stalker installations.\n\n:class:`.Repository` class will be explained in detail in upcoming sections.\n\nCreating a Project\n------------------\n\nNow, let's create the project:\n\n.. code-block:: python\n\n    new_project = Project(\n        name=\"Fancy Commercial\",\n        code='FC',\n        repositories=[commercial_repo],\n    )\n\nAdding Project Details\n----------------------\n\nLet's add more details to the project:\n\n.. code-block:: python\n\n    import tzlocal\n    import datetime\n    from stalker import ImageFormat\n\n    new_project.description = (\n        \"The commercial is about this fancy product. The \"\n        \"client want us to have a shiny look with their \"\n        \"product bla bla bla...\"\n    )\n\n    new_project.image_format = ImageFormat(\n        name=\"HD 1080\",\n        width=1920,\n        height=1080\n    )\n\n    new_project.fps = 25\n    local_tz = tzlocal.get_localzone()\n    new_project.end = datetime.datetime(2024, 5, 15, tzinfo=local_tz)\n    new_project.users.append(me)\n\nSaving the Project\n------------------\n\nTo save the project and its associated data to the database:\n\n.. code-block:: python\n\n    DBSession.add(new_project)\n    DBSession.commit()\n\nEven though we've created multiple objects (project, repository etc.), we only\nneed to add the ``new_project`` object to the database. Stalker will handle the\nrelationships and save the related objects automatically.\n\n.. note::\n\n   Starting with Stalker v0.2.18, all the datetime information must include\n   timezone information. In the example, we've used the local timezone.\n\nCreating Sequences and Shots\n----------------------------\n\nA :class:`.Project` is typically composed of :class:`.Task` instances, which\nrepresent units of work that need to be completed. A :class:`.Task` in Stalker\ndefines the total `effort` required to be considered finished. Tasks can also\nbe `duration` or `length` based, in which case they define the required time\nto be considered finished. Leaf tasks, the final tasks in a task hierarchy,\nare assigned to specific :class:`.User` instances who are responsible for\ncompleting them. More details about :class:`.Task` and its attributes can be\nfound in the :class:`.Task` class documentation. :class:`.Asset`,\n:class:`.Shot` and :class:`.Sequences` are specialized types of Tasks.\n\nLet's create a :class:`.Sequence`:\n\n.. code-block:: python\n\n    from stalker import Sequence\n\n    seq1 = Sequence(\n        name=\"Sequence 1\",\n        code=\"SEQ1\",\n        project=new_project,\n    )\n\nAnd some :class:`.Shot`\\ s withing the sequence:\n\n.. code-block:: python\n\n    from stalker import Shot\n\n    sh001 = Shot(\n        name='SH001',\n        code='SH001',\n        project=new_project,\n        sequences=[seq1]\n    )\n    sh002 = Shot(\n        code='SH002',\n        project=new_project,\n        sequences=[seq1]\n    )\n    sh003 = Shot(\n        code='SH003',\n        project=new_project,\n        sequences=[seq1]\n    )\n\nSave the changes to the database:\n\n.. code-block:: python\n\n    DBsession.add_all([sh001, sh002, sh003])\n    DBsession.commit()\n\n.. note::\n\n   * While we've created :class:`.Shot` objects with a :class:`.Sequence`\n     instance, it's not strictly necessary. You can create :class:`.Shot`\n     objects without assigning them to a Sequence.\n\n   * For smaller projects like commercials, you might skip creating sequences\n     altogether.\n\n   * For larger projects like feature films, using sequences to group shots is\n     recommended.\n"
  },
  {
    "path": "docs/source/tutorial/extending_som.rst",
    "content": ".. _tutorial_extending_som_toplevel:\n\nExtending SOM (coming)\n======================\n\nThis part will be covered soon\n"
  },
  {
    "path": "docs/source/tutorial/pipeline.rst",
    "content": ".. _tutorial_pipeline_toplevel:\n\nPipeline\n========\n\nSo far, we've covered the basics of creating data in Stalker. However. to fully\nutilize Stalker's power, we need to define our studio's **pipeline**. This\ninvolves creating tasks and establishing dependencies between them.\n\nCreating Tasks\n--------------\n\nLet's create some :class:`.Task`\\ s for one of the shots we created earlier:\n\n.. code-block:: python\n\n    from stalker import Task\n\n    previs = Task(\n        name=\"Previs\",\n        parent=sh001\n    )\n\n    matchmove = Task(\n        name=\"Matchmove\",\n        parent=sh001\n    )\n\n    anim = Task(\n        name=\"Animation\",\n        parent=sh001\n    )\n\n    lighting = Task(\n        name=\"Lighting\",\n        parent=sh001\n    )\n\n    comp = Task(\n        name=\"Comp\",\n        parent=sh001\n    )\n\nDefining Dependencies\n---------------------\n\nNow, let's define the dependencies between these tasks:\n\n.. code-block:: python\n\n    comp.depends_on = [lighting]\n    lighting.depends_on = [anim]\n    anim.depends_on = [previs, matchmove]\n\nBy establishing these dependencies, we're telling Stalker that certain tasks\nneed to be completed before others can begin. For example, the \"Comp\" task\ndepends on the \"Lighting\" task, meaning the \"Lighting\" task must be finished\nbefore the \"Comp\" task can start. Stalker uses these dependencies to schedule\ntasks effectively. \n\nWe'll delve deeper into task scheduling and other pipeline-related concepts\nlater in this tutorial.\n"
  },
  {
    "path": "docs/source/tutorial/query_update_delete_data.rst",
    "content": ".. _tutorial_query_update_delete_data_toplevel:\n\nQuerying, Updating and Deleting Data\n====================================\n\nNow that you've created some data, let's explore how to update and delete it.\n\nUpdating Data\n-------------\n\nImagine you created a shot with incorrect information:\n\n.. code-block:: python\n\n    sh004 = Shot(\n        code='SH004',\n        project=new_project,\n        sequences=[seq1]\n    )\n    DBSession.add(sh004)\n    DBSession.commit()\n\nLater, you realize you need to fix the code:\n\n.. code-block:: python\n\n    sh004.code = \"SH005\"\n    DBsession.commit()\n\nRetrieving Data\n---------------\n\nTo retrieve a shot from the database, you can use a query:\n\n.. code-block:: python\n\n    wrong_shot = Shot.query.filter_by(code=\"SH005\").first()\n\nThis retrieves the first shot with the code \"SH005\".\n\nUpdating Retrieved Data\n-----------------------\n\nIf you need to modify the retrieve data:\n\n.. code-block:: python\n\n    wrong_shot.code = \"SH004\"  # Correct the code\n    DBsession.commit()  # Save the changes\n\nDeleting Data\n-------------\n\nTo delete data, use the :meth:`DBSession.delete()` method:\n\n.. code-block:: python\n\n    DBsession.delete(wrong_shot)\n    DBsession.commit()\n\nAfter deleting data, you program variables might still hold references to the\ndeleted objects, but those objects no longer exist in the database.\n\n.. code-block:: python\n\n    wrong_shot = Shot.query.filter_by(code=\"SH005\").first()\n    print(wrong_shot) # This will print None\n\nFor More information\n--------------------\n\nFor advanced update and delete options (like cascades) in SQLAlchemy, refer to\nthe official `SQLAlchemy documentation`_.\n\n.. _SQLAlchemy documentation: http://www.sqlalchemy.org/docs/orm/session.html\n"
  },
  {
    "path": "docs/source/tutorial/scheduling.rst",
    "content": ".. _tutorial_scheduling_toplevel:\n\nScheduling\n==========\n\nNow that we've defined tasks, resources, and dependencies, let's schedule our\nproject!\n\n\nTaskJuggler Integration\n-----------------------\n\nStalker utilizes `TaskJuggler`_ to solve scheduling problems and determine when\nresources should work on specific tasks.\n\n.. warning::\n\n   * Ensure you have `TaskJuggler`_ installed on your system.\n\n   * Configure Stalker to locate the ``tj3`` executable:\n\n     * **Linux:** This is usually straightforward under Linux, just install\n       `TaskJuggler`_ and Stalker will be able to use it.\n\n     * **macOS & Windows:** Create a ``STALKER_PATH`` environment variable\n       pointing to a folder containing a ``config.py`` file. Add the following\n       line to ``config.py``:\n\n       .. code-block:: python\n\n            tj_command = r\"C:\\Path\\to\\tj3.exe\"\n\n   The default value for ``tj_command`` is ``/usr/local/bin/tj3``. If you run\n   ``which tj3`` on Linux or macOS and it returns this value, no additional\n   setup is needed.\n\n   .. _TaskJuggler: http://www.taskjuggler.org/\n\nScheduling Your Project\n-----------------------\n\nLet's schedule our project using the :class:`.Studio` instance that we've\ncreated at the beginning of this tutorial:\n\n.. code-block:: python\n\n    from stalker import TaskJugglerScheduler\n\n    my_studio.scheduler = TaskJugglerScheduler()\n    # Set a large duration (e.g., 1 year) to avoid TaskJuggler complaining the\n    # project is not fitting into the time frame.\n    my_studio.duration = datetime.timedelta(days=365)\n    my_studio.schedule(scheduled_by=me)\n    DBsession.commit()  # Save changes\n\nThis process might take a few seconds for small project or long for larger\nones.\n\nViewing Scheduled Dates\n-----------------------\n\nOnce completed, each task will have its ``computed_start`` and ``computed_end``\nvalues populated:\n\n.. code-block:: python\n\n    for task in [previs, matchmove, anim, lighting, comp]:\n        print(\"{:16s} {} -> {}\".format(\n            task.name,\n            task.computed_start,\n            task.computed_end\n        ))\n\nOutputs:\n\n.. code-block:: shell\n\n    Previs           2024-04-02 16:00 -> 2024-04-15 15:00\n    Matchmove        2024-04-15 15:00 -> 2024-04-17 13:00\n    Animation        2024-04-17 13:00 -> 2024-04-23 17:00\n    Lighting         2024-04-23 17:00 -> 2024-04-24 11:00\n    Comp             2024-04-24 11:00 -> 2024-04-24 17:00\n\nUnderstanding the Output\n------------------------\n\nThe output will display start and end dates for each task, reflecting the\ndependencies. In this example, since each task has only one assigned resource\n(you), they follow one another.\n\nFurther Explorations\n--------------------\n\nScheduling is complex topic. For in-depth information, refer to the\n`TaskJuggler`_ documentation.\n\n\nTaskJuggler Project Representation\n----------------------------------\n\nYou can check the ``to_tjp`` values of the data objects:\n\n.. code-block:: python\n\n    print(my_studio.to_tjp)\n    print(me.to_tjp)\n    print(comp.to_tjp)\n    print(new_project.to_tjp)\n\nIf you're familiar with TaskJuggler, you'll recognize the output format.\nStalker maps its data to TaskJuggler-compatible strings. Although, Stalker is\ncurrently supporting a subset of directives, it is enough for scheduling\ncomplex projects with intricate dependencies and hierarchies. Support for\nadditional TaskJuggler directives will grow with future Stalker versions.\n"
  },
  {
    "path": "docs/source/tutorial/task_and_resource_management.rst",
    "content": ".. _tutorial_task_resource_management_toplevel:\n\nTask and Resource Management\n============================\n\nNow that we have created shots and tasks, we need to assign resources (users)\nto these tasks to complete the work.\n\nLet's assign ourselves to all the tasks:\n\n.. code-block:: python\n\n    previs.resources = [me]\n    previs.schedule_timing = 10\n    previs.schedule_unit = 'd'\n\n    matchmove.resources = [me]\n    matchmove.schedule_timing = 2\n    matchmove.schedule_unit = 'd'\n\n    anim.resources = [me]\n    anim.schedule_timing = 5\n    anim.schedule_unit = 'd'\n\n    lighting.resources = [me]\n    lighting.schedule_timing = 3\n    lighting.schedule_unit = 'd'\n\n    comp.resources = [me]\n    comp.schedule_timing = 6\n    comp.schedule_unit = 'h'\n\nHere, we've assigned ourselves as the resource for each task and specified the\nestimated time to complete the task using ``schedule_timing`` and\n``schedule_unit`` attributes.\n\nSaving Changes\n--------------\n\nTo save these changes to the database:\n\n.. code-block:: python\n\n    DBsession.commit()\n\nNote that we didn't explicitly add any new object to the session. Since all the\ntasks are related to the ``sh001`` shot, which is already tracked by the\nsession, SQLAlchemy will automatically track and save the changes to the\ndatabase.\n\nWith this information, Stalker can now schedule these tasks, taking info\naccount dependencies and resource availability. This will help you plan and\nmanage your project more efficiently.\n"
  },
  {
    "path": "docs/source/tutorial/tutorial_files/tutorial.py",
    "content": "# -*- coding: utf-8 -*-\n\nimport os\n\nimport stalker.db.setup\n\nstalker.db.setup.setup({\"sqlalchemy.url\": \"sqlite:///\"})\nstalker.db.setup.init()\n\n\nfrom stalker import Studio\n\nmy_studio = Studio(name=\"My Great Studio\")\n\nfrom stalker import User\n\nme = User(\n    name=\"Erkan Ozgur Yilmaz\",\n    login=\"eoyilmaz\",\n    email=\"some_email_address@gmail.com\",\n    password=\"secret\",\n    description=\"This is me\",\n)\n\nfrom stalker import Department\n\ntds_department = Department(name=\"TDs\", description=\"This is the TDs department\")\n\ntds_department.users.append(me)\n\nprint(me.departments)\n# you should get something like\n# [<TDs (Department)>]\n\n\nfrom stalker.db.session import DBSession\n\nDBSession.add(my_studio)\nDBSession.add(me)\nDBSession.add(tds_department)\nDBSession.commit()\n\n\nall_departments = Department.query.all()\nprint(all_departments)\n# This should print something like\n# [<admins (Department)>, <TDs (Department)>]\n# \"admins\" department is created by default\n\nadmins = all_departments[0]\ntds = all_departments[1]\n\nall_users = tds.users  # Department.users is a synonym for Department.members\n# they are essentially the same attribute\nprint(all_users[0])\n# this should print\n# <Erkan Ozgur Yilmaz ('eoyilmaz') (User)>\n\n# we will reuse the Statuses created by default (in db.init())\nfrom stalker import Status\n\nstatus_new = Status.query.filter_by(code=\"NEW\").first()\nstatus_wip = Status.query.filter_by(code=\"WIP\").first()\nstatus_cmpl = Status.query.filter_by(code=\"CMPL\").first()\n\n# a status list which is suitable for Project instances\nfrom stalker import StatusList, Project\n\nproject_statuses = StatusList(\n    name=\"Project Status List\",\n    statuses=[status_new, status_wip, status_cmpl],\n    target_entity_type=\"Project\"  # you can also use Project which is the\n    # class itself\n)\n\nfrom stalker import Repository\n\n# and the repository itself\ncommercial_repo = Repository(name=\"Commercial Repository\", code=\"CR\")\n\nnew_project = Project(\n    name=\"Fancy Commercial\",\n    code=\"FC\",\n    status_list=project_statuses,\n    repositories=[commercial_repo],\n)\n\nimport tzlocal\nimport datetime\nfrom stalker import ImageFormat\n\nnew_project.description = \"\"\"The commercial is about this fancy product. The\nclient want us to have a shiny look with their\nproduct bla bla bla...\"\"\"\n\nnew_project.image_format = ImageFormat(name=\"HD 1080\", width=1920, height=1080)\n\nnew_project.fps = 25\nlocal_tz = tzlocal.get_localzone()\nnew_project.end = datetime.datetime(2014, 5, 15, tzinfo=local_tz)\nnew_project.users.append(me)\n\nDBSession.add(new_project)\nDBSession.commit()\n\nfrom stalker import Sequence\n\nseq1 = Sequence(\n    name=\"Sequence 1\",\n    code=\"SEQ1\",\n    project=new_project,\n)\n\nfrom stalker import Shot\n\nsh001 = Shot(name=\"SH001\", code=\"SH001\", project=new_project, sequences=[seq1])\nsh002 = Shot(code=\"SH002\", project=new_project, sequences=[seq1])\nsh003 = Shot(code=\"SH003\", project=new_project, sequences=[seq1])\n\nDBSession.add_all([sh001, sh002, sh003])\nDBSession.commit()\n\nsh004 = Shot(code=\"SH004\", project=new_project, sequences=[seq1])\nDBSession.add(sh004)\nDBSession.commit()\n\nsh004.code = \"SH005\"\nDBSession.commit()\n\n# first find the data\nwrong_shot = Shot.query.filter_by(code=\"SH005\").first()\n\n# now update it\nwrong_shot.code = \"SH004\"\n\n# commit the changes to the database\nDBSession.commit()\n\nDBSession.delete(wrong_shot)\nDBSession.commit()\n\nwrong_shot = Shot.query.filter_by(code=\"SH005\").first()\nprint(wrong_shot)\n# should print None\n\nfrom stalker import Task\n\nprevis = Task(name=\"Previs\", parent=sh001)\n\nmatchmove = Task(name=\"Matchmove\", parent=sh001)\n\nanim = Task(name=\"Animation\", parent=sh001)\n\nlighting = Task(name=\"Lighting\", parent=sh001)\n\ncomp = Task(name=\"comp\", parent=sh001)\n\ncomp.depends_on = [lighting]\nlighting.depends_on = [anim]\nanim.depends_on = [previs, matchmove]\n\nprevis.resources = [me]\nprevis.schedule_timing = 10\nprevis.schedule_unit = \"d\"\n\nmatchmove.resources = [me]\nmatchmove.schedule_timing = 2\nmatchmove.schedule_unit = \"d\"\n\nanim.resources = [me]\nanim.schedule_timing = 5\nanim.schedule_unit = \"d\"\n\nlighting.resources = [me]\nlighting.schedule_timing = 3\nlighting.schedule_unit = \"d\"\n\ncomp.resources = [me]\ncomp.schedule_timing = 6\ncomp.schedule_unit = \"h\"\n\nDBSession.commit()\n\nfrom stalker import TaskJugglerScheduler\n\nmy_studio.scheduler = TaskJugglerScheduler()\nmy_studio.duration = datetime.timedelta(days=365)  # we are setting the\nmy_studio.schedule(scheduled_by=me)  # duration to 1 year just\n# to be sure that TJ3\n# will not complain\n# about the project is not\n# fitting in to the time\n# frame.\n\nDBSession.commit()  # to reflect the change\n\nprint(previs.computed_start)  # 2014-04-02 16:00:00\nprint(previs.computed_end)  # 2014-04-15 15:00:00\n\nprint(matchmove.computed_start)  # 2014-04-15 15:00:00\nprint(matchmove.computed_end)  # 2014-04-17 13:00:00\n\nprint(anim.computed_start)  # 2014-04-17 13:00:00\nprint(anim.computed_end)  # 2014-04-23 17:00:00\n\nprint(lighting.computed_start)  # 2014-04-23 17:00:00\nprint(lighting.computed_end)  # 2014-04-24 11:00:00\n\nprint(comp.computed_start)  # 2014-04-24 11:00:00\nprint(comp.computed_end)  # 2014-04-24 17:00:00\n\nprint(my_studio.to_tjp)\nprint(me.to_tjp)\nprint(comp.to_tjp)\nprint(new_project.to_tjp)\n\ncommercial_repo.linux_path = \"/mnt/M/commercials\"\ncommercial_repo.macos_path = \"/Volumes/M/commercials\"\ncommercial_repo.windows_path = \"M:/commercials\"  # you can use reverse slashes\n# (\\\\) if you want\n\nprint(commercial_repo.path)\n# under Windows outputs:\n# M:/commercials\n#\n# in Linux and variants:\n# /mnt/M/commercials\n#\n# and in macOS:\n# /Volumes/M/commercials\n\n\nfrom stalker import Structure\n\ncommercial_project_structure = Structure(name=\"Commercial Projects Structure\")\n\n# now assign this structure to our project\nnew_project.structure = commercial_project_structure\n\nfrom stalker import FilenameTemplate\n\ntask_template = FilenameTemplate(\n    name=\"Task Template for Commercials\",\n    target_entity_type=\"Task\",\n    path=\"$REPO{{project.repository.id}}/{{project.code}}/{%- for p in parent_tasks -%}{{p.nice_name}}/{%- endfor -%}\",\n    filename='{{version.nice_name}}_v{{\"%03d\"|format(version.version_number)}}',\n)\n\n# and append it to our project structure\ncommercial_project_structure.templates.append(task_template)\n\n# commit to database\nDBSession.commit()  # no need to add anything, project is already on db\n\nfrom stalker import Version\n\nvers1 = Version(task=comp)\n\n# we need to update the paths\npath = vers1.generate_path()\n\n# check the path and filename\nprint(path.parent)  # '$REPO33/FC/SH001/comp'\nprint(path.name)  # 'SH001_comp_Main_v001'\nprint(path)  # '$REPO33/FC/SH001/comp/SH001_comp_Main_v001'\n# now the absolute values, values with repository root\n# because I'm running this code in a Linux laptop, my results are using the\n# linux path of the repository\nprint(vers1.absolute_path)  # '/mnt/M/commercials/FC/SH001/comp'\nprint(\n    vers1.absolute_full_path\n)  # '/mnt/M/commercials/FC/SH001/comp/SH001_comp_Main_v001'\n\n# check the version_number\nprint(vers1.version_number)  # 1\n\n# commit to database\nDBSession.commit()\n\nvers1.is_published = False  # I still work on this version, this is not a\n# usable one\n\n# be sure that you've committed the previous version to the database\n# to let Stalker now what number to give for the next version\nvers2 = Version(task=comp)\nvers2.generate_path()  # this call probably will disappear in next version of\n# Stalker, so Stalker will automatically update the\n# paths on Version.__init__()\n\nprint(vers2.version_number)  # 2\nprint(vers2.filename)  # 'SH001_comp_Main_v002'\n\n# before creating a new version commit this one to db\nDBSession.commit()\n\n# now create a new version\nvers3 = Version(task=comp)\nvers3.generate_path()\n\nprint(vers3.version_number)  # 3\nprint(vers3.filename)  # 'SH001_comp_Main_v002'\n\n# using pure Python\nvers_from_python = comp.versions  # [<FC_SH001_comp_Main_v001 (Version)>,\n#  <FC_SH001_comp_Main_v002 (Version)>,\n#  <FC_SH001_comp_Main_v003 (Version)>]\n\n# or using a query\nvers_from_query = Version.query.filter_by(task=comp).all()\n\n# again returns\n# [<FC_SH001_comp_Main_v001 (Version)>,\n#  <FC_SH001_comp_Main_v002 (Version)>,\n#  <FC_SH001_comp_Main_v003 (Version)>]\n\nassert vers_from_python == vers_from_query\n\ncommercial_project_structure.custom_template = \"\"\"\nTemp\nReferences\nReferences/Movies\nReferences/Images\n\"\"\"\n"
  },
  {
    "path": "docs/source/tutorial.rst",
    "content": ".. _tutorial_toplevel:\n\n============\nAPI Tutorial\n============\n\n.. _tutorial_contents:\n\nTable of Contents\n=================\n\n.. toctree::\n    :maxdepth: 3\n    \n    tutorial/basics.rst\n    tutorial/creating_simple_data.rst\n    tutorial/query_update_delete_data.rst\n    tutorial/pipeline.rst\n    tutorial/task_and_resource_management.rst\n    tutorial/scheduling.rst\n    task_review_workflow.rst\n    tutorial/asset_management.rst\n    tutorial/collaboration.rst\n    tutorial/extending_som.rst\n    tutorial/conclusion.rst\n\nIntroduction\n============\n\nStalker leverages the powerful `SQLAlchemy ORM`_ to facilitate interaction with\ndatabases using the Stalker Object Model (SOM). This tutorial introduces you to\nthe Stalker Python API and SOM. If you're familiar with SQLAlchemy, you'll find\nthe transition smooth. Otherwise, SOM offers a user-friendly way to manage\ndatabases.\n\n.. _SQLAlchemy ORM: http://www.sqlalchemy.org/docs/orm/tutorial.html\n"
  },
  {
    "path": "docs/source/upgrade_db.rst",
    "content": ".. upgrade_db_toplevel:\n\n==================\nUpgrading Database\n==================\n\nIntroduction\n============\n\nFrom time to time, with new releases of Stalker, your Stalker database may need\nto be upgraded. This is done with the `Alembic`_ library, which is a database\nmigration library for `SQLAlchemy`_.\n\n.. _Alembic: http://alembic.zzzcomputing.com/en/latest/\n.. _SQLAlchemy: http://www.sqlalchemy.org\n\nInstructions\n============\n\nThe upgrade is easy, just run the following command on the root of the stalker\ninstallation directory::\n\n  # for Windows\n  ..\\Scripts\\alembic.exe upgrade head\n\n  # for Linux or macOS\n  ../bin/alembic upgrade head\n\n  # this should output something like that:\n  #\n  # INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.\n  # INFO  [alembic.runtime.migration] Will assume transactional DDL.\n  # INFO  [alembic.runtime.migration] Running upgrade 745b210e6907 -> f2005d1fbadc, added ProjectClients\n\nThat's it, your database is now migrated to the latest version."
  },
  {
    "path": "examples/__init__.py",
    "content": ""
  },
  {
    "path": "examples/extending/__init__.py",
    "content": ""
  },
  {
    "path": "examples/extending/camera_lens.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nIn this example we are going to extend the Stalker Object Model (SOM) with two\nnew type of classes which are derived from the\n:class:`stalker.models.entity.Entity` class.\n\nOne of our new classes is going to hold information about Camera, or more\nspecifically it will hold the information of the Camera used on the set for a\nshooting. The Camera class should hold these information:\n\n * The make of the camera\n * The model of the camera\n * specifications like:\n   * aperture gate\n   * horizontal film back size\n   * vertical film back size\n   * cropping factor\n * media it uses (film or digital)\n * The web page of the product if available\n\nThe other class is going to be the Lens. A Lens class should hold these\ninformation:\n\n * The make of the lens\n * The model of the lens\n * min focal length\n * max focal length (for zooms, it will be the same for prime lenses)\n * The web page of the product if available\n\nTo make this example simple and to not introduce the\n:class:`~stalker.models.mixins.ReferenceMixin` in this example, which is\nexplained in other examples, we are going to use simple STRINGs for the web\npage links of the manufacturer.\n\nAnd because we don't want to again make things complex we are not\ngoing to touch the :class:`stalker.models.shot.Shot` class which probably\nwill benefit these two classes. In normal circumstances we would like to\nintroduce a new class which derives from the original\n:class:`stalker.models.shot.Shot` and add these Camera and Lens\nrelations to it. But again to not to make things complex we are just going to\nsettle with these two.\n\nDon't forget that, for the sake of brevity we are skipping a lot of things\nwhile creating these classes, first of all we are not doing any validation on\nthe data given to us. Secondly we are not using any properties, but we are\ngiving the bare class variables to the users of our classes. And because we are\nnot using any properties we are mapping the tables directly to our classes\nwithout setting up any synonyms for our attributes.\n\"\"\"\n\nfrom sqlalchemy import Column, Integer, Float, ForeignKey, String\nfrom stalker import Entity\n\n\nclass Camera(Entity):\n    \"\"\"The Camera class holds basic information about the Camera used on the sets.\n\n    Args:\n        make (str): the make of the camera\n        model (str): the model of the camera\n        aperture_gate (float): the aperture gate opening distance\n        horizontal_film_back (float): the horizontal length of the film back\n        vertical_film_back (float): the vertical length of the film back\n        web_page (str): the web page of the camera\n    \"\"\"\n\n    __tablename__ = \"Cameras\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Camera\"}\n\n    camera_id = Column(\"id\", Integer, ForeignKey(\"Entities.id\"), primary_key=True)\n    make = Column(String)\n    model = Column(String)\n    aperture_gate = Column(Float(precision=4), default=0)\n    horizontal_film_back = Column(Float(presicion=4), default=0)\n    vertical_film_back = Column(Float(precision=4), default=0)\n    web_page = Column(String)\n\n    def __init__(\n        self,\n        make=\"\",\n        model=\"\",\n        aperture_gate=0,\n        horizontal_film_back=0,\n        vertical_film_back=0,\n        web_page=\"\",\n        **kwargs\n    ):\n        # pass all the extra data to the super (which is Entity)\n        super(Camera, self).__init__(**kwargs)\n\n        self.make = make\n        self.model = model\n        self.aperture_gate = aperture_gate\n        self.horizontal_film_back = horizontal_film_back\n        self.vertical_film_back = vertical_film_back\n        self.web_page = web_page\n\n\nclass Lens(Entity):\n    \"\"\"The Lens class holds data about lenses used in shootings\n\n    Args:\n        make (str): the make of the lens\n        model (str): the model of the lens\n        min_focal_length (float): the min_focal_length.\n        max_focal_length (float): the max_focal_length\n        web_page (str): the product web page\n    \"\"\"\n\n    __tablename__ = \"Lenses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Lens\"}\n\n    lens_id = Column(\"id\", Integer, ForeignKey(\"Entities.id\"), primary_key=True)\n    make = Column(String)\n    model = Column(String)\n    min_focal_length = Column(Float(precision=1))\n    max_focal_length = Column(Float(precision=1))\n    web_page = Column(String)\n\n    def __init__(\n        self,\n        make=\"\",\n        model=\"\",\n        min_focal_length=0,\n        max_focal_length=0,\n        web_page=\"\",\n        **kwargs\n    ):\n        # pass all the extra data to the super (which is Entity)\n        super(Lens, self).__init__(**kwargs)\n\n        self.make = make\n        self.model = model\n        self.min_focal_length = min_focal_length\n        self.max_focal_length = max_focal_length\n        self.web_page = web_page\n\n\n# now we have extended SOM with two new classes\n"
  },
  {
    "path": "examples/extending/great_entity.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nIn this example we are going to extend stalker with a new entity type, which\nis also mixed in with a :class:`stalker.models.mixins.ReferenceMixin`.\n\nTo create your own data type, just derive it from a suitable SOM class.\n\"\"\"\n\nfrom sqlalchemy import Column, Integer, ForeignKey\nfrom stalker import SimpleEntity, ReferenceMixin\n\n\nclass GreatEntity(SimpleEntity, ReferenceMixin):\n    \"\"\"The new great entity class, which is a new simpleEntity with ReferenceMixin.\"\"\"\n\n    __tablename__ = \"GreatEntities\"\n    __mapper_args__ = {\"polymorphic_identity\": \"GreatEntity\"}\n    great_entity_id = Column(\n        \"id\", Integer, ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n"
  },
  {
    "path": "examples/extending/statused_entity.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"\nIn this example we are going to extend Stalker with a new entity type, which\nis also mixed in with :class:`stalker.models.mixins.StatusMixin`.\n\"\"\"\n\nfrom sqlalchemy import Column, Integer, ForeignKey\nfrom stalker import SimpleEntity, StatusMixin\n\n\nclass NewStatusedEntity(SimpleEntity, StatusMixin):\n    \"\"\"The new statused entity class, which is a new simpleEntity with status abilities.\n    \"\"\"\n\n    __tablename__ = \"NewStatusedEntities\"\n    __mapper_args__ = {\"polymorphic_identity\": \"NewStatusedEntity\"}\n\n    new_statused_entity_id = Column(\n        \"id\", Integer, ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n\n# voilà now we have introduced a new type to the SOM and also mixed it with a\n# StatusMixin\n"
  },
  {
    "path": "examples/flat_project_example.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"This is an example which uses two different folder structure in two\ndifferent projects.\n\nThe first one prefers to use a flat one, in which all the files are in the same\nfolder.\n\nThe second project uses a more traditional folder structure where every\nTask/Asset/Shot/Sequence has its own folder and the Task hierarchy is directly\nreflected to folder hierarchy.\n\"\"\"\nimport os\n\nimport stalker.db.setup\nfrom stalker import (\n    db,\n    Project,\n    Repository,\n    Structure,\n    FilenameTemplate,\n    Task,\n    Status,\n    StatusList,\n    Version,\n    Sequence,\n    Shot,\n)\n\n# initialize an in memory sqlite3 database\nstalker.db.setup.setup()\n\n# fill in default data\nstalker.db.setup.init()\n\n# create a new repository\nrepo = Repository(\n    name=\"Test Repository\",\n    linux_path=\"/mnt/T/stalker_tests/\",\n    macos_path=\"/Volumes/T/stalker_tests/\",\n    windows_path=\"T:/stalker_tests/\",\n)\n\n# create a Structure for our flat project\nflat_task_template = FilenameTemplate(\n    name=\"Flat Task Template\",\n    target_entity_type=\"Task\",\n    path=\"{{project.code}}\",  # everything will be under the same folder\n    filename=\"{{task.nice_name}}_{{version.variant_name}}\"\n    '_v{{\"%03d\"|format(version.version_number)}}{{extension}}'\n    # you can customize this as you wish, you can even use a uuid4\n    # as the file name\n)\n\nflat_struct = Structure(\n    name=\"Flat Project Structure\",\n    templates=[flat_task_template]  # we need another template for Assets,\n    #                                 Shots and Sequences but I'm skipping it\n    #                                 for now\n)\n\n# query a couple of statuses\nstatus_new = Status.query.filter_by(code=\"NEW\").first()\nstatus_wip = Status.query.filter_by(code=\"WIP\").first()\nstatus_cmpl = Status.query.filter_by(code=\"CMPL\").first()\n\nproj_statuses = StatusList(\n    name=\"Project Statuses\",\n    target_entity_type=\"Project\",\n    statuses=[status_new, status_wip, status_cmpl],\n)\n\np1 = Project(\n    name=\"Flat Project Example\",\n    code=\"FPE\",\n    status_list=proj_statuses,\n    repository=repo,\n    structure=flat_struct,\n)\n\n# now lets create a Task\nt1 = Task(name=\"Building 1\", project=p1)\nt2 = Task(name=\"Model\", parent=t1)\nt3 = Task(name=\"Lighting\", parent=t1, depends_on=[t2])\n\n# store all the data in the database\ndb.DBSession.add_all([t1, t2, t3])  # this is enough to store the rest\n\n\n# lets create a Maya file for the Model task\nt2_v1 = Version(task=t1)\n# set the extension for maya\npath1 = t2_v1.generate_path(extension=\".ma\")  # for now this is needed to render the template, but will\n\n# lets create a new version for Lighting\nt3_v1 = Version(task=t3)\npath2 = t3_v1.generate_path(extension=\".ma\")\n\n# you should see that all are in the same folder\nprint(os.path.expandvars(path1))\nprint(os.path.expandvars(path2))\n\n#\n# Lets create a second Project that use some other folder structure\n#\n\n# create a new project structure\nnormal_task_template = FilenameTemplate(\n    name=\"Flat Task Template\",\n    target_entity_type=\"Task\",\n    path=\"{{project.code}}/{%- for parent_task in parent_tasks -%}\"\n    \"{{parent_task.nice_name}}/{%- endfor -%}\",  # all in different folder\n    filename=\"{{task.nice_name}}_{{version.variant_name}}\"\n    '_v{{\"%03d\"|format(version.version_number)}}{{extension}}',\n)\n\n# we will use sequences and shots in this project so lets define a template\n# for each of the types\n\n# because we will use Sequences, Shots and Assets for this type of projects\n# we need to supply a new FilenameTemplate for each type (we will not do it\n# again for other new projects that will use this structure).\n#\n# Also, we can use the same template variables from the normal_task_template\nseq_template = FilenameTemplate(\n    name=\"Sequence Template\",\n    target_entity_type=\"Sequence\",\n    path=normal_task_template.path,\n    filename=normal_task_template.filename,\n)\nshot_template = FilenameTemplate(\n    name=\"Shot Template\",\n    target_entity_type=\"Shot\",\n    path=normal_task_template.path,\n    filename=normal_task_template.filename,\n)\nasset_template = FilenameTemplate(\n    name=\"Asset Template\",\n    target_entity_type=\"Asset\",\n    path=normal_task_template.path,\n    filename=normal_task_template.filename,\n)\n\nnormal_struct = Structure(\n    name=\"Normal Project Structure\",\n    templates=[normal_task_template, seq_template, shot_template, asset_template],\n)\n\np2 = Project(\n    name=\"Normal Project Example\",\n    code=\"NPE\",\n    status_list=proj_statuses,\n    repository=repo,  # can be freely in the same repo\n    structure=normal_struct,  # but uses a different structure\n)\n\n# now create new tasks for the normal project\nseq1 = Sequence(name=\"Sequence\", code=\"SEQ001\", project=p2)\nshot1 = Shot(name=\"SEQ001_0010\", code=\"SEQ001_0010\", parent=seq1, sequence=seq1)\ncomp = Task(name=\"Comp\", parent=shot1)\n# you probably will supply a different name/code\n\n# it is a good idea to commit the data now\ndb.DBSession.add(shot1)  # this should be enough to add the rest\n\n# now create new maya files for them\ncomp_v1 = Version(task=comp, variant_name=\"Test\")\npath = comp_v1.generate_path(extension=\".ma\")\n\n# as you see it is in a proper shot folder\nprint(os.path.expandvars(path))\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"setuptools\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nauthors = [\n    {name = \"Erkan Özgür Yılmaz\", email = \"eoyilmaz@gmail.com\"},\n]\nclassifiers = [\n    \"Programming Language :: Python\",\n    \"Programming Language :: Python :: 3.8\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)\",\n    \"Operating System :: OS Independent\",\n    \"Development Status :: 5 - Production/Stable\",\n    \"Intended Audience :: Developers\",\n    \"Intended Audience :: End Users/Desktop\",\n    \"Topic :: Database\",\n    \"Topic :: Software Development\",\n    \"Topic :: Utilities\",\n    \"Topic :: Office/Business :: Scheduling\",\n]\ndescription = \"A Production Asset Management (ProdAM) System\"\ndynamic = [\"version\", \"dependencies\"]\nkeywords = [\n    \"production\",\n    \"asset\",\n    \"management\",\n    \"vfx\",\n    \"animation\",\n    \"maya\",\n    \"houdini\",\n    \"nuke\",\n    \"fusion\",\n    \"softimage\",\n    \"blender\",\n    \"vue\",\n]\nlicense = { file = \"LICENSE\" }\nmaintainers = [\n    {name = \"Erkan Özgür Yılmaz\", email = \"eoyilmaz@gmail.com\"},\n]\nname = \"stalker\"\nreadme = \"README.md\"\nrequires-python = \">= 3.8\"\n\n[project.urls]\n\"Home Page\" = \"https://github.com/eoyilmaz/stalker\"\nGitHub = \"https://github.com/eoyilmaz/stalker\"\nDocumentation = \"https://stalker.readthedocs.io\"\nRepository = \"https://github.com/eoyilmaz/stalker.git\"\n\n[tool.setuptools]\ninclude-package-data = true\n\n[tool.setuptools.packages.find]\nwhere = [\"src\"]\n\n[tool.setuptools.package-data]\nstalker = [\"VERSION\", \"py.typed\"]\n\n[tool.setuptools.exclude-package-data]\nstalker = [\"alembic\", \"docs\", \"tests\"]\n\n[tool.setuptools.dynamic]\ndependencies = { file = [\"requirements.txt\"] }\noptional-dependencies.test = { file = [\"requirements-dev.txt\"] }\nversion = { file = [\"VERSION\"] }\n\n[tool.distutils.bdist_wheel]\nuniversal = false\n\n[tool.pytest.ini_options]\npythonpath = [\".\"]\naddopts = \"-n auto -W ignore -W always::DeprecationWarning --color=yes --cov=src --cov-report term --cov-report html --cov-append tests\"\n\n[tool.black]\n\n[tool.flake8]\nexclude = [\n    \".github\",\n    \"__pycache__\",\n    \".coverage\",\n    \".DS_Store\",\n    \".pytest_cache\",\n    \".venv\",\n    \".vscode\",\n    \"build\",\n    \"dist\",\n    \"stalker.egg-info\",\n]\nextend-select = [\"B950\"]\nignore = [\"D107\", \"E203\", \"E501\", \"E701\", \"SC200\", \"W503\"]\nmax-complexity = 12\nmax-line-length = 80\n\n[tool.tox]\nrequires = [\"tox>=4.23.2\"]\nenv_list = [\"3.8\", \"3.9\", \"3.10\", \"3.11\", \"3.12\", \"3.13\"]\n\n[tool.tox.env_run_base]\ndescription = \"run the tests with pytest\"\npackage = \"wheel\"\nwheel_build_env = \".pkg\"\nset_env = { SQLALCHEMY_WARN_20 = \"1\" }\ndeps = [\n    \"pytest>=6\",\n    \"pytest-cov\",\n    \"pytest-xdist\",\n]\ncommands = [\n    [\"pytest\"],\n]\n\n[tool.mypy]\n"
  },
  {
    "path": "requirements-dev.txt",
    "content": "\nblack\ncoverage\ndarglint\nflake8\nflake8-bugbear\nflake8-docstrings\nflake8-import-order\nflake8-mutable\nflake8-pep3101\nflake8-spellcheck\nflake8-pyproject\nfuro\nmypy\npyglet\npytest\npytest-cov\npytest-github-actions-annotate-failures\npytest-xdist\nsphinx\nsphinx-autoapi\n# sphinx-findthedocs\ntox\ntwine\ntypes-pytz\nwheel"
  },
  {
    "path": "requirements.txt",
    "content": "alembic\nbuild\njinja2\npsycopg2-binary\npytz\nsix\nsqlalchemy >= 2\ntzlocal"
  },
  {
    "path": "setup.py",
    "content": "# -*- coding: utf-8 -*-\nfrom setuptools import setup\n\nsetup()\n"
  },
  {
    "path": "src/stalker/VERSION",
    "content": "1.1.2.1"
  },
  {
    "path": "src/stalker/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Stalker is a Production Asset Management (ProdAM) designed for Animation/VFX Studios.\n\nSee docs for more information.\n\"\"\"\nfrom stalker.version import __version__  # noqa: F401\nfrom stalker import config, log  # noqa: I100\n\nif True:\n    defaults: config.Config = config.Config()\nfrom stalker.models.asset import Asset\nfrom stalker.models.auth import (\n    AuthenticationLog,\n    Group,\n    LocalSession,\n    Permission,\n    Role,\n    User,\n)\nfrom stalker.models.budget import Budget, BudgetEntry, Good, Invoice, Payment, PriceList\nfrom stalker.models.client import Client, ClientUser\nfrom stalker.models.department import Department, DepartmentUser\nfrom stalker.models.entity import Entity, EntityGroup, SimpleEntity\nfrom stalker.models.format import ImageFormat\nfrom stalker.models.file import File\nfrom stalker.models.message import Message\nfrom stalker.models.mixins import (\n    ACLMixin,\n    AmountMixin,\n    CodeMixin,\n    DAGMixin,\n    DateRangeMixin,\n    ProjectMixin,\n    ReferenceMixin,\n    ScheduleMixin,\n    StatusMixin,\n    TargetEntityTypeMixin,\n    UnitMixin,\n    WorkingHoursMixin,\n)\nfrom stalker.models.note import Note\nfrom stalker.models.project import (\n    Project,\n    ProjectClient,\n    ProjectRepository,\n    ProjectUser,\n)\nfrom stalker.models.repository import Repository\nfrom stalker.models.review import Daily, DailyFile, Review\nfrom stalker.models.scene import Scene\nfrom stalker.models.schedulers import SchedulerBase, TaskJugglerScheduler\nfrom stalker.models.sequence import Sequence\nfrom stalker.models.shot import Shot\nfrom stalker.models.status import Status, StatusList\nfrom stalker.models.structure import Structure\nfrom stalker.models.studio import Studio, Vacation, WorkingHours\nfrom stalker.models.tag import Tag\nfrom stalker.models.task import Task, TaskDependency, TimeLog\nfrom stalker.models.template import FilenameTemplate\nfrom stalker.models.ticket import Ticket, TicketLog\nfrom stalker.models.type import EntityType, Type\nfrom stalker.models.variant import Variant\nfrom stalker.models.version import Version\nfrom stalker.models.wiki import Page\n\n__all__ = [\n    \"ACLMixin\",\n    \"AmountMixin\",\n    \"Asset\",\n    \"AuthenticationLog\",\n    \"Budget\",\n    \"BudgetEntry\",\n    \"Client\",\n    \"ClientUser\",\n    \"CodeMixin\",\n    \"DAGMixin\",\n    \"Daily\",\n    \"DailyFile\",\n    \"DateRangeMixin\",\n    \"Department\",\n    \"DepartmentUser\",\n    \"Entity\",\n    \"EntityGroup\",\n    \"EntityType\",\n    \"File\",\n    \"FilenameTemplate\",\n    \"Good\",\n    \"Group\",\n    \"ImageFormat\",\n    \"Invoice\",\n    \"LocalSession\",\n    \"Message\",\n    \"Note\",\n    \"Page\",\n    \"Payment\",\n    \"Permission\",\n    \"PriceList\",\n    \"Project\",\n    \"ProjectClient\",\n    \"ProjectMixin\",\n    \"ProjectRepository\",\n    \"ProjectUser\",\n    \"ReferenceMixin\",\n    \"Repository\",\n    \"Review\",\n    \"Role\",\n    \"Scene\",\n    \"ScheduleMixin\",\n    \"SchedulerBase\",\n    \"Sequence\",\n    \"Shot\",\n    \"SimpleEntity\",\n    \"Status\",\n    \"StatusList\",\n    \"StatusMixin\",\n    \"Structure\",\n    \"Studio\",\n    \"Tag\",\n    \"TargetEntityTypeMixin\",\n    \"Task\",\n    \"TaskDependency\",\n    \"TaskJugglerScheduler\",\n    \"Ticket\",\n    \"TicketLog\",\n    \"TimeLog\",\n    \"Type\",\n    \"UnitMixin\",\n    \"User\",\n    \"Vacation\",\n    \"Variant\",\n    \"Version\",\n    \"WorkingHours\",\n    \"WorkingHoursMixin\",\n]\n\n\nlogger = log.get_logger(__name__)\n"
  },
  {
    "path": "src/stalker/config.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Config related functions and classes are situated here.\"\"\"\nimport datetime\nimport os\nimport sys\nfrom typing import Any, Dict\n\nfrom stalker import log\n\nlogger = log.get_logger(__name__)\n\n\nclass ConfigBase(object):\n    \"\"\"Config abstraction.\n\n    This is based on Sphinx's config idiom.\n    \"\"\"\n\n    default_config_values: Dict[str, Any] = {}\n\n    def __init__(self) -> None:\n        self.config_values = self.default_config_values.copy()\n        self.user_config: Dict[str, Any] = {}\n        self._parse_settings()\n\n    def _parse_settings(self) -> None:\n        \"\"\"Parse the settings.\n\n        The priority order is:\n\n            stalker.config\n            config.py under .stalker_rc directory\n            config.py under $STALKER_PATH\n\n        Raises:\n            RuntimeError: If there is a Syntax error in the configuration.\n        \"\"\"\n        # for now just use $STALKER_PATH\n        # try to get the environment variable\n        if self.env_key not in os.environ:\n            # don't do anything\n            logger.debug(\"no environment key found for user settings\")\n        else:\n            logger.debug(\"environment key found\")\n\n            resolved_path = os.path.expanduser(\n                os.path.join(os.environ[self.env_key], \"config.py\")\n            )\n\n            # using `while` is not safe to expand variables\n            # so expand vars for 100 times which already is ridiculously\n            # complex\n            max_recursion = 100\n            i = 0\n            while \"$\" in resolved_path and i < max_recursion:\n                resolved_path = os.path.expandvars(resolved_path)\n                i += 1\n\n            try:\n                logger.debug(\"importing user config\")\n                with open(resolved_path) as f:\n                    exec(f.read(), self.user_config)\n            except IOError:\n                logger.warning(\n                    f\"The $STALKER_PATH: {resolved_path} doesn't exists! \"\n                    \"skipping user config\"\n                )\n            except SyntaxError as e:\n                raise RuntimeError(\n                    f\"There is a syntax error in your configuration file: {e}\"\n                )\n            finally:\n                # append the data to the current settings\n                logger.debug(\"updating system config\")\n                for key in self.user_config:\n                    # if key in self.config_values:\n                    self.config_values[key] = self.user_config[key]\n\n    def __getattr__(self, name: str) -> Any:\n        \"\"\"Return the config value as if it is an attribute look up.\n\n        Args:\n            name (str): The name of the config value.\n\n        Returns:\n            Any: The value related to the given config value.\n        \"\"\"\n        return self.config_values[name]\n\n    def __getitem__(self, name: str) -> Any:\n        \"\"\"Return item with the key.\n\n        Args:\n            name (str): The key to find the value of.\n\n        Returns:\n            Any: The value related to the given key.\n        \"\"\"\n        return getattr(self, name)\n\n    def __setitem__(self, name: str, value: Any) -> None:\n        \"\"\"Set the item with index of name to value.\n\n        Args:\n            name (str): The name as the index.\n            value (Any): The value to set the item to.\n        \"\"\"\n        self.config_values[name] = value\n\n    def __delitem__(self, name: str) -> None:\n        \"\"\"Delete the item with the given name.\n\n        Args:\n            name (str): The name of the item to delete.\n        \"\"\"\n        self.config_values.pop(name)\n\n    def __contains__(self, name: str) -> bool:\n        \"\"\"Check if this contains the name.\n\n        Args:\n            name (str): The config name.\n\n        Returns:\n            bool: True if this contains the name, False otherwise.\n        \"\"\"\n        return name in self.config_values\n\n\nclass Config(ConfigBase):\n    \"\"\"Holds system-wide configuration variables.\n\n    See `configuring stalker`_ for more detail.\n\n    .. _configuring stalker: ../configure.html\n    \"\"\"\n\n    env_key = \"STALKER_PATH\"\n\n    default_config_values = dict(\n        #\n        # The default settings for the database, see sqlalchemy.create_engine\n        # for possible parameters\n        #\n        database_engine_settings={\n            \"sqlalchemy.url\": \"sqlite://\",\n            \"sqlalchemy.echo\": False,\n            # \"sqlalchemy.pool_pre_ping\": True,\n        },\n        database_session_settings={},\n        # Local storage path\n        local_storage_path=os.path.expanduser(\"~/.strc\"),\n        local_session_data_file_name=\"local_session_data\",\n        # Storage for uploaded files\n        server_side_storage_path=os.path.expanduser(\"~/Stalker_Storage\"),\n        repo_env_var_template=\"REPO{code}\",\n        #\n        # Tells Stalker to create an admin by default\n        #\n        auto_create_admin=True,\n        #\n        # these are for new projects\n        # after creating the project you can change them from the interface\n        #\n        admin_name=\"admin\",\n        admin_login=\"admin\",\n        admin_password=\"admin\",\n        admin_email=\"admin@admin.com\",\n        admin_department_name=\"admins\",\n        admin_group_name=\"admins\",\n        # the default keyword which is going to be used in password scrambling\n        key=\"stalker_default_key\",\n        actions=[\"Create\", \"Read\", \"Update\", \"Delete\", \"List\"],  # CRUDL\n        # Tickets\n        ticket_label=\"Ticket\",\n        # define the available actions per Status\n        ticket_status_names=[\"New\", \"Accepted\", \"Assigned\", \"Reopened\", \"Closed\"],\n        ticket_status_codes=[\"NEW\", \"ACP\", \"ASG\", \"ROP\", \"CLS\"],\n        ticket_resolutions=[\n            \"fixed\",\n            \"invalid\",\n            \"wontfix\",\n            \"duplicate\",\n            \"worksforme\",\n            \"cantfix\",\n        ],\n        ticket_workflow={\n            \"resolve\": {\n                \"New\": {\"new_status\": \"Closed\", \"action\": \"set_resolution\"},\n                \"Accepted\": {\"new_status\": \"Closed\", \"action\": \"set_resolution\"},\n                \"Assigned\": {\"new_status\": \"Closed\", \"action\": \"set_resolution\"},\n                \"Reopened\": {\"new_status\": \"Closed\", \"action\": \"set_resolution\"},\n            },\n            \"accept\": {\n                \"New\": {\"new_status\": \"Accepted\", \"action\": \"set_owner\"},\n                \"Accepted\": {\"new_status\": \"Accepted\", \"action\": \"set_owner\"},\n                \"Assigned\": {\"new_status\": \"Accepted\", \"action\": \"set_owner\"},\n                \"Reopened\": {\"new_status\": \"Accepted\", \"action\": \"set_owner\"},\n            },\n            \"reassign\": {\n                \"New\": {\"new_status\": \"Assigned\", \"action\": \"set_owner\"},\n                \"Accepted\": {\"new_status\": \"Assigned\", \"action\": \"set_owner\"},\n                \"Assigned\": {\"new_status\": \"Assigned\", \"action\": \"set_owner\"},\n                \"Reopened\": {\"new_status\": \"Assigned\", \"action\": \"set_owner\"},\n            },\n            \"reopen\": {\n                \"Closed\": {\"new_status\": \"Reopened\", \"action\": \"del_resolution\"}\n            },\n        },\n        # Task Management\n        timing_resolution=datetime.timedelta(hours=1),\n        task_priority=500,\n        working_hours={\n            \"mon\": [[540, 1080]],  # 9:00 - 18:00\n            \"tue\": [[540, 1080]],  # 9:00 - 18:00\n            \"wed\": [[540, 1080]],  # 9:00 - 18:00\n            \"thu\": [[540, 1080]],  # 9:00 - 18:00\n            \"fri\": [[540, 1080]],  # 9:00 - 18:00\n            \"sat\": [],  # saturday off\n            \"sun\": [],  # sunday off\n        },\n        # this is strongly related with the working_hours settings,\n        # this should match each other\n        daily_working_hours=9,\n        weekly_working_days=5,\n        weekly_working_hours=45,\n        yearly_working_days=261,  # math.ceil(5 * 52.1428)\n        day_order=[\"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\", \"sun\"],\n        datetime_units=[\"min\", \"h\", \"d\", \"w\", \"m\", \"y\"],\n        datetime_unit_names=[\"minute\", \"hour\", \"day\", \"week\", \"month\", \"year\"],\n        datetime_units_to_timedelta_kwargs={\n            \"min\": {\"name\": \"minutes\", \"multiplier\": 1},\n            \"h\": {\"name\": \"hours\", \"multiplier\": 1},\n            \"d\": {\"name\": \"days\", \"multiplier\": 1},\n            \"w\": {\"name\": \"weeks\", \"multiplier\": 1},\n            \"m\": {\"name\": \"days\", \"multiplier\": 30},\n            \"y\": {\"name\": \"days\", \"multiplier\": 365},\n        },\n        task_status_names=[\n            \"Waiting For Dependency\",\n            \"Ready To Start\",\n            \"Work In Progress\",\n            \"Pending Review\",\n            \"Has Revision\",\n            \"Dependency Has Revision\",\n            \"On Hold\",\n            \"Stopped\",\n            \"Completed\",\n        ],\n        task_status_codes=[\n            \"WFD\",\n            \"RTS\",\n            \"WIP\",\n            \"PREV\",\n            \"HREV\",\n            \"DREV\",\n            \"OH\",\n            \"STOP\",\n            \"CMPL\",\n        ],\n        project_status_names=[\"Ready To Start\", \"Work In Progress\", \"Completed\"],\n        project_status_codes=[\"RTS\", \"WIP\", \"CMPL\"],\n        review_status_names=[\"New\", \"Requested Revision\", \"Approved\"],\n        review_status_codes=[\"NEW\", \"RREV\", \"APP\"],\n        daily_status_names=[\"Open\", \"Closed\"],\n        daily_status_codes=[\"OPEN\", \"CLS\"],\n        task_schedule_models=[\"effort\", \"length\", \"duration\"],\n        task_dependency_gap_models=[\"length\", \"duration\"],\n        task_dependency_targets=[\"onend\", \"onstart\"],\n        allocation_strategy=[\n            \"minallocated\",\n            \"maxloaded\",\n            \"minloaded\",\n            \"order\",\n            \"random\",\n        ],\n        persistent_allocation=True,\n        tjp_main_template2=\"\"\"# Generated By Stalker v{{stalker.__version__}}\n{{studio.to_tjp}}\n\n# resources\nresource resources \"Resources\" {\n{%- for vacation in studio.vacations %}\n{{vacation.to_tjp}}\n{%- endfor %}\n{%- for user in studio.users %}\n{{user.to_tjp}}\n{%- endfor %}\n}\n\n# tasks\n{{ tasks_buffer }}\n\n# reports\ntaskreport breakdown \"{{csv_file_name}}\"{\n    formats csv\n    timeformat \"%Y-%m-%d-%H:%M\"\n    columns id, start, end {%- if compute_resources %}, resources{% endif %}\n}\"\"\",\n        tj_command=\"tj3\" if sys.platform == \"win32\" else \"/usr/local/bin/tj3\",\n        path_template=\"{{project.code}}/{%- for parent_task in parent_tasks -%}{{parent_task.nice_name}}/{%- endfor -%}\",  # noqa: B950\n        filename_template='{{version.nice_name}}_r{{\"%02d\"|format(version.revision_number)}}_v{{\"%03d\"|format(version.version_number)}}',  # noqa: B950\n        # --------------------------------------------\n        # the following settings came from oyProjectManager\n        sequence_format=\"%h%p%t %R\",\n        file_size_format=\"%.2f MB\",\n        date_time_format=\"%Y.%m.%d %H:%M\",\n        resolution_presets={\n            \"PC Video\": [640, 480, 1.0],\n            \"NTSC\": [720, 486, 0.91],\n            \"NTSC 16:9\": [720, 486, 1.21],\n            \"PAL\": [720, 576, 1.067],\n            \"PAL 16:9\": [720, 576, 1.46],\n            \"HD 720\": [1280, 720, 1.0],\n            \"HD 1080\": [1920, 1080, 1.0],\n            \"1K Super 35\": [1024, 778, 1.0],\n            \"2K Super 35\": [2048, 1556, 1.0],\n            \"4K Super 35\": [4096, 3112, 1.0],\n            \"A4 Portrait\": [2480, 3508, 1.0],\n            \"A4 Landscape\": [3508, 2480, 1.0],\n            \"A3 Portrait\": [3508, 4960, 1.0],\n            \"A3 Landscape\": [4960, 3508, 1.0],\n            \"A2 Portrait\": [4960, 7016, 1.0],\n            \"A2 Landscape\": [7016, 4960, 1.0],\n            \"50x70cm Poster Portrait\": [5905, 8268, 1.0],\n            \"50x70cm Poster Landscape\": [8268, 5905, 1.0],\n            \"70x100cm Poster Portrait\": [8268, 11810, 1.0],\n            \"70x100cm Poster Landscape\": [11810, 8268, 1.0],\n            \"1k Square\": [1024, 1024, 1.0],\n            \"2k Square\": [2048, 2048, 1.0],\n            \"3k Square\": [3072, 3072, 1.0],\n            \"4k Square\": [4096, 4096, 1.0],\n        },\n        default_resolution_preset=\"HD 1080\",\n        project_structure=\"\"\"{% for shot in project.shots %}\n                Shots/{{shot.code}}\n                Shots/{{shot.code}}/Plate\n                Shots/{{shot.code}}/Reference\n                Shots/{{shot.code}}/Texture\n            {% endfor %}\n        {% for asset in project.assets%}\n            {% set asset_path = project.full_path + '/Assets/' + asset.type.name + '/' + asset.code %}\n            {{asset_path}}/Texture\n            {{asset_path}}/Reference\n        {% endfor %}\n        \"\"\",  # noqa: B950\n        thumbnail_format=\"jpg\",\n        thumbnail_quality=70,\n        thumbnail_size=[320, 180],\n    )\n"
  },
  {
    "path": "src/stalker/db/__init__.py",
    "content": ""
  },
  {
    "path": "src/stalker/db/declarative.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The declarative base class is situated here.\"\"\"\nimport logging\nfrom typing import Any, Type\n\nfrom sqlalchemy.orm import declarative_base\n\nfrom stalker.db.session import DBSession\nfrom stalker.log import get_logger\nfrom stalker.utils import make_plural\n\nlogger: logging.Logger = get_logger(__name__)\n\n\nclass ORMClass(object):\n    \"\"\"The base of the Base class.\"\"\"\n\n    query = DBSession.query_property()\n\n    @property\n    def plural_class_name(self) -> str:\n        \"\"\"Return plural name of this class.\n\n        Returns:\n            str: The plural version of this class.\n        \"\"\"\n        return make_plural(self.__class__.__name__)\n\n\nBase: Type[Any] = declarative_base(cls=ORMClass)\n"
  },
  {
    "path": "src/stalker/db/session.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The venerable DBSession is situated here.\n\nThis is a runtime storage for the DB session. Greatly simplifying the usage of a\nscoped session.\n\"\"\"\nfrom typing import Any, List, TYPE_CHECKING, Union\n\nfrom sqlalchemy.orm import scoped_session, sessionmaker\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.entity import SimpleEntity\n\n\nclass ExtendedScopedSession(scoped_session):\n    \"\"\"A customized scoped_session which adds new functionality.\"\"\"\n\n    def save(self, data: Union[None, List[Any], \"SimpleEntity\"] = None) -> None:\n        \"\"\"Add and commits data at once.\n\n        Args:\n            data (Union[list, stalker.models.entity.SimpleEntity]): Either a single or\n                a list of :class:`stalker.models.entity.SimpleEntity` or derivatives.\n        \"\"\"\n        if data is not None:\n            if isinstance(data, list):\n                self.add_all(data)\n            else:\n                self.add(data)\n        self.commit()\n\n\nDBSession = ExtendedScopedSession(sessionmaker(future=True))\n"
  },
  {
    "path": "src/stalker/db/setup.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Database module of Stalker.\n\nWhenever stalker.db or something under it imported, the :func:`stalker.db.setup` becomes\navailable to let one set up the database.\n\"\"\"\nimport logging\nimport os\nfrom typing import Any, Dict, List, Optional, Union\n\nfrom sqlalchemy import Column, Table, Text, engine_from_config, text\nfrom sqlalchemy.exc import IntegrityError, OperationalError, ProgrammingError\n\nfrom stalker import (\n    DateRangeMixin,\n    Department,\n    EntityType,\n    Group,\n    Permission,\n    ReferenceMixin,\n    Repository,\n    ScheduleMixin,\n    Status,\n    StatusList,\n    StatusMixin,\n    Studio,\n    Type,\n    User,\n    defaults,\n    log,\n)\nfrom stalker.db.declarative import Base\nfrom stalker.db.session import DBSession\n\n\nlogger: logging.Logger = log.get_logger(__name__)\n\n# TODO: Try to get it from the API (it was not working inside a package before)\nalembic_version: str = \"9f9b88fef376\"\n\n\ndef setup(settings: Optional[Dict[str, Any]] = None) -> None:\n    \"\"\"Connect the system to the given database.\n\n    If the database is None then it sets up using the default database in the\n    settings file.\n\n    Args:\n        settings (Dict[str, Any]): This is a dictionary which has keys prefixed\n            with \"sqlalchemy\" and shows the settings. The most important one is\n            the engine. The default is None, and in this case it uses the\n            settings from stalker.config.Config.database_engine_settings\n    \"\"\"\n    if settings is None:\n        settings = defaults.database_engine_settings\n        logger.debug(\"no settings given, using the default setting\")\n\n    # logger.debug(f\"settings: {settings}\")\n    # create engine\n\n    logger.debug(f\"settings: {settings}\")\n    engine = engine_from_config(settings, \"sqlalchemy.\")\n\n    logger.debug(f\"engine: {engine}\")\n\n    # create the Session class\n    DBSession.remove()\n    DBSession.configure(bind=engine)\n\n    # check alembic versions of the database\n    # and raise an error if it is not matching with the system\n    check_alembic_version()\n\n    # create the database\n    logger.debug(\"creating the tables\")\n\n    Base.metadata.create_all(engine)\n    DBSession.commit()\n\n    # update defaults\n    update_defaults_with_studio()\n\n    # create repo env variables\n    create_repo_vars()\n\n\ndef update_defaults_with_studio() -> None:\n    \"\"\"Update the default values from Studio instance.\n\n    Update only if a database and a Studio instance is present.\n    \"\"\"\n    with DBSession.no_autoflush:\n        studio = Studio.query.first()\n        # studio = DBSession.query(Studio).first()\n        logger.debug(\"studio: {}\".format(studio))\n        if studio:\n            logger.debug(\"found a studio, updating defaults\")\n            studio.update_defaults()\n\n\ndef init() -> None:\n    \"\"\"Fill the database with default values.\"\"\"\n    logger.debug(\"initializing database\")\n\n    # register all Actions available for all SOM classes\n    class_names = [\n        \"Asset\",\n        \"AuthenticationLog\",\n        \"Budget\",\n        \"BudgetEntry\",\n        \"Client\",\n        \"Daily\",\n        \"Department\",\n        \"Entity\",\n        \"EntityGroup\",\n        \"File\",\n        \"FilenameTemplate\",\n        \"Good\",\n        \"Group\",\n        \"ImageFormat\",\n        \"Invoice\",\n        \"Message\",\n        \"Note\",\n        \"Page\",\n        \"Payment\",\n        \"Permission\",\n        \"PriceList\",\n        \"Project\",\n        \"Repository\",\n        \"Review\",\n        \"Role\",\n        \"Scene\",\n        \"Sequence\",\n        \"Shot\",\n        \"SimpleEntity\",\n        \"Status\",\n        \"StatusList\",\n        \"Structure\",\n        \"Studio\",\n        \"Tag\",\n        \"Task\",\n        \"Ticket\",\n        \"TicketLog\",\n        \"TimeLog\",\n        \"Type\",\n        \"User\",\n        \"Vacation\",\n        \"Variant\",\n        \"Version\",\n    ]\n\n    for class_name in class_names:\n        _temp = __import__(\"stalker\", globals(), locals(), [class_name], 0)\n        class_ = eval(\"_temp.{}\".format(class_name))\n        register(class_)\n\n    # create the admin if needed\n    admin = None\n\n    if defaults.auto_create_admin:\n        admin = __create_admin__()\n\n    # create statuses\n    create_ticket_statuses()\n\n    # create statuses for Tickets\n    create_entity_statuses(\n        entity_type=\"Daily\",\n        status_names=defaults.daily_status_names,\n        status_codes=defaults.daily_status_codes,\n        user=admin,\n    )\n    create_entity_statuses(\n        entity_type=\"Project\",\n        status_names=defaults.project_status_names,\n        status_codes=defaults.project_status_codes,\n        user=admin,\n    )\n    create_entity_statuses(\n        entity_type=\"Task\",\n        status_names=defaults.task_status_names,\n        status_codes=defaults.task_status_codes,\n        user=admin,\n    )\n    create_entity_statuses(\n        entity_type=\"Review\",\n        status_names=defaults.review_status_names,\n        status_codes=defaults.review_status_codes,\n        user=admin,\n    )\n\n    # create alembic revision table\n    create_alembic_table()\n\n    logger.debug(\"finished initializing the database\")\n\n\ndef create_repo_vars() -> None:\n    \"\"\"Create environment variables for all the repositories in the current database.\"\"\"\n    # get all the repositories\n    all_repos = Repository.query.all()\n    for repo in all_repos:\n        os.environ[repo.env_var] = repo.path\n\n\ndef get_alembic_version() -> Union[None, str]:\n    \"\"\"Return the alembic version of the database.\n\n    Returns:\n        str: The alembic version.\n    \"\"\"\n    # try to query the version value\n    conn = DBSession.connection()\n    engine = conn.engine\n    if not engine.dialect.has_table(conn, \"alembic_version\"):\n        return None\n\n    sql_query = \"select version_num from alembic_version\"\n    try:\n        return DBSession.connection().execute(text(sql_query)).fetchone()[0]\n    except (OperationalError, ProgrammingError, TypeError):\n        DBSession.rollback()\n        return None\n\n\ndef check_alembic_version() -> None:\n    \"\"\"Check the alembic version of the database.\n\n    Raises:\n        ValueError: If the alembic version is not matching with current version of\n            Stalker.\n    \"\"\"\n    current_alembic_version = get_alembic_version()\n    logger.debug(f\"current_alembic_version: {current_alembic_version}\")\n    if current_alembic_version and current_alembic_version != alembic_version:\n        # invalidate the connection\n        DBSession.connection().invalidate()\n\n        # and raise a ValueError (which I'm not sure is the correct exception)\n        raise ValueError(f\"Please update the database to version: {alembic_version}\")\n\n\ndef create_alembic_table() -> None:\n    \"\"\"Create the default alembic_version table.\n\n    Also create the data so that any new database will be considered as the latest\n    version.\n    \"\"\"\n    # Now, this is not the correct way of doing this, there is a proper way of\n    # doing it and it is explained nicely in the Alembic library documentation.\n    #\n    # But it is simply not working when Stalker is installed as a package.\n    #\n    # So as a workaround here we are doing it manually\n    # don't forget to update the version_num (and the corresponding test\n    # whenever a new alembic revision is created)\n\n    version_num = alembic_version\n    table_name = \"alembic_version\"\n    conn = DBSession.connection()\n    engine = conn.engine\n\n    # check if the table is already there\n    table = Table(\n        table_name, Base.metadata, Column(\"version_num\", Text), extend_existing=True\n    )\n    if not engine.dialect.has_table(conn, table_name):\n        logger.debug(\"creating alembic_version table\")\n\n        # create the table no matter if it exists or not we need it either way\n        Base.metadata.create_all(engine)\n\n    # first try to query the version value\n    sql_query = \"select version_num from alembic_version\"\n    try:\n        version_num = DBSession.connection().execute(text(sql_query)).fetchone()[0]\n    except TypeError:\n        logger.debug(f\"inserting {version_num} to alembic_version table\")\n        # the table is there but there is no value so insert it\n        ins = table.insert().values(version_num=version_num)\n        DBSession.connection().execute(ins)\n        DBSession.commit()\n        logger.debug(\"alembic_version table is created and initialized\")\n    else:\n        # the value is there do not touch the table\n        logger.debug(\"alembic_version table is already there, not doing anything!\")\n\n\ndef __create_admin__() -> User:\n    \"\"\"Create the admin.\n\n    Returns:\n        User: The admin user.\n    \"\"\"\n    logger.debug(\"creating the default administrator user\")\n\n    # create the admin department\n    admin_department = Department.query.filter_by(\n        name=defaults.admin_department_name\n    ).first()\n\n    if not admin_department:\n        admin_department = Department(name=defaults.admin_department_name)\n        DBSession.add(admin_department)\n        DBSession.commit()\n        # create the admins group\n    admins_group = Group.query.filter_by(name=defaults.admin_group_name).first()\n\n    if not admins_group:\n        admins_group = Group(name=defaults.admin_group_name)\n        DBSession.add(admins_group)\n\n    # check if there is already an admin in the database\n    admin = User.query.filter_by(name=defaults.admin_name).first()\n    if admin:\n        # there should be an admin user do nothing\n        logger.debug(\"there is an admin already\")\n        return admin\n    else:\n        admin = User(\n            name=defaults.admin_name,\n            login=defaults.admin_login,\n            password=defaults.admin_password,\n            email=defaults.admin_email,\n            departments=[admin_department],\n            groups=[admins_group],\n        )\n\n        DBSession.add(admin)\n        DBSession.commit()\n\n        # admin.created_by = admin\n        # admin.updated_by = admin\n\n        # update the department as created and updated by admin user\n        admin_department.created_by = admin\n        admin_department.updated_by = admin\n\n        admins_group.created_by = admin\n        admins_group.updated_by = admin\n\n        DBSession.add(admin)\n        DBSession.commit()\n\n    return admin\n\n\ndef create_ticket_statuses() -> None:\n    \"\"\"Create the default ticket statuses.\"\"\"\n    # create as admin\n    admin = User.query.filter(User.login == defaults.admin_name).first()\n\n    # create statuses for Tickets\n    ticket_names = defaults.ticket_status_names\n    ticket_codes = defaults.ticket_status_codes\n    create_entity_statuses(\"Ticket\", ticket_names, ticket_codes, admin)\n\n    # Again I hate doing this in this way\n    types = Type.query.filter_by(target_entity_type=\"Ticket\").all()\n    t_names = [t.name for t in types]\n\n    # create Ticket Types\n    logger.debug(\"Creating Ticket Types\")\n    if \"Defect\" not in t_names:\n        ticket_type_1 = Type(\n            name=\"Defect\",\n            code=\"Defect\",\n            target_entity_type=\"Ticket\",\n            created_by=admin,\n            updated_by=admin,\n        )\n        DBSession.add(ticket_type_1)\n\n    if \"Enhancement\" not in t_names:\n        ticket_type_2 = Type(\n            name=\"Enhancement\",\n            code=\"Enhancement\",\n            target_entity_type=\"Ticket\",\n            created_by=admin,\n            updated_by=admin,\n        )\n        DBSession.add(ticket_type_2)\n\n    try:\n        DBSession.commit()\n    except IntegrityError:\n        DBSession.rollback()\n        logger.debug(\"Ticket Types are already in the database!\")\n    else:\n        # DBSession.flush()\n        logger.debug(\"Ticket Types are created successfully\")\n\n\ndef create_entity_statuses(\n    entity_type: str = \"\",\n    status_names: Optional[List[str]] = None,\n    status_codes: Optional[List[str]] = None,\n    user: Optional[User] = None,\n) -> None:\n    \"\"\"Create the default task statuses.\n\n    Args:\n        entity_type (str): The entity type.\n        status_names (List[str]): The list of status names.\n        status_codes (List[str]): The list of status codes in the same order of the\n            ``status_names`` args.\n        user (stalker.model.auth.User): The :class:`stalker.model.auth.User` instance\n            to use as the creator for the newly created data.\n\n    Raises:\n        ValueError: If entity_type, status_names or status_codes are empty.\n    \"\"\"\n    if not entity_type:\n        DBSession.rollback()\n        raise ValueError(\"Please supply entity_type\")\n\n    if not status_names:\n        DBSession.rollback()\n        raise ValueError(\"Please supply status names\")\n\n    if not status_codes:\n        DBSession.rollback()\n        raise ValueError(\"Please supply status codes\")\n\n    # create statuses for entity\n    logger.debug(f\"Creating {entity_type} Statuses\")\n\n    with DBSession.no_autoflush:\n        statuses = Status.query.filter(Status.name.in_(status_names)).all()\n\n    logger.debug(f\"status_names: {status_names}\")\n    logger.debug(f\"statuses: {statuses}\")\n    status_names_in_db = list(map(lambda x: x.name, statuses))\n    logger.debug(f\"statuses_names_in_db: {status_names_in_db}\")\n\n    for name, code in zip(status_names, status_codes):\n        if name not in status_names_in_db:\n            logger.debug(f\"Creating Status: {name} ({code})\")\n            new_status = Status(name=name, code=code, created_by=user, updated_by=user)\n            statuses.append(new_status)\n            DBSession.add(new_status)\n        else:\n            logger.debug(f\"Status {name} ({code}) is already created skipping!\")\n\n    # create the Status List\n    status_list = StatusList.query.filter(\n        StatusList.target_entity_type == entity_type\n    ).first()\n\n    if status_list is None:\n        logger.debug(f\"No {entity_type} Status List found, creating new!\")\n        status_list = StatusList(\n            name=f\"{entity_type} Statuses\",\n            target_entity_type=entity_type,\n            created_by=user,\n            updated_by=user,\n        )\n    else:\n        logger.debug(f\"{entity_type} Status List already created, updating statuses\")\n\n    status_list.statuses = statuses\n    DBSession.add(status_list)\n\n    try:\n        DBSession.commit()\n    except (IntegrityError, OperationalError) as e:\n        logger.debug(f\"error in DBSession.commit, rolling back: {e}\")\n        DBSession.rollback()\n    else:\n        logger.debug(f\"Created {entity_type} Statuses successfully\")\n        DBSession.flush()\n\n\ndef register(class_: Type) -> None:\n    \"\"\"Register the given class to the database.\n\n    It is mainly used to create the :class:`.Action` s needed for the\n    :class:`.User` s and :class:`.Group` s to be able to interact with the\n    given class. Whatever class you have created needs to be registered.\n\n    Example, let's say that you have a data class which is specific to your\n    studio and it is not present in Stalker Object Model (SOM), so you need to\n    extend SOM with a new data type. Here is a simple Data class inherited from\n    the :class:`.SimpleEntity` class (which is the simplest class you should\n    inherit your classes from or use more complex classes down to the\n    hierarchy)::\n\n      from sqlalchemy import Column, Integer, ForeignKey\n      from sqlalchemy.orm import Mapped, mapped_column\n      from stalker.models.entity import SimpleEntity\n\n      class MyDataClass(SimpleEntity):\n        '''This is an example class holding a studio specific data which is not\n        present in SOM.\n        '''\n\n        __tablename__ = 'MyData'\n        __mapper_arguments__ = {'polymorphic_identity': 'MyData'}\n\n        my_data_id : Mapped[int] = mapped_column(\n            'id',\n            ForeignKey('SimpleEntities.c.id'),\n            primary_key=True,\n        )\n\n    Now because Stalker is using Pyramid authorization mechanism it needs to be\n    able to have an :class:`.Permission` about your new class, so you can\n    assign this :class;`.Permission` to your :class:`.User` s or\n    :class:`.Group` s. So you need to register your new class with\n    :func:`stalker.db.register` like shown below::\n\n    .. code-block: python\n\n        from stalker.db import setup\n        setup.register(MyDataClass)\n\n    This will create the necessary Actions in the 'Actions' table on your\n    database, then you can create :class:`.Permission` s and assign these to\n    your :class:`.User` s and :class:`.Group` s so they are Allowed or Denied\n    to do the specified Action.\n\n    Args:\n        class_ (Type): The class itself that needs to be registered.\n\n    Raises:\n        TypeError: If the class_ arg is not a ``type`` instance.\n    \"\"\"\n    # create the Permissions\n    permissions_db = Permission.query.all()\n\n    if not isinstance(class_, type):\n        raise TypeError(\"To register a class please supply the class itself.\")\n\n    # register the class name to entity_types table\n    class_name = class_.__name__\n    if not EntityType.query.filter_by(name=class_name).first():\n        new_entity_type = EntityType(class_name)\n        # update attributes\n        if issubclass(class_, StatusMixin):\n            new_entity_type.statusable = True\n        if issubclass(class_, DateRangeMixin):\n            new_entity_type.dateable = True\n        if issubclass(class_, ScheduleMixin):\n            new_entity_type.schedulable = True\n        if issubclass(class_, ReferenceMixin):\n            new_entity_type.accepts_references = True\n\n        DBSession.add(new_entity_type)\n\n    for action in defaults.actions:\n        for access in [\"Allow\", \"Deny\"]:\n            permission_obj = Permission(access, action, class_name)\n            if permission_obj not in permissions_db:\n                DBSession.add(permission_obj)\n\n    try:\n        DBSession.commit()\n    except IntegrityError:\n        DBSession.rollback()\n"
  },
  {
    "path": "src/stalker/db/types.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Stalker specific data types are situated here.\"\"\"\nimport datetime\nimport json\nfrom typing import Any, Dict, TYPE_CHECKING, Union\n\nimport pytz\n\nfrom sqlalchemy.types import DateTime, JSON, TEXT, TypeDecorator\n\nimport tzlocal\n\nif TYPE_CHECKING:  # pragma: no cover\n    from sqlalchemy.engine.interfaces import Dialect\n\n\nclass JSONEncodedDict(TypeDecorator):\n    \"\"\"Stores and retrieves JSON as TEXT.\"\"\"\n\n    impl = TEXT\n\n    def process_bind_param(self, value: Union[None, Any], dialect: \"Dialect\") -> str:\n        \"\"\"Process bind param.\n\n        Args:\n            value (Union[None, Any]): The object to convert to JSON.\n            dialect (sqlalchemy.engine.interface.Dialect): The dialect.\n\n        Returns:\n            str: The str representation of the JSON data.\n        \"\"\"\n        if value is not None:\n            value = json.dumps(value)\n        return value\n\n    def process_result_value(\n        self, value: Union[None, str], dialect: \"Dialect\"\n    ) -> Union[None, Dict[str, Any]]:\n        \"\"\"Process result value.\n\n        Args:\n            value (Union[None, Any]): The str representation of the JSON data.\n            dialect (sqlalchemy.engine.interface.Dialect): The dialect.\n\n        Returns:\n            dict: The dict representation of the JSON data.\n        \"\"\"\n        return_value: Union[None, Dict[str, Any]] = None\n        if value is not None:\n            return_value = json.loads(value)\n        return return_value\n\n\nGenericJSON = JSON().with_variant(JSONEncodedDict, \"sqlite\")\n\"\"\"A JSON variant that can be used both for PostgreSQL and SQLite3\n\nIt will be native JSON for PostgreSQL and JSONEncodedDict for SQLite3\n\"\"\"\n\n\nclass DateTimeUTC(TypeDecorator):\n    \"\"\"Store UTC internally without the timezone info.\n\n    Inject timezone info as the data comes back from db.\n    \"\"\"\n\n    impl = DateTime\n\n    def process_bind_param(self, value: Any, dialect: str) -> datetime.datetime:\n        \"\"\"Process bind param.\n\n        Args:\n            value (Any): The value.\n            dialect (str): The dialect.\n\n        Returns:\n            datetime.datetime: The datetime value with UTC timezone.\n        \"\"\"\n        if value is not None:\n            # convert the datetime object to have UTC\n            # and strip the datetime value out (which is automatic for SQLite3)\n            value = value.astimezone(pytz.utc)\n        return value\n\n    def process_result_value(self, value: Any, dialect: str) -> datetime.datetime:\n        \"\"\"Process result value.\n\n        Args:\n            value (Any): The value.\n            dialect (str): The dialect.\n\n        Returns:\n            datetime.datetime: The datetime value with UTC timezone.\n        \"\"\"\n        if value is not None:\n            # inject utc and then convert to local timezone\n            local_tz = tzlocal.get_localzone()\n            value = value.replace(tzinfo=pytz.utc).astimezone(local_tz)\n        return value\n\n\nGenericDateTime = DateTime(timezone=True).with_variant(DateTimeUTC, \"sqlite\")\n\"\"\"A DateTime variant that can be used with both PostgreSQL and SQLite3 and\nadds support to timezones in SQLite3.\n\"\"\"\n"
  },
  {
    "path": "src/stalker/exceptions.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Errors for the system.\n\nThis module contains the Errors in Stalker.\n\"\"\"\n\n\nclass LoginError(Exception):\n    \"\"\"Raised when the login information is not correct.\"\"\"\n\n    def __init__(self, value=\"\") -> None:\n        super(LoginError, self).__init__(value)\n        self.value = value\n\n    def __str__(self) -> str:\n        \"\"\"Return the string representation of this exception.\n\n        Returns:\n            str: The string representation of this exception.\n        \"\"\"\n        return self.value\n\n\nclass CircularDependencyError(Exception):\n    \"\"\"Raised when there is circular dependencies within Tasks.\"\"\"\n\n    def __init__(self, value=\"\") -> None:\n        super(CircularDependencyError, self).__init__(value)\n        self.value = value\n\n    def __str__(self) -> str:\n        \"\"\"Return the string representation of this exception.\n\n        Returns:\n            str: The string representation of this exception.\n        \"\"\"\n        return self.value\n\n\nclass OverBookedError(Exception):\n    \"\"\"Raised when a resource is booked more than once for the same time period.\"\"\"\n\n    def __init__(self, value=\"\") -> None:\n        super(OverBookedError, self).__init__(value)\n        self.value = value\n\n    def __str__(self) -> str:\n        \"\"\"Return the string representation of this exception.\n\n        Returns:\n            str: The string representation of this exception.\n        \"\"\"\n        return self.value\n\n\nclass StatusError(Exception):\n    \"\"\"Raised when the status of an entity is not suitable for the desired action.\"\"\"\n\n    def __init__(self, value=\"\") -> None:\n        super(StatusError, self).__init__(value)\n        self.value = value\n\n    def __str__(self) -> str:\n        \"\"\"Return the string representation of this exception.\n\n        Returns:\n            str: The string representation of this exception.\n        \"\"\"\n        return self.value\n\n\nclass DependencyViolationError(Exception):\n    \"\"\"Raised when a TimeLog violates the dependency relation between tasks.\"\"\"\n\n    def __init__(self, value=\"\") -> None:\n        super(DependencyViolationError, self).__init__(value)\n        self.value = value\n\n    def __str__(self) -> str:\n        \"\"\"Return the string representation of this exception.\n\n        Returns:\n            str: The string representation of this exception.\n        \"\"\"\n        return self.value\n"
  },
  {
    "path": "src/stalker/log.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Logging related functions are situated here.\n\nThis module allows registering any number of logger so that it is possible\nto update the logging level all together at runtime (without relaying on weird\nhacks).\n\"\"\"\n\nimport logging\n\nlogging.basicConfig()\nlogging_level = logging.INFO\nloggers = []\n\n\ndef get_logger(name: str) -> logging.Logger:\n    \"\"\"Get a logger.\n\n    Args:\n        name (str): The name of the logger.\n\n    Returns:\n        logging.Logger: The logger.\n    \"\"\"\n    logger = logging.getLogger(name)\n    register_logger(logger)\n    return logger\n\n\ndef register_logger(logger: logging.Logger) -> None:\n    \"\"\"Register logger.\n\n    Args:\n        logger (logging.Logger): A logging.Logger instance.\n\n    Raises:\n        TypeError: If the logger is not a logging.Logger instance.\n    \"\"\"\n    if not isinstance(logger, logging.Logger):\n        raise TypeError(\n            \"logger should be a logging.Logger instance, not {}: '{}'\".format(\n                logger.__class__.__name__, logger\n            )\n        )\n\n    if logger not in loggers:\n        loggers.append(logger)\n\n    logger.setLevel(logging_level)\n\n\ndef set_level(level: int) -> None:\n    \"\"\"Update all registered loggers to the given level.\n\n    Args:\n        level (int): The logging level. The value should be valid with the\n            logging library and should be one of [NOTSET, DEBUG, INFO, WARN,\n            WARNING, ERROR, FATAL, CRITICAL] of the logging library (or anything\n            that is registered as a proper logging level).\n\n    Raises:\n        TypeError: If level is not an integer.\n        ValueError: If level is not a valid value for the logging library.\n    \"\"\"\n    if not isinstance(level, int):\n        raise TypeError(\n            \"level should be an integer value one of [0, 10, 20, 30, 40, 50] \"\n            \"or [NOTSET, DEBUG, INFO, WARN, WARNING, ERROR, FATAL, CRITICAL] \"\n            \"of the logging library, not {}: '{}'\".format(\n                level.__class__.__name__, level\n            )\n        )\n\n    level_names = logging._levelToName\n\n    if level not in level_names:\n        raise ValueError(\n            \"level should be an integer value one of [0, 10, 20, 30, 40, 50] \"\n            \"or [NOTSET, DEBUG, INFO, WARN, WARNING, ERROR, FATAL, CRITICAL] \"\n            \"of the logging library, not {}.\".format(level)\n        )\n\n    for logger in loggers:\n        logger.setLevel(level)\n"
  },
  {
    "path": "src/stalker/models/__init__.py",
    "content": ""
  },
  {
    "path": "src/stalker/models/asset.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Asset related classes.\"\"\"\n\nimport logging\nfrom typing import Any\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import log\nfrom stalker.models.mixins import CodeMixin, ReferenceMixin\nfrom stalker.models.task import Task\n\nlogger: logging.Logger = log.get_logger(__name__)\nlog.set_level(log.logging_level)\n\n\nclass Asset(Task, CodeMixin):\n    \"\"\"The Asset class is the whole idea behind Stalker.\n\n    *Assets* are containers of :class:`.Task` s. And :class:`.Task` s are the\n    smallest meaningful part that should be accomplished to complete the\n    :class:`.Project`.\n\n    An example could be given as follows; you can create an asset for one of\n    the characters in your project. Than you can divide this character asset in\n    to :class:`.Task` s. These :class:`.Task` s can be defined by the type of\n    the :class:`.Asset`, which is a :class:`.Type` object created specifically\n    for :class:`.Asset` (ie. has its :attr:`.Type.target_entity_type` set to\n    \"Asset\"),\n\n    An :class:`.Asset` instance should be initialized with a :class:`.Project`\n    instance (as the other classes which are mixed with the\n    :class:`.TaskMixin`). And when a :class:`.Project` instance is given then\n    the asset will append itself to the :attr:`.Project.assets` list.\n\n    ..versionadded: 0.2.0:\n        No more Asset to Shot connection:\n\n        Assets now are not directly related to Shots. Instead a\n        :class:`.Version` will reference the Asset and then it is easy to track\n        which shots are referencing this Asset by querying with a join of Shot\n        Versions referencing this Asset.\n    \"\"\"\n\n    __auto_name__ = False\n    __strictly_typed__ = True\n    __tablename__ = \"Assets\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Asset\"}\n\n    asset_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Tasks.id\"), primary_key=True\n    )\n\n    def __init__(self, code, **kwargs) -> None:\n        kwargs[\"code\"] = code\n\n        super(Asset, self).__init__(**kwargs)\n        CodeMixin.__init__(self, **kwargs)\n        ReferenceMixin.__init__(self, **kwargs)\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object equals to this asset.\n        \"\"\"\n        return (\n            super(Asset, self).__eq__(other)\n            and isinstance(other, Asset)\n            and self.type == other.type\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Asset, self).__hash__()\n"
  },
  {
    "path": "src/stalker/models/auth.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Authentication related classes and functions situated here.\"\"\"\nimport base64\nimport copy\nimport json\nimport os\nimport re\nfrom datetime import datetime, timedelta\nfrom typing import Any, Dict, List, Optional, TYPE_CHECKING, Union\n\nimport pytz\n\nfrom sqlalchemy import Column, Enum, ForeignKey, Integer, String, Table\nfrom sqlalchemy.ext.associationproxy import association_proxy\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, synonym, validates\nfrom sqlalchemy.schema import UniqueConstraint\n\nfrom stalker import defaults, log\nfrom stalker.db.declarative import Base\nfrom stalker.db.types import GenericDateTime\nfrom stalker.models.entity import Entity, SimpleEntity\nfrom stalker.models.mixins import ACLMixin\nfrom stalker.models.status import Status\nfrom stalker.utils import datetime_to_millis, millis_to_datetime\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.client import Client, ClientUser\n    from stalker.models.department import Department, DepartmentUser\n    from stalker.models.project import Project, ProjectUser\n    from stalker.models.task import Task, TimeLog\n    from stalker.models.ticket import Ticket\n    from stalker.models.studio import Vacation\n\nlogger = log.get_logger(__name__)\n\nLOGIN = \"login\"\nLOGOUT = \"logout\"\n\n\nclass Permission(Base):\n    \"\"\"A class to hold permissions.\n\n    Permissions in Stalker defines what one can do or do not. A Permission\n    instance is composed by three attributes; access, action and class_name.\n\n    Permissions for all the classes in SOM are generally created by Stalker\n    when initializing the database.\n\n    If you created any custom classes to extend SOM you are also responsible to\n    create the Permissions for it by calling :meth:`stalker.db.register` and\n    passing your class to it. See the :mod:`stalker.db` documentation for\n    details.\n\n    Example: Let say that you want to create a Permission specifying a Group of\n    Users are allowed to create Projects::\n\n    .. code-block:: Python\n\n        from stalker import db\n        from stalker import db\n        from stalker.models.auth import User, Group, Permission\n\n        # first setup the db with the default database\n        #\n        # stalker.db.init() will create all the Actions possible with the\n        # SOM classes automatically\n        #\n        # What is left to you is to create the permissions\n        db.setup()\n\n        user1 = User(\n            name='Test User',\n            login='test_user1',\n            password='1234',\n            email='testuser1@test.com'\n        )\n        user2 = User(\n            name='Test User',\n            login='test_user2',\n            password='1234',\n            email='testuser2@test.com'\n        )\n\n        group1 = Group(name='users')\n        group1.users = [user1, user2]\n\n        # get the permissions for the Project class\n        project_permissions = Permission.query\\\n            .filter(Permission.access='Allow')\\\n            .filter(Permission.action='Create')\\\n            .filter(Permission.class_name='Project')\\\n            .first()\n\n        # now we have the permission specifying the allowance of creating a\n        # Project\n\n        # to make group1 users able to create a Project we simply add this\n        # Permission to the groups permission attribute\n        group1.permissions.append(permission)\n\n        # and persist this information in the database\n        DBSession.add(group)\n        DBSession.commit()\n\n    Args:\n        access (str): An Enum value which can have the one of the values of\n            ``Allow`` or ``Deny``.\n        action (str): An Enum value from the list ['Create', 'Read', 'Update',\n            'Delete', 'List']. Cannot be None. The list can be changed from\n            stalker.config.Config.default_actions.\n\n        class_name (str): The name of the class that this action is applied\n            to. Cannot be None or an empty string.\n    \"\"\"\n\n    __tablename__ = \"Permissions\"\n    __table_args__ = (\n        UniqueConstraint(\"access\", \"action\", \"class_name\"),\n        {\"extend_existing\": True},\n    )\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    _access: Mapped[Optional[str]] = mapped_column(\n        \"access\", Enum(\"Allow\", \"Deny\", name=\"AccessNames\")\n    )\n    _action: Mapped[Optional[str]] = mapped_column(\n        \"action\", Enum(*defaults.actions, name=\"AuthenticationActions\")\n    )\n    _class_name: Mapped[Optional[str]] = mapped_column(\"class_name\", String(32))\n\n    def __init__(self, access: str, action: str, class_name: str) -> None:\n        super(Permission, self).__init__()\n        self._access = self._validate_access(access)\n        self._action = self._validate_action(action)\n        self._class_name = self._validate_class_name(class_name)\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return hash(self.access + self.action + self.class_name)\n\n    def _validate_access(self, access: str) -> str:\n        \"\"\"Validate the given access value.\n\n        Args:\n            access (str): The access value to be validated.\n\n        Raises:\n            TypeError: If the given access value is not a str.\n            ValueError: If the access is not a one of [\"Access\", \"Deny\"].\n\n        Returns:\n            str: The access value.\n        \"\"\"\n        if not isinstance(access, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.access should be an instance of str, \"\n                f\"not {access.__class__.__name__}: '{access}'\"\n            )\n\n        if access not in [\"Allow\", \"Deny\"]:\n            raise ValueError(\n                f'{self.__class__.__name__}.access should be \"Allow\" or \"Deny\" '\n                f\"not {access}\"\n            )\n\n        return access\n\n    def _access_getter(self) -> str:\n        \"\"\"Return the _access value.\n\n        Returns:\n            str: Returns the access value.\n        \"\"\"\n        return self._access\n\n    access: Mapped[Optional[str]] = synonym(\n        \"_access\", descriptor=property(_access_getter)\n    )\n\n    def _validate_class_name(self, class_name: str) -> str:\n        \"\"\"Validate the given class_name value.\n\n        Args:\n            class_name (str): The class name.\n\n        Raises:\n            TypeError: If the class_name is not a str.\n\n        Returns:\n            str: The validated class_name.\n        \"\"\"\n        if not isinstance(class_name, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.class_name should be an instance of str, \"\n                f\"not {class_name.__class__.__name__}: '{class_name}'\"\n            )\n\n        return class_name\n\n    def _class_name_getter(self) -> str:\n        \"\"\"Return the _class_name attribute value.\n\n        Returns:\n            str: The class name.\n        \"\"\"\n        return self._class_name\n\n    class_name: Mapped[str] = synonym(\n        \"_class_name\", descriptor=property(_class_name_getter)\n    )\n\n    def _validate_action(self, action: str) -> str:\n        \"\"\"Validate the given action value.\n\n        Args:\n            action (str): The action value.\n\n        Raises:\n            TypeError: If the action is not a str value.\n            ValueError: If the given action is not in the \"defaults.actions\" list.\n\n        Returns:\n            str: The validated action value.\n        \"\"\"\n        if not isinstance(action, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.action should be an instance of str, \"\n                f\"not {action.__class__.__name__}: '{action}'\"\n            )\n\n        if action not in defaults.actions:\n            raise ValueError(\n                f\"{self.__class__.__name__}.action should be one of the values of \"\n                f\"{defaults.actions} not '{action}'\"\n            )\n\n        return action\n\n    def _action_getter(self) -> str:\n        \"\"\"Return the _action value.\n\n        Returns:\n            str: Returns the action value.\n        \"\"\"\n        return self._action\n\n    action: Mapped[Optional[str]] = synonym(\n        \"_action\", descriptor=property(_action_getter)\n    )\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check if the other Permission is equal to this one.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a Permission instance and has the same\n                access, action and class_name attributes.\n        \"\"\"\n        return (\n            isinstance(other, Permission)\n            and other.access == self.access\n            and other.action == self.action\n            and other.class_name == self.class_name\n        )\n\n\nclass Group(Entity, ACLMixin):\n    \"\"\"Creates groups for users to be used in authorization system.\n\n    A Group instance is nothing more than a list of :class:`.User` s created\n    to be able to assign permissions in a group level.\n\n    The Group class, as with the :class:`.User` class, is mixed with the\n    :class:`.ACLMixin` which adds ability to hold :class:`.Permission`\n    instances and serve ACLs to Pyramid.\n\n    Args:\n        name (str): The name of this group.\n        users list: A list of :class:`.User` instances holding the desired\n            users in this group.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Groups\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Group\"}\n\n    gid: Mapped[int] = mapped_column(\"id\", ForeignKey(\"Entities.id\"), primary_key=True)\n\n    users: Mapped[Optional[List[\"User\"]]] = relationship(\n        \"User\",\n        secondary=\"Group_Users\",\n        back_populates=\"groups\",\n        doc=\"\"\"Users in this group.\n\n        Accepts:class:`.User` instance.\n        \"\"\",\n    )\n\n    def __init__(self, name=\"\", users=None, permissions=None, **kwargs) -> None:\n        if users is None:\n            users = []\n\n        if permissions is None:\n            permissions = []\n\n        kwargs.update({\"name\": name})\n        super(Group, self).__init__(**kwargs)\n\n        self.users = users\n        self.permissions = permissions\n\n    @validates(\"users\")\n    def _validate_users(self, key: str, user: \"User\") -> \"User\":\n        \"\"\"Validate the given user value.\n\n        Args:\n            key (str): The name of the validated column.\n            user (User): The :class:`.User` instance.\n\n        Raises:\n            TypeError: If the given user is not a :class:`.User` instance.\n\n        Returns:\n            User: The validated :class:`.User` instance.\n        \"\"\"\n        if not isinstance(user, User):\n            raise TypeError(\n                f\"{self.__class__.__name__}.users should only contain \"\n                \"instances of stalker.models.auth.User, \"\n                f\"not {user.__class__.__name__}: '{user}'\"\n            )\n\n        return user\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Group, self).__hash__()\n\n\nclass User(Entity, ACLMixin):\n    \"\"\"The user class is designed to hold data about a User in the system.\n\n    .. note::\n       .. versionadded 0.2.0: Task Watchers\n\n       New to version 0.2.0 users can be assigned to a :class:`.Task` as a\n       **Watcher**. Which can be used to inform the users in watchers list\n       about the updates of certain Tasks.\n\n    .. note::\n       .. versionadded 0.2.0: Vacations\n\n       It is now possible to define Vacations per user.\n\n    .. note::\n       .. versionadded 0.2.7: Resource Efficiency\n\n    .. note::\n       .. versionadded 0.2.11:\n\n          Users not have a :attr:`.rate` attribute.\n\n    Args:\n        rate: For future usage a rate attribute is added to the User to record\n            the daily cost of this user as a resource. It should be either 0 or\n            a positive integer or float value. Default is 0.\n        efficiency : The efficiency is a multiplier for a user as a resource to\n            a task and defines how much of the time spent for that particular\n            task is counted as an actual effort. The default value is 1.0,\n            lowest possible value is 0.0 and there is no upper limit.\n\n            The efficiency of a resource can be used for three purposes. First\n            you can use it as a crude way to model a team. A team of 5 people\n            should have an efficiency of 5.0. Keep in mind that you cannot track\n            the members of the team individually if you use this feature. They\n            always act as a group.\n\n            Another use is to model performance variations between your resources.\n            Again, this is a fairly crude mechanism and should be used with care.\n            A resource that isn't every good at some task might be pretty good\n            at another. This can't be taken into account as the resource efficiency\n            can only set globally for all tasks.\n\n            One another and interesting use is to model the availability of passive\n            resources like a meeting room or a workstation or something that needs\n            to be free for a task to take place but does not contribute to a task\n            as an active resource.\n\n            All resources that do not contribute effort to the task, that is a\n            passive resource, should have an efficiency of 0.0. Again a typical\n            example would be a conference room. It's necessary for a meeting,\n            but it does not contribute any work.\n        email (str): holds the e-mail of the user, should be in [part1]@[part2]\n            format.\n        login (str): This is the login name of the user, it should be all lower\n            case. Giving a string that has uppercase letters, it will be converted\n            to lower case. It cannot be an empty string or None and it cannot\n            contain any white space inside.\n        departments (List[Department]): It is the departments that the user is\n            a part of. It should be a list of Department objects. One user can\n            be listed in multiple departments.\n        password (str): it is the password of the user, can contain any character.\n            Stalker doesn't store the raw passwords of the users. To check a stored\n            password with a raw password use :meth:`.check_password` and to set\n            the password you can use the :attr:`.password` property directly.\n        groups (List[Group]): It is a list of :class:`.Group` instances that this\n            user belongs to.\n        tasks (List[Task]): it is a list of Task objects which holds the tasks\n            that this user has been assigned to.\n        last_login (datetime): it is a datetime object holds the last login\n            date of the user (not implemented yet).\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Users\"\n    __mapper_args__ = {\"polymorphic_identity\": \"User\"}\n\n    user_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    departments = association_proxy(\n        \"department_role\", \"department\", creator=lambda d: create_department_user(d)\n    )\n\n    department_role: Mapped[Optional[List[\"DepartmentUser\"]]] = relationship(\n        back_populates=\"user\",\n        cascade=\"all, delete-orphan\",\n        primaryjoin=\"Users.c.id==Department_Users.c.uid\",\n        doc=\"\"\"A list of :class:`.Department` s that\n        this user is a part of\"\"\",\n    )\n\n    companies = association_proxy(\n        \"company_role\", \"client\", creator=lambda n: create_client_user(n)\n    )\n\n    company_role: Mapped[Optional[List[\"ClientUser\"]]] = relationship(\n        back_populates=\"user\",\n        cascade=\"all, delete-orphan\",\n        primaryjoin=\"Users.c.id==Client_Users.c.uid\",\n        doc=\"\"\"A list of :class:`.Client` s that this user is a part of.\"\"\",\n    )\n\n    email: Mapped[str] = mapped_column(\n        String(256),\n        unique=True,\n        nullable=False,\n        doc=\"email of the user, accepts string\",\n    )\n\n    password: Mapped[str] = mapped_column(\n        String(256),\n        nullable=False,\n        doc=\"\"\"The password of the user.\n\n        It is scrambled before it is stored.\n        \"\"\",\n    )\n\n    login: Mapped[str] = mapped_column(\n        String(256),\n        nullable=False,\n        unique=True,\n        doc=\"\"\"The login name of the user.\n\n        Cannot be empty.\n        \"\"\",\n    )\n\n    authentication_logs: Mapped[Optional[List[\"AuthenticationLog\"]]] = relationship(\n        primaryjoin=\"AuthenticationLogs.c.uid==Users.c.id\",\n        back_populates=\"user\",\n        cascade=\"all, delete-orphan\",\n        doc=\"\"\"A list of :class:`.AuthenticationLog` instances which holds the\n        login/logout info for this :class:`.User`.\n        \"\"\",\n    )\n\n    groups: Mapped[Optional[List[\"Group\"]]] = relationship(\n        secondary=\"Group_Users\",\n        back_populates=\"users\",\n        doc=\"\"\"Permission groups that this users is a member of.\n\n        Accepts :class:`.Group` object.\n        \"\"\",\n    )\n\n    projects = association_proxy(\n        \"project_role\", \"project\", creator=lambda p: create_project_user(p)\n    )\n\n    project_role: Mapped[Optional[List[\"ProjectUser\"]]] = relationship(\n        back_populates=\"user\",\n        cascade=\"all, delete-orphan\",\n        primaryjoin=\"Users.c.id==Project_Users.c.user_id\",\n    )\n\n    tasks: Mapped[Optional[List[\"Task\"]]] = relationship(\n        secondary=\"Task_Resources\",\n        back_populates=\"resources\",\n        doc=\"\"\":class:`.Task` s assigned to this user.\n\n        It is a list of :class:`.Task` instances.\n        \"\"\",\n    )\n\n    watching: Mapped[Optional[List[\"Task\"]]] = relationship(\n        secondary=\"Task_Watchers\",\n        back_populates=\"watchers\",\n        doc=\"\"\":class:`.Tasks` s that this user is\n        assigned as a watcher.\n\n        It is a list of :class:`.Task` instances.\n        \"\"\",\n    )\n\n    responsible_of: Mapped[Optional[List[\"Task\"]]] = relationship(\n        secondary=\"Task_Responsible\",\n        primaryjoin=\"Users.c.id==Task_Responsible.c.responsible_id\",\n        secondaryjoin=\"Task_Responsible.c.task_id==Tasks.c.id\",\n        back_populates=\"_responsible\",\n        doc=\"\"\"A list of :class:`.Task` instances that this user is responsible\n        of.\"\"\",\n    )\n\n    time_logs: Mapped[Optional[List[\"TimeLog\"]]] = relationship(\n        primaryjoin=\"TimeLogs.c.resource_id==Users.c.id\",\n        back_populates=\"resource\",\n        cascade=\"all, delete-orphan\",\n        doc=\"\"\"A list of :class:`.TimeLog` instances which\n        holds the time logs created for this :class:`.User`.\n        \"\"\",\n    )\n\n    vacations: Mapped[Optional[List[\"Vacation\"]]] = relationship(\n        primaryjoin=\"Vacations.c.user_id==Users.c.id\",\n        back_populates=\"user\",\n        cascade=\"all, delete-orphan\",\n        doc=\"\"\"A list of :class:`.Vacation` instances\n        which holds the vacations created for this :class:`.User`\n        \"\"\",\n    )\n\n    efficiency: Mapped[Optional[float]] = mapped_column(default=1.0)\n\n    rate: Mapped[Optional[float]] = mapped_column(default=0.0)\n\n    def __init__(\n        self,\n        name: Optional[str] = None,\n        login: Optional[str] = None,\n        email: Optional[str] = None,\n        password: Optional[str] = None,\n        departments: Optional[\"Department\"] = None,\n        companies: Optional[\"Client\"] = None,\n        groups: Optional[\"Group\"] = None,\n        efficiency: float = 1.0,\n        rate: float = 0.0,\n        **kwargs: Optional[Dict[str, Any]],\n    ) -> None:\n        kwargs[\"name\"] = name\n\n        super(User, self).__init__(**kwargs)\n\n        self.login = login\n\n        if departments is None:\n            departments = []\n\n        self.departments = departments\n\n        if companies is None:\n            companies = []\n        self.companies = companies\n\n        self.email = email\n\n        # to be able to mangle the password do it like this\n        self.password = password\n\n        if groups is None:\n            groups = []\n        self.groups = groups\n\n        self.tasks = []\n\n        self.efficiency = efficiency\n        self.rate = rate\n\n    def __repr__(self) -> str:\n        \"\"\"Return the representation of the current User.\n\n        Returns:\n            str: The str representation of this User.\n        \"\"\"\n        return f\"<{self.name} ('{self.login}') (User)>\"\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check if the other User is equal to this one.\n\n        Args:\n            other (Any): The other user instance.\n\n        Returns:\n            bool: If the other object is equal to this one, meaning that it is a User\n                instance, has the same name, login and email values then returns True.\n        \"\"\"\n        return (\n            super(User, self).__eq__(other)\n            and isinstance(other, User)\n            and self.email == other.email\n            and self.login == other.login\n            and self.name == other.name\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(User, self).__hash__()\n\n    @validates(\"login\")\n    def _validate_login(self, key: str, login: str) -> str:\n        \"\"\"Validate and format the given login value.\n\n        Args:\n            key (str): The name of the validated column.\n            login (str): The login to be validated.\n\n        Raises:\n            TypeError: If the login is not a str.\n            ValueError: If the login is an empty string after formatting.\n\n        Returns:\n            str: The validated and formatted login value.\n        \"\"\"\n        if login is None:\n            raise TypeError(f\"{self.__class__.__name__}.login cannot be None\")\n\n        login = self._format_login(login)\n\n        # raise a ValueError if the login is an empty string after formatting\n        if login == \"\":\n            raise ValueError(\n                f\"{self.__class__.__name__}.login cannot be an empty string\"\n            )\n\n        logger.debug(f\"name out: {login}\")\n\n        return login\n\n    @validates(\"email\")\n    def _validate_email(self, key: str, email: str) -> str:\n        \"\"\"Validate the given email value.\n\n        Args:\n            key (str): The name of the validated column.\n            email (str): The email to be validated.\n\n        Raises:\n            TypeError: If the given email is not a str.\n\n        Returns:\n            str: The validated email value.\n        \"\"\"\n        # check if email is an instance of string\n        if not isinstance(email, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.email should be an instance of str, not \"\n                f\"{email.__class__.__name__}: '{email}'\"\n            )\n        return self._validate_email_format(email)\n\n    def _validate_email_format(self, email: str) -> str:\n        \"\"\"Validate the email format.\n\n        Args:\n            email (str): The email value to be validated.\n\n        Raises:\n            ValueError: If the email doesn't have a \"@\" sign in it, or it has more than\n                one \"@\" sign in it, or after formatting the account name part or the\n                domain name part becomes an empty string.\n\n        Returns:\n            str: The validated email value.\n        \"\"\"\n        # split the mail from @ sign\n        splits = email.split(\"@\")\n        len_splits = len(splits)\n\n        # there should be one and only one @ sign\n        if len_splits > 2:\n            raise ValueError(\n                f\"check the formatting of {self.__class__.__name__}.email, \"\n                \"there are more than one @ sign\"\n            )\n\n        if len_splits < 2:\n            raise ValueError(\n                f\"check the formatting of {self.__class__.__name__}.email, \"\n                \"there is no @ sign\"\n            )\n\n        if splits[0] == \"\":\n            raise ValueError(\n                f\"check the formatting of {self.__class__.__name__}.email, \"\n                \"the name part is missing\"\n            )\n\n        if splits[1] == \"\":\n            raise ValueError(\n                f\"check the formatting {self.__class__.__name__}.email, \"\n                \"the domain part is missing\"\n            )\n\n        return email\n\n    @classmethod\n    def _format_login(cls, login: str) -> str:\n        \"\"\"Format the given login value.\n\n        Args:\n            login (str): The login value.\n\n        Returns:\n            str: The formatted login value.\n        \"\"\"\n        # strip white spaces from start and end\n        login = login.strip()\n\n        # remove all the spaces\n        login = login.replace(\" \", \"\")\n\n        # make it lowercase\n        login = login.lower()\n\n        # remove any illegal characters\n        login = re.sub(\"[^\\\\(a-zA-Z0-9)]+\", \"\", login)\n\n        # remove any number at the beginning\n        login = re.sub(\"^[0-9]+\", \"\", login)\n\n        return login\n\n    @validates(\"password\")\n    def _validate_password(self, key: str, password: str) -> str:\n        \"\"\"Validate the given password value.\n\n        Note:\n            This function was updated to support both Python 2.7 and 3.5+. It will now\n            explicitly convert the base64 bytes object into a string object.\n\n        Args:\n            key (str): The name of the validated column.\n            password (str): The password value.\n\n        Raises:\n            TypeError: If the given password is None.\n            ValueError: If the given password is an empty string.\n\n        Returns:\n            str: The mangled password.\n        \"\"\"\n        if password is None:\n            raise TypeError(f\"{self.__class__.__name__}.password cannot be None\")\n\n        if password == \"\":\n            raise ValueError(\n                f\"{self.__class__.__name__}.password cannot be an empty string\"\n            )\n\n        # mangle the password\n        mangled_password_bytes = base64.b64encode(password.encode(\"utf-8\"))\n        mangled_password_str = str(mangled_password_bytes.decode(\"utf-8\"))\n        return mangled_password_str\n\n    def check_password(self, raw_password: str) -> bool:\n        \"\"\"Check the given raw_password.\n\n        Check the given raw_password with the current User object's mangled password.\n        Handles the encryption process behind the scene.\n\n        Note:\n            This function was updated to support both Python 2.7 and 3.5+.\n            It will now compare the string (str) versions of the given\n            raw_password and the current Users object encrypted password.\n\n        Args:\n            raw_password (str): The raw password.\n\n        Returns:\n            bool: If the given raw password matches the password stored in the db.\n        \"\"\"\n        mangled_password_str = str(self.password)\n        raw_password_bytes = base64.b64encode(bytes(raw_password.encode(\"utf-8\")))\n        raw_password_encrypted_str = str(raw_password_bytes.decode(\"utf-8\"))\n        return mangled_password_str == raw_password_encrypted_str\n\n    @validates(\"groups\")\n    def _validate_groups(self, key: str, group: Group) -> Group:\n        \"\"\"Validate the given group value.\n\n        Args:\n            key (str): The name of the validated column.\n            group (Group): The :class:`.Group` instance to be validated.\n\n        Raises:\n            TypeError: If the given group arg value is not a :class:`.Group` instance.\n\n        Returns:\n            Group: The validated :class:`.Group` instance.\n        \"\"\"\n        if not isinstance(group, Group):\n            raise TypeError(\n                f\"Any group in {self.__class__.__name__}.groups should be an instance \"\n                \"of stalker.models.auth.Group, \"\n                f\"not {group.__class__.__name__}: '{group}'\"\n            )\n\n        return group\n\n    @validates(\"tasks\")\n    def _validate_tasks(self, key: str, task: \"Task\") -> \"Task\":\n        \"\"\"Validate the given tasks attribute.\n\n        Args:\n            key (str): The name of the validated column.\n            task (stalker.models.task.Task): The :class:`stalker.models.task.Task`\n                instance to be validated.\n\n        Raises:\n            TypeError: If the given task arg value is not a\n                :class:`stalker.models.task.Task` instance.\n\n        Returns:\n            Task: The validated :class:`stalker.models.task.Task` instance.\n        \"\"\"\n        from stalker.models.task import Task\n\n        if not isinstance(task, Task):\n            raise TypeError(\n                f\"Any element in {self.__class__.__name__}.tasks should be an instance \"\n                f\"of stalker.models.task.Task, not {task.__class__.__name__}: '{task}'\"\n            )\n        return task\n\n    @validates(\"watching\")\n    def _validate_watching(self, key: str, task: \"Task\") -> \"Task\":\n        \"\"\"Validate the given watching attribute.\n\n        Args:\n            key (str): The name of the validated column.\n            task (stalker.models.task.Task): The :class:`stalker.models.task.Task`\n                instance that the user will watch.\n\n        Raises:\n            TypeError: If the given task arg value is not a\n                :class:`stalker.models.task.Task` instance.\n\n        Returns:\n            Task: The validated :class:`stalker.models.task.Task` instance.\n        \"\"\"\n        from stalker.models.task import Task\n\n        if not isinstance(task, Task):\n            raise TypeError(\n                f\"Any element in {self.__class__.__name__}.watching should be an \"\n                \"instance of stalker.models.task.Task, \"\n                f\"not {task.__class__.__name__}: '{task}'\"\n            )\n        return task\n\n    @validates(\"vacations\")\n    def _validate_vacations(self, key: str, vacation: \"Vacation\") -> \"Vacation\":\n        \"\"\"Validate the given vacation value.\n\n        Args:\n            key (str): The name of the validated column.\n            vacation (stalker.models.studio.Vacation): The\n                :class:`stalker.models.studio.Vacation` instance.\n\n        Raises:\n            TypeError: If the given vacation argument value is not a\n                :class:`stalker.models.vacation.Vacation` instance.\n\n        Returns:\n            Vacation: The validated vacation value.\n        \"\"\"\n        from stalker.models.studio import Vacation\n\n        if not isinstance(vacation, Vacation):\n            raise TypeError(\n                f\"All of the elements in {self.__class__.__name__}.vacations should be \"\n                \"a stalker.models.studio.Vacation instance, \"\n                f\"not {vacation.__class__.__name__}: '{vacation}'\"\n            )\n        return vacation\n\n    @validates(\"efficiency\")\n    def _validate_efficiency(\n        self, key: str, efficiency: Union[None, int, float]\n    ) -> float:\n        \"\"\"Validate the given efficiency value.\n\n        Args:\n            key (str): The name of the validated column.\n            efficiency (Union[None, int, float]): The efficiency of this User instance.\n                This shows how efficient the user works. It is a number between 0-1. If\n                None given, a default value of 1.0 will be used.\n\n        Raises:\n            TypeError: If the given efficiency is not one of [None, int, float].\n            ValueError: If the given efficiency is a negative value.\n\n        Returns:\n            float: The validated efficiency value.\n        \"\"\"\n        if efficiency is None:\n            efficiency = 1.0\n\n        if not isinstance(efficiency, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.efficiency should be a float number \"\n                \"greater or equal to 0.0, \"\n                f\"not {efficiency.__class__.__name__}: '{efficiency}'\"\n            )\n\n        if efficiency < 0:\n            raise ValueError(\n                f\"{self.__class__.__name__}.efficiency should be a float number \"\n                f\"greater or equal to 0.0, not {efficiency}\"\n            )\n\n        return float(efficiency)\n\n    @validates(\"rate\")\n    def _validate_rate(self, key: str, rate: Union[int, float]) -> float:\n        \"\"\"Validate the given rate value.\n\n        Args:\n            key (str): The name of the validated column.\n            rate (Union[int, float]): An int or float value representing the User hourly\n                rate.\n\n        Raises:\n            TypeError: If the given rate is not an int or float.\n            ValueError: If the given rate is a negative number.\n\n        Returns:\n            float: The validated rate value.\n        \"\"\"\n        if rate is None:\n            rate = 0.0\n\n        if not isinstance(rate, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.rate should be a float number greater or \"\n                f\"equal to 0.0, not {rate.__class__.__name__}: '{rate}'\"\n            )\n\n        if rate < 0:\n            raise ValueError(\n                f\"{self.__class__.__name__}.rate should be a float number greater or \"\n                f\"equal to 0.0, not {rate}\"\n            )\n\n        return float(rate)\n\n    @property\n    def tickets(self) -> List[\"Ticket\"]:\n        \"\"\"Return the list of :class:`.Ticket` s that this user has.\n\n        Returns:\n            List[Ticket]: The list of :class:`.Ticket` instances which this user is the\n                owner of.\n        \"\"\"\n        # do it with sqlalchemy\n        from stalker.models.ticket import Ticket\n\n        return Ticket.query.filter(Ticket.owner == self).all()\n\n    @property\n    def open_tickets(self) -> List[\"Ticket\"]:\n        \"\"\"Return the list of open :class:`.Ticket` s that this user has.\n\n        Returns:\n             List[Ticket]: A list of :class:`.Ticket` instances which are not closed and\n                this user is assigned as the owner.\n        \"\"\"\n        from stalker import Ticket\n\n        return (\n            Ticket.query.join(Status, Ticket.status)\n            .filter(Ticket.owner == self)\n            .filter(Status.code != \"CLS\")\n            .all()\n        )\n\n    @property\n    def to_tjp(self) -> str:\n        \"\"\"Return a TaskJuggler compatible str representation of this User instance.\n\n        Returns:\n            str: The TaskJuggler compatible representation of this User instance.\n        \"\"\"\n        tab = \"    \"\n        indent = tab\n        tjp = f'resource {self.tjp_id} \"{self.tjp_id}\" {{'\n        tjp += f\"\\n{indent}efficiency {self.efficiency}\"\n        for vacation in self.vacations:\n            tjp += \"\\n\"\n            tjp += \"\\n\".join(f\"{indent}{line}\" for line in vacation.to_tjp.split(\"\\n\"))\n        tjp += \"\\n}\"\n        return tjp\n\n\nclass LocalSession(object):\n    \"\"\"A simple temporary session object which simple stores session data.\n\n    This class will later be removed, it is here because we need a login window\n    for the Qt user interfaces.\n\n    On initialize it will load the SessionData from the users .strc folder\n    \"\"\"\n\n    def __init__(self) -> None:\n        self.logged_in_user_id = None\n        self.valid_to = None\n        self.session_data = None\n        self.load()\n\n    def load(self) -> None:\n        \"\"\"Load the data from the saved local session.\"\"\"\n        try:\n            with open(LocalSession.session_file_full_path(), \"r\") as s:\n                # try:\n                json_object = json.load(s)\n                valid_to = millis_to_datetime(json_object.get(\"valid_to\"))\n                if valid_to > datetime.now(pytz.utc):\n                    # fill __dict__ with the loaded one\n                    self.valid_to = valid_to\n                    self.logged_in_user_id = json_object.get(\"logged_in_user_id\")\n        except IOError:\n            pass\n\n    @property\n    def logged_in_user(self) -> \"User\":\n        \"\"\"Return the logged-in user.\n\n        Returns:\n            User: The logged-in user.\n        \"\"\"\n        return User.query.filter_by(id=self.logged_in_user_id).first()\n\n    def store_user(self, user: \"User\") -> None:\n        \"\"\"Store the given user instance.\n\n        Args:\n            user (User): The :class:`.User` instance.\n        \"\"\"\n        if user:\n            self.logged_in_user_id = user.id\n\n    def save(self) -> None:\n        \"\"\"Remember the data in user local file system.\"\"\"\n        self.valid_to = datetime.now(pytz.utc) + timedelta(days=10)\n        # serialize self\n        dumped_data = json.dumps(\n            {\n                \"valid_to\": datetime_to_millis(self.valid_to),\n                \"logged_in_user_id\": self.logged_in_user_id,\n            },\n        )\n        logger.debug(f\"dumped session data : {dumped_data}\")\n        self._write_data(dumped_data)\n\n    def delete(self) -> None:\n        \"\"\"Remove the cache file.\"\"\"\n        try:\n            os.remove(self.session_file_full_path())\n        except OSError:\n            pass\n\n    @classmethod\n    def session_file_full_path(cls) -> str:\n        \"\"\"Return the session file full path.\n\n        Returns:\n            str: The session file full path.\n        \"\"\"\n        return os.path.normpath(\n            os.path.join(\n                defaults.local_storage_path, defaults.local_session_data_file_name\n            )\n        )\n\n    def _write_data(self, data: str) -> None:\n        \"\"\"Write the given data to the local session file.\n\n        Args:\n            data (str): The data to be written (generally serialized LocalSession class\n                itself)\n        \"\"\"\n        file_full_path = self.session_file_full_path()\n\n        # create the path first\n        file_path = os.path.dirname(file_full_path)\n        try:\n            os.makedirs(file_path)\n        except OSError:\n            # dir exists\n            pass\n        finally:\n            with open(file_full_path, \"w\") as data_file:\n                data_file.write(data)\n\n\nclass Role(Entity):\n    \"\"\"Defines a User role.\n\n    .. versionadded 0.2.11: Roles\n\n    When :class:`.User` s are assigned to a\n    :class:`.Client`/:class:`.Department`, they also can be assigned to a role\n    for that client/department.\n\n    Also, because Users can be assigned to multiple clients/departments they\n    can have different roles for each of this clients/departments.\n\n    The duty of this class is to defined different roles that can be reused\n    when required. So one can defined a **Lead** role and then assign a User to\n    a department with its role is set to \"lead\". This essentially generalizes\n    the previous implementation of now removed *Department.lead* attribute.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Roles\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Role\"}\n\n    role_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs) -> None:\n        super(Role, self).__init__(**kwargs)\n\n\ndef create_department_user(department: \"Department\") -> \"DepartmentUser\":\n    \"\"\"Create DepartmentUser instance on association proxy.\n\n    Args:\n        department (stalker.models.department.Department): The\n            :class:`stalker.models.department.Department` instance.\n\n    Returns:\n        stalker.models.department.DepartmentUser: The\n            :class:`stalker.models.department.DepartmentUser` instance.\n    \"\"\"\n    from stalker.models.department import DepartmentUser\n\n    return DepartmentUser(department=department)\n\n\ndef create_client_user(client: \"Client\") -> \"ClientUser\":\n    \"\"\"Create ClientUser instance on association proxy.\n\n    Args:\n        client (stalker.models.client.Client): The\n            :class:`stalker.models.project.Project` instance.\n\n    Returns:\n        stalker.models.client.ClientUser: The\n            :class:`stalker.models.client.ClientUser` instance.\n    \"\"\"\n    from stalker.models.client import ClientUser\n\n    return ClientUser(client=client)\n\n\ndef create_project_user(project: \"Project\") -> \"ProjectUser\":\n    \"\"\"Create ProjectUser instance on association proxy.\n\n    Args:\n        project (stalker.models.project.Project): The\n            :class:`stalker.models.project.Project` instance.\n\n    Returns:\n        stalker.models.project.ProjectUser: The\n            :class:`stalker.models.project.ProjectUser` instance.\n    \"\"\"\n    from stalker.models.project import ProjectUser\n\n    return ProjectUser(project=project)\n\n\n# Group_Users\nGroup_Users = Table(\n    \"Group_Users\",\n    Base.metadata,\n    Column(\"uid\", Integer, ForeignKey(\"Users.id\"), primary_key=True),\n    Column(\"gid\", Integer, ForeignKey(\"Groups.id\"), primary_key=True),\n)\n\n\nclass AuthenticationLog(SimpleEntity):\n    \"\"\"Keeps track of login/logout dates and the action (login or logout).\"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"AuthenticationLogs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"AuthenticationLog\"}\n\n    log_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n    user_id: Mapped[int] = mapped_column(\"uid\", Integer, ForeignKey(\"Users.id\"))\n    user: Mapped[User] = relationship(\n        primaryjoin=\"AuthenticationLogs.c.uid==Users.c.id\",\n        uselist=False,\n        back_populates=\"authentication_logs\",\n        doc=\"The :class:`.User` instance that this AuthenticationLog is created for\",\n    )\n    action: Mapped[str] = mapped_column(Enum(LOGIN, LOGOUT, name=\"ActionNames\"))\n    date: Mapped[datetime] = mapped_column(GenericDateTime)\n\n    def __init__(self, user=None, date=None, action=LOGIN, **kwargs) -> None:\n        super(AuthenticationLog, self).__init__(**kwargs)\n        self.user = user\n        self.date = date\n        self.action = action\n\n    def __lt__(self, other: \"AuthenticationLog\") -> bool:\n        \"\"\"Make this object order-able.\n\n        Args:\n            other (.AuthenticationLog): The other :class:`.AuthenticationLog`\n                instance.\n\n        Returns:\n            Tuple(str, str): The str key to be used for ordering.\n        \"\"\"\n        return self.date < other.date\n\n    @validates(\"user\")\n    def __validate_user__(self, key: str, user: \"User\") -> \"User\":\n        \"\"\"Validate the given user argument value.\n\n        Args:\n            key (str): The name of the validated column.\n            user (User): The :class:`.User` instance to be validated.\n\n        Raises:\n            TypeError: If the given user args is not a :class:`.User` instance.\n\n        Returns:\n            .User: The validated :class:`.User` instance.\n        \"\"\"\n        if not isinstance(user, User):\n            raise TypeError(\n                f\"{self.__class__.__name__}.user should be a User instance, \"\n                f\"not {user.__class__.__name__}: '{user}'\"\n            )\n\n        return user\n\n    @validates(\"action\")\n    def __validate_action__(self, key: str, action: str) -> str:\n        \"\"\"Validate the given action argument value.\n\n        Args:\n            key (str): The name of the validated column.\n            action (str): One of LOGIN or LOGOUT enum values.\n\n        Raises:\n            ValueError: If the given value is not one of LOGIN or LOGOUT.\n\n        Returns:\n            str: The validated action value.\n        \"\"\"\n        if action is None:\n            action = copy.copy(LOGIN)\n\n        if action not in [LOGIN, LOGOUT]:\n            raise ValueError(\n                f'{self.__class__.__name__}.action should be one of \"login\" or '\n                f'\"logout\", not \"{action}\"'\n            )\n\n        return action\n\n    @validates(\"date\")\n    def __validate_date__(self, key: str, date: datetime) -> datetime:\n        \"\"\"Validate the given date value.\n\n        Args:\n            key (str): The name of the validated column.\n            date (datetime): The datetime instance.\n\n        Raises:\n            TypeError: If the given date is not a datetime instance.\n\n        Returns:\n            datetime: Returns the validated datetime instance.\n        \"\"\"\n        if date is None:\n            date = datetime.now(pytz.utc)\n\n        if not isinstance(date, datetime):\n            raise TypeError(\n                f\"{self.__class__.__name__}.date should be a datetime.datetime \"\n                f\"instance, not {date.__class__.__name__}: '{date}'\"\n            )\n\n        return date\n"
  },
  {
    "path": "src/stalker/models/budget.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Budget related classes and functions are situated here.\"\"\"\nfrom typing import Any, Dict, List, Optional, TYPE_CHECKING, Union\n\nfrom sqlalchemy import Column, Float, ForeignKey, Integer, String, Table\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, validates\n\nfrom stalker.db.declarative import Base\nfrom stalker.models.entity import Entity\nfrom stalker.models.mixins import (\n    AmountMixin,\n    DAGMixin,\n    ProjectMixin,\n    StatusMixin,\n    UnitMixin,\n)\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.client import Client\n\n\nclass Good(Entity, UnitMixin):\n    \"\"\"Manages commercial items that is served by the Studio.\n\n    A Studio can define service prices or items that's been sold by the Studio\n    by using a list of commercial items.\n\n    .. note::\n       .. versionadded 0.2.20: Client Specific Goods\n\n       Clients now can own a list of :class:`.Good` s attached to them.\n       So one can define a list of :class:`.Good` s with special prices\n       adjusted for a particular ``Client``, then get them back from the db by\n       querying the :class:`.Good` s those have their ``client`` attribute set\n       to that particular ``Client`` instance. Removing a ``Good`` from a\n       :class:`.Client` will not delete it from the database, but deleting a\n       :class:`.Client` will also delete the ``Good`` s attached to that\n       particular :class:`.Client`.\n\n    .. ::\n       don't forget to update the Client documentation, which also has the same\n       text.\n\n    A Good has the following attributes\n\n    Args:\n        cost (Union[int, float]): The cost of this item to the Studio, so\n            generally it is better to keep price of the related BudgetEntry\n            bigger than this value to get profit by selling this item.\n        msrp (Union[int, float]): The suggested retail price for this item.\n        unit (str): The unit of this item.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Goods\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Good\"}\n\n    good_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    price_lists: Mapped[\"PriceList\"] = relationship(\n        secondary=\"PriceList_Goods\",\n        primaryjoin=\"Goods.c.id==PriceList_Goods.c.good_id\",\n        secondaryjoin=\"PriceList_Goods.c.price_list_id==PriceLists.c.id\",\n        back_populates=\"goods\",\n        doc=\"PriceLists that this good is related to.\",\n    )\n\n    cost: Mapped[Optional[float]] = mapped_column(default=0.0)\n    msrp: Mapped[Optional[float]] = mapped_column(Float, default=0.0)\n    unit: Mapped[Optional[str]] = mapped_column(String(64))\n\n    client_id: Mapped[Optional[int]] = mapped_column(\n        \"client_id\", ForeignKey(\"Clients.id\")\n    )\n    client: Mapped[\"Client\"] = relationship(\n        primaryjoin=\"Goods.c.client_id==Clients.c.id\",\n        back_populates=\"goods\",\n        uselist=False,\n    )\n\n    def __init__(\n        self,\n        cost: Union[int, float] = 0.0,\n        msrp: Union[int, float] = 0.0,\n        unit: str = \"\",\n        client: Optional[\"Client\"] = None,\n        **kwargs,\n    ) -> None:\n        super(Good, self).__init__(**kwargs)\n        UnitMixin.__init__(self, unit=unit)\n        self.cost = cost\n        self.msrp = msrp\n        self.client = client\n\n    @validates(\"cost\")\n    def _validate_cost(self, key: str, cost: Union[int, float]) -> Union[int, float]:\n        \"\"\"Validate the given cost value.\n\n        Args:\n            key (str): The name of the validated column.\n            cost (Union[int, float]): The cost value to be validated.\n\n        Raises:\n            TypeError: If the given cost value is not an int or float.\n            ValueError: If the given cost is a negative value.\n\n        Returns:\n            float: The validated cost value.\n        \"\"\"\n        if cost is None:\n            cost = 0.0\n\n        if not isinstance(cost, (float, int)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.cost should be a non-negative number, \"\n                f\"not {cost.__class__.__name__}: '{cost}'\"\n            )\n\n        if cost < 0.0:\n            raise ValueError(\n                f\"{self.__class__.__name__}.cost should be a non-negative number\"\n            )\n\n        return cost\n\n    @validates(\"msrp\")\n    def _validate_msrp(self, key: str, msrp: Union[int, float]) -> Union[int, float]:\n        \"\"\"Validate the given msrp value.\n\n        Args:\n            key (str): The name of the validated column.\n            msrp (Union[int, float]): The msrp value to be validated.\n\n        Raises:\n            TypeError: If the given msrp value is not an int or float.\n            ValueError: If the msrp is a negative value.\n\n        Returns:\n            float: The validated msrp value.\n        \"\"\"\n        if msrp is None:\n            msrp = 0.0\n\n        if not isinstance(msrp, (float, int)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.msrp should be a non-negative number, \"\n                f\"not {msrp.__class__.__name__}: '{msrp}'\"\n            )\n\n        if msrp < 0.0:\n            raise ValueError(\n                f\"{self.__class__.__name__}.msrp should be a non-negative number\"\n            )\n\n        return msrp\n\n    @validates(\"client\")\n    def _validate_client(self, key: str, client: \"Client\") -> \"Client\":\n        \"\"\"Validate the given client value.\n\n        Args:\n            key (str): The name of the validated column.\n            client (Client): The client value to be validated.\n\n        Raises:\n            TypeError: If the given client arg value is not a\n                :class:`stalker.models.client.Client` instance.\n\n        Returns:\n            Client: The validated :class:`stalker.models.client.Client` instance.\n        \"\"\"\n        if client is not None:\n            from stalker.models.client import Client\n\n            if not isinstance(client, Client):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.client attribute should be a \"\n                    \"stalker.models.client.Client instance, \"\n                    f\"not {client.__class__.__name__}: '{client}'\"\n                )\n        return client\n\n\nclass PriceList(Entity):\n    \"\"\"Contains CommercialItems to create a list of items that is sold by the Studio.\n\n    You can create different lists for items sold in this studio.\n\n    Args:\n        goods (List[Good]): A list of :class:`.Good` instances to be put into this\n            :class:`.PriceList` instance.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"PriceLists\"\n    __mapper_args__ = {\"polymorphic_identity\": \"PriceList\"}\n\n    price_list_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    goods: Mapped[Optional[List[\"Good\"]]] = relationship(\n        secondary=\"PriceList_Goods\",\n        primaryjoin=\"PriceLists.c.id==PriceList_Goods.c.price_list_id\",\n        secondaryjoin=\"PriceList_Goods.c.good_id==Goods.c.id\",\n        back_populates=\"price_lists\",\n        doc=\"Goods in this list.\",\n    )\n\n    def __init__(\n        self,\n        goods: Optional[List[\"Good\"]] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        super(PriceList, self).__init__(**kwargs)\n        if goods is None:\n            goods = []\n        self.goods = goods\n\n    @validates(\"goods\")\n    def _validate_goods(self, key: str, good: \"Good\") -> \"Good\":\n        \"\"\"Validate the given good value.\n\n        Args:\n            key (str): The name of the validated column.\n            good (Good): The good value to be validated.\n\n        Raises:\n            TypeError: If the given good arg value is not a :class:`.Good` instance.\n\n        Returns:\n            Good: The validated :class:`.Good` instance.\n        \"\"\"\n        if not isinstance(good, Good):\n            raise TypeError(\n                f\"{self.__class__.__name__}.goods should only contain \"\n                \"instances of stalker.model.budget.Good, \"\n                f\"not {good.__class__.__name__}: '{good}'\"\n            )\n        return good\n\n\nPriceList_Goods = Table(\n    \"PriceList_Goods\",\n    Base.metadata,\n    Column(\"price_list_id\", Integer, ForeignKey(\"PriceLists.id\"), primary_key=True),\n    Column(\"good_id\", Integer, ForeignKey(\"Goods.id\"), primary_key=True),\n)\n\n\nclass Budget(Entity, ProjectMixin, DAGMixin, StatusMixin):\n    \"\"\"Manages project budgets.\n\n    Budgets manager :class:`.Project` budgets. You can create entries as\n    instances of :class:`.BudgetEntry` class.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Budgets\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Budget\"}\n\n    budget_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    __id_column__ = \"budget_id\"\n\n    entries: Mapped[Optional[List[\"BudgetEntry\"]]] = relationship(\n        primaryjoin=\"BudgetEntries.c.budget_id==Budgets.c.id\",\n        cascade=\"all, delete-orphan\",\n    )\n\n    invoices: Mapped[Optional[List[\"Invoice\"]]] = relationship(\n        primaryjoin=\"Invoices.c.budget_id==Budgets.c.id\",\n        cascade=\"all, delete-orphan\",\n    )\n\n    def __init__(self, **kwargs: Dict[str, Any]) -> None:\n        super(Budget, self).__init__(**kwargs)\n        ProjectMixin.__init__(self, **kwargs)\n        DAGMixin.__init__(self, **kwargs)\n        StatusMixin.__init__(self, **kwargs)\n\n    @validates(\"entries\")\n    def _validate_entry(self, key: str, entry: \"BudgetEntry\") -> \"BudgetEntry\":\n        \"\"\"Validate the given entry value.\n\n        Args:\n            key (str): The name of the validated column.\n            entry (BudgetEntry): The entry value to be validated.\n\n        Raises:\n            TypeError: If the given entry value is not a :class:`.BudgetEntry` instance.\n\n        Returns:\n            BudgetEntry: The validated BudgetEntry instance.\n        \"\"\"\n        if not isinstance(entry, BudgetEntry):\n            raise TypeError(\n                f\"{self.__class__.__name__}.entries should only contain \"\n                \"instances of BudgetEntry, \"\n                f\"not {entry.__class__.__name__}: '{entry}'\"\n            )\n        return entry\n\n\nclass BudgetEntry(Entity, AmountMixin, UnitMixin):\n    \"\"\"Manages entries in a Budget.\n\n    With BudgetEntries one can manage project budget entries one by one. Each\n    entry shows one component of a bigger budget. Entries are generally a\n    reflection of a :class:`.Good` instance and shows how many of that Good has\n    been included in this Budget, and what was the discounted price of that\n    Good.\n\n    Args:\n        budget (Budget): The :class:`.Budget` that this entry is a part of.\n        good (Good): Stores a :class:`.Good` instance to carry all the cost/msrp/unit\n            data from.\n        price (float): The decided price of this entry. This is generally bigger than\n            the :attr:`.cost` and should be also bigger than :attr:`.msrp` but the\n            person that is editing the budget which this entry is related to can decide\n            to do a discount on this entry and give a different price. This attribute\n            holds the proposed final price.\n        realized_total (float): This attribute is for holding the realized price of this\n            entry. It can be the same number of the :attr:`.price` multiplied by the\n            :attr:`.amount` or can be something else that reflects the reality.\n            Generally it is for calculating the \"service\" cost/profit.\n        amount (float): Defines the amount of :class:`Good` that is in consideration for\n            this entry.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"BudgetEntries\"\n    __mapper_args__ = {\"polymorphic_identity\": \"BudgetEntry\"}\n\n    entry_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n    budget_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Budgets.id\"))\n    budget: Mapped[\"Budget\"] = relationship(\n        primaryjoin=\"BudgetEntries.c.budget_id==Budgets.c.id\",\n        back_populates=\"entries\",\n        uselist=False,\n    )\n\n    good_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Goods.id\"))\n\n    good: Mapped[\"Good\"] = relationship(\n        primaryjoin=\"BudgetEntries.c.good_id==Goods.c.id\", uselist=False\n    )\n\n    cost: Mapped[Optional[float]] = mapped_column(default=0.0)\n    msrp: Mapped[Optional[float]] = mapped_column(default=0.0)\n\n    price: Mapped[Optional[float]] = mapped_column(default=0.0)\n    realized_total: Mapped[Optional[float]] = mapped_column(default=0.0)\n\n    def __init__(\n        self,\n        budget: Optional[Budget] = None,\n        good: Optional[Good] = None,\n        price: Union[float, int] = 0,\n        realized_total: Union[float, int] = 0,\n        amount: Union[float, int] = 0.0,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        super(BudgetEntry, self).__init__(**kwargs)\n\n        self.budget = budget\n        self.good = good\n        self.cost = good.cost\n        self.msrp = good.msrp\n\n        kwargs[\"unit\"] = good.unit\n        kwargs[\"amount\"] = amount\n\n        AmountMixin.__init__(self, **kwargs)\n        UnitMixin.__init__(self, **kwargs)\n\n        self.price = price\n        self.realized_total = realized_total\n\n    @validates(\"budget\")\n    def _validate_budget(self, key: str, budget: \"Budget\") -> \"Budget\":\n        \"\"\"Validate the given budget value.\n\n        Args:\n            key (str): The name of the validated column.\n            budget (Budget): The budget that needs to be validated.\n\n        Raises:\n            TypeError: If the given budget is not a :class:`.Budget` instance.\n\n        Returns:\n            Budget: The validated :class:`.Budget` instance.\n        \"\"\"\n        if not isinstance(budget, Budget):\n            raise TypeError(\n                f\"{self.__class__.__name__}.budget should be a Budget instance, \"\n                f\"not {budget.__class__.__name__}: '{budget}'\"\n            )\n        return budget\n\n    @validates(\"cost\")\n    def _validate_cost(self, key: str, cost: Union[float, int]) -> float:\n        \"\"\"Validate the given cost value.\n\n        Args:\n            key (str): The name of the validated column.\n            cost (Union[int, float]): The cost value to be validated.\n\n        Raises:\n            TypeError: If the given cost value is not an int or float.\n\n        Returns:\n            float: The validated cost value.\n        \"\"\"\n        if cost is None:\n            cost = 0.0\n\n        if not isinstance(cost, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.cost should be a number, \"\n                f\"not {cost.__class__.__name__}: '{cost}'\"\n            )\n\n        return float(cost)\n\n    @validates(\"msrp\")\n    def _validate_msrp(self, key: str, msrp: Union[float, int]) -> float:\n        \"\"\"Validate the given msrp value.\n\n        Args:\n            key (str): The name of the validated column.\n            msrp (Union[int, float]): The msrp value to be validated.\n\n        Raises:\n            TypeError: If the given msrp value is not an int or float.\n\n        Returns:\n            float: The validated msrp value.\n        \"\"\"\n        if msrp is None:\n            msrp = 0.0\n\n        if not isinstance(msrp, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.msrp should be a number, \"\n                f\"not {msrp.__class__.__name__}: '{msrp}'\"\n            )\n\n        return float(msrp)\n\n    @validates(\"price\")\n    def _validate_price(self, key: str, price: Union[float, int]) -> float:\n        \"\"\"Validate the given price value.\n\n        Args:\n            key (str): The name of the validated column.\n            price (Union[int, float]): The price value to be validated.\n\n        Raises:\n            TypeError: If the given price value is not an int or float.\n\n        Returns:\n            float: The validated price value.\n        \"\"\"\n        if price is None:\n            price = 0.0\n\n        if not isinstance(price, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.price should be a number, \"\n                f\"not {price.__class__.__name__}: '{price}'\"\n            )\n\n        return float(price)\n\n    @validates(\"realized_total\")\n    def _validate_realized_total(\n        self, key: str, realized_total: Union[float, int]\n    ) -> float:\n        \"\"\"Validate  the given realized_total value.\n\n        Args:\n            key (str): The name of the validated column.\n            realized_total (Union[int, float]): The realized_total value to be\n                validated.\n\n        Raises:\n            TypeError: If the given realized_total value is not an int or float.\n\n        Returns:\n            float: The validated realized_total value.\n        \"\"\"\n        if realized_total is None:\n            realized_total = 0.0\n\n        if not isinstance(realized_total, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.realized_total should be a number, \"\n                f\"not {realized_total.__class__.__name__}: '{realized_total}'\"\n            )\n\n        return float(realized_total)\n\n    @validates(\"good\")\n    def _validate_good(self, key: str, good: \"Good\") -> \"Good\":\n        \"\"\"Validate the given good value.\n\n        Args:\n            key (str): The name of the validated column.\n            good (Good): The good to be validated.\n\n        Raises:\n            TypeError: If the given good is not a :class:`.Good` instance.\n\n        Returns:\n            Good: Returns the validated :class:`.Good` instance.\n        \"\"\"\n        if not isinstance(good, Good):\n            raise TypeError(\n                f\"{self.__class__.__name__}.good should be a \"\n                \"stalker.models.budget.Good instance, \"\n                f\"not {good.__class__.__name__}: '{good}'\"\n            )\n\n        return good\n\n\nclass Invoice(Entity, AmountMixin, UnitMixin):\n    \"\"\"Holds information about invoices.\n\n    Invoices are part of :class:`.Budgets`. The main purpose of invoices are\n    to track the :class:`.Payment` s. It is a very primitive entity. It is\n    by no means designed to hold real financial information (at least for now).\n\n    Args:\n        client (Client): The :class:`.Client` instance that shows the payer for this\n            invoice.\n        budget (Budget): The :class:`.Budget` instance that owns this invoice.\n        amount (Union[int, float]): The amount of this invoice. Without the\n            :attr:`.Invoice.unit` attribute it is meaningless. This cannot be skipped.\n        unit (str): The unit of the issued amount. This cannot be skipped.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"Invoices\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Invoice\"}\n\n    invoice_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n    budget_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Budgets.id\"))\n\n    budget: Mapped[Optional[\"Budget\"]] = relationship(\n        primaryjoin=\"Invoices.c.budget_id==Budgets.c.id\",\n        back_populates=\"invoices\",\n        uselist=False,\n    )\n\n    client_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Clients.id\"))\n\n    client: Mapped[Optional[\"Client\"]] = relationship(\n        primaryjoin=\"Invoices.c.client_id==Clients.c.id\", uselist=False\n    )\n\n    payments: Mapped[Optional[List[\"Payment\"]]] = relationship(\n        primaryjoin=\"Payments.c.invoice_id==Invoices.c.id\",\n        cascade=\"all, delete-orphan\",\n    )\n\n    def __init__(\n        self,\n        budget: Optional[\"Budget\"] = None,\n        client: Optional[\"Client\"] = None,\n        amount: Union[float, int] = 0,\n        unit: Optional[str] = None,\n        **kwargs,\n    ) -> None:\n        super(Invoice, self).__init__(**kwargs)\n        AmountMixin.__init__(self, amount=amount)\n        UnitMixin.__init__(self, unit=unit)\n        self.budget = budget\n        self.client = client\n\n    @validates(\"budget\")\n    def _validate_budget(self, key: str, budget: \"Budget\") -> \"Budget\":\n        \"\"\"Validate the given budget value.\n\n        Args:\n            key (str): The name of the validated column.\n            budget (Budget): The :class:`.Budget` instance to be validated.\n\n        Raises:\n            TypeError: If the given budget arg value is not a :class:`.Budget` instance.\n\n        Returns:\n            Budget: The validated :class:`.Budget` instance.\n        \"\"\"\n        if not isinstance(budget, Budget):\n            raise TypeError(\n                f\"{self.__class__.__name__}.budget should be a Budget instance, \"\n                f\"not {budget.__class__.__name__}: '{budget}'\"\n            )\n        return budget\n\n    @validates(\"client\")\n    def _validate_client(self, key: str, client: \"Client\") -> \"Client\":\n        \"\"\"Validate the given client value.\n\n        Args:\n            key (str): The name of the validated column.\n            client (Client): The :class:`stalker.models.client.Client` instance to be\n                validated.\n\n        Raises:\n            TypeError: If the ``client`` is not a :class:`stalker.models.client.Client`\n                instance.\n\n        Returns:\n            Client: The validated :class:`stalker.models.client.Client` instance.\n        \"\"\"\n        from stalker.models.client import Client\n\n        if not isinstance(client, Client):\n            raise TypeError(\n                f\"{self.__class__.__name__}.client should be a Client instance, \"\n                f\"not {client.__class__.__name__}: '{client}'\"\n            )\n        return client\n\n\nclass Payment(Entity, AmountMixin, UnitMixin):\n    \"\"\"Holds information about the payments.\n\n    Each payment should be related with an :class:`.Invoice` instance. Use the\n    :attr:`.type` attribute to diversify payments (ex. \"Advance\").\n\n    Args:\n        invoice (Invoice): The :class:`.Invoice` instance that this payment is related\n            to. This cannot be skipped.\n        amount (Union[int, float]): The amount value.\n        unit (Optional[str]): The unit of this mixed in class.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"Payments\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Payment\"}\n\n    payment_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n    invoice_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Invoices.id\"))\n    invoice: Mapped[Optional[\"Invoice\"]] = relationship(\n        primaryjoin=\"Payments.c.invoice_id==Invoices.c.id\",\n        back_populates=\"payments\",\n        uselist=False,\n    )\n\n    def __init__(\n        self,\n        invoice: Optional[\"Invoice\"] = None,\n        amount: Union[int, float] = 0,\n        unit: Optional[str] = None,\n        **kwargs,\n    ) -> None:\n        super(Payment, self).__init__(**kwargs)\n        AmountMixin.__init__(self, amount=amount)\n        UnitMixin.__init__(self, unit=unit)\n        self.invoice = invoice\n\n    @validates(\"invoice\")\n    def _validate_invoice(self, key: str, invoice: \"Invoice\") -> \"Invoice\":\n        \"\"\"Validate the invoice value.\n\n        Args:\n            key (str): The name of the validated column.\n            invoice (Invoice): The :class:`.Invoice` instance to validate.\n\n        Raises:\n            TypeError: The :class:`.Invoice` instance to be validated.\n\n        Returns:\n            Invoice: The validated :class:`.Invoice` instance.\n        \"\"\"\n        if not isinstance(invoice, Invoice):\n            raise TypeError(\n                f\"{self.__class__.__name__}.invoice should be an Invoice instance, \"\n                f\"not {invoice.__class__.__name__}: '{invoice}'\"\n            )\n        return invoice\n"
  },
  {
    "path": "src/stalker/models/client.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Client related classes and functions are situated here.\"\"\"\n\nfrom typing import Any, Dict, List, Optional, TYPE_CHECKING\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.ext.associationproxy import association_proxy\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, validates\n\nfrom stalker import log\nfrom stalker.db.declarative import Base\nfrom stalker.models.entity import Entity\nfrom stalker.models.project import create_project_client\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.auth import Role, User\n    from stalker.models.budget import Good\n    from stalker.models.project import Project, ProjectClient\n\nlogger = log.get_logger(__name__)\n\n\nclass Client(Entity):\n    \"\"\"The Client (e.g. a company) which users may be part of.\n\n    The information that a Client object holds is like:\n\n      * The users of the client\n      * The projects affiliated with the client\n      * and all the other things those are inherited from the Entity class\n\n    .. note::\n       .. versionadded 0.2.20: Client Specific Goods\n\n       Clients now can own a list of :class:`.Good` s attached to them.\n       So one can define a list of class:`.Good` s with special prices\n       adjusted for a particular ``Client``, then get them back from the db by\n       querying the :class:`.Good` s those have their ``client`` attribute set\n       to that particular ``Client`` instance. Removing a ``Good`` from a\n       :class:`.Client` will not delete it from the database, but deleting a\n       :class:`.Client` will also delete the ``Good`` s attached to that\n       particular :class:`.Client`.\n\n    .. ::\n       don't forget to update the Good documentation, which also has the same\n       text.\n\n    Two Client object considered the same if they have the same name.\n\n    So creating a client object needs the following parameters:\n\n    Args:\n        users (:class:`.User`): It can be an empty list, so one client can be created\n            without any user in it. But this parameter should be a list of User objects.\n\n        projects (List[Project]): it can be an empty list, so one client can be created\n            without any project in it. But this parameter should be a list of Project\n            objects.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Clients\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Client\"}\n    client_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    users = association_proxy(\"user_role\", \"user\", creator=lambda n: ClientUser(user=n))\n\n    user_role: Mapped[Optional[List[\"ClientUser\"]]] = relationship(\n        back_populates=\"client\",\n        cascade=\"all, delete-orphan\",\n        primaryjoin=\"Clients.c.id==Client_Users.c.cid\",\n        doc=\"\"\"List of users representing the members of this client.\"\"\",\n    )\n\n    projects = association_proxy(\n        \"project_role\", \"project\", creator=lambda p: create_project_client(p)\n    )\n\n    project_role: Mapped[Optional[List[\"ProjectClient\"]]] = relationship(\n        back_populates=\"client\",\n        cascade=\"all, delete-orphan\",\n        primaryjoin=\"Clients.c.id==Project_Clients.c.client_id\",\n    )\n\n    goods: Mapped[Optional[List[\"Good\"]]] = relationship(\n        \"Good\",\n        back_populates=\"client\",\n        cascade=\"all\",  # do not include \"delete-orphan\" we want to keep goods\n        # if they are detached on purpose\n        primaryjoin=\"Clients.c.id==Goods.c.client_id\",\n    )\n\n    def __init__(\n        self,\n        users: Optional[List[\"User\"]] = None,\n        projects: Optional[List[\"Project\"]] = None,\n        **kwargs: Optional[Dict[str, Any]],\n    ) -> None:\n        super(Client, self).__init__(**kwargs)\n\n        if users is None:\n            users = []\n\n        if projects is None:\n            projects = []\n\n        self.users = users\n        self.projects = projects\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check if the other Client is equal to this once.\n\n        Args:\n            other (Any): The other Client instance.\n\n        Returns:\n            bool: Returns True, if other object is a Client instance and equal to this\n                one.\n        \"\"\"\n        return super(Client, self).__eq__(other) and isinstance(other, Client)\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Client, self).__hash__()\n\n    @property\n    def to_tjp(self) -> str:\n        \"\"\"Return a TaskJuggler compatible str representation of this Client instance.\n\n        Returns:\n            str: The TaskJuggler compatible representation of this Client instance.\n        \"\"\"\n        return \"\"\n\n    @validates(\"goods\")\n    def _validate_good(self, key: str, good: \"Good\") -> \"Good\":\n        \"\"\"Validate the given good value.\n\n        Args:\n            key (str): The name of the validated column.\n            good (Good): The good value to be validated.\n\n        Raises:\n            TypeError: If the given good is not a :class:`stalker.models.budget.Good`\n                instance.\n\n        Returns:\n            Good: The validated good value.\n        \"\"\"\n        from stalker.models.budget import Good\n\n        if not isinstance(good, Good):\n            raise TypeError(\n                f\"{self.__class__.__name__}.goods should only \"\n                \"contain instances of stalker.models.budget.Good, \"\n                f\"not {good.__class__.__name__}: '{good}'\"\n            )\n\n        return good\n\n\nclass ClientUser(Base):\n    \"\"\"The association object used in Client-to-User relation.\n\n    Args:\n        client (Client): The client which the user is affiliated with.\n        user (User): A :class:`.User` instance.\n    \"\"\"\n\n    __tablename__ = \"Client_Users\"\n    user_id: Mapped[int] = mapped_column(\n        \"uid\", ForeignKey(\"Users.id\"), primary_key=True\n    )\n    user: Mapped[\"User\"] = relationship(\n        back_populates=\"company_role\",\n        primaryjoin=\"ClientUser.user_id==User.user_id\",\n    )\n    client_id: Mapped[int] = mapped_column(\n        \"cid\", ForeignKey(\"Clients.id\"), primary_key=True\n    )\n    client: Mapped[\"Client\"] = relationship(\n        back_populates=\"user_role\",\n        primaryjoin=\"ClientUser.client_id==Client.client_id\",\n    )\n    role_id: Mapped[Optional[int]] = mapped_column(\"rid\", ForeignKey(\"Roles.id\"))\n    role: Mapped[Optional[\"Role\"]] = relationship(\n        primaryjoin=\"ClientUser.role_id==Role.role_id\"\n    )\n\n    def __init__(self, client=None, user=None, role=None):\n        self.user = user\n        self.client = client\n        self.role = role\n\n    @validates(\"client\")\n    def _validate_client(self, key: str, client: \"Client\") -> \"Client\":\n        \"\"\"Validate the given client value.\n\n        Args:\n            key (str): The name of the validated column.\n            client (Client): The client instance to be validated.\n\n        Raises:\n            TypeError: If the given client value is not a :class:`.Client` instance.\n\n        Returns:\n            Client: The validated client instance.\n        \"\"\"\n        if client is not None:\n            if not isinstance(client, Client):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.client should be instance of \"\n                    \"stalker.models.client.Client, \"\n                    f\"not {client.__class__.__name__}: '{client}'\"\n                )\n        return client\n\n    @validates(\"user\")\n    def _validate_user(self, key: str, user: \"User\") -> \"User\":\n        \"\"\"Validate the given user value.\n\n        Args:\n            key (str): The name of the validated column.\n            user (User): The user instance to validate.\n\n        Raises:\n            TypeError: If the given user value is not a\n                :class:`stalker.models.auth.User` instance.\n\n        Returns:\n            User: The validated user value.\n        \"\"\"\n        if user is not None:\n            from stalker.models.auth import User\n\n            if not isinstance(user, User):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.user should be an instance of \"\n                    \"stalker.models.auth.User, \"\n                    f\"not {user.__class__.__name__}: '{user}'\"\n                )\n        return user\n\n    @validates(\"role\")\n    def _validate_role(self, key: str, role: \"Role\") -> \"Role\":\n        \"\"\"Validate the given role instance.\n\n        Args:\n            key (str): The name of the validated column.\n            role (Role): The role value to be validated.\n\n        Raises:\n            TypeError: If the given role value is not a\n                :class:`stalker.models.auth.Role` instance.\n\n        Returns:\n            Role: The validated :class:`stalker.models.auth.Role` instance.\n        \"\"\"\n        if role is not None:\n            from stalker.models.auth import Role\n\n            if not isinstance(role, Role):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.role should be a \"\n                    \"stalker.models.auth.Role instance, \"\n                    f\"not {role.__class__.__name__}: '{role}'\"\n                )\n        return role\n"
  },
  {
    "path": "src/stalker/models/department.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Department related classes and functions are situated here.\"\"\"\n\nfrom typing import Any, Dict, List, Optional, Union\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.ext.associationproxy import association_proxy\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, validates\n\nfrom stalker.db.declarative import Base\nfrom stalker.log import get_logger\nfrom stalker.models.auth import Role, User\nfrom stalker.models.entity import Entity\n\nlogger = get_logger(__name__)\n\n\nclass Department(Entity):\n    \"\"\"The departments that forms the studio itself.\n\n    The information that a Department object holds is like:\n\n      * The members of the department\n      * and all the other things those are inherited from the AuditEntity class\n\n    Two Department object considered the same if they have the same name, the\n    the users list is not important, a \"Modeling\" department\n    should of course be the same with another department which has the name\n    \"Modeling\" again.\n\n    so creating a department object needs the following parameters:\n\n    Args:\n        users (List[User]): it can be an empty list, so one department can be\n            created without any member in it. But this parameter should be a list\n            of User objects.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Departments\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Department\"}\n    department_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    users = association_proxy(\n        \"user_role\", \"user\", creator=lambda u: DepartmentUser(user=u)\n    )\n\n    user_role: Mapped[Optional[List[\"DepartmentUser\"]]] = relationship(\n        back_populates=\"department\",\n        cascade=\"all, delete-orphan\",\n        primaryjoin=\"Departments.c.id==Department_Users.c.did\",\n        doc=\"\"\"List of users representing the members of this department.\"\"\",\n    )\n\n    def __init__(\n        self, users: Optional[List[User]] = None, **kwargs: Optional[Dict[str, Any]]\n    ) -> None:\n        super(Department, self).__init__(**kwargs)\n\n        if users is None:\n            users = []\n\n        self.users = users\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check if the other is equal to this one.\n\n        Args:\n            other (Any): The other Department instance.\n\n        Returns:\n            bool: True if the other object is also a Department and all the attributes\n                are equal.\n        \"\"\"\n        return super(Department, self).__eq__(other) and isinstance(other, Department)\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Department, self).__hash__()\n\n    @validates(\"user_role\")\n    def _validate_user_role(\n        self, key: str, user_role: \"DepartmentUser\"\n    ) -> \"DepartmentUser\":\n        \"\"\"Validate the given user_role value.\n\n        Args:\n            key (str): The name of the validated column.\n            user_role (DepartmentUser): The user_role value to be validated.\n\n        Returns:\n            DepartmentUser: The validated user_role value.\n        \"\"\"\n        return user_role\n\n    @property\n    def to_tjp(self) -> str:\n        \"\"\"Output a TaskJuggler compatible representation.\n\n        Returns:\n            str: The TaskJuggler compatible representation.\n        \"\"\"\n        tab = \"    \"\n        indent = tab\n        tjp = f'resource {self.tjp_id} \"{self.tjp_id}\" {{'\n        for resource in self.users:\n            tjp += \"\\n\"\n            tjp += \"\\n\".join(f\"{indent}{line}\" for line in resource.to_tjp.split(\"\\n\"))\n        tjp += \"\\n}\"\n        return tjp\n\n\n# DEPARTMENTS_USERS\nclass DepartmentUser(Base):\n    \"\"\"The association object used in Department-to-User relation.\"\"\"\n\n    __tablename__ = \"Department_Users\"\n\n    user_id: Mapped[int] = mapped_column(\n        \"uid\", ForeignKey(\"Users.id\"), primary_key=True\n    )\n    user: Mapped[\"User\"] = relationship(\n        back_populates=\"department_role\",\n        primaryjoin=\"DepartmentUser.user_id==User.user_id\",\n        uselist=False,\n    )\n    department_id: Mapped[int] = mapped_column(\n        \"did\", ForeignKey(\"Departments.id\"), primary_key=True\n    )\n    department: Mapped[Department] = relationship(\n        back_populates=\"user_role\",\n        primaryjoin=\"DepartmentUser.department_id==Department.department_id\",\n        uselist=False,\n    )\n    role_id: Mapped[Optional[int]] = mapped_column(\"rid\", ForeignKey(\"Roles.id\"))\n    role: Mapped[Role] = relationship(\n        primaryjoin=\"DepartmentUser.role_id==Role.role_id\"\n    )\n\n    def __init__(self, department=None, user=None, role=None):\n        self.department = department\n        self.user = user\n        self.role = role\n\n    @validates(\"department\")\n    def _validate_department(\n        self, key: str, department: Union[None, Department]\n    ) -> Union[None, Department]:\n        \"\"\"Validate the given department value.\n\n        Args:\n            key (str): The name of the validated column.\n            department (Department): The department value to be validated.\n\n        Raises:\n            TypeError: If the given user value is not a :class:`.Department` instance.\n\n        Returns:\n            Department: The validated department value.\n        \"\"\"\n        if department is not None:\n            # check if it is instance of Department object\n            if not isinstance(department, Department):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.department should be a \"\n                    \"stalker.models.department.Department instance, \"\n                    f\"not {department.__class__.__name__}: '{department}'\"\n                )\n        return department\n\n    @validates(\"user\")\n    def _validate_user(\n        self, key: str, user: Union[None, \"User\"]\n    ) -> Union[None, \"User\"]:\n        \"\"\"Validate the given user value.\n\n        Args:\n            key (str): The name of the validated column.\n            user (User): The user value to be validated.\n\n        Raises:\n            TypeError: If the given user value is not a\n                :class:`stalker.models.auth.User` instance.\n\n        Returns:\n            User: The validated user value.\n        \"\"\"\n        if user is not None:\n            if not isinstance(user, User):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.user should be a \"\n                    \"stalker.models.auth.User instance, \"\n                    f\"not {user.__class__.__name__}: '{user}'\"\n                )\n        return user\n\n    @validates(\"role\")\n    def _validate_role(self, key: str, role: Union[None, Role]) -> Union[None, Role]:\n        \"\"\"Validate the given role instance.\n\n        Args:\n            key (str): The name of the validated column.\n            role (Union[None, Role]): The role value to be validated.\n\n        Raises:\n            TypeError: If the given role value is not a\n                :class:`stalker.models.auth.Role` instance.\n\n        Returns:\n            Union[None, Role]: The validated role value.\n        \"\"\"\n        if role is not None:\n            if not isinstance(role, Role):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.role should be a \"\n                    \"stalker.models.auth.Role instance, \"\n                    f\"not {role.__class__.__name__}: '{role}'\"\n                )\n        return role\n"
  },
  {
    "path": "src/stalker/models/entity.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"SimpleEntity, Entity, EntityGroup and other related functions are situated here.\"\"\"\n\nimport functools\nimport re\nimport uuid\nfrom datetime import datetime\nfrom typing import Any, Dict, List, Optional, TYPE_CHECKING, Union\n\nimport pytz\n\nfrom sqlalchemy import Column, ForeignKey, Integer, String, Table, Text\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, validates\n\nfrom stalker.db.declarative import Base\nfrom stalker.db.types import GenericDateTime\nfrom stalker.log import get_logger\n\nlogger = get_logger(__name__)\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.auth import User\n    from stalker.models.file import File\n    from stalker.models.note import Note\n    from stalker.models.tag import Tag\n    from stalker.models.type import Type\n\n\nclass SimpleEntity(Base):\n    \"\"\"The base class of all the others.\n\n    The ``SimpleEntity`` is the starting point of the Stalker Object Model, it\n    starts by adding the basic information about an entity which are\n    :attr:`.name`, :attr:`.description`, the audit information like\n    :attr:`.created_by`, :attr:`.updated_by`, :attr:`.date_created`,\n    :attr:`.date_updated` and a couple of naming attributes like\n    :attr:`.nice_name` and last but not least the :attr:`.type` attribute which\n    is very important for entities that needs a type.\n\n    .. versionadded: 0.2.2.3\n        :attr:`.html_style` and :attr:`.html_class` attributes:\n\n        SimpleEntity instances now have two new attributes called\n        :attr:`.html_style` and :attr:`.html_class` which can be used to store\n        html styles and html classes per entity. (Hint: Can be used to colorize\n        different type of Tasks in different colors or different statused tasks\n        in different classes etc.)\n\n    .. note::\n\n       For derived classes if the\n       :attr:`.SimpleEntity.type` needed to be specifically specified, that is\n       it cannot be None or nothing else then a :class:`.Type` instance, set\n       the ``strictly_typed`` class attribute to True::\n\n           class NewClass(SimpleEntity):\n               __strictly_typed__ = True\n\n       This will ensure that the derived class always have a proper\n       :attr:`.SimpleEntity.type` attribute and cannot be initialized without\n       one.\n\n    Two SimpleEntities considered to be equal if they have the same\n    :attr:`.name`, the other attributes doesn't matter.\n\n    .. versionadded:: 0.2.0\n       Name attribute can be skipped. Starting from version 0.2.0 the ``name``\n       attribute can be skipped. For derived classes use the ``__auto_name__``\n       class attribute to control auto naming behavior.\n\n    Args:\n        name (str): A string value that holds the name of this entity. It\n            should not contain any white space at the beginning and at the end\n            of the string. Valid characters are [a-zA-Z0-9_/S].\n\n            Advanced::\n\n                For classes derived from the SimpleEntity, if an automatic name\n                is desired, the ``__auto_name__`` class attribute can be set to\n                True. Then Stalker will automatically generate an uuid4\n                sequence for the name attribute.\n\n        description (str): A string attribute that holds the description of\n            this entity object, it could be an empty string, and it could not\n            again have white spaces at the beginning and at the end of the\n            string, again any given objects will be converted to strings\n        generic_text (str): A string attribute that holds any text based\n            information that should be affiliated with this entity, it could be\n            an empty string, and it could not again have white spaces at the\n            beginning and at the end of the string, again any given objects\n            will be converted to strings.\n        created_by (User): The :class:`.User` who has created this object.\n        updated_by (User): The :class:`.User` who has updated this object\n            lastly. The created_by and updated_by attributes point the same\n            object if this object is just created.\n        date_created (datetime): The date that this object is created.\n        date_updated (datetime): The date that this object is updated lastly.\n            For newly created entities this is equal to date_created and the\n            date_updated cannot point a date which is before date_created.\n        type (Type): The type of the current SimpleEntity. Used across several\n            places in Stalker. Can be None. The default value is None.\n    \"\"\"\n\n    # auto generate name values\n    __auto_name__ = True\n    __strictly_typed__ = False\n\n    # TODO: Allow the user to specify the formatting of the name attribute with\n    #       a formatter function\n    __name_formatter__ = None\n\n    __tablename__ = \"SimpleEntities\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n\n    entity_type: Mapped[str] = mapped_column(String(128), nullable=False)\n    __mapper_args__ = {\n        \"polymorphic_on\": entity_type,\n        \"polymorphic_identity\": \"SimpleEntity\",\n    }\n\n    name: Mapped[str] = mapped_column(\n        String(256), nullable=False, doc=\"Name of this object\"\n    )\n\n    description: Mapped[Optional[str]] = mapped_column(\n        Text, doc=\"Description of this object.\"\n    )\n\n    created_by_id: Mapped[Optional[int]] = mapped_column(\n        ForeignKey(\"Users.id\", use_alter=True, name=\"SimpleEntities_created_by_id_fkey\"),\n        doc=\"The id of the :class:`.User` who has created this entity.\",\n    )\n\n    created_by: Mapped[Optional[\"User\"]] = relationship(\n        backref=\"entities_created\",\n        primaryjoin=\"SimpleEntity.created_by_id==User.user_id\",\n        doc=\"The :class:`.User` who has created this object.\",\n    )\n\n    updated_by_id: Mapped[Optional[int]] = mapped_column(\n        \"updated_by_id\",\n        ForeignKey(\"Users.id\", use_alter=True, name=\"SimpleEntities_updated_by_id_fkey\"),\n        nullable=True,\n        doc=\"The id of the :class:`.User` who has updated this entity.\",\n    )\n\n    updated_by: Mapped[Optional[\"User\"]] = relationship(\n        backref=\"entities_updated\",\n        primaryjoin=\"SimpleEntity.updated_by_id==User.user_id\",\n        post_update=True,\n        doc=\"The :class:`.User` who has updated this object.\",\n    )\n\n    date_created: Mapped[Optional[datetime]] = mapped_column(\n        GenericDateTime,\n        default=functools.partial(datetime.now, pytz.utc),\n        doc=\"\"\"A :class:`datetime` instance showing the creation date and time\n        of this object.\"\"\",\n    )\n\n    date_updated: Mapped[Optional[datetime]] = mapped_column(\n        GenericDateTime,\n        default=functools.partial(datetime.now, pytz.utc),\n        doc=\"\"\"A :class:`datetime` instance showing the update date and time of\n        this object.\"\"\",\n    )\n\n    type_id: Mapped[Optional[int]] = mapped_column(\n        ForeignKey(\"Types.id\", use_alter=True, name=\"SimpleEntities_type_id_fkey\"),\n        doc=\"\"\"The id of the :class:`.Type` of this entity. Mainly used by\n        SQLAlchemy to create a Many-to-One relates between SimpleEntities and\n        Types.\n        \"\"\",\n    )\n\n    type: Mapped[Optional[\"Type\"]] = relationship(\n        primaryjoin=\"SimpleEntities.c.type_id==Types.c.id\",\n        post_update=True,\n        doc=\"\"\"The type of the object.\n\n        It is a :class:`.Type` instance with a proper\n        :attr:`.Type.target_entity_type`.\n        \"\"\",\n    )\n\n    generic_data: Mapped[Optional[List[\"SimpleEntity\"]]] = relationship(\n        secondary=\"SimpleEntity_GenericData\",\n        primaryjoin=\"SimpleEntities.c.id==\"\n        \"SimpleEntity_GenericData.c.simple_entity_id\",\n        secondaryjoin=\"SimpleEntity_GenericData.c.other_simple_entity_id==\"\n        \"SimpleEntities.c.id\",\n        post_update=True,\n        doc=\"This attribute can hold any kind of data which exists in SOM.\",\n    )\n\n    generic_text: Mapped[Optional[str]] = mapped_column(\n        \"generic_text\", Text, doc=\"This attribute can hold any text.\"\n    )\n\n    thumbnail_id: Mapped[Optional[int]] = mapped_column(\n        ForeignKey(\n            \"Files.id\",\n            use_alter=True,\n            name=\"SimpleEntities_thumbnail_id_fkey\",\n        )\n    )\n\n    thumbnail: Mapped[Optional[\"File\"]] = relationship(\n        primaryjoin=\"SimpleEntities.c.thumbnail_id==Files.c.id\",\n        post_update=True,\n    )\n\n    html_style: Mapped[Optional[str]] = mapped_column(\n        String(64), nullable=True, default=\"\"\n    )\n    html_class: Mapped[Optional[str]] = mapped_column(\n        String(64), nullable=True, default=\"\"\n    )\n\n    stalker_version: Mapped[Optional[str]] = mapped_column(String(256))\n\n    def __init__(\n        self,\n        name: Optional[str] = None,\n        description: Optional[str] = \"\",\n        generic_text: str = \"\",\n        type: Optional[\"Type\"] = None,\n        created_by: Optional[\"User\"] = None,\n        updated_by: Optional[\"User\"] = None,\n        date_created: Optional[datetime] = None,\n        date_updated: Optional[datetime] = None,\n        thumbnail: Optional[\"File\"] = None,\n        html_style: Optional[str] = \"\",\n        html_class: Optional[str] = \"\",\n        **kwargs: Optional[Dict[str, Any]],\n    ) -> None:  # noqa: W0613\n\n        # name and nice_name\n        self._nice_name = \"\"\n\n        self.name = name\n\n        self.description = description\n        self.created_by = created_by\n        self.updated_by = updated_by\n        if date_created is None:\n            date_created = datetime.now(pytz.utc)\n        if date_updated is None:\n            date_updated = date_created\n\n        self.date_created = date_created\n        self.date_updated = date_updated\n        self.type = type\n        self.thumbnail = thumbnail\n        self.generic_text = generic_text\n        self.html_style = html_style\n        self.html_class = html_class\n\n        import stalker\n\n        self.stalker_version = stalker.__version__\n\n    def __repr__(self) -> str:\n        \"\"\"Return the str representation of this SimpleEntity.\n\n        Returns:\n            str: The str representation of this SimpleEntity.\n        \"\"\"\n        return f\"<{self.name} ({self.entity_type})>\"\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check equality.\n\n        Args:\n            other (Any): An object to check the equality of.\n\n        Returns:\n            bool: If the other is a SimpleEntity and its name equals to this one.\n        \"\"\"\n        from stalker.db.session import DBSession\n\n        with DBSession.no_autoflush:\n            return isinstance(other, SimpleEntity) and self.name == other.name\n\n    def __ne__(self, other: Any) -> bool:\n        \"\"\"Check inequality.\n\n        This uses the __eq__ operator to get the inequality.\n\n        Args:\n            other (Any): An object to check the inequality of.\n\n        Returns:\n            bool: True if other is not equal to this instance.\n        \"\"\"\n        return not self.__eq__(other)\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return hash(\"{}:{}:{}\".format(self.id, self.name, self.entity_type))\n\n    @validates(\"description\")\n    def _validate_description(self, key: str, description: str) -> str:\n        \"\"\"Validate the given description value.\n\n        Args:\n            key (str): The name of the validated column.\n            description (str): The description value to be validated.\n\n        Raises:\n            TypeError: If the description is not None and not a str.\n\n        Returns:\n            str: The validated description value.\n        \"\"\"\n        if description is None:\n            description = \"\"\n\n        if not isinstance(description, str):\n            raise TypeError(\n                \"{}.description should be a string, not {}: '{}'\".format(\n                    self.__class__.__name__, description.__class__.__name__, description\n                )\n            )\n        return description\n\n    @validates(\"generic_text\")\n    def _validate_generic_text(self, key: str, generic_text: str) -> str:\n        \"\"\"Validate the given generic_text value.\n\n        Args:\n            key (str): The name of the validated column.\n            generic_text (str): The generic_text value to be validated.\n\n        Raises:\n            TypeError: If the generic_text is not None and not a str.\n\n        Returns:\n            str: The validated generic_text value.\n        \"\"\"\n        if generic_text is None:\n            generic_text = \"\"\n\n        if not isinstance(generic_text, str):\n            raise TypeError(\n                \"{}.generic_text should be a string, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    generic_text.__class__.__name__,\n                    generic_text,\n                )\n            )\n        return generic_text\n\n    @validates(\"name\")\n    def _validate_name(self, key: str, name: str) -> str:\n        \"\"\"Validate the name value.\n\n        Args:\n            key (str): The name of the validated column.\n            name (str): The name value to be validated.\n\n        Raises:\n            TypeError: If the name is not a str.\n            ValueError: If the name becomes an empty str after formatting.\n\n        Returns:\n            str: The validated name value.\n        \"\"\"\n        if self.__auto_name__:\n            if name is None or name == \"\":\n                # generate a uuid4\n                name = \"{}_{}\".format(\n                    self.__class__.__name__,\n                    uuid.uuid4().urn.split(\":\")[2],\n                )\n\n        # it is None\n        if name is None:\n            raise TypeError(f\"{self.__class__.__name__}.name cannot be None\")\n\n        if not isinstance(name, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.name should be a string, \"\n                f\"not {name.__class__.__name__}: '{name}'\"\n            )\n\n        name = self._format_name(name)\n\n        # it is empty\n        if name == \"\":\n            raise ValueError(\n                f\"{self.__class__.__name__}.name cannot be an empty string\"\n            )\n\n        # also set the nice_name\n        self._nice_name = self._format_nice_name(name)\n\n        return name\n\n    @classmethod\n    def _format_name(cls, name: str) -> str:\n        \"\"\"Format the name value.\n\n        Args:\n            name (str): The name value.\n\n        Returns:\n            str: The formatted name value.\n        \"\"\"\n        # remove unnecessary characters from the string\n        name = name.strip()\n\n        # remove multiple spaces\n        name = re.sub(r\"\\s+\", \" \", name)\n\n        return name\n\n    @classmethod\n    def _format_nice_name(cls, nice_name: str) -> str:\n        \"\"\"Format the given nice name value.\n\n        Args:\n            nice_name (str): The nice_name value to be formatted.\n\n        Returns:\n            str: The formatted nice name.\n        \"\"\"\n        # remove unnecessary characters from the string\n        nice_name = nice_name.strip()\n        nice_name = re.sub(r\"([^a-zA-Z0-9\\s_\\-@]+)\", \"\", nice_name).strip()\n\n        # remove all the characters which are not alphabetic from the start of\n        # the string\n        nice_name = re.sub(r\"(^[^a-zA-Z0-9]+)\", \"\", nice_name)\n\n        # remove multiple spaces\n        nice_name = re.sub(r\"\\s+\", \" \", nice_name)\n\n        # # replace camel case letters\n        # nice_name = re.sub(r\"(.+?[a-z]+)([A-Z])\", r\"\\1_\\2\", nice_name)\n\n        # replace white spaces and dashes with underscore\n        nice_name = re.sub(\"([ -])+\", r\"_\", nice_name)\n\n        # remove multiple underscores\n        nice_name = re.sub(r\"(_+)\", r\"_\", nice_name)\n\n        return nice_name\n\n    @property\n    def nice_name(self) -> str:\n        \"\"\"Nice name of this object.\n\n        It has the same value with the name (contextually) but with a different\n        format like, all the white spaces replaced by underscores (\"_\"), all the\n        CamelCase form will be expanded by underscore (_) characters, and it is always\n        lower case.\n\n        Returns:\n            str: The nice name value.\n        \"\"\"\n        # also set the nice_name\n        # if self._nice_name is None or self._nice_name == \"\":\n        self._nice_name = self._format_nice_name(self.name)\n        return self._nice_name\n\n    @validates(\"created_by\")\n    def _validate_created_by(\n        self, key: str, created_by: Union[None, \"User\"]\n    ) -> Union[None, \"User\"]:\n        \"\"\"Validate the given created_by value.\n\n        Args:\n            key (str): The name of the validated column.\n            created_by (Union[None, User]): The created_by value to be validated.\n\n        Raises:\n            TypeError: If the created_by value is not None and not a\n                :class:`stalker.models.auth.User` instance.\n\n        Returns:\n            Union[None, User]: The validated created_by value.\n        \"\"\"\n        from stalker.models.auth import User\n\n        if created_by is not None:\n            if not isinstance(created_by, User):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.created_by should be a \"\n                    \"stalker.models.auth.User instance, \"\n                    f\"not {created_by.__class__.__name__}: '{created_by}'\"\n                )\n        return created_by\n\n    @validates(\"updated_by\")\n    def _validate_updated_by(\n        self, key: str, updated_by: Union[None, \"User\"]\n    ) -> Union[None, \"User\"]:\n        \"\"\"Validate the given updated_by value.\n\n        Args:\n            key (str): The name of the validated column.\n            updated_by (Union[None, User]): The updated_by value to be validated.\n\n        Raises:\n            TypeError: If the updated_by value is not None and not a\n                :class:`stalker.models.auth.User` instance.\n\n        Returns:\n            Union[None, User]: The validated updated_by value.\n        \"\"\"\n        from stalker.models.auth import User\n\n        if updated_by is None:\n            # set it to what created_by attribute has\n            updated_by = self.created_by\n\n        if updated_by is not None:\n            if not isinstance(updated_by, User):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.updated_by should be a \"\n                    \"stalker.models.auth.User instance, \"\n                    f\"not {updated_by.__class__.__name__}: '{updated_by}'\"\n                )\n        return updated_by\n\n    @validates(\"date_created\")\n    def _validate_date_created(self, key: str, date_created: datetime) -> datetime:\n        \"\"\"Validate the given date_created value.\n\n        Args:\n            key (str): The name of the validated column.\n            date_created (datetime): The value to be validated.\n\n        Raises:\n            TypeError: If the given date_created value is None or not a datetime\n                instance.\n\n        Returns:\n            datetime: The validated date_created value.\n        \"\"\"\n        if date_created is None:\n            raise TypeError(f\"{self.__class__.__name__}.date_created cannot be None\")\n\n        if not isinstance(date_created, datetime):\n            raise TypeError(\n                f\"{self.__class__.__name__}.date_created should be a \"\n                \"datetime.datetime instance, \"\n                f\"not {date_created.__class__.__name__}: '{date_created}'\"\n            )\n\n        return date_created\n\n    @validates(\"date_updated\")\n    def _validate_date_updated(self, key: str, date_updated: datetime) -> datetime:\n        \"\"\"Validate the given date_updated.\n\n        Args:\n            key (str): The name of the validated column.\n            date_updated (datetime): The date_updated to be validated.\n\n        Raises:\n            TypeError: If the date_updated value is ``None`` or date_updated is\n                not a ``datetime`` instance.\n            ValueError: If the date_updated is before than the date_created.\n\n        Returns:\n            datetime: The validated datetime_updated value.\n        \"\"\"\n        # it is None\n        if date_updated is None:\n            raise TypeError(f\"{self.__class__.__name__}.date_updated cannot be None\")\n\n        # it is not a datetime instance\n        if not isinstance(date_updated, datetime):\n            raise TypeError(\n                f\"{self.__class__.__name__}.date_updated should be a \"\n                \"datetime.datetime instance, \"\n                f\"not {date_updated.__class__.__name__}: '{date_updated}'\"\n            )\n\n        # lower than date_created\n        if date_updated < self.date_created:\n            raise ValueError(\n                \"{class_name}.date_updated could not be set to a date before \"\n                \"{class_name}.date_created, try setting the ``date_created`` \"\n                \"first.\".format(class_name=self.__class__.__name__)\n            )\n        return date_updated\n\n    @validates(\"type\")\n    def _validate_type(self, key: str, type_: \"Type\") -> \"Type\":\n        \"\"\"Validate the given type value.\n\n        Args:\n            key (str): The name of the validated column.\n            type_ (Type): The type value to be validated.\n\n        Raises:\n            TypeError: If this class is a strictly typed class and the type_ is not\n                None and not a Type instance.\n\n        Returns:\n            Type: The validated type_ value.\n        \"\"\"\n        if self.__strictly_typed__ or type_ is not None:\n            from stalker.models.type import Type\n\n            if not isinstance(type_, Type):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.type must be a \"\n                    \"stalker.models.type.Type instance, \"\n                    f\"not {type_.__class__.__name__}: '{type_}'\"\n                )\n        return type_\n\n    @validates(\"thumbnail\")\n    def _validate_thumbnail(self, key: str, thumb: \"File\") -> \"File\":\n        \"\"\"Validate the given thumb value.\n\n        Args:\n            key (str): The name of the validated column.\n            thumb (File): The thumb value to be validated.\n\n        Raises:\n            TypeError: If the given thumb value is not None and not a File\n                instance.\n\n        Returns:\n            Union[None, File]: The validated thumb value.\n        \"\"\"\n        if thumb is not None:\n            from stalker import File\n\n            if not isinstance(thumb, File):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.thumbnail should be a \"\n                    \"stalker.models.file.File instance, \"\n                    f\"not {thumb.__class__.__name__}: '{thumb}'\"\n                )\n        return thumb\n\n    @property\n    def tjp_id(self) -> str:\n        \"\"\"Return TaskJuggler compatible id.\n\n        Returns:\n            str: The TaskJuggler compatible id.\n        \"\"\"\n        return f\"{self.__class__.__name__}_{self.id}\"\n\n    @property\n    def to_tjp(self) -> str:\n        \"\"\"Render a TaskJuggler compliant str used for TaskJuggler integration.\n\n        Needs to be overridden in inherited classes.\n\n        Raises:\n            NotImplementedError: Always.\n        \"\"\"\n        raise NotImplementedError(\n            f\"This property is not implemented in {self.__class__.__name__}\"\n        )\n\n    @validates(\"html_style\")\n    def _validate_html_style(self, key: str, html_style: str) -> str:\n        \"\"\"Validate the given html_style value.\n\n        Args:\n            key (str): The name of the validated column.\n            html_style (str): The html_style to be validated.\n\n        Raises:\n            TypeError: If the given html_style is not a str.\n\n        Returns:\n            str: The validated html_style value.\n        \"\"\"\n        if html_style is None:\n            html_style = \"\"\n\n        if not isinstance(html_style, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.html_style should be a str, \"\n                f\"not {html_style.__class__.__name__}: '{html_style}'\"\n            )\n        return html_style\n\n    @validates(\"html_class\")\n    def _validate_html_class(self, key: str, html_class: str) -> str:\n        \"\"\"Validate the given html_class value.\n\n        Args:\n            key (str): The name of the validated column.\n            html_class (str): The html_class to be validated.\n\n        Raises:\n            TypeError: If the html_class is not a str.\n\n        Returns:\n            str: The validated html_class value.\n        \"\"\"\n        if html_class is None:\n            html_class = \"\"\n\n        if not isinstance(html_class, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.html_class should be a str, \"\n                f\"not {html_class.__class__.__name__}: '{html_class}'\"\n            )\n        return html_class\n\n\nclass Entity(SimpleEntity):\n    \"\"\"Another base data class that adds tags and notes to the attributes list.\n\n    This is the entity class which is derived from the SimpleEntity and adds\n    only tags to the list of parameters.\n\n    Two Entities considered equal if they have the same name. It doesn't matter\n    if they have different tags or notes.\n\n    Args:\n        tags (List[Tag]): A list of :class:`.Tag` objects related to this entity.\n            Tags could be an empty list, or when omitted it will be set to an\n            empty list.\n        notes (List[Note]): A list of :class:`.Note` instances. Can be an empty\n            list, or when omitted it will be set to an empty list, when set to\n            None it will be converted to an empty list.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"Entities\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Entity\"}\n    entity_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    tags: Mapped[Optional[List[\"Tag\"]]] = relationship(\n        \"Tag\",\n        secondary=\"Entity_Tags\",\n        backref=\"entities\",\n        doc=\"\"\"A list of tags attached to this object.\n\n        It is a list of :class:`.Tag` instances which shows\n        the tags of this object\"\"\",\n    )\n\n    notes: Mapped[Optional[List[\"Note\"]]] = relationship(\n        \"Note\",\n        secondary=\"Entity_Notes\",\n        backref=\"entities\",\n        doc=\"\"\"All the :class:`.Notes` s attached to this entity.\n\n        It is a list of :class:`.Note` instances or an\n        empty list, setting it to None will raise a TypeError.\n        \"\"\",\n    )\n\n    def __init__(\n        self, tags: Optional[List[\"Tag\"]] = None, notes=None, **kwargs\n    ) -> None:\n        super(Entity, self).__init__(**kwargs)\n\n        if tags is None:\n            tags = []\n\n        if notes is None:\n            notes = []\n\n        self.tags = tags\n        self.notes = notes\n\n    @validates(\"notes\")\n    def _validate_notes(self, key: str, note: \"Note\") -> \"Note\":\n        \"\"\"Validate the given note value.\n\n        Args:\n            key (str): The name of the validated column.\n            note (Note): The note value to be validated.\n\n        Raises:\n            TypeError: If the given note value is not a Note instance.\n\n        Returns:\n            Note: The validated note value.\n        \"\"\"\n        from stalker.models.note import Note\n\n        if not isinstance(note, Note):\n            raise TypeError(\n                f\"{self.__class__.__name__}.note should be a stalker.models.note.Note \"\n                f\"instance, not {note.__class__.__name__}: '{note}'\"\n            )\n        return note\n\n    @validates(\"tags\")\n    def _validate_tags(self, key: str, tag: \"Tag\") -> \"Tag\":\n        \"\"\"Validate the given tag value.\n\n        Args:\n            key (str): The name of the validated column.\n            tag (Tag): The tag value to be validated.\n\n        Raises:\n            TypeError: If the given tag value is not a Tag instance.\n\n        Returns:\n            Tag: The validated tag value.\n        \"\"\"\n        from stalker.models.tag import Tag\n\n        if not isinstance(tag, Tag):\n            raise TypeError(\n                f\"{self.__class__.__name__}.tag should be a stalker.models.tag.Tag \"\n                f\"instance, not {tag.__class__.__name__}: '{tag}'\"\n            )\n        return tag\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check if the other object is equal to this one.\n\n        Args:\n            other (Any): An object.\n\n        Returns:\n            bool: True if the other object is also an Entity instance and has the same\n                basic attribute values.\n        \"\"\"\n        return super(Entity, self).__eq__(other) and isinstance(other, Entity)\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Entity, self).__hash__()\n\n\nclass EntityGroup(Entity):\n    \"\"\"Groups a wide variety of objects together to let one easily reach them.\n\n    :class:`.EntityGroup` helps to group different types of entities together to let one\n    easily reach to them.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"EntityGroups\"\n    __mapper_args__ = {\"polymorphic_identity\": \"EntityGroup\"}\n    entity_group_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    entities: Mapped[Optional[List[SimpleEntity]]] = relationship(\n        secondary=\"EntityGroup_Entities\",\n        post_update=True,\n        backref=\"entity_groups\",\n        doc=\"All the :class:`.SimpleEntity`s grouped in this EntityGroup.\",\n    )\n\n    def __init__(\n        self,\n        entities: Optional[List[Entity]] = None,\n        **kwargs: Optional[Dict[str, Any]],\n    ) -> None:\n        super(Entity, self).__init__(**kwargs)\n\n        if entities is None:\n            entities = []\n\n        self.entities = entities\n\n    @validates(\"entities\")\n    def _validate_entities(self, key: str, entity: SimpleEntity) -> SimpleEntity:\n        \"\"\"Validate the given entity value.\n\n        Args:\n            key (str): The name of the validated column.\n            entity (SimpleEntity): The entity value to be validated.\n\n        Raises:\n            TypeError: If the entity is not a SimpleEntity instance.\n\n        Returns:\n            SimpleEntity: The validated entity value.\n        \"\"\"\n        if not isinstance(entity, SimpleEntity):\n            raise TypeError(\n                f\"{self.__class__.__name__}.entities should be a list of \"\n                f\"SimpleEntities, not {entity.__class__.__name__}: '{entity}'\"\n            )\n\n        return entity\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check if the other is equal to this instance.\n\n        Args:\n            other (Any): The other EntityGroup to check the equality of.\n\n        Returns:\n            bool: True if the other is also a EntityGroup instance and has the same\n                attribute values.\n        \"\"\"\n        return (\n            super(EntityGroup, self).__eq__(other)\n            and isinstance(other, EntityGroup)\n            and self.entities == other.entities\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(EntityGroup, self).__hash__()\n\n\n# Entity Tags\nEntity_Tags = Table(\n    \"Entity_Tags\",\n    Base.metadata,\n    Column(\n        \"entity_id\",\n        Integer,\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    ),\n    Column(\n        \"tag_id\",\n        Integer,\n        ForeignKey(\"Tags.id\"),\n        primary_key=True,\n    ),\n)\n\n# Entity Notes\nEntity_Notes = Table(\n    \"Entity_Notes\",\n    Base.metadata,\n    Column(\n        \"entity_id\",\n        Integer,\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    ),\n    Column(\n        \"note_id\",\n        Integer,\n        ForeignKey(\"Notes.id\"),\n        primary_key=True,\n    ),\n)\n\n# SimpleEntity Generic Data\nSimpleEntity_GenericData = Table(\n    \"SimpleEntity_GenericData\",\n    Base.metadata,\n    Column(\n        \"simple_entity_id\", Integer, ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    ),\n    Column(\n        \"other_simple_entity_id\",\n        Integer,\n        ForeignKey(\"SimpleEntities.id\"),\n        primary_key=True,\n    ),\n)\n\n# EntityGroup Entities\nEntityGroup_Entities = Table(\n    \"EntityGroup_Entities\",\n    Base.metadata,\n    Column(\"entity_group_id\", Integer, ForeignKey(\"EntityGroups.id\"), primary_key=True),\n    Column(\n        \"other_entity_id\", Integer, ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    ),\n)\n"
  },
  {
    "path": "src/stalker/models/enum.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Enum classes are situated here.\"\"\"\n\nfrom enum import Enum, IntEnum\nfrom typing import Union\n\nfrom sqlalchemy import Enum as saEnum, Integer, TypeDecorator\n\n\nclass ScheduleConstraint(IntEnum):\n    \"\"\"The schedule constraint enum.\"\"\"\n\n    NONE = 0\n    Start = 1\n    End = 2\n    Both = 3\n\n    def __repr__(self) -> str:\n        \"\"\"Return the enum name for str().\n\n        Returns:\n            str: The name as the string representation of this\n                ScheduleConstraint.\n        \"\"\"\n        return self.name if self.name != \"NONE\" else \"None\"\n\n    __str__ = __repr__\n\n    @classmethod\n    def to_constraint(\n        cls, constraint: Union[int, str, \"ScheduleConstraint\"]\n    ) -> \"ScheduleConstraint\":\n        \"\"\"Validate and return type enum from an input int or str value.\n\n        Args:\n            constraint (Union[str, ScheduleConstraint]): Input `constraint` value.\n\n        Raises:\n            TypeError: Input value type is invalid.\n            ValueError: Input value is invalid.\n\n        Returns:\n            ScheduleConstraint: ScheduleConstraint value.\n        \"\"\"\n        # Check if it's a valid str type for a constraint.\n        if constraint is None:\n            constraint = ScheduleConstraint.NONE\n\n        if not isinstance(constraint, (int, str, ScheduleConstraint)):\n            raise TypeError(\n                \"constraint should be a ScheduleConstraint enum value or an \"\n                \"int or a str, \"\n                f\"not {constraint.__class__.__name__}: '{constraint}'\"\n            )\n\n        if isinstance(constraint, str):\n            constraint_name_lut = dict(\n                [\n                    (c.name.lower(), c.name.title() if c.name != \"NONE\" else \"NONE\")\n                    for c in cls\n                ]\n            )\n            # also add int values\n            constraint_lower_case = constraint.lower()\n            if constraint_lower_case not in constraint_name_lut:\n                raise ValueError(\n                    \"constraint should be a ScheduleConstraint enum value or \"\n                    \"one of {}, not '{}'\".format(\n                        [e.name.title() for e in cls], constraint\n                    )\n                )\n\n            # Return the enum status for the status value.\n            return cls.__members__[constraint_name_lut[constraint_lower_case]]\n        else:\n            return ScheduleConstraint(constraint)\n\n\nclass ScheduleConstraintDecorator(TypeDecorator):\n    \"\"\"Store ScheduleConstraint as an integer and restore as ScheduleConstraint.\"\"\"\n\n    cache_ok = True\n    impl = Integer\n\n    def process_bind_param(self, value, dialect) -> int:\n        \"\"\"Return the integer value of the ScheduleConstraint.\n\n        Args:\n            value (ScheduleConstraint): The ScheduleConstraint value.\n            dialect (str): The name of the dialect.\n\n        Returns:\n            int: The value of the ScheduleConstraint.\n        \"\"\"\n        # just return the value\n        return value.value\n\n    def process_result_value(self, value: int, dialect: str) -> ScheduleConstraint:\n        \"\"\"Return a ScheduleConstraint.\n\n        Args:\n            value (int): The integer value.\n            dialect (str): The name of the dialect.\n\n        Returns:\n            ScheduleConstraint: ScheduleConstraint created from the DB data.\n        \"\"\"\n        return ScheduleConstraint.to_constraint(value)\n\n\nclass TimeUnit(Enum):\n    \"\"\"The time unit enum.\"\"\"\n\n    Minute = \"min\"\n    Hour = \"h\"\n    Day = \"d\"\n    Week = \"w\"\n    Month = \"m\"\n    Year = \"y\"\n\n    def __str__(self) -> str:\n        \"\"\"Return the string representation.\n\n        Returns:\n            str: The string representation.\n        \"\"\"\n        return str(self.value)\n\n    @classmethod\n    def to_unit(cls, unit: Union[str, \"TimeUnit\"]) -> \"TimeUnit\":\n        \"\"\"Convert the given unit value to a TimeUnit enum.\n\n        Args:\n            unit (Union[str, TimeUnit]): The value to convert to a\n                TimeUnit.\n\n        Raises:\n            TypeError: Input value type is invalid.\n            ValueError: Input value is invalid.\n\n        Returns:\n            TimeUnit: The enum.\n        \"\"\"\n        if not isinstance(unit, (str, TimeUnit)):\n            raise TypeError(\n                \"unit should be a TimeUnit enum value or one of {}, \"\n                \"not {}: '{}'\".format(\n                    [u.name.title() for u in cls] + [u.value for u in cls],\n                    unit.__class__.__name__,\n                    unit,\n                )\n            )\n        if isinstance(unit, str):\n            unit_name_lut = dict([(u.name.lower(), u.name) for u in cls])\n            unit_name_lut.update(dict([(u.value.lower(), u.name) for u in cls]))\n            unit_lower_case = unit.lower()\n            if unit_lower_case not in unit_name_lut:\n                raise ValueError(\n                    \"unit should be a TimeUnit enum value or one of {}, \"\n                    \"not '{}'\".format(\n                        [u.name.title() for u in cls] + [u.value for u in cls], unit\n                    )\n                )\n\n            return cls.__members__[unit_name_lut[unit_lower_case]]\n\n        return unit\n\n\nclass TimeUnitDecorator(TypeDecorator):\n    \"\"\"Store TimeUnit as an str and restore as TimeUnit.\"\"\"\n\n    cache_ok = True\n    impl = saEnum(*[u.value for u in TimeUnit], name=\"TimeUnit\")\n\n    def process_bind_param(self, value: TimeUnit, dialect: str) -> str:\n        \"\"\"Return the str value of the TimeUnit.\n\n        Args:\n            value (TimeUnit): The TimeUnit value.\n            dialect (str): The name of the dialect.\n\n        Returns:\n            str: The value of the TimeUnit.\n        \"\"\"\n        # just return the value\n        return value.value\n\n    def process_result_value(self, value: str, dialect: str) -> TimeUnit:\n        \"\"\"Return a TimeUnit.\n\n        Args:\n            value (str): The string value to convert to TimeUnit.\n            dialect (str): The name of the dialect.\n\n        Returns:\n            TimeUnit: The TimeUnit which is created from the DB data.\n        \"\"\"\n        return TimeUnit.to_unit(value)\n\n\nclass ScheduleModel(Enum):\n    \"\"\"The schedule model enum.\"\"\"\n\n    Effort = \"effort\"\n    Duration = \"duration\"\n    Length = \"length\"\n\n    def __str__(self) -> str:\n        \"\"\"Return the string representation.\n\n        Returns:\n            str: The string representation.\n        \"\"\"\n        return str(self.value)\n\n    @classmethod\n    def to_model(cls, model: Union[str, \"ScheduleModel\"]) -> \"ScheduleModel\":\n        \"\"\"Convert the given model value to a ScheduleModel enum.\n\n        Args:\n            model (Union[str, ScheduleModel]): The value to convert to a\n                ScheduleModel.\n\n        Raises:\n            TypeError: Input value type is invalid.\n            ValueError: Input value is invalid.\n\n        Returns:\n            ScheduleModel: The enum.\n        \"\"\"\n        if not isinstance(model, (str, ScheduleModel)):\n            raise TypeError(\n                \"model should be a ScheduleModel enum value or one of {}, \"\n                \"not {}: '{}'\".format(\n                    [m.name.title() for m in cls] + [m.value for m in cls],\n                    model.__class__.__name__,\n                    model,\n                )\n            )\n        if isinstance(model, str):\n            model_name_lut = dict([(m.name.lower(), m.name) for m in cls])\n            model_name_lut.update(dict([(m.value.lower(), m.name) for m in cls]))\n            model_lower_case = model.lower()\n            if model_lower_case not in model_name_lut:\n                raise ValueError(\n                    \"model should be a ScheduleModel enum value or one of {}, \"\n                    \"not '{}'\".format(\n                        [m.name.title() for m in cls] + [m.value for m in cls], model\n                    )\n                )\n\n            return cls.__members__[model_name_lut[model_lower_case]]\n\n        return model\n\n\nclass ScheduleModelDecorator(TypeDecorator):\n    \"\"\"Store ScheduleModel as a str and restore as ScheduleModel.\"\"\"\n\n    cache_ok = True\n    impl = saEnum(*[m.value for m in ScheduleModel], name=\"ScheduleModel\")\n\n    def process_bind_param(self, value, dialect) -> str:\n        \"\"\"Return the str value of the ScheduleModel.\n\n        Args:\n            value (ScheduleModel): The ScheduleModel value.\n            dialect (str): The name of the dialect.\n\n        Returns:\n            str: The value of the ScheduleModel.\n        \"\"\"\n        # just return the value\n        return value.value\n\n    def process_result_value(self, value: str, dialect: str) -> ScheduleModel:\n        \"\"\"Return a ScheduleModel.\n\n        Args:\n            value (str): The string value to convert to ScheduleModel.\n            dialect (str): The name of the dialect.\n\n        Returns:\n            ScheduleModel: The ScheduleModel created from the DB data.\n        \"\"\"\n        return ScheduleModel.to_model(value)\n\n\nclass DependencyTarget(Enum):\n    \"\"\"The dependency target enum.\"\"\"\n\n    OnStart = \"onstart\"\n    OnEnd = \"onend\"\n\n    def __str__(self) -> str:\n        \"\"\"Return the string representation.\n\n        Returns:\n            str: The string representation.\n        \"\"\"\n        return str(self.value)\n\n    @classmethod\n    def to_target(cls, target: Union[str, \"DependencyTarget\"]) -> \"DependencyTarget\":\n        \"\"\"Convert the given target value to a DependencyTarget enum.\n\n        Args:\n            target (Union[str, DependencyTarget]): The value to convert to a\n                DependencyTarget.\n\n        Raises:\n            TypeError: Input value type is invalid.\n            ValueError: Input value is invalid.\n\n        Returns:\n            DependencyTarget: The enum.\n        \"\"\"\n        if not isinstance(target, (str, DependencyTarget)):\n            raise TypeError(\n                \"target should be a DependencyTarget enum value or one of {}, \"\n                \"not {}: '{}'\".format(\n                    [t.name for t in cls] + [t.value for t in cls],\n                    target.__class__.__name__,\n                    target,\n                )\n            )\n        if isinstance(target, str):\n            target_name_lut = dict([(t.name.lower(), t.name) for t in cls])\n            target_name_lut.update(dict([(t.value.lower(), t.name) for t in cls]))\n            target_lower_case = target.lower()\n            if target_lower_case not in target_name_lut:\n                raise ValueError(\n                    \"target should be a DependencyTarget enum value or one of {}, \"\n                    \"not '{}'\".format(\n                        [t.name for t in cls] + [t.value for t in cls], target\n                    )\n                )\n\n            return cls.__members__[target_name_lut[target_lower_case]]\n\n        return target\n\n\nclass DependencyTargetDecorator(TypeDecorator):\n    \"\"\"Store DependencyTarget as an enum and restore as DependencyTarget.\"\"\"\n\n    cache_ok = True\n    impl = saEnum(*[m.value for m in DependencyTarget], name=\"TaskDependencyTarget\")\n\n    def process_bind_param(self, value, dialect) -> str:\n        \"\"\"Return the str value of the DependencyTarget.\n\n        Args:\n            value (DependencyTarget): The DependencyTarget value.\n            dialect (str): The name of the dialect.\n\n        Returns:\n            str: The value of the DependencyTarget.\n        \"\"\"\n        # just return the value\n        return value.value\n\n    def process_result_value(self, value: str, dialect: str) -> DependencyTarget:\n        \"\"\"Return a DependencyTarget.\n\n        Args:\n            value (str): The string value to convert to DependencyTarget.\n            dialect (str): The name of the dialect.\n\n        Returns:\n            DependencyTarget: The DependencyTarget created from str.\n        \"\"\"\n        return DependencyTarget.to_target(value)\n\n\nclass TraversalDirection(IntEnum):\n    \"\"\"The traversal direction enum.\"\"\"\n\n    DepthFirst = 0\n    BreadthFirst = 1\n\n    def __repr__(self) -> str:\n        \"\"\"Return the enum name for str().\n\n        Returns:\n            str: The name as the string representation of this\n                ScheduleConstraint.\n        \"\"\"\n        return self.name if self.name != \"NONE\" else \"None\"\n\n    __str__ = __repr__\n\n    @classmethod\n    def to_direction(\n        cls, direction: Union[int, str, \"TraversalDirection\"]\n    ) -> \"TraversalDirection\":\n        \"\"\"Convert the given direction value to a TraversalDirection enum.\n\n        Args:\n            direction (Union[int, str, TraversalDirection]): The value to\n                convert to a TraversalDirection.\n\n        Raises:\n            TypeError: Input value type is invalid.\n            ValueError: Input value is invalid.\n\n        Returns:\n            TraversalDirection: The enum.\n        \"\"\"\n        if not isinstance(direction, (int, str, TraversalDirection)):\n            raise TypeError(\n                \"direction should be a TraversalDirection enum value \"\n                \"or one of {}, not {}: '{}'\".format(\n                    [d.name for d in cls] + [d.value for d in cls],\n                    direction.__class__.__name__,\n                    direction,\n                )\n            )\n        if isinstance(direction, str):\n            direction_name_lut = dict([(d.name.lower(), d.name) for d in cls])\n            direction_name_lut.update(dict([(d.value, d.name) for d in cls]))\n            direction_lower_case = direction.lower()\n            if direction_lower_case not in direction_name_lut:\n                raise ValueError(\n                    \"direction should be a TraversalDirection enum value or \"\n                    \"one of {}, not '{}'\".format(\n                        [d.name for d in cls] + [d.value for d in cls],\n                        direction,\n                    )\n                )\n\n            return cls.__members__[direction_name_lut[direction_lower_case]]\n\n        return direction\n"
  },
  {
    "path": "src/stalker/models/file.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"File related classes and utility functions are situated here.\"\"\"\n\nimport os\nfrom typing import Any, Dict, Generator, List, Optional, Union\n\nfrom sqlalchemy import ForeignKey, String, Text\nfrom sqlalchemy.orm import Mapped, mapped_column, validates\n\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity\nfrom stalker.models.enum import TraversalDirection\nfrom stalker.models.mixins import ReferenceMixin\nfrom stalker.utils import walk_hierarchy\n\n\nlogger = get_logger(__name__)\n\n\nclass File(Entity, ReferenceMixin):\n    \"\"\"Holds data about files or file sequences.\n\n    Files are all about giving some external information to the current entity\n    (external to the database, so it can be something on the\n    :class:`.Repository` or in the Web or anywhere that the server can reach).\n    The type of the file (general, file, folder, web page, image, image\n    sequence, video, movie, sound, text etc.) can be defined by a\n    :class:`.Type` instance (you can also use multiple :class:`.Tag` instances\n    to add more information, and to filter them back). Again it is defined by\n    the needs of the studio.\n\n    For sequences of files the file name should be in \"%h%p%t %R\" format in\n    PySeq_ formatting rules.\n\n    There are three secondary attributes (properties to be more precise)\n    ``path``, ``filename`` and ``extension``. These attributes are derived from\n    the :attr:`.full_path` attribute and they modify it.\n\n    Path\n        It is the path part of the full_path.\n\n    Filename\n        It is the filename part of the full_path, also includes the extension,\n        so changing the filename also changes the extension part.\n\n    Extension\n        It is the extension part of the full_path. It also includes the\n        extension separator ('.' for most of the file systems).\n\n    .. versionadded:: 1.1.0\n\n       Inputs or references can now be tracked per File instance through the\n       :attr:`.File.references` attribute. So, that all the references can be\n       tracked per individual file instance.\n\n    Args:\n        full_path (str): The full path to the File, it can be a path to a\n            folder or a file in the file system, or a web page. For file\n            sequences use \"%h%p%t %R\" format, for more information see\n            `PySeq Documentation`_. It can be set to empty string (or None\n            which will be converted to an empty string automatically).\n\n    .. _PySeq: http://packages.python.org/pyseq/\n    .. _PySeq Documentation: http://packages.python.org/pyseq/\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"Files\"\n    __mapper_args__ = {\"polymorphic_identity\": \"File\"}\n\n    file_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    # this is a limit for most\n    original_filename: Mapped[Optional[str]] = mapped_column(String(256))\n    # file systems\n    full_path: Mapped[Optional[str]] = mapped_column(\n        Text, doc=\"The full path of the url to the file.\"\n    )\n\n    created_with: Mapped[Optional[str]] = mapped_column(String(256))\n\n    def __init__(\n        self,\n        full_path: Optional[str] = \"\",\n        original_filename: Optional[str] = \"\",\n        references: Optional[List[\"File\"]] = None,\n        created_with: Optional[str] = None,\n        **kwargs: Optional[Dict[str, Any]],\n    ) -> None:\n        super(File, self).__init__(**kwargs)\n        ReferenceMixin.__init__(self, references=references)\n        self.full_path = full_path\n        self.original_filename = original_filename\n        self.created_with = created_with\n\n    @validates(\"full_path\")\n    def _validate_full_path(self, key: str, full_path: Union[None, str]) -> str:\n        \"\"\"Validate the given full_path value.\n\n        Args:\n            key (str): The name of the validated column.\n            full_path (str): The full_path value to be validated.\n\n        Raises:\n            TypeError: If the given full_path is not a str.\n\n        Returns:\n            str: The validated full_path value.\n        \"\"\"\n        if full_path is None:\n            full_path = \"\"\n\n        if not isinstance(full_path, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.full_path should be a str, \"\n                f\"not {full_path.__class__.__name__}: '{full_path}'\"\n            )\n\n        return self._format_path(full_path)\n\n    @validates(\"created_with\")\n    def _validate_created_with(\n        self, key: str, created_with: Union[None, str]\n    ) -> Union[None, str]:\n        \"\"\"Validate the given created_with value.\n\n        Args:\n            key (str): The name of the validated column.\n            created_with (str): The name of the application used to create this\n                File.\n\n        Raises:\n            TypeError: If the given created_with attribute is not None and not\n                a string.\n\n        Returns:\n            Union[None, str]: The validated created with value.\n        \"\"\"\n        if created_with is not None and not isinstance(created_with, str):\n            raise TypeError(\n                \"{}.created_with should be an instance of str, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    created_with.__class__.__name__,\n                    created_with,\n                )\n            )\n        return created_with\n\n    @validates(\"original_filename\")\n    def _validate_original_filename(\n        self, key: str, original_filename: Union[None, str]\n    ) -> str:\n        \"\"\"Validate the given original_filename value.\n\n        Args:\n            key (str): The name of the validated column.\n            original_filename (str): The original filename value to be validated.\n\n        Raises:\n            TypeError: If the given original_filename value is not a str.\n\n        Returns:\n            str: The validated original_filename value.\n        \"\"\"\n        filename_from_path = os.path.basename(self.full_path)\n        if original_filename is None:\n            original_filename = filename_from_path\n\n        if original_filename == \"\":\n            original_filename = filename_from_path\n\n        if not isinstance(original_filename, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.original_filename should be a str, \"\n                f\"not {original_filename.__class__.__name__}: '{original_filename}'\"\n            )\n\n        return original_filename\n\n    @staticmethod\n    def _format_path(path: Union[bytes, str]) -> str:\n        \"\"\"Format the path to internal format.\n\n        The path is using the Linux forward slashes for path separation.\n\n        Args:\n            path (Union[bytes, str]): The path value to be formatted.\n\n        Returns:\n            str: The formatted path value.\n        \"\"\"\n        if isinstance(path, bytes):\n            path = path.decode(\"utf-8\")\n\n        return path.replace(\"\\\\\", \"/\")\n\n    @property\n    def path(self) -> str:\n        \"\"\"Return the path part of the full_path.\n\n        Returns:\n            str: The path part of the full_path value.\n        \"\"\"\n        return os.path.split(self.full_path)[0]\n\n    @path.setter\n    def path(self, path: str) -> None:\n        \"\"\"Set the path part of the full_path attribute.\n\n        Args:\n            path (str): The new path value.\n\n        Raises:\n            TypeError: If the given path value is not a str.\n            ValueError: If the given path is an empty str.\n        \"\"\"\n        if path is None:\n            raise TypeError(f\"{self.__class__.__name__}.path cannot be set to None\")\n\n        if not isinstance(path, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.path should be a str, \"\n                f\"not {path.__class__.__name__}: '{path}'\"\n            )\n\n        if path == \"\":\n            raise ValueError(\n                f\"{self.__class__.__name__}.path cannot be an empty string\"\n            )\n\n        self.full_path = self._format_path(os.path.join(path, self.filename))\n\n    @property\n    def filename(self) -> str:\n        \"\"\"Return the filename part of the full_path attribute.\n\n        Returns:\n            str: The filename part of the full_path attribute.\n        \"\"\"\n        return os.path.split(self.full_path)[1]\n\n    @filename.setter\n    def filename(self, filename: Union[None, str]) -> None:\n        \"\"\"Set the filename part of the full_path attr.\n\n        Args:\n            filename (Union[None, str]): The new filename.\n\n        Raises:\n            TypeError: If the given filename is not a str.\n        \"\"\"\n        if filename is None:\n            filename = \"\"\n\n        if not isinstance(filename, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.filename should be a str, \"\n                f\"not {filename.__class__.__name__}: '{filename}'\"\n            )\n\n        self.full_path = self._format_path(os.path.join(self.path, filename))\n\n    @property\n    def extension(self) -> str:\n        \"\"\"Return the extension value.\n\n        Returns:\n            str: The extension extracted from the full_path value.\n        \"\"\"\n        return os.path.splitext(self.full_path)[1]\n\n    @extension.setter\n    def extension(self, extension: Union[None, str]) -> None:\n        \"\"\"Set the extension value.\n\n        Args:\n            extension (Union[None, str]): The new extension value.\n\n        Raises:\n            TypeError: If the given extension value is not a str.\n        \"\"\"\n        if extension is None:\n            extension = \"\"\n\n        if not isinstance(extension, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.extension should be a str, \"\n                f\"not {extension.__class__.__name__}: '{extension}'\"\n            )\n\n        if extension != \"\":\n            if not extension.startswith(os.path.extsep):\n                extension = os.path.extsep + extension\n\n        self.filename = os.path.splitext(self.filename)[0] + extension\n\n    @property\n    def absolute_full_path(self) -> str:\n        \"\"\"Return the absolute full path of the file.\n\n        Returns:\n            str: The absolute full path of the file.\n        \"\"\"\n        return os.path.normpath(os.path.expandvars(self.full_path))\n\n    @property\n    def absolute_path(self) -> str:\n        \"\"\"Return the absolute path of the file.\n\n        Returns:\n            str: The absolute path of the file.\n        \"\"\"\n        return os.path.dirname(self.absolute_full_path)\n\n    def walk_references(\n        self,\n        method: Union[int, str, TraversalDirection] = TraversalDirection.DepthFirst,\n    ) -> Generator[None, \"File\", None]:\n        \"\"\"Walk the references of this file.\n\n        Args:\n            method (Union[int, str, TraversalDirection]): The walk method\n                defined by the :class:`.TraversalDirection` enum.\n\n        Yields:\n            File: Yield the File instances.\n        \"\"\"\n        for v in walk_hierarchy(self, \"references\", method=method):\n            yield v\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check if the other is equal to this File.\n\n        Args:\n            other (Any): The other object to be checked for equality.\n\n        Returns:\n            bool: If the other object is a File instance and has the same\n                full_path and type value.\n        \"\"\"\n        return (\n            super(File, self).__eq__(other)\n            and isinstance(other, File)\n            and self.full_path == other.full_path\n            and self.type == other.type\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(File, self).__hash__()\n"
  },
  {
    "path": "src/stalker/models/format.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Image format related classes and utility functions are situated here.\"\"\"\nfrom typing import Any, Optional, Union\n\nfrom sqlalchemy import ForeignKey, Integer\nfrom sqlalchemy.orm import Mapped, mapped_column, validates\n\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity\n\nlogger = get_logger(__name__)\n\n\nclass ImageFormat(Entity):\n    \"\"\"Common image formats for the :class:`.Project` s.\n\n    Args:\n        width (Union[int, float]): The width of the format, it cannot be zero or\n            negative, if a float number is given it will be converted to integer.\n\n        height (Union[int, float]): The height of the format, it cannot be zero or\n            negative, if a float number is given it will be converted to integer.\n\n        pixel_aspect ((Union[int, float])): The pixel aspect ratio of the current\n            ImageFormat object, it cannot be zero or negative, and if given as an\n            integer it will be converted to a float, the default value is 1.0.\n\n        print_resolution (Union[int, float]): The print resolution of the ImageFormat\n            given as DPI (dot-per-inch). It cannot be zero or negative.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"ImageFormats\"\n    __mapper_args__ = {\"polymorphic_identity\": \"ImageFormat\"}\n\n    imageFormat_id: Mapped[int] = mapped_column(\n        \"id\",\n        Integer,\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    width: Mapped[Optional[int]] = mapped_column(\n        doc=\"\"\"The width of this format.\n\n        * the width should be set to a positive non-zero integer\n        * integers are also accepted but will be converted to float\n        * for improper inputs the object will raise an exception.\n        \"\"\",\n    )\n\n    height: Mapped[Optional[int]] = mapped_column(\n        doc=\"\"\"The height of this format\n\n        * the height should be set to a positive non-zero integer\n        * integers are also accepted but will be converted to float\n        * for improper inputs the object will raise an exception.\n        \"\"\",\n    )\n\n    pixel_aspect: Mapped[Optional[float]] = mapped_column(\n        default=1.0,\n        doc=\"\"\"The pixel aspect ratio of this format.\n\n        * the pixel_aspect should be set to a positive non-zero float\n        * integers are also accepted but will be converted to float\n        * for improper inputs the object will raise an exception\n        \"\"\",\n    )\n\n    print_resolution: Mapped[Optional[float]] = mapped_column(\n        default=300.0,\n        doc=\"\"\"The print resolution of this format\n\n        * it should be set to a positive non-zero float or integer\n        * integers are also accepted but will be converted to float\n        * for improper inputs the object will raise an exception.\n        \"\"\",\n    )\n\n    def __init__(\n        self,\n        width: Union[int, float],\n        height: Union[int, float],\n        pixel_aspect: Optional[Union[int, float]] = 1.0,\n        print_resolution: Optional[Union[int, float]] = 300,\n        **kwargs,\n    ) -> None:\n        super(ImageFormat, self).__init__(**kwargs)\n\n        self.width = width\n        self.height = height\n        self.pixel_aspect = pixel_aspect\n        self.print_resolution = print_resolution\n        # self._device_aspect = 1.0\n\n    @validates(\"width\")\n    def _validate_width(self, key: str, width: Union[int, float]) -> int:\n        \"\"\"Validate the given width.\n\n        Args:\n            key (str): The name of the validated column.\n            width (Union[int, float]): The width value to be validated.\n\n        Raises:\n            TypeError: If the width is not an int or float.\n            ValueError: If the width is 0 or a negative value.\n\n        Returns:\n            int: The validated width value.\n        \"\"\"\n        if not isinstance(width, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.width should be an instance of int or \"\n                f\"float, not {width.__class__.__name__}: '{width}'\"\n            )\n\n        if width <= 0:\n            raise ValueError(\n                f\"{self.__class__.__name__}.width cannot be zero or negative\"\n            )\n\n        return int(width)\n\n    @validates(\"height\")\n    def _validate_height(self, key: str, height: Union[int, float]) -> int:\n        \"\"\"Validate the given height.\n\n        Args:\n            key (str): The name of the validated column.\n            height (Union[int, float]): The height value to be validated.\n\n        Raises:\n            TypeError: If the height is not an int or float.\n            ValueError: If the height is 0 or a negative value.\n\n        Returns:\n            int: The validated height value.\n        \"\"\"\n        if not isinstance(height, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.height should be an instance of int or \"\n                f\"float, not {height.__class__.__name__}: '{height}'\"\n            )\n\n        if height <= 0:\n            raise ValueError(\n                f\"{self.__class__.__name__}.height cannot be zero or negative\"\n            )\n\n        return int(height)\n\n    @validates(\"pixel_aspect\")\n    def _validate_pixel_aspect(\n        self, key: str, pixel_aspect: Union[int, float]\n    ) -> float:\n        \"\"\"Validate the given pixel aspect.\n\n        Args:\n            key (str): The name of the validated column.\n            pixel_aspect (Union[int, float]): The pixel_aspect value to be validated.\n\n        Raises:\n            TypeError: If the given pixel_aspect value is not an int for float.\n            ValueError: If the pixel_aspect is 0 or a negative value.\n\n        Returns:\n            float: The validated pixel_aspect value.\n        \"\"\"\n        if not isinstance(pixel_aspect, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.pixel_aspect should be an instance of int \"\n                f\"or float, not {pixel_aspect.__class__.__name__}: '{pixel_aspect}'\"\n            )\n\n        if pixel_aspect <= 0:\n            raise ValueError(\n                f\"{self.__class__.__name__}.pixel_aspect cannot be zero or a negative \"\n                \"value\"\n            )\n\n        return float(pixel_aspect)\n\n    @validates(\"print_resolution\")\n    def _validate_print_resolution(\n        self, key: str, print_resolution: Union[int, float]\n    ) -> float:\n        \"\"\"Validate the print resolution value.\n\n        Args:\n            key (str): The name of the validated column.\n            print_resolution (Union[int, float]): The print_resolution value to be\n                validated.\n\n        Raises:\n            TypeError: If the given print_resolution is not an int or float.\n            ValueError: If the print_resolution is 0 or negative value.\n\n        Returns:\n            float: The validated print_resolution value.\n        \"\"\"\n        if not isinstance(print_resolution, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.print_resolution should be an instance of \"\n                \"int or float, \"\n                f\"not {print_resolution.__class__.__name__}: '{print_resolution}'\"\n            )\n\n        if print_resolution <= 0:\n            raise ValueError(\n                f\"{self.__class__.__name__}.print_resolution cannot be zero or negative\"\n            )\n\n        return float(print_resolution)\n\n    @property\n    def device_aspect(self) -> float:\n        \"\"\"Return the device aspect.\n\n        Because the device_aspect is calculated from the width/height*pixel\n        formula, this property is read-only.\n\n        Returns:\n            float: The device aspect ratio.\n        \"\"\"\n        return float(self.width) / float(self.height) * self.pixel_aspect\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check if other is equal to this ImageFormat.\n\n        Args:\n            other (Any): The object to check the equality of.\n\n        Returns:\n            bool: True if the other is an ImageFormat instance and the width, height and\n                pixel_aspect values are all equal.\n        \"\"\"\n        return (\n            super(ImageFormat, self).__eq__(other)\n            and isinstance(other, ImageFormat)\n            and self.width == other.width\n            and self.height == other.height\n            and self.pixel_aspect == other.pixel_aspect\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(ImageFormat, self).__hash__()\n"
  },
  {
    "path": "src/stalker/models/message.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"The Message related classes and functions are situated here.\"\"\"\nfrom typing import Any, Dict\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity\nfrom stalker.models.mixins import StatusMixin\n\nlogger = get_logger(__name__)\n\n\nclass Message(Entity, StatusMixin):\n    \"\"\"The base of the messaging system in Stalker.\n\n    Messages are one of the ways to collaborate in Stalker. The model of the\n    messages is taken from the e-mail system. So it is pretty similar to an\n    e-mail message.\n\n    Args:\n        from (User): The :class:`.User` object sending the message.\n        to (User): The list of :class:`.User` s to receive this message.\n        subject (str): The subject of the message.\n        body (str): tThe body of the message.\n        in_reply_to (Message): The :class:`.Message` object which this message is a\n            reply to.\n        replies (Message): The list of :class:`.Message` objects which are the direct\n            replies of this message.\n        attachments (SimpleEntity): A list of :class:`.SimpleEntity` objects attached to\n            this message (so anything can be attached to a message).\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"Messages\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Message\"}\n    message_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs: Dict[str, Any]) -> None:\n        super(Message, self).__init__(**kwargs)\n        StatusMixin.__init__(self, **kwargs)\n"
  },
  {
    "path": "src/stalker/models/mixins.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Mixins are situated here.\"\"\"\n\nimport datetime\nfrom typing import (\n    Any,\n    Dict,\n    Generator,\n    List,\n    Optional,\n    TYPE_CHECKING,\n    Tuple,\n    Type,\n    Union,\n)\nfrom typing_extensions import Self\n\nimport pytz\n\nfrom sqlalchemy import (\n    Column,\n    Float,\n    ForeignKey,\n    Integer,\n    Interval,\n    String,\n    Table,\n)\nfrom sqlalchemy.exc import OperationalError, UnboundExecutionError\nfrom sqlalchemy.ext.declarative import declared_attr\nfrom sqlalchemy.orm import (\n    Mapped,\n    backref,\n    mapped_column,\n    relationship,\n    synonym,\n    validates,\n)\n\nfrom stalker import defaults\nfrom stalker.db.declarative import Base\nfrom stalker.db.session import DBSession\nfrom stalker.db.types import GenericDateTime\nfrom stalker.log import get_logger\nfrom stalker.models.enum import (\n    ScheduleConstraint,\n    ScheduleConstraintDecorator,\n    ScheduleModel,\n    ScheduleModelDecorator,\n    TimeUnit,\n    TimeUnitDecorator,\n    TraversalDirection,\n)\nfrom stalker.utils import check_circular_dependency, make_plural, walk_hierarchy\n\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.auth import Permission\n    from stalker.models.project import Project\n    from stalker.models.status import Status, StatusList\n    from stalker.models.file import File\n    from stalker.models.studio import WorkingHours\n\n\nlogger = get_logger(__name__)\n\n\ndef create_secondary_table(\n    primary_cls_name: str,\n    secondary_cls_name: str,\n    primary_cls_table_name: str,\n    secondary_cls_table_name: str,\n    secondary_table_name: Optional[str] = None,\n) -> Table:\n    \"\"\"Create any secondary table.\n\n    Args:\n        primary_cls_name (str): The primary class name.\n        secondary_cls_name (str): The secondary class name.\n        primary_cls_table_name (str): The primary class table name.\n        secondary_cls_table_name (str): The secondary class table name.\n        secondary_table_name (Union[None, str]): Optional secondary table name.\n\n    Raises:\n        TypeError: If primary_cls_name is not a str.\n        TypeError: If secondary_cls_name is not a str.\n        TypeError: If primary_cls_table_name is not a str.\n        TypeError: If secondary_cls_table_name is not a str.\n        TypeError: If secondary_table_name is not a str.\n        ValueError: If primary_cls_name is an empty str.\n        ValueError: If secondary_cls_name is an empty str.\n        ValueError: If primary_cls_table_name is an empty str.\n        ValueError: If secondary_cls_table_name is an empty str.\n        ValueError: If secondary_table_name is an empty str.\n\n    Returns:\n        Table: The secondary table.\n    \"\"\"\n    # validate data\n    # primary_cls_name\n    if not isinstance(primary_cls_name, str):\n        raise TypeError(\n            \"primary_cls_name should be a str containing the primary class name, \"\n            f\"not {primary_cls_name.__class__.__name__}: '{primary_cls_name}'\"\n        )\n\n    if primary_cls_name == \"\":\n        raise ValueError(\n            \"primary_cls_name should be a str containing the primary class name, \"\n            f\"not: '{primary_cls_name}'\"\n        )\n\n    # secondary_cls_name\n    if not isinstance(secondary_cls_name, str):\n        raise TypeError(\n            \"secondary_cls_name should be a str containing the secondary class name, \"\n            f\"not {secondary_cls_name.__class__.__name__}: '{secondary_cls_name}'\"\n        )\n\n    if secondary_cls_name == \"\":\n        raise ValueError(\n            \"secondary_cls_name should be a str containing the secondary class name, \"\n            f\"not: '{secondary_cls_name}'\"\n        )\n\n    # primary_cls_table_name\n    if not isinstance(primary_cls_table_name, str):\n        raise TypeError(\n            \"primary_cls_table_name should be a str containing the primary class \"\n            f\"table name, not {primary_cls_table_name.__class__.__name__}: \"\n            f\"'{primary_cls_table_name}'\"\n        )\n\n    if primary_cls_table_name == \"\":\n        raise ValueError(\n            \"primary_cls_table_name should be a str containing the primary class \"\n            f\"table name, not: '{primary_cls_table_name}'\"\n        )\n\n    # secondary_cls_table_name\n    if not isinstance(secondary_cls_table_name, str):\n        raise TypeError(\n            \"secondary_cls_table_name should be a str containing the secondary class \"\n            f\"table name, not {secondary_cls_table_name.__class__.__name__}: \"\n            f\"'{secondary_cls_table_name}'\"\n        )\n\n    if secondary_cls_table_name == \"\":\n        raise ValueError(\n            \"secondary_cls_table_name should be a str containing the secondary class \"\n            f\"table name, not: '{secondary_cls_table_name}'\"\n        )\n\n    # secondary_table_name\n    if secondary_table_name is not None and not isinstance(secondary_table_name, str):\n        raise TypeError(\n            \"secondary_table_name should be a str containing the secondary table \"\n            \"name, or it can be None or an empty string to let Stalker to auto \"\n            f\"generate one, not {secondary_table_name.__class__.__name__}: \"\n            f\"'{secondary_table_name}'\"\n        )\n\n    plural_secondary_cls_name = make_plural(secondary_cls_name)\n\n    # use the given class_name and the class_table\n    if not secondary_table_name:\n        secondary_table_name = f\"{primary_cls_name}_{plural_secondary_cls_name}\"\n\n    # check if the table is already defined\n    if secondary_table_name not in Base.metadata:\n        secondary_table = Table(\n            secondary_table_name,\n            Base.metadata,\n            Column(\n                f\"{primary_cls_name.lower()}_id\",\n                Integer,\n                ForeignKey(f\"{primary_cls_table_name}.id\"),\n                primary_key=True,\n            ),\n            Column(\n                f\"{secondary_cls_name.lower()}_id\",\n                Integer,\n                ForeignKey(f\"{secondary_cls_table_name}.id\"),\n                primary_key=True,\n            ),\n        )\n    else:\n        secondary_table = Base.metadata.tables[secondary_table_name]\n\n    return secondary_table\n\n\nclass TargetEntityTypeMixin(object):\n    \"\"\"Adds target_entity_type attribute to mixed in class.\n\n    Args:\n        target_entity_type (Union[str, type]): The target entity type which this class\n            is designed for. Should be a class or a class name.\n\n        For example::\n\n            from stalker import SimpleEntity, TargetEntityTypeMixin, Project\n\n            class A(SimpleEntity, TargetEntityTypeMixin):\n                __tablename__ = \"As\"\n                __mapper_args__ = {\"polymorphic_identity\": \"A\"}\n\n                def __init__(self, **kwargs):\n                    super(A, self).__init__(**kwargs)\n                    TargetEntityTypeMixin.__init__(self, **kwargs)\n\n            a_obj = A(target_entity_type=Project)\n\n        The ``a_obj`` will only be accepted by :class:`.Project` instances. You cannot\n        assign it to any other class which accepts a :class:`.Type` instance.\n\n        To control the mixed-in class behavior add these class variables to the\n        mixed in class:\n\n            __nullable_target__ : controls if the target_entity_type can be\n                nullable or not. Default is False.\n\n            __unique_target__ : controls if the target_entity_type should be unique, so\n                there is only one object for one type. Default is False.\n    \"\"\"\n\n    __nullable_target__ = False\n    __unique_target__ = False\n\n    @declared_attr\n    def _target_entity_type(cls) -> Mapped[str]:\n        \"\"\"Create the _target_entity_type attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the _target_entity_type attribute.\n        \"\"\"\n        return mapped_column(\n            \"target_entity_type\",\n            String(128),\n            nullable=cls.__nullable_target__,\n            unique=cls.__unique_target__,\n        )\n\n    def __init__(self, target_entity_type: Optional[str] = None, **kwargs) -> None:\n        self._target_entity_type = self._validate_target_entity_type(target_entity_type)\n\n    def _validate_target_entity_type(self, target_entity_type: Union[str, Type]) -> str:\n        \"\"\"Validate the given target_entity_type value.\n\n        Args:\n            target_entity_type (Union[str, type]): The target_entity_type that this\n                entity is valid for.\n\n        Raises:\n            TypeError: If the given target_entity_type value is None.\n            ValueError: If the given target_entity_type value is an empty str.\n\n        Returns:\n            str: The validated target_entity_type value.\n        \"\"\"\n        # it cannot be None\n        if target_entity_type is None:\n            raise TypeError(\n                f\"{self.__class__.__name__}.target_entity_type cannot be None\"\n            )\n\n        # check if it is a class\n        if isinstance(target_entity_type, type):\n            target_entity_type = target_entity_type.__name__\n\n        if target_entity_type == \"\":\n            raise ValueError(\n                f\"{self.__class__.__name__}.target_entity_type cannot be empty\"\n            )\n\n        return target_entity_type\n\n    def _target_entity_type_getter(self) -> str:\n        \"\"\"Return the _target_entity_type attribute value.\n\n        Returns:\n            str: The _target_entity_type attribute value.\n        \"\"\"\n        return self._target_entity_type\n\n    @declared_attr\n    def target_entity_type(cls) -> Mapped[str]:\n        \"\"\"Create the target_entity_type attribute as a declared attribute.\n\n        Returns:\n            SynonymProperty: The target_entity_type property.\n        \"\"\"\n        return synonym(\n            \"_target_entity_type\",\n            descriptor=property(\n                fget=cls._target_entity_type_getter,\n                doc=\"\"\"The entity type which this object is valid for.\n\n                Usually it is set to the TargetClass directly.\n                \"\"\",\n            ),\n        )\n\n\nclass StatusMixin(object):\n    \"\"\"Makes the mixed in object statusable.\n\n    This mixin adds status and status_list attributes to the mixed in class.\n    Any object that needs a status and a corresponding status list can include\n    this mixin.\n\n    When mixed with a class which don't have an __init__ method, the mixin\n    supplies one, and in this case the parameters below must be defined.\n\n    Args:\n        status_list (StatusList): this attribute holds a status list object, which\n            shows the possible statuses that this entity could be in. This attribute\n            cannot be empty or None. Giving a StatusList object, the\n            StatusList.target_entity_type should match the current class.\n\n            .. versionadded:: 0.1.2.a4\n\n                The status_list argument now can be skipped or can be None if there\n                is an active database connection and there is a suitable\n                :class:`.StatusList` instance in the database whom\n                :attr:`.StatusList.target_entity_type` attribute is set to the\n                current mixed-in class name.\n\n        status (Status): It is a :class:`.Status` instance which shows the current\n            status of the statusable object. Integer values are also accepted,\n            which shows the index of the desired status in the ``status_list``\n            attribute of the current statusable object. If a :class:`.Status`\n            instance is supplied, it should also be present in the ``status_list``\n            attribute. If set to None then the first :class:`.Status` instance\n            in the ``status_list`` will be used.\n\n            .. versionadded:: 0.2.0\n\n                Status attribute as Status instance:\n\n                It is now possible to set the status of the instance by a\n                :class:`.Status` instance directly. And the :attr:`.StatusMixin.status`\n                will return a proper :class:`.Status` instance.\n    \"\"\"\n\n    def __init__(\n        self,\n        status: Union[None, \"Status\"] = None,\n        status_list: Union[None, \"StatusList\"] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        self.status_list = status_list\n        self.status = status\n\n    @declared_attr\n    def status_id(cls) -> Mapped[int]:\n        \"\"\"Create the status_id attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the status_id attribute.\n        \"\"\"\n        return mapped_column(\n            \"status_id\",\n            ForeignKey(\"Statuses.id\"),\n            nullable=False,\n            # This is set to nullable=True but it is impossible to set the\n            # status to None by using this Declarative approach.\n            #\n            # This is done in that way cause SQLAlchemy was flushing the data\n            # (AutoFlush) preliminarily while checking if the given Status was\n            # in the related StatusList, and it was complaining about the\n            # status cannot be null\n        )\n\n    @declared_attr\n    def status(cls) -> Mapped[\"Status\"]:\n        \"\"\"Create the status attribute as a declared attribute.\n\n        Returns:\n            relationship: The relationship object related to the status attribute.\n        \"\"\"\n        return relationship(\n            \"Status\",\n            primaryjoin=f\"{cls.__name__}.status_id==Status.status_id\",\n            doc=\"\"\"The current status of the object.\n\n            It is a :class:`.Status` instance which\n            is one of the Statuses stored in the ``status_list`` attribute\n            of this object.\n            \"\"\",\n        )\n\n    @declared_attr\n    def status_list_id(cls) -> Mapped[int]:\n        \"\"\"Create the status_list_id attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the status_list_id attribute.\n        \"\"\"\n        return mapped_column(\n            \"status_list_id\", ForeignKey(\"StatusLists.id\"), nullable=False\n        )\n\n    @declared_attr\n    def status_list(cls) -> Mapped[\"StatusList\"]:\n        \"\"\"Create the status_list attribute as a declared attribute.\n\n        Returns:\n            relationship: The relationship object related to the status_list attribute.\n        \"\"\"\n        return relationship(\n            \"StatusList\",\n            primaryjoin=f\"{cls.__name__}.status_list_id==StatusList.status_list_id\",\n        )\n\n    @validates(\"status_list\")\n    def _validate_status_list(\n        self, key: str, status_list: Union[None, \"StatusList\"]\n    ) -> \"StatusList\":\n        \"\"\"Validate the given status_list value.\n\n        Args:\n            key (str): The name of the validated column.\n            status_list (Union[None, StatusList]): The status_list value to be\n                validated.\n\n        Raises:\n            TypeError: If the given status_list value is not a StatusList instance.\n\n        Returns:\n            StatusList: The validated status_list value.\n        \"\"\"\n        from stalker.models.status import StatusList\n\n        super_names = [mro.__name__ for mro in self.__class__.__mro__]\n\n        if status_list is None:\n            # check if there is a db setup and try to get the appropriate\n            # StatusList from the database\n\n            # disable autoflush to prevent premature class initialization\n            with DBSession.no_autoflush:\n                try:\n                    # try to get a StatusList with the target_entity_type is\n                    # matching the class name\n                    status_list = StatusList.query.filter(\n                        StatusList.target_entity_type.in_(super_names)\n                    ).first()\n                except (UnboundExecutionError, OperationalError):\n                    # it is not mapped just skip it\n                    pass\n\n        # if it is still None\n        if status_list is None:\n            # there is no db so raise an error because there is no way\n            # to get an appropriate StatusList\n            raise TypeError(\n                f\"{self.__class__.__name__} instances cannot be initialized without a \"\n                \"stalker.models.status.StatusList instance, please pass a \"\n                \"suitable StatusList \"\n                f\"(StatusList.target_entity_type={self.__class__.__name__}) with the \"\n                \"'status_list' argument\"\n            )\n        else:\n            # it is not an instance of status_list\n            if not isinstance(status_list, StatusList):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.status_list should be an instance of \"\n                    \"stalker.models.status.StatusList, \"\n                    f\"not {status_list.__class__.__name__}: '{status_list}'\"\n                )\n\n            # check if the entity_type matches to the\n            # StatusList.target_entity_type\n            if status_list.target_entity_type not in super_names:\n                raise TypeError(\n                    \"The given StatusLists' target_entity_type is \"\n                    f\"{status_list.target_entity_type}, \"\n                    \"whereas the entity_type of this object is \"\n                    f\"{self.__class__.__name__}\"\n                )\n\n        return status_list\n\n    @validates(\"status\")\n    def _validate_status(self, key: str, status: \"Status\") -> \"Status\":\n        \"\"\"Validate the given status value.\n\n        Args:\n            key (str): The name of the validated column.\n            status (Status): The status value to be validated.\n\n        Raises:\n            TypeError: If the given status value is not a Status instance or an int.\n            ValueError: If the status is a negative int value or the given int value\n                is equal or bigger than the length of the `StatusList.statuses` or if\n                the given Status instance is not in the `StatusList.statuses` list.\n\n        Returns:\n            Status: The validated status value.\n        \"\"\"\n        from stalker.models.status import Status\n\n        # it is set to None\n        if status is None:\n            with DBSession.no_autoflush:\n                status = self.status_list.statuses[0]\n\n        # it is not an instance of status or int\n        if not isinstance(status, (Status, int)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.status must be an instance of \"\n                \"stalker.models.status.Status or an integer showing the index of the \"\n                f\"Status object in the {self.__class__.__name__}.status_list, \"\n                f\"not {status.__class__.__name__}: '{status}'\"\n            )\n\n        if isinstance(status, int):\n            # if it is not in the correct range:\n            if status < 0:\n                raise ValueError(\n                    f\"{self.__class__.__name__}.status must be a non-negative integer\"\n                )\n\n            if status >= len(self.status_list.statuses):\n                raise ValueError(\n                    f\"{self.__class__.__name__}.status cannot be bigger than the \"\n                    \"length of the status_list\"\n                )\n                # get the status instance out of the status_list instance\n            status = self.status_list[status]\n\n        # check if the given status is in the status_list\n        if status not in self.status_list:\n            raise ValueError(\n                f\"The given Status instance for {self.__class__.__name__}.status is \"\n                f\"not in the {self.__class__.__name__}.status_list, please supply a \"\n                \"status from that list.\"\n            )\n\n        return status\n\n\nclass DateRangeMixin(object):\n    \"\"\"Adds date range info to the mixed in class.\n\n    Adds date range information like ``start``, ``end`` and ``duration``. These\n    attributes will be used in TaskJuggler. Because ``effort`` is only\n    meaningful if there are some ``resources`` this attribute has been left\n    special for :class:`.Task` class. The ``length`` has not been implemented\n    because of its rare use.\n\n    The preceding order for the attributes is as follows::\n\n      start > end > duration\n\n    So if all of the parameters are given only the ``start`` and the ``end``\n    will be used and the ``duration`` will be calculated accordingly. In any\n    other conditions the missing parameter will be calculated from the\n    following table:\n\n    +-------+-----+----------+----------------------------------------+\n    | start | end | duration | DEFAULTS                               |\n    +=======+=====+==========+========================================+\n    |       |     |          | start = datetime.datetime.now(pytz.utc)|\n    |       |     |          |                                        |\n    |       |     |          | duration = datetime.timedelta(days=10) |\n    |       |     |          |                                        |\n    |       |     |          | end = start + duration                 |\n    +-------+-----+----------+----------------------------------------+\n    |   X   |     |          | duration = datetime.timedelta(days=10) |\n    |       |     |          |                                        |\n    |       |     |          | end = start + duration                 |\n    +-------+-----+----------+----------------------------------------+\n    |   X   |  X  |          | duration = end - start                 |\n    +-------+-----+----------+----------------------------------------+\n    |   X   |     |    X     | end = start + duration                 |\n    +-------+-----+----------+----------------------------------------+\n    |   X   |  X  |    X     | duration = end - start                 |\n    +-------+-----+----------+----------------------------------------+\n    |       |  X  |    X     | start = end - duration                 |\n    +-------+-----+----------+----------------------------------------+\n    |       |  X  |          | duration = datetime.timedelta(days=10) |\n    |       |     |          |                                        |\n    |       |     |          | start = end - duration                 |\n    +-------+-----+----------+----------------------------------------+\n    |       |     |    X     | start = datetime.datetime.now(pytz.utc)|\n    |       |     |          |                                        |\n    |       |     |          | end = start + duration                 |\n    +-------+-----+----------+----------------------------------------+\n\n    Only the ``start``, ``end`` will be stored. The ``duration`` attribute is\n    the direct difference of the the ``start`` and ``end`` attributes, so there\n    is no need to store it. But if will be used in calculation of the start and\n    end values.\n\n    The start and end attributes have a ``computed`` companion. Which are the\n    return values from TaskJuggler. so for ``start`` there is the\n    ``computed_start`` and for ``end`` there is the ``computed_end``\n    attributes.\n\n    The date attributes can be managed with timezones. Follow the Python idioms\n    shown in the `documentation of datetime`_\n\n    .. _documentation of datetime: https://docs.python.org/library/datetime.html\n\n    Args:\n        start (datetime.datetime): the start date of the entity, should be a\n            datetime.datetime instance, the start is the pin point for the date\n            calculation. In any condition if the start is available then the value\n            will be preserved. If start passes the end the end is also changed\n            to a date to keep the timedelta between dates. The default value is\n            datetime.datetime.now(pytz.utc)\n\n        end (datetime.datetime): the end of the entity, should be a datetime.datetime\n            instance, when the start is changed to a date passing the end, then\n            the end is also changed to a later date so the timedelta between the\n            dates is kept.\n\n        duration (datetime.timedelta): The duration of the entity. It is a\n            :class:`datetime.timedelta` instance. The default value is read from\n            he :class:`.Config` class. See the table above for the initialization\n            rules.\n    \"\"\"\n\n    def __init__(\n        self,\n        start: Optional[datetime.datetime] = None,\n        end: Optional[datetime.datetime] = None,\n        duration: Optional[datetime.timedelta] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        self._start, self._end, self._duration = self._validate_dates(\n            start, end, duration\n        )\n\n    @declared_attr\n    def _end(cls) -> Mapped[Optional[datetime.datetime]]:\n        return mapped_column(\"end\", GenericDateTime)\n\n    def _end_getter(self) -> datetime.datetime:\n        \"\"\"Return the date that the entity should be delivered.\n\n        The end can be set to a datetime.timedelta and in this case it will be\n        calculated as an offset from the start and converted to datetime.datetime again.\n        Setting the start to a date passing the end will also set the end, so the\n        timedelta between them is preserved, default value is 10 days.\n\n        Returns:\n            datetime.datetime: The end datetime.\n        \"\"\"\n        with DBSession.no_autoflush:\n            return self._end\n\n    def _end_setter(self, end: datetime.datetime) -> None:\n        \"\"\"Set the end attribute value.\n\n        Args:\n            end (datetime.datetime): The end datetime value.\n        \"\"\"\n        self._start, self._end, self._duration = self._validate_dates(\n            self.start, end, self.duration\n        )\n\n    @declared_attr\n    def end(cls) -> Mapped[Optional[datetime.datetime]]:\n        \"\"\"Create the end attribute as a declared attribute.\n\n        Returns:\n            SynonymProperty: The end property.\n        \"\"\"\n        return synonym(\"_end\", descriptor=property(cls._end_getter, cls._end_setter))\n\n    @declared_attr\n    def _start(cls) -> Mapped[Optional[datetime.datetime]]:\n        \"\"\"Create the start attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the start attribute.\n        \"\"\"\n        return mapped_column(\"start\", GenericDateTime)\n\n    def _start_getter(self) -> datetime.datetime:\n        \"\"\"Return the date that this entity should start.\n\n        Also effects the :attr:`.DateRangeMixin.end` attribute value in certain\n        conditions, if the :attr:`.DateRangeMixin.start` is set to a time passing the\n        :attr:`.DateRangeMixin.end` it will also offset the :attr:`.DateRangeMixin.end`\n        to keep the :attr:`.DateRangeMixin.duration` value fixed.\n        :attr:`.DateRangeMixin.start` should be an instance of class:`datetime.datetime`\n        and the default value is :func:`datetime.datetime.now(pytz.utc)`.\n\n        Returns:\n            datetime.datetime: The start datetime value.\n        \"\"\"\n        with DBSession.no_autoflush:\n            return self._start\n\n    def _start_setter(self, start: datetime.datetime) -> None:\n        \"\"\"Set the start attribute.\n\n        Args:\n            start (datetime.datetime): The start date and time.\n        \"\"\"\n        self._start, self._end, self._duration = self._validate_dates(\n            start, self.end, self.duration\n        )\n\n    @declared_attr\n    def start(cls) -> Mapped[Optional[datetime.datetime]]:\n        \"\"\"Create the start attribute as a declared attribute.\n\n        Returns:\n            SynonymProperty: The start property.\n        \"\"\"\n        return synonym(\n            \"_start\",\n            descriptor=property(\n                cls._start_getter,\n                cls._start_setter,\n            ),\n        )\n\n    @declared_attr\n    def _duration(cls) -> Mapped[Optional[datetime.timedelta]]:\n        \"\"\"Create the duration attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the duration attribute.\n        \"\"\"\n        return mapped_column(\"duration\", Interval)\n\n    def _duration_getter(self) -> datetime.timedelta:\n        \"\"\"Return the duration value.\n\n        Returns:\n            datetime.timedelta: The duration value.\n        \"\"\"\n        with DBSession.no_autoflush:\n            return self._duration\n\n    def _duration_setter(self, duration: datetime.timedelta) -> None:\n        \"\"\"Set the duration value.\n\n        Args:\n            duration (datetime.timedelta): The duration value.\n        \"\"\"\n        if duration is not None:\n            if isinstance(duration, datetime.timedelta):\n                # set the end to None\n                # to make it recalculated\n                self._start, self._end, self._duration = self._validate_dates(\n                    self.start, None, duration\n                )\n            else:\n                # use the end\n                self._start, self._end, self._duration = self._validate_dates(\n                    self.start, self.end, duration\n                )\n        else:\n            self._start, self._end, self._duration = self._validate_dates(\n                self.start, self.end, duration\n            )\n\n    @declared_attr\n    def duration(self) -> Mapped[Optional[datetime.timedelta]]:\n        \"\"\"Return the duration attr as a synonym.\n\n        Returns:\n            SynonymProperty: The duration property.\n        \"\"\"\n        return synonym(\n            \"_duration\",\n            descriptor=property(\n                self._duration_getter,\n                self._duration_setter,\n                doc=\"\"\"Duration of the entity.\n\n                It is a datetime.timedelta instance. Showing the difference of\n                the :attr:`.start` and the :attr:`.end`. If edited it changes\n                the :attr:`.end` attribute value.\"\"\",\n            ),\n        )\n\n    def _validate_dates(\n        self,\n        start: datetime.datetime,\n        end: datetime.datetime,\n        duration: datetime.timedelta,\n    ) -> Tuple[datetime.datetime, datetime.datetime, datetime.timedelta]:  # noqa: C901\n        \"\"\"Update the date values.\n\n        Args:\n            start (datetime.datetime): The start datetime value.\n            end (datetime.datetime): The end datetime value.\n            duration (datetime.timedelta): The duration value.\n\n        Returns:\n            Tuple(datetime.datetime, datetime.datetime, datetime.timedelta): The\n                validated and calculated start, end dates and duration value.\n        \"\"\"\n        # logger.debug(f\"start    : {start}\")\n        # logger.debug(f\"end      : {end}\")\n        # logger.debug(f\"duration : {duration}\")\n        if not isinstance(start, datetime.datetime):\n            start = None\n\n        if not isinstance(end, datetime.datetime):\n            end = None\n\n        if not isinstance(duration, datetime.timedelta):\n            duration = None\n\n        # check start\n        if start is None:\n            # try to calculate the start from end and duration\n            if end is None:\n                # set the defaults\n                start = datetime.datetime.now(pytz.utc)\n\n                if duration is None:\n                    # set the defaults\n                    duration = defaults.timing_resolution\n\n                end = start + duration\n            else:\n                if duration is None:\n                    duration = defaults.timing_resolution\n\n                # try:\n                start = end - duration\n                # except OverflowError: # end is datetime.datetime.min\n                #     start = end\n\n        # check end\n        if end is None:\n            if duration is None:\n                duration = defaults.timing_resolution\n\n            end = start + duration\n\n        if end < start:\n            # check duration\n            if duration is None or duration < datetime.timedelta(1):\n                duration = datetime.timedelta(1)\n\n            # try:\n            end = start + duration\n            # except OverflowError: # start is datetime.datetime.max\n            #     end = start\n\n        # round the dates to the timing_resolution\n        rounded_start = self.round_time(start)\n        rounded_end = self.round_time(end)\n        rounded_duration = rounded_end - rounded_start\n\n        if rounded_duration < defaults.timing_resolution:\n            rounded_duration = defaults.timing_resolution\n            rounded_end = rounded_start + rounded_duration\n\n        return rounded_start, rounded_end, rounded_duration\n\n    @declared_attr\n    def computed_start(cls) -> Mapped[Optional[datetime.datetime]]:\n        \"\"\"Create the computed_start attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the computed_start attribute.\n        \"\"\"\n        return mapped_column(\"computed_start\", GenericDateTime)\n\n    @declared_attr\n    def computed_end(cls) -> Mapped[Optional[datetime.datetime]]:\n        \"\"\"Create the computed_end attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the computed_end attribute.\n        \"\"\"\n        return mapped_column(\"computed_end\", GenericDateTime)\n\n    @property\n    def computed_duration(self) -> datetime.timedelta:\n        \"\"\"Calculate the computed duration.\n\n        The computed_duration is calculated as the difference of computed_start and\n        computed_end if there are computed_start and computed_end otherwise returns\n        None.\n\n        Returns:\n            Union[None, datetime.timedelta]: None if one of computed_start or\n                computed_end value is None else the difference as datetime.timedelta\n                instance.\n        \"\"\"\n        return (\n            self.computed_end - self.computed_start\n            if self.computed_end and self.computed_start\n            else None\n        )\n\n    @classmethod\n    def round_time(cls, dt: datetime.datetime) -> datetime.datetime:\n        \"\"\"Round the given datetime object to the defaults.timing_resolution.\n\n        Use the  :class:`stalker.defaults.timing_resolution` as the closest number of\n        seconds to round to.\n\n        Based on Thierry Husson's answer in `Stackoverflow`_\n\n        _`Stackoverflow` : https://stackoverflow.com/a/10854034/1431079\n\n        Args:\n            dt (datetime.datetime): The datetime object, defaults to now.\n\n        Returns:\n            datetime.datetime: The rounded datetime.datetime instance.\n        \"\"\"\n        # to be compatible with python 2.6 use the following instead of\n        # total_seconds()\n        timing_resolution = defaults.timing_resolution\n        trs = timing_resolution.days * 86400 + timing_resolution.seconds\n\n        # convert to seconds\n        epoch = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc)\n\n        diff = dt - epoch\n        diff_in_seconds = diff.days * 86400 + diff.seconds\n        return epoch + datetime.timedelta(\n            seconds=(diff_in_seconds + trs * 0.5) // trs * trs\n        )\n\n    @property\n    def total_seconds(self) -> float:\n        \"\"\"Return the duration as seconds.\n\n        Returns:\n            float: The calculated total seconds value.\n        \"\"\"\n        return self.duration.days * 86400 + self.duration.seconds\n\n    @property\n    def computed_total_seconds(self) -> float:\n        \"\"\"Return the computed_total_seconds as seconds.\n\n        Returns:\n            float: The computed_total_seconds value.\n        \"\"\"\n        return self.computed_duration.days * 86400 + self.computed_duration.seconds\n\n\nclass ProjectMixin(object):\n    \"\"\"Allows connecting a :class:`.Project` to the mixed in object.\n\n    This also forces a ``all, delete-orphan`` cascade, so when a :class:``.Project``\n    instance is deleted then all the class instances that are inherited from\n    ``ProjectMixin`` will also be deleted. Meaning that, a class which also derives from\n    ``ProjectMixin`` will not be able to exists without a project (``delete-orphan``\n    case).\n\n    Args:\n        project (Project): A :class:`.Project` instance holding the project which this\n            object is related to. It cannot be None, or anything other than a\n            :class:`.Project` instance.\n    \"\"\"\n\n    #    # add this lines for Sphinx\n    #    __tablename__ = \"ProjectMixins\"\n\n    @declared_attr\n    def project_id(cls) -> Mapped[Optional[int]]:\n        \"\"\"Create the project_id attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the project_id attribute.\n        \"\"\"\n        return mapped_column(\n            \"project_id\",\n            Integer,\n            ForeignKey(\"Projects.id\"),\n            # cannot use nullable cause a Project object needs\n            # insert itself as the project and it needs post_update\n            # thus nullable should be True\n        )\n\n    @declared_attr\n    def project(cls) -> Mapped[Optional[\"Project\"]]:\n        \"\"\"Create the project attribute as a declared attribute.\n\n        Returns:\n            relationship: The relationship object related to the project attribute.\n        \"\"\"\n        backref_table_name = cls.__tablename__.lower()\n        doc = \"\"\"The :class:`.Project` instance that this object belongs to.\"\"\"\n\n        return relationship(\n            \"Project\",\n            primaryjoin=f\"{cls.__tablename__}.c.project_id==Projects.c.id\",\n            post_update=True,  # for project itself\n            uselist=False,\n            backref=backref(backref_table_name, cascade=\"all, delete-orphan\"),\n            doc=doc,\n        )\n\n    def __init__(\n        self, project: Optional[\"Project\"] = None, **kwargs: Dict[str, Any]\n    ) -> None:\n        self.project = project\n\n    @validates(\"project\")\n    def _validate_project(self, key: str, project: \"Project\") -> \"Project\":\n        \"\"\"Validate the given project value.\n\n        Args:\n            key (str): The name of the validated column.\n            project (Project): The project value to be validated.\n\n        Raises:\n            TypeError: If the project is None or not a Project instance.\n\n        Returns:\n            Project: The validated project value.\n        \"\"\"\n        from stalker.models.project import Project\n\n        if project is None:\n            raise TypeError(\n                f\"{self.__class__.__name__}.project cannot be None it must be an \"\n                \"instance of stalker.models.project.Project\"\n            )\n\n        if not isinstance(project, Project):\n            raise TypeError(\n                f\"{self.__class__.__name__}.project should be an instance of \"\n                \"stalker.models.project.Project instance, \"\n                f\"not {project.__class__.__name__}: '{project}'\"\n            )\n        return project\n\n\nclass ReferenceMixin(object):\n    \"\"\"Adds reference capabilities to the mixed in class.\n\n    References are :class:`stalker.models.file.File` instances or anything\n    derived from it, which adds information to the attached objects. The aim of\n    the References are generally to give more info to direct the evolution of\n    the object.\n\n    Args:\n        references (File): A list of :class:`.File` instances.\n    \"\"\"\n\n    # add this lines for Sphinx\n    #    __tablename__ = \"ReferenceMixins\"\n\n    def __init__(\n        self, references: Optional[List[\"File\"]] = None, **kwargs: Dict[str, Any]\n    ) -> None:\n        if references is None:\n            references = []\n\n        self.references = references\n\n    @declared_attr\n    def references(cls) -> Mapped[Optional[List[\"File\"]]]:\n        \"\"\"Create the references attribute as a declared attribute.\n\n        Returns:\n            relationship: The relationship object related to the references attribute.\n        \"\"\"\n        primary_cls_name = f\"{cls.__name__}\"\n        secondary_cls_name = \"Reference\"\n        primary_cls_table_name = f\"{cls.__tablename__}\"\n        secondary_cls_table_name = \"Files\"\n        secondary_table_name = f\"{cls.__name__}_References\"\n\n        # get secondary table\n        secondary_table = create_secondary_table(\n            primary_cls_name=primary_cls_name,\n            secondary_cls_name=secondary_cls_name,\n            primary_cls_table_name=primary_cls_table_name,\n            secondary_cls_table_name=secondary_cls_table_name,\n            secondary_table_name=secondary_table_name,\n        )\n        # return the relationship\n        return relationship(\n            secondary=secondary_table,\n            primaryjoin=f\"{primary_cls_table_name}.c.id=={secondary_table_name}.c.{primary_cls_name.lower()}_id\",\n            secondaryjoin=f\"{secondary_table_name}.c.{secondary_cls_name.lower()}_id=={secondary_cls_table_name}.c.id\",\n            doc=\"\"\"A list of :class:`.File` instances given as a reference for\n            this entity.\n            \"\"\",\n        )\n\n    @validates(\"references\")\n    def _validate_references(self, key: str, reference: \"File\") -> \"File\":\n        \"\"\"Validate the given reference.\n\n        Args:\n            key (str): The name of the validated column.\n            reference (File): The reference value to be validated.\n\n        Raises:\n            TypeError: If the reference is not a File instance.\n\n        Returns:\n            File: The validated reference value.\n        \"\"\"\n        from stalker.models.file import File\n\n        # all items should be instance of stalker.models.entity.Entity\n        if not isinstance(reference, File):\n            raise TypeError(\n                f\"{self.__class__.__name__}.references should only contain \"\n                \"instances of stalker.models.file.File, \"\n                f\"not {reference.__class__.__name__}: '{reference}'\"\n            )\n        return reference\n\n\nclass ACLMixin(object):\n    \"\"\"A Mixin for adding ACLs to mixed in class.\n\n    Access control lists or ACLs are used to determine if the given resource has the\n    permission to access the given data. It is based on Pyramids Authorization system\n    but organized to fit in Stalker style.\n\n    The ACLMixin adds an attribute called ``permissions`` and a property called\n    ``__acl__`` to be able to pass the permission data to Pyramid framework.\n    \"\"\"\n\n    @declared_attr\n    def permissions(cls) -> Mapped[List[\"Permission\"]]:\n        \"\"\"Create the permissions attribute as a declared attribute.\n\n        Returns:\n            relationship: The relationship object related to the permissions attribute.\n        \"\"\"\n        # get the secondary table\n        secondary_table = create_secondary_table(\n            cls.__name__, \"Permission\", cls.__tablename__, \"Permissions\"\n        )\n        return relationship(\"Permission\", secondary=secondary_table)\n\n    @validates(\"permissions\")\n    def _validate_permissions(self, key: str, permission: \"Permission\") -> \"Permission\":\n        \"\"\"Validate the given permission value.\n\n        Args:\n            key (str): The name of the validated column.\n            permission (Permission): The permission value to be validated.\n\n        Raises:\n            TypeError: If the given permission value is not a Permission instance.\n\n        Returns:\n            Permission: The validated permission value.\n        \"\"\"\n        from stalker.models.auth import Permission\n\n        if not isinstance(permission, Permission):\n            raise TypeError(\n                f\"{self.__class__.__name__}.permissions should be all instances of \"\n                \"stalker.models.auth.Permission, \"\n                f\"not {permission.__class__.__name__}: '{permission}'\"\n            )\n\n        return permission\n\n    @property\n    def __acl__(self) -> List[Tuple[str, str, str]]:\n        \"\"\"Return Pyramid friendly ACL list.\n\n        The ACL list is composed by the:\n\n          * Permission.access (Ex: 'Allow' or 'Deny')\n          * The Mixed in class name and the object name (Ex: 'User:eoyilmaz')\n          * The Action and the target class name (Ex: 'Create_Asset')\n\n        Thus, a list of tuple is returned as follows::\n\n          __acl__ = [\n              ('Allow', 'User:eoyilmaz', 'Create_Asset'),\n          ]\n\n        For the last example user eoyilmaz can grant access to views requiring\n        'Add_Project' permission.\n\n        Returns:\n            List[Tuple[str, str, str]]: A list of tuples containing the ACL .\n        \"\"\"\n        return [\n            (\n                perm.access,\n                f\"{self.__class__.__name__}:{self.name}\",\n                f\"{perm.action}_{perm.class_name}\",\n            )\n            for perm in self.permissions\n        ]\n\n\nclass CodeMixin(object):\n    \"\"\"Adds code info to the mixed in class.\n\n    .. versionadded:: 0.2.0\n\n      The code attribute of the SimpleEntity is now introduced as a separate\n      mixin. To let it be used by the classes it is really needed.\n\n    The CodeMixin just adds a new field called ``code``. It is a very simple attribute\n    and is used for simplifying long names (like Project.name etc.).\n\n    Contrary to previous implementations the code attribute is not formatted in any way,\n    so care needs to be taken if the code attribute is going to be used in filesystem as\n    file and directory names.\n\n    Args:\n        code (str): The code attribute is a string, cannot be empty or cannot be None.\n    \"\"\"\n\n    def __init__(\n        self,\n        code: Optional[str] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        logger.debug(f\"code: {code}\")\n        self.code = code\n\n    @declared_attr\n    def code(cls) -> Mapped[str]:\n        \"\"\"Create the code attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the code attribute.\n        \"\"\"\n        return mapped_column(\n            \"code\",\n            String(256),\n            nullable=False,\n            doc=\"\"\"The code name of this object.\n\n                It accepts strings. Cannot be None.\"\"\",\n        )\n\n    @validates(\"code\")\n    def _validate_code(self, key: str, code: str) -> str:\n        \"\"\"Validate the given code attribute.\n\n        Args:\n            key (str): The name of the validated column.\n            code (str): The code value to be validated.\n\n        Raises:\n            TypeError: If the given code is not a str.\n            ValueError: If the given code value is an empty str.\n\n        Returns:\n            str: The validated code value.\n        \"\"\"\n        logger.debug(f\"validating code value of: {code}\")\n        if code is None:\n            raise TypeError(f\"{self.__class__.__name__}.code cannot be None\")\n\n        if not isinstance(code, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.code should be a string, \"\n                f\"not {code.__class__.__name__}: '{code}'\"\n            )\n\n        if code == \"\":\n            raise ValueError(\n                f\"{self.__class__.__name__}.code cannot be an empty string\"\n            )\n\n        return code\n\n\nclass WorkingHoursMixin(object):\n    \"\"\"Set working hours for the mixed in class.\n\n    Generally is meaningful for users, departments and studio.\n\n    Args:\n        working_hours (WorkingHours): A :class:`.WorkingHours` instance showing the\n            working hours settings.\n    \"\"\"\n\n    def __init__(\n        self, working_hours: Optional[\"WorkingHours\"] = None, **kwargs: Dict[str, Any]\n    ) -> None:\n        self.working_hours = working_hours\n\n    @declared_attr\n    def working_hours_id(cls) -> Mapped[Optional[int]]:\n        \"\"\"Create the working_hours_id attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the working_hours_id attribute.\n        \"\"\"\n        return mapped_column(\"working_hours_id\", Integer, ForeignKey(\"WorkingHours.id\"))\n\n    @declared_attr\n    def working_hours(cls) -> Mapped[Optional[\"WorkingHours\"]]:\n        \"\"\"Create the working_hours attribute as a declared attribute.\n\n        Returns:\n            relationship: The relationship object related to the working_hours\n                attribute.\n        \"\"\"\n        return relationship(\n            \"WorkingHours\",\n            primaryjoin=f\"{cls.__name__}.working_hours_id==WorkingHours.working_hours_id\",\n        )\n\n    @validates(\"working_hours\")\n    def _validate_working_hours(\n        self, key, wh: Union[None, \"WorkingHours\"]\n    ) -> \"WorkingHours\":\n        \"\"\"Validate the given working hours value.\n\n        Args:\n            key (str): The name of the validated column.\n            wh (WorkingHours): The working hours value to be validated.\n\n        Raises:\n            TypeError: If the working hours is not None and not a WorkingHours instance.\n\n        Returns:\n            WorkingHours: The validated WorkingHours value.\n        \"\"\"\n        from stalker.models.studio import WorkingHours\n\n        if wh is None:\n            wh = WorkingHours()  # without any argument this will use the\n            # default.working_hours settings\n        elif not isinstance(wh, WorkingHours):\n            raise TypeError(\n                f\"{self.__class__.__name__}.working_hours should be a \"\n                \"stalker.models.studio.WorkingHours instance, \"\n                f\"not {wh.__class__.__name__}: '{wh}'\"\n            )\n\n        return wh\n\n\nclass ScheduleMixin(object):\n    \"\"\"Add schedule info to the mixed in class.\n\n    Add attributes like schedule_timing, schedule_unit and schedule_model\n    attributes to the mixed in class.\n\n    Use the ``__default_schedule_attr_name__`` attribute to customize the\n    column names.\n    \"\"\"\n\n    # some default values that can be overridden in Mixed in classes\n    __default_schedule_attr_name__ = \"schedule\"\n    __default_schedule_timing__ = defaults.timing_resolution.seconds / 60\n    __default_schedule_unit__ = TimeUnit.Hour\n    __default_schedule_model__ = ScheduleModel.Effort\n\n    def __init__(\n        self,\n        schedule_timing: Optional[float] = None,\n        schedule_unit: TimeUnit = TimeUnit.Hour,\n        schedule_model: Optional[ScheduleModel] = ScheduleModel.Effort,\n        schedule_constraint: ScheduleConstraint = ScheduleConstraint.NONE,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        self.schedule_constraint = schedule_constraint\n        self.schedule_model = schedule_model\n        self.schedule_timing = schedule_timing\n        self.schedule_unit = schedule_unit\n\n    @declared_attr\n    def schedule_timing(cls) -> Mapped[Optional[float]]:\n        \"\"\"Create the schedule_timing attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the schedule_timing attribute.\n        \"\"\"\n        return mapped_column(\n            f\"{cls.__default_schedule_attr_name__}_timing\",\n            Float,\n            nullable=True,\n            default=0,\n            doc=\"\"\"It is the value of the {attr} timing. It is a float value.\n\n            The timing value can either be as Work Time or Calendar Time\n            defined by the {attr}_model attribute. So when the {attr}_model\n            is `duration` then the value of this attribute is in Calendar Time,\n            and if the {attr}_model is either `length` or `effort` then the\n            value is considered as Work Time.\n            \"\"\".format(\n                attr=cls.__default_schedule_attr_name__\n            ),\n        )\n\n    @declared_attr\n    def schedule_unit(cls) -> Mapped[Optional[TimeUnit]]:\n        \"\"\"Create the schedule_unit attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the schedule_unit attribute.\n        \"\"\"\n        return mapped_column(\n            f\"{cls.__default_schedule_attr_name__}_unit\",\n            TimeUnitDecorator(),\n            nullable=True,\n            default=TimeUnit.Hour,\n            doc=f\"It is the unit of the {cls.__default_schedule_attr_name__} \"\n            \"timing. It is a TimeUnit enum value.\",\n        )\n\n    @declared_attr\n    def schedule_model(cls) -> Mapped[ScheduleModel]:\n        \"\"\"Create the schedule_model attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the schedule_model attribute.\n        \"\"\"\n        return mapped_column(\n            f\"{cls.__default_schedule_attr_name__}_model\",\n            ScheduleModelDecorator(),\n            default=ScheduleModel.Effort,\n            nullable=False,\n            doc=\"\"\"Defines the schedule model which is used by **TaskJuggler**\n            while scheduling this Projects. It is handled as a ScheduleModel\n            enum value which has three possible values; **effort**,\n            **duration**, **length**. :attr:`.ScheduleModel.Effort` is the\n            default value. Each value causes this task to be scheduled in\n            different ways:\n\n            ======== ==========================================================\n            effort   If the :attr:`.schedule_model` attribute is set to\n                     **\"effort\"** then the start and end date values are\n                     calculated so that a resource should spent this much of\n                     work time to complete a Task. For example, a task with\n                     :attr:`.schedule_timing` of 4 days, needs 4 working days.\n                     So it can take 4 working days to complete the Task, but it\n                     doesn't mean that the task duration will be 4 days. If the\n                     resource works overtime then the task will be finished\n                     before 4 days or if the resource will not be available\n                     (due to a vacation or task coinciding to a weekend day)\n                     then the task duration can be much more bigger than\n                     required effort.\n\n            duration The duration of the task will exactly be equal to\n                     :attr:`.schedule_timing` regardless of the resource\n                     availability. So the difference between :attr:`.start`\n                     and :attr:`.end` attribute values are equal to\n                     :attr:`.schedule_timing`. Essentially making the task\n                     duration in calendar days instead of working days.\n\n            length   In this model the duration of the task will exactly be\n                     equal to the given length value in working days regardless\n                     of the resource availability. So a task with the\n                     :attr:`.schedule_timing` is set to 4 days will be\n                     completed in 4 working days. But again it will not be\n                     always 4 calendar days due to the weekends or non working\n                     days.\n            ======== ==========================================================\n            \"\"\",\n        )\n\n    @declared_attr\n    def schedule_constraint(cls) -> Mapped[ScheduleConstraint]:\n        \"\"\"Create the schedule_constraint attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the schedule_constraint attribute.\n        \"\"\"\n        return mapped_column(\n            f\"{cls.__default_schedule_attr_name__}_constraint\",\n            ScheduleConstraintDecorator(),\n            default=0,\n            nullable=False,\n            doc=\"\"\"A ScheduleConstraint value showing the constraint schema\n            for this task.\n\n            Possible values are:\n\n             ===== ===============\n               0   Constrain None\n               1   Constrain Start\n               2   Constrain End\n               3   Constrain Both\n             ===== ===============\n\n            This value is going to be used to constrain the start and end date\n            values of this task. So if you want to pin the start of a task to a\n            certain date. Set its :attr:`.schedule_constraint` value to\n            :attr:`.ScheduleConstraint.Start`. When the task is scheduled by\n            **TaskJuggler** the start date will be pinned to the :attr:`start`\n            attribute of this task.\n\n            And if both of the date values (start and end) wanted to be pinned\n            to certain dates (making the task effectively a ``duration`` task)\n            set the desired :attr:`start` and :attr:`end` and then set the\n            :attr:`schedule_constraint` to :att:`.ScheduleConstraint.Both`.\n            \"\"\",\n        )\n\n    @validates(\"schedule_constraint\")\n    def _validate_schedule_constraint(\n        self,\n        key: str,\n        schedule_constraint: Union[None, int, str],\n    ) -> ScheduleConstraint:\n        \"\"\"Validate the given schedule_constraint value.\n\n        Args:\n            key (str): The name of the validated column.\n            schedule_constraint (Union[None, int, str]): The value to be\n                validated.\n\n        Returns:\n            ScheduleConstraint: The validated schedule_constraint value.\n        \"\"\"\n        if schedule_constraint is None:\n            schedule_constraint = ScheduleConstraint.NONE\n\n        schedule_constraint = ScheduleConstraint.to_constraint(schedule_constraint)\n\n        return schedule_constraint\n\n    @validates(\"schedule_model\")\n    def _validate_schedule_model(\n        self, key: str, schedule_model: Union[None, str, ScheduleModel]\n    ) -> ScheduleModel:\n        \"\"\"Validate the given schedule_model value.\n\n        Args:\n            key (str): The name of the validated column.\n            schedule_model (Union[None, str]): The schedule_model value to be\n                validated.\n\n        Returns:\n            ScheduleModel: The validated schedule_model value.\n        \"\"\"\n        if schedule_model is None:\n            schedule_model = self.__default_schedule_model__\n        else:\n            schedule_model = ScheduleModel.to_model(schedule_model)\n\n        return schedule_model\n\n    @validates(\"schedule_unit\")\n    def _validate_schedule_unit(\n        self, key: str, schedule_unit: Union[None, str, TimeUnit]\n    ) -> TimeUnit:\n        \"\"\"Validate the given schedule_unit.\n\n        Args:\n            key (str): The name of the validated column.\n            schedule_unit (Union[None, str, TimeUnit]): The schedule_unit value\n                to be validated.\n\n        Returns:\n            TimeUnit: The validated schedule_unit value.\n        \"\"\"\n        if schedule_unit is None:\n            schedule_unit = self.__default_schedule_unit__\n\n        schedule_unit = TimeUnit.to_unit(schedule_unit)\n\n        return schedule_unit\n\n    @validates(\"schedule_timing\")\n    def _validate_schedule_timing(\n        self,\n        key: str,\n        schedule_timing: Union[None, int, float],\n    ) -> float:\n        \"\"\"Validate the given schedule_timing.\n\n        Args:\n            key (str): The name of the validated column.\n            schedule_timing (Union[None, int, float]): The schedule_timing\n                value to be validated.\n\n        Raises:\n            TypeError: If the given schedule_timing is not an int or float.\n\n        Returns:\n            float: The validated schedule_timing value.\n        \"\"\"\n        if schedule_timing is None:\n            schedule_timing = self.__default_schedule_timing__\n            self.schedule_unit = self.__default_schedule_unit__\n\n        if not isinstance(schedule_timing, (int, float)):\n            raise TypeError(\n                \"{cls}.{attr}_timing should be an integer or float \"\n                \"number showing the value of the {attr} timing of this \"\n                \"{cls}, not {timing_class}: '{timing}'\".format(\n                    cls=self.__class__.__name__,\n                    attr=self.__default_schedule_attr_name__,\n                    timing_class=schedule_timing.__class__.__name__,\n                    timing=schedule_timing,\n                )\n            )\n\n        return schedule_timing\n\n    @classmethod\n    def least_meaningful_time_unit(\n        cls, seconds: int, as_work_time: bool = True\n    ) -> Tuple[int, TimeUnit]:\n        \"\"\"Return the least meaningful time unit that corresponds to the given seconds.\n\n        So if:\n\n          as_work_time == True\n              seconds % (1 year work time as seconds) == 0 --> 'y' else:\n              seconds % (1 month work time as seconds) == 0 --> 'm' else:\n              seconds % (1 week work time as seconds) == 0 --> 'w' else:\n              seconds % (1 day work time as seconds) == 0 --> 'd' else:\n              seconds % (1 hour work time as seconds) == 0 --> 'h' else:\n              seconds % (1 minute work time as seconds) == 0 --> 'min' else:\n              raise RuntimeError\n          as_work_time == False\n              seconds % (1 years as seconds) == 0 --> 'y' else:\n              seconds % (1 month as seconds) == 0 --> 'm' else:\n              seconds % (1 week as seconds) == 0 --> 'w' else:\n              seconds % (1 day as seconds) == 0 --> 'd' else:\n              seconds % (1 hour as seconds) == 0 --> 'h' else:\n              seconds % (1 minutes as seconds) == 0 --> 'min' else:\n              raise RuntimeError\n\n        Args:\n            seconds (int): An integer showing the total seconds to be\n                converted.\n            as_work_time (bool): Should the input be considered as work time or\n                calendar time.\n\n        Returns:\n            int, TimeUnit: Returns one integer and a TimeUnit enum value,\n                showing the timing value and the unit.\n        \"\"\"\n        minutes = 60\n        hour = 3600\n        day = 86400\n        week = 604800\n        month = 2419200\n        year = 31536000\n\n        day_wt = defaults.daily_working_hours * 3600\n        week_wt = defaults.weekly_working_days * day_wt\n        month_wt = 4 * week_wt\n        year_wt = int(defaults.yearly_working_days) * day_wt\n\n        if as_work_time:\n            logger.debug(\"calculating in work time\")\n            if seconds % year_wt == 0:  # noqa: S001\n                return seconds // year_wt, TimeUnit.Year\n            elif seconds % month_wt == 0:  # noqa: S001\n                return seconds // month_wt, TimeUnit.Month\n            elif seconds % week_wt == 0:  # noqa: S001\n                return seconds // week_wt, TimeUnit.Week\n            elif seconds % day_wt == 0:  # noqa: S001\n                return seconds // day_wt, TimeUnit.Day\n        else:\n            logger.debug(\"calculating in calendar time\")  # noqa: S001\n            if seconds % year == 0:  # noqa: S001\n                return seconds // year, TimeUnit.Year\n            elif seconds % month == 0:  # noqa: S001\n                return seconds // month, TimeUnit.Month\n            elif seconds % week == 0:  # noqa: S001\n                return seconds // week, TimeUnit.Week\n            elif seconds % day == 0:  # noqa: S001\n                return seconds // day, TimeUnit.Day\n\n        # in either case\n        if seconds % hour == 0:  # noqa: S001\n            return seconds // hour, TimeUnit.Hour\n\n        # at this point we understand that it has a residual of less then one\n        # minute so return in minutes\n        return seconds // minutes, TimeUnit.Minute\n\n    @classmethod\n    def to_seconds(\n        cls,\n        timing: float,\n        unit: Union[None, str, TimeUnit],\n        model: Union[str, ScheduleModel],\n    ) -> Union[None, float]:\n        \"\"\"Convert the schedule values to seconds.\n\n        Depending on to the schedule_model the value will differ. So if the\n        schedule_model is 'effort' or 'length' then the schedule_time and schedule_unit\n        values are interpreted as work time, if the schedule_model is 'duration' then\n        the schedule_time and schedule_unit values are considered as calendar time.\n\n        Args:\n            timing (float): The timing value.\n            unit (Union[None, str, TimeUnit]): The unit value, a TimeUnit enum\n                value or one of ['min', 'h', 'd', 'w', 'm', 'y', 'Minute',\n                'Hour', 'Day', 'Week', 'Month', 'Year'].\n            model (str): The schedule model, preferably a ScheduleModel enum\n                value or one of 'effort', 'length' or 'duration'.\n\n        Returns:\n            Union[None, float]: The converted seconds value.\n        \"\"\"\n        if not unit:\n            return None\n\n        unit = TimeUnit.to_unit(unit)\n\n        lut = {\n            TimeUnit.Minute: 60,\n            TimeUnit.Hour: 3600,\n            TimeUnit.Day: 86400,\n            TimeUnit.Week: 604800,\n            TimeUnit.Month: 2419200,\n            TimeUnit.Year: 31536000,\n        }\n\n        if model in [ScheduleModel.Effort, ScheduleModel.Length]:\n            day_wt = defaults.daily_working_hours * 3600\n            week_wt = defaults.weekly_working_days * day_wt\n            month_wt = 4 * week_wt\n            year_wt = int(defaults.yearly_working_days) * day_wt\n\n            lut = {\n                TimeUnit.Minute: 60,\n                TimeUnit.Hour: 3600,\n                TimeUnit.Day: day_wt,\n                TimeUnit.Week: week_wt,\n                TimeUnit.Month: month_wt,\n                TimeUnit.Year: year_wt,\n            }\n\n        return timing * lut[unit]\n\n    @classmethod\n    def to_unit(\n        cls,\n        seconds: int,\n        unit: Union[None, str, TimeUnit],\n        model: Union[str, ScheduleModel],\n    ) -> float:\n        \"\"\"Convert the ``seconds`` value to the given ``unit``.\n\n        Depending on to the ``schedule_model`` the value will differ. So if the\n        ``schedule_model`` is 'effort' or 'length' then the ``seconds`` and\n        ``schedule_unit`` values are interpreted as work time, if the\n        ``schedule_model`` is :attr:`ScheduleModel.Duration` then the\n        ``seconds`` and ``schedule_unit`` values are considered as calendar\n        time.\n\n        Args:\n            seconds (int): The seconds to convert.\n            unit (Union[None, str, TimeUnit]): The unit value, a TimeUnit enum\n                value one of ['min', 'h', 'd', 'w', 'm', 'y', 'Minute', 'Hour',\n                'Day', 'Week', 'Month', 'Year'] or a TimeUnit enum value.\n            model (Union[str, ScheduleModel]): The schedule model, either a\n                ScheduleModel enum value or one of 'effort', 'length' or\n                'duration'.\n\n        Returns:\n            float: The seconds converted to the given unit considering the\n                given model.\n        \"\"\"\n        if unit is None:\n            return None\n\n        unit = TimeUnit.to_unit(unit)\n        model = ScheduleModel.to_model(model)\n\n        lut = {\n            TimeUnit.Minute: 60,\n            TimeUnit.Hour: 3600,\n            TimeUnit.Day: 86400,\n            TimeUnit.Week: 604800,\n            TimeUnit.Month: 2419200,\n            TimeUnit.Year: 31536000,\n        }\n\n        if model in [ScheduleModel.Effort, ScheduleModel.Length]:\n            day_wt = defaults.daily_working_hours * 3600\n            week_wt = defaults.weekly_working_days * day_wt\n            month_wt = 4 * week_wt\n            year_wt = int(defaults.yearly_working_days) * day_wt\n\n            lut = {\n                TimeUnit.Minute: 60,\n                TimeUnit.Hour: 3600,\n                TimeUnit.Day: day_wt,\n                TimeUnit.Week: week_wt,\n                TimeUnit.Month: month_wt,\n                TimeUnit.Year: year_wt,\n            }\n\n        return seconds / lut[unit]\n\n    @property\n    def schedule_seconds(self) -> float:\n        \"\"\"Return the schedule values as seconds.\n\n        Depending on to the schedule_model the value will differ. So if the\n        schedule_model is 'effort' or 'length' then the schedule_time and schedule_unit\n        values are interpreted as work time, if the schedule_model is 'duration' then\n        the schedule_time and schedule_unit values are considered as calendar time.\n\n        Returns:\n            float: The schedule_seconds as seconds.\n        \"\"\"\n        return self.to_seconds(\n            self.schedule_timing, self.schedule_unit, self.schedule_model\n        )\n\n\nclass DAGMixin(object):\n    \"\"\"DAG mixin adds attributes required for parent/child relationship.\n\n    Create a parent/child or a directed acyclic graph (DAG) relation on the mixed in\n    class by introducing two new attributes called parent and children.\n\n    Please set the ``__id_column__`` attribute to the id column of the mixed in\n    class to be able to use this mixin::\n\n    .. code-block: python\n\n        class MixedInClass(SomeBaseClass, DAGMixin):\n\n            id : Mapped[int] = mapped_column('id', primary_key=True)\n            __id_column__ = id\n\n    Use the :attr:``.__dag_cascade__`` to control the cascade behavior.\n    \"\"\"\n\n    __dag_cascade__ = \"all, delete\"\n\n    @declared_attr\n    def parent_id(cls) -> Mapped[Optional[int]]:\n        \"\"\"Create the parent_id attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the parent_id attribute.\n        \"\"\"\n        return mapped_column(\n            \"parent_id\", Integer, ForeignKey(f\"{cls.__tablename__}.id\")\n        )\n\n    @declared_attr\n    def parent(cls) -> Mapped[Self]:\n        \"\"\"Create the parent attribute as a declared attribute.\n\n        Returns:\n            relationship: The relationship object related to the parent attribute.\n        \"\"\"\n        return relationship(\n            cls.__name__,\n            remote_side=[getattr(cls, cls.__id_column__)],\n            primaryjoin=\"{ct}.c.parent_id=={ct}.c.id\".format(ct=cls.__tablename__),\n            back_populates=\"children\",\n            post_update=True,\n            doc=\"\"\"A :class:`{c}` instance which is the parent of this {c}.\n            In Stalker it is possible to create a hierarchy of {c}.\n            \"\"\".format(\n                c=cls.__name__\n            ),\n        )\n\n    @declared_attr\n    def children(cls) -> Mapped[List[Self]]:\n        \"\"\"Create the children attribute as a declared attribute.\n\n        Returns:\n            relationship: The relationship object related to the parent attribute.\n        \"\"\"\n        return relationship(\n            cls.__name__,\n            primaryjoin=\"{ct}.c.id=={ct}.c.parent_id\".format(ct=cls.__tablename__),\n            back_populates=\"parent\",\n            post_update=True,\n            cascade=cls.__dag_cascade__,\n            doc=\"\"\"Other :class:`Budget` instances which are the children of this\n            one. This attribute along with the :attr:`.parent` attribute is used in\n            creating a DAG hierarchy of tasks.\n            \"\"\",\n        )\n\n    def __init__(self, parent: Optional[Self] = None, **kwargs: Dict[str, Any]) -> None:\n        self.parent = parent\n\n    @validates(\"parent\")\n    def _validate_parent(\n        self, key: str, parent: Union[None, Self]\n    ) -> Union[None, Self]:\n        \"\"\"Validate the given parent value.\n\n        Args:\n            key (str): The name of the validated column.\n            parent (object): The parent object to be validated.\n\n        Raises:\n            TypeError: If the parent is not None and not deriving from the same class\n                with this instance.\n\n        Returns:\n            Union[None, Self]: The validated parent value.\n        \"\"\"\n        if parent is None:\n            return parent\n\n        if not isinstance(parent, self.__class__):\n            raise TypeError(\n                \"{cls}.parent should be an instance of {cls} class or \"\n                \"derivative, not {parent_cls}: '{parent}'\".format(\n                    cls=self.__class__.__name__,\n                    parent_cls=parent.__class__.__name__,\n                    parent=parent,\n                )\n            )\n        check_circular_dependency(self, parent, \"children\")\n\n        return parent\n\n    @validates(\"children\")\n    def _validate_children(self, key: str, child: Self) -> Self:\n        \"\"\"Validate the given child.\n\n        Args:\n            key (str): The name of the validated column.\n            child (Self): The child value to be validated.\n\n        Raises:\n            TypeError: If any of the child objects are not deriving from the same class\n                as this one.\n\n        Returns:\n            Self: The validated child instance.\n        \"\"\"\n        if not isinstance(child, self.__class__):\n            raise TypeError(\n                \"{cls}.children should only contain instances of {cls} \"\n                \"(or derivative), not {child_cls}: '{child}'\".format(\n                    cls=self.__class__.__name__,\n                    child_cls=child.__class__.__name__,\n                    child=child,\n                )\n            )\n\n        return child\n\n    @property\n    def is_root(self) -> bool:\n        \"\"\"Return True if the Task has no parent.\n\n        Returns:\n            bool: True if the Task has no parent.\n        \"\"\"\n        return not bool(self.parent)\n\n    @property\n    def is_container(self) -> bool:\n        \"\"\"Return True if the Task has children Tasks.\n\n        Returns:\n            bool: True if the Task has children Tasks.\n        \"\"\"\n        with DBSession.no_autoflush:\n            return bool(len(self.children))\n\n    @property\n    def is_leaf(self) -> bool:\n        \"\"\"Return True if the Task has no children Tasks.\n\n        Returns:\n            bool: True if the Task has no children Tasks.\n        \"\"\"\n        return not self.is_container\n\n    @property\n    def parents(self) -> List[Self]:\n        \"\"\"Return all of the parents of this mixed in class starting from the root.\n\n        Returns:\n            List[Self]: List of tasks showing the parent of this Task.\n        \"\"\"\n        parents = []\n        entity = self.parent\n        # TODO: make this a generator\n        while entity:\n            parents.append(entity)\n            entity = entity.parent\n        parents.reverse()\n        return parents\n\n    def walk_hierarchy(\n        self,\n        method: Union[int, str, TraversalDirection] = TraversalDirection.DepthFirst,\n    ) -> Generator[None, Self, None]:\n        \"\"\"Walk the hierarchy of this task.\n\n        Args:\n            method (Union[int, str, TraversalDirection]): The walk method\n                defined by the :class:`.TraversalDirection` enum value. The\n                default is :attr:`.TraversalDirection.DepthFirst`.\n\n        Yields:\n            Task: The child Task.\n        \"\"\"\n        for c in walk_hierarchy(self, \"children\", method=method):\n            yield c\n\n\nclass AmountMixin(object):\n    \"\"\"Adds ``amount`` attribute to the mixed in class.\n\n    Args:\n        amount (Union[int, float]): The amount value.\n    \"\"\"\n\n    def __init__(self, amount: Union[int, float] = 0, **kwargs: Dict[str, Any]) -> None:\n        self.amount = amount\n\n    @declared_attr\n    def amount(cls) -> Mapped[Optional[float]]:\n        \"\"\"Create the amount attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the amount attribute.\n        \"\"\"\n        return mapped_column(Float, default=0.0)\n\n    @validates(\"amount\")\n    def _validate_amount(self, key: str, amount: Union[int, float]) -> float:\n        \"\"\"Validate the given amount value.\n\n        Args:\n            key (str): The name of the validated column.\n            amount (Union[int, float]): The amount value to be validated.\n\n        Raises:\n            TypeError: If the given amount value is not a int or float.\n\n        Returns:\n            float: The validated amount value.\n        \"\"\"\n        if amount is None:\n            amount = 0.0\n\n        if not isinstance(amount, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.amount should be a number, \"\n                f\"not {amount.__class__.__name__}: '{amount}'\"\n            )\n\n        return float(amount)\n\n\nclass UnitMixin(object):\n    \"\"\"Adds ``unit`` attribute to the mixed in class.\n\n    Args:\n        unit (str): The unit of this mixed in class.\n    \"\"\"\n\n    def __init__(self, unit: str = \"\", **kwargs: Dict[str, Any]) -> None:\n        self.unit = unit\n\n    @declared_attr\n    def unit(cls) -> Mapped[Optional[str]]:\n        \"\"\"Create the unit attribute as a declared attribute.\n\n        Returns:\n            Column: The Column related to the unit attribute.\n        \"\"\"\n        return mapped_column(String(64))\n\n    @validates(\"unit\")\n    def _validate_unit(self, key: str, unit: Union[None, str]) -> str:\n        \"\"\"Validate the given unit value.\n\n        Args:\n            key (str): The name of the validated column.\n            unit (str): The unit value to be validated.\n\n        Raises:\n            TypeError: If the given unit is not a str.\n\n        Returns:\n            str: The validated unit value.\n        \"\"\"\n        if unit is None:\n            unit = \"\"\n\n        if not isinstance(unit, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.unit should be a string, \"\n                f\"not {unit.__class__.__name__}: '{unit}'\"\n            )\n\n        return unit\n"
  },
  {
    "path": "src/stalker/models/note.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Note class lies here.\"\"\"\nfrom typing import Any, Dict, Optional\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column, synonym\n\nfrom stalker.log import get_logger\nfrom stalker.models.entity import SimpleEntity\n\nlogger = get_logger(__name__)\n\n\nclass Note(SimpleEntity):\n    \"\"\"Notes for any of the SOM objects.\n\n    To leave notes in Stalker use the Note class.\n\n    Args:\n        content (str): The content of the note.\n        attached_to (Entity): The object that this note is attached to.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"Notes\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Note\"}\n\n    note_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"SimpleEntities.id\"),\n        primary_key=True,\n    )\n\n    content: Mapped[Optional[str]] = synonym(\n        \"description\",\n        doc=\"\"\"The content of this :class:`.Note` instance.\n\n        Content is a string representing the content of this Note, can be an\n        empty.\n        \"\"\",\n    )\n\n    def __init__(self, content: str = \"\", **kwargs: Dict[str, Any]) -> None:\n        super(Note, self).__init__(**kwargs)\n        self.content = content\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a Note instance and has the same content.\n        \"\"\"\n        return (\n            super(Note, self).__eq__(other)\n            and isinstance(other, Note)\n            and self.content == other.content\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Note, self).__hash__()\n"
  },
  {
    "path": "src/stalker/models/project.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Project related classes and functions are situated here.\"\"\"\n\nfrom typing import Any, List, Optional, TYPE_CHECKING, Union\n\nfrom sqlalchemy import Float, ForeignKey\nfrom sqlalchemy.ext.associationproxy import association_proxy\nfrom sqlalchemy.ext.orderinglist import ordering_list\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, validates\n\nfrom stalker.db.declarative import Base\nfrom stalker.db.session import DBSession\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity\nfrom stalker.models.mixins import CodeMixin, DateRangeMixin, ReferenceMixin, StatusMixin\nfrom stalker.models.status import Status\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.asset import Asset\n    from stalker.models.auth import Role, User\n    from stalker.models.client import Client\n    from stalker.models.format import ImageFormat\n    from stalker.models.repository import Repository\n    from stalker.models.sequence import Sequence\n    from stalker.models.shot import Shot\n    from stalker.models.structure import Structure\n    from stalker.models.task import Task\n    from stalker.models.ticket import Ticket\n\nlogger = get_logger(__name__)\n\n\nclass ProjectRepository(Base):\n    \"\"\"The association object for Project to Repository instances.\"\"\"\n\n    __tablename__ = \"Project_Repositories\"\n\n    project_id: Mapped[int] = mapped_column(\n        ForeignKey(\"Projects.id\"),\n        primary_key=True,\n    )\n\n    project: Mapped[\"Project\"] = relationship(\n        back_populates=\"repositories_proxy\",\n        primaryjoin=\"Project.project_id==ProjectRepository.project_id\",\n    )\n\n    repository_id: Mapped[int] = mapped_column(\n        ForeignKey(\"Repositories.id\"),\n        primary_key=True,\n    )\n\n    repository: Mapped[\"Repository\"] = relationship(\n        primaryjoin=\"ProjectRepository.repository_id==Repository.repository_id\",\n    )\n\n    position: Mapped[Optional[int]] = mapped_column()\n\n    def __init__(\n        self,\n        project: Optional[\"Project\"] = None,\n        repository: Optional[\"Repository\"] = None,\n        position: Optional[int] = None,\n    ) -> None:\n        self.project = project\n        self.repository = repository\n        self.position = position\n\n    @validates(\"project\")\n    def _validate_project(self, key: str, project: \"Project\") -> \"Project\":\n        \"\"\"Validate the given project value.\n\n        Args:\n            key (str): The name of the validated column.\n            project (Project): The project value to be validated.\n\n        Returns:\n            project: The validated project value.\n        \"\"\"\n        # TODO: Why we are not validating the Project here?\n        #       Is it already validated somewhere else?\n        return project\n\n    @validates(\"repository\")\n    def _validate_repository(\n        self,\n        key: str,\n        repository: Union[None, \"Repository\"],\n    ) -> Union[None, \"Repository\"]:\n        \"\"\"Validate the given repository value.\n\n        Args:\n            key (str): The name of the validated column.\n            repository (Repository): The repository to be validated.\n\n        Raises:\n            TypeError: If the repository is not a Repository instance.\n\n        Returns:\n            Repository: The repository value.\n        \"\"\"\n        if repository is not None:\n            from stalker.models.repository import Repository\n\n            if not isinstance(repository, Repository):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.repositories should be a list of \"\n                    \"stalker.models.repository.Repository instances or \"\n                    f\"derivatives, not {repository.__class__.__name__}: '{repository}'\"\n                )\n\n        return repository\n\n\nclass Project(Entity, ReferenceMixin, StatusMixin, DateRangeMixin, CodeMixin):\n    \"\"\"All the information about a Project in Stalker is hold in this class.\n\n    Project is one of the main classes that will direct the others. A project\n    in Stalker is a gathering point.\n\n    It is mixed with :class:`.ReferenceMixin`, :class:`.StatusMixin`,\n    :class:`.DateRangeMixin` and :class:`.CodeMixin` to give reference, status,\n    schedule and code attribute. Please read the individual documentation of\n    each of the mixins.\n\n    **Project Users**\n\n    The :attr:`.Project.users` attribute lists the users in this project. UIs\n    like task creation for example will only list these users as available\n    resources for this project.\n\n    **TaskJuggler Integration**\n\n    Stalker uses TaskJuggler for scheduling the project tasks. The\n    :attr:`.Project.to_tjp` attribute generates a tjp compliant string which\n    includes the project definition, the tasks of the project, the resources in\n    the project including the vacation definitions and all the time logs\n    recorded for the project.\n\n    For custom attributes or directives that needs to be passed to TaskJuggler\n    you can use the :attr:`.Project.custom_tjp` attribute which will be\n    attached to the generated tjp file (inside the \"project\" directive).\n\n    To manage all the studio projects at once (schedule them at once please use\n    :class:`.Studio`).\n\n    **Repositories**\n\n    .. versionadded:: 0.2.13\n       Multiple Repositories per Project\n\n       Starting with v0.2.13 Project instances can have multiple Repositories,\n       which allows the project files to be placed in more than one repository\n       according to the need of the studio pipeline. One great advantage of\n       having multiple repositories is to be able to place Published versions\n       in to another repository which is placed on to a faster server.\n\n       Also, the :attr:`.repositories` attribute is not a read-only attribute\n       anymore.\n\n    **Clients**\n\n    .. versionadded:: 0.2.15\n       Multiple Clients per Project\n\n       It is now possible to attach multiple :class:`.Client` instances to one\n       :class:`.Project` allowing to hold complex Projects to Client relations\n       by using the :attr:`.ProjectClient.role` attribute of the\n       :class:`.ProjectClient` class.\n\n    **Deleting a Project**\n\n    Deleting a :class:`.Project` instance will cascade the delete operation to\n    all the :class:`.Task` s related to that particular Project and it will\n    cascade the delete operation to :class:`.TimeLog` s, :class:`.Version` s,\n    :class:`.File` s and :class:`.Review` s etc.. So one can delete a\n    :class:`.Project` instance without worrying about the non-project related\n    data like :class:`.User` s or :class:`.Department` s to be deleted.\n\n    Args:\n        clients (List[Client]): The clients which the project is affiliated with.\n            Default value is an empty list.\n\n        image_format (ImageFormat): The output image format of the project. Default\n            value is None.\n\n        fps (float): The FPS of the project, it should be a integer or float number, or\n            a string literal which can be correctly converted to a float. Default value\n            is 25.0.\n\n        type (Type): The type of the project. Default value is None.\n\n        structure (Structure): The structure of the project. Default value is None.\n\n        repositories (List[Repository]): A list of :class:`.Repository` instances that\n            the project files are going to be stored in. You cannot create a project\n            without specifying the repositories argument and passing a\n            :class:`.Repository` to it. Default value is None which raises a TypeError.\n\n        is_stereoscopic (bool): a bool value, showing if the project is going to be a\n            stereo 3D project, anything given as the argument will be converted to True\n            or False. Default value is False.\n\n        users (List[User]): A list of :class:`.User` s holding the users in this\n            project. This will create a reduced or grouped list of studio workers and\n            will make it easier to define the resources for a Task related to this\n            project. The default value is an empty list.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Projects\"\n    project_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    __mapper_args__ = {\n        \"polymorphic_identity\": \"Project\",\n        \"inherit_condition\": project_id == Entity.entity_id,\n    }\n\n    clients = association_proxy(\n        \"client_role\", \"client\", creator=lambda n: ProjectClient(client=n)\n    )\n\n    client_role: Mapped[Optional[List[\"ProjectClient\"]]] = relationship(\n        back_populates=\"project\",\n        cascade=\"all, delete-orphan\",\n        cascade_backrefs=False,\n        primaryjoin=\"Projects.c.id==Project_Clients.c.project_id\",\n    )\n\n    tasks: Mapped[Optional[List[\"Task\"]]] = relationship(\n        primaryjoin=\"Tasks.c.project_id==Projects.c.id\",\n        cascade=\"all, delete-orphan\",\n    )\n\n    users = association_proxy(\n        \"user_role\", \"user\", creator=lambda n: ProjectUser(user=n)\n    )\n\n    user_role: Mapped[Optional[List[\"ProjectUser\"]]] = relationship(\n        back_populates=\"project\",\n        cascade=\"all, delete-orphan\",\n        cascade_backrefs=False,\n        primaryjoin=\"Projects.c.id==Project_Users.c.project_id\",\n    )\n\n    repositories_proxy: Mapped[Optional[List[\"ProjectRepository\"]]] = relationship(\n        back_populates=\"project\",\n        cascade=\"all, delete-orphan\",\n        cascade_backrefs=False,\n        order_by=\"ProjectRepository.position\",\n        primaryjoin=\"Projects.c.id==Project_Repositories.c.project_id\",\n        collection_class=ordering_list(\"position\"),\n        doc=\"\"\"The :class:`.Repository` that this project files should reside.\n\n        Should be a list of :class:`.Repository` instances.\n        \"\"\",\n    )\n\n    repositories = association_proxy(\n        \"repositories_proxy\",\n        \"repository\",\n        creator=lambda n: ProjectRepository(repository=n),\n    )\n\n    structure_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Structures.id\"))\n    structure: Mapped[Optional[\"Structure\"]] = relationship(\n        primaryjoin=\"Project.structure_id==Structure.structure_id\",\n        doc=\"\"\"The structure of the project. Should be an instance of\n        :class:`.Structure` class\"\"\",\n    )\n\n    image_format_id: Mapped[Optional[int]] = mapped_column(\n        ForeignKey(\"ImageFormats.id\")\n    )\n    image_format: Mapped[Optional[\"ImageFormat\"]] = relationship(\n        primaryjoin=\"Projects.c.image_format_id==ImageFormats.c.id\",\n        doc=\"\"\"The :class:`.ImageFormat` of this project.\n\n        This value defines the output image format of the project, should be an\n        instance of :class:`.ImageFormat`.\n        \"\"\",\n    )\n\n    fps: Mapped[Optional[float]] = mapped_column(\n        Float(precision=3),\n        doc=\"\"\"The fps of the project.\n\n        It is a float value, any other types will be converted to float. The\n        default value is 25.0.\n        \"\"\",\n    )\n\n    is_stereoscopic: Mapped[Optional[bool]] = mapped_column(\n        doc=\"\"\"True if the project is a stereoscopic project\"\"\"\n    )\n\n    tickets: Mapped[Optional[List[\"Ticket\"]]] = relationship(\n        primaryjoin=\"Tickets.c.project_id==Projects.c.id\",\n        cascade=\"all, delete-orphan\",\n    )\n\n    def __init__(\n        self,\n        name: Optional[str] = None,\n        code: Optional[str] = None,\n        clients: Optional[List[\"Client\"]] = None,\n        repositories: Optional[List[\"Repository\"]] = None,\n        structure: Optional[\"Structure\"] = None,\n        image_format: Optional[\"ImageFormat\"] = None,\n        fps: float = 25.0,\n        is_stereoscopic: bool = False,\n        users: Optional[List[\"User\"]] = None,\n        **kwargs,\n    ) -> None:\n        # a projects project should be self\n        # initialize the project argument to self\n        kwargs[\"project\"] = self\n        kwargs[\"name\"] = name\n\n        super(Project, self).__init__(**kwargs)\n        # call the mixin __init__ methods\n        ReferenceMixin.__init__(self, **kwargs)\n        StatusMixin.__init__(self, **kwargs)\n        DateRangeMixin.__init__(self, **kwargs)\n\n        self.code = code\n\n        if users is None:\n            users = []\n        self.users = users\n\n        if repositories is None:\n            repositories = []\n        self.repositories = repositories\n\n        self.structure = structure\n\n        if clients is None:\n            clients = []\n        self.clients = clients\n\n        self._sequences = []\n        self._assets = []\n\n        self.image_format = image_format\n        self.fps = fps\n        self.is_stereoscopic = bool(is_stereoscopic)\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a Project and equal as an Entity.\n        \"\"\"\n        return super(Project, self).__eq__(other) and isinstance(other, Project)\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Project, self).__hash__()\n\n    @validates(\"fps\")\n    def _validate_fps(self, key: str, fps: Union[int, float]) -> float:\n        \"\"\"Validate the given fps value.\n\n        Args:\n            key (str): The name of the validated column.\n            fps (Union[int, float]): The fps value to be validated.\n\n        Raises:\n            TypeError: If the fps value is not an int or float.\n            ValueError: If the fps is 0 or a negative number.\n\n        Returns:\n            float: The validated fps value.\n        \"\"\"\n        if not isinstance(fps, (int, float)):\n            raise TypeError(\n                f\"{self.__class__.__name__}.fps should be a positive float or int, \"\n                f\"not {fps.__class__.__name__}: '{fps}'\"\n            )\n\n        fps = float(fps)\n        if fps <= 0:\n            raise ValueError(\n                f\"{self.__class__.__name__}.fps should be a positive float or int, \"\n                f\"not {fps}\"\n            )\n        return float(fps)\n\n    @validates(\"image_format\")\n    def _validate_image_format(\n        self, key: str, image_format: Union[None, \"ImageFormat\"]\n    ) -> Union[None, \"ImageFormat\"]:\n        \"\"\"Validate the given image format.\n\n        Args:\n            key (str): The name of the validated column.\n            image_format (Union[None, ImageFormat]): The image_format value to\n                be validated.\n\n        Raises:\n            TypeError: If the given image format is not a ImageFormat instance.\n\n        Returns:\n            Union[None, ImageFormat]: The validated image_format value.\n        \"\"\"\n        from stalker.models.format import ImageFormat\n\n        if image_format is not None and not isinstance(image_format, ImageFormat):\n            raise TypeError(\n                f\"{self.__class__.__name__}.image_format should be an instance of \"\n                \"stalker.models.format.ImageFormat, \"\n                f\"not {image_format.__class__.__name__}: '{image_format}'\"\n            )\n        return image_format\n\n    @validates(\"structure\")\n    def _validate_structure(\n        self,\n        key: str,\n        structure: Union[None, \"Structure\"],\n    ) -> Union[None, \"Structure\"]:\n        \"\"\"Validate the given structure value.\n\n        Args:\n            key (str): The name of the validated column.\n            structure (Structure): The structure to be validated.\n\n        Raises:\n            TypeError: If the given structure is not a Structure instance.\n\n        Returns:\n            Structure: The validated Structure value.\n        \"\"\"\n        from stalker.models.structure import Structure\n\n        if structure is not None and not isinstance(structure, Structure):\n            raise TypeError(\n                \"{}.structure should be an instance of \"\n                \"stalker.models.structure.Structure, not {}: '{}'\".format(\n                    self.__class__.__name__, structure.__class__.__name__, structure\n                )\n            )\n        return structure\n\n    @validates(\"is_stereoscopic\")\n    def _validate_is_stereoscopic(\n        self,\n        key: str,\n        is_stereoscopic: bool,\n    ) -> bool:\n        \"\"\"Validate the is_stereoscopic value.\n\n        Args:\n            key (str): The name of the validated column.\n            is_stereoscopic (bool): The is_stereoscopic value to be validated.\n\n        Returns:\n            bool: The bool representation of the is_stereoscopic value.\n        \"\"\"\n        return bool(is_stereoscopic)\n\n    @property\n    def root_tasks(self) -> List[\"Task\"]:\n        \"\"\"Return a list of Tasks which have no parents.\n\n        Returns:\n            List[Task]: The list of root :class:`Task`s in this project.\n        \"\"\"\n        from stalker.models.task import Task\n        from stalker.db.session import DBSession\n\n        # TODO: add a fallback method\n        with DBSession.no_autoflush:\n            return (\n                Task.query.filter(Task.project == self)\n                .filter(Task.parent == None)  # noqa: E711\n                .all()\n            )\n\n    @property\n    def assets(self) -> List[\"Asset\"]:\n        \"\"\"Return the assets in this project.\n\n        Returns:\n            List[Asset]: The list of :class:`Asset`s in this project.\n        \"\"\"\n        from stalker.models.asset import Asset\n        from stalker.db.session import DBSession\n\n        # TODO: add a fallback method\n        with DBSession.no_autoflush:\n            return Asset.query.filter(Asset.project == self).all()\n\n    @property\n    def sequences(self) -> List[\"Sequence\"]:\n        \"\"\"Return the sequences in this project.\n\n        Returns:\n            List[Sequence]: List of :class:`Sequence`s in this project.\n        \"\"\"\n        # sequences are tasks, use self.tasks\n        from stalker.models.sequence import Sequence\n\n        return Sequence.query.filter(Sequence.project == self).all()\n\n    @property\n    def shots(self) -> List[\"Shot\"]:\n        \"\"\"Return the shots in this project.\n\n        Returns:\n            List[Shot]: List of :class:`Shot`s in this project.\n        \"\"\"\n        # shots are tasks, use self.tasks\n        from stalker.models.shot import Shot\n\n        return Shot.query.filter(Shot.project == self).all()\n\n    @property\n    def to_tjp(self) -> str:\n        \"\"\"Return the TaskJuggler compatible representation of this project.\n\n        Returns:\n            str: The TaskJuggler compatible representation of this project.\n        \"\"\"\n        tab = \"    \"\n        indent = tab\n        tjp = f'task {self.tjp_id} \"{self.tjp_id}\" {{'\n        for task in self.root_tasks:\n            tjp += \"\\n\"\n            tjp += \"\\n\".join(f\"{indent}{line}\" for line in task.to_tjp.split(\"\\n\"))\n\n        tjp += \"\\n}\"\n        return tjp\n\n    @property\n    def is_active(self) -> bool:\n        \"\"\"Return True if this project is active, False otherwise.\n\n        This is a predicate for `Project.active` attribute.\n\n        Returns:\n            bool: True if the project is active, False otherwise.\n        \"\"\"\n        with DBSession.no_autoflush:\n            wip = Status.query.filter_by(code=\"WIP\").first()\n        return self.status == wip\n\n    @property\n    def total_logged_seconds(self) -> int:\n        \"\"\"Return the total TimeLog seconds recorded in child tasks.\n\n        Returns:\n            int: The total amount of logged seconds in the child tasks.\n        \"\"\"\n        total_logged_seconds = 0\n        for task in self.root_tasks:\n            total_logged_seconds += task.total_logged_seconds\n        logger.debug(f\"project.total_logged_seconds: {total_logged_seconds}\")\n        return total_logged_seconds\n\n    @property\n    def schedule_seconds(self) -> int:\n        \"\"\"Return the total amount of schedule timing of the child tasks in seconds.\n\n        Returns:\n            int: The total amount of schedule timing of the child tasks in seconds.\n        \"\"\"\n        schedule_seconds = 0\n        for task in self.root_tasks:\n            schedule_seconds += task.schedule_seconds\n        logger.debug(f\"project.schedule_seconds: {schedule_seconds}\")\n        return schedule_seconds\n\n    @property\n    def percent_complete(self) -> float:\n        \"\"\"Return the percent_complete value.\n\n        The percent_complete value is based on the total_logged_seconds and\n        schedule_seconds of the root tasks.\n\n        Returns:\n            float: The percent_complete value.\n        \"\"\"\n        total_logged_seconds = self.total_logged_seconds\n        schedule_seconds = self.schedule_seconds\n        if schedule_seconds > 0:\n            return total_logged_seconds / schedule_seconds * 100\n        else:\n            return 0\n\n    @property\n    def open_tickets(self) -> List[\"Ticket\"]:\n        \"\"\"Return the list of open :class:`.Ticket` s in this project.\n\n        Returns:\n             List[Ticket]: A list of :class:`.Ticket` instances which has a status of\n                `Open` and created in this project.\n        \"\"\"\n        from stalker import Ticket, Status\n\n        return (\n            Ticket.query.join(Status, Ticket.status)\n            .filter(Ticket.project == self)\n            .filter(Status.code != \"CLS\")\n            .all()\n        )\n\n    @property\n    def repository(self) -> \"Repository\":\n        \"\"\"Return the first repository in the `project.repositories` or None.\n\n        Compatibility attribute for pre v0.2.13 systems.\n\n        Returns:\n            Union[None, Repository]: The Repository instance if there are any or None.\n        \"\"\"\n        if self.repositories:\n            return self.repositories[0]\n        else:\n            return None\n\n\nclass ProjectUser(Base):\n    \"\"\"The association object used in User-to-Project relation.\"\"\"\n\n    __tablename__ = \"Project_Users\"\n\n    user_id: Mapped[int] = mapped_column(\n        \"user_id\", ForeignKey(\"Users.id\"), primary_key=True\n    )\n\n    user: Mapped[\"User\"] = relationship(\n        back_populates=\"project_role\",\n        cascade_backrefs=False,\n        primaryjoin=\"ProjectUser.user_id==User.user_id\",\n    )\n\n    project_id: Mapped[int] = mapped_column(ForeignKey(\"Projects.id\"), primary_key=True)\n\n    project: Mapped[Project] = relationship(\n        back_populates=\"user_role\",\n        cascade_backrefs=False,\n        primaryjoin=\"ProjectUser.project_id==Project.project_id\",\n    )\n\n    role_id: Mapped[Optional[int]] = mapped_column(\"rid\", ForeignKey(\"Roles.id\"))\n\n    role: Mapped[Optional[\"Role\"]] = relationship(\n        \"Role\", cascade_backrefs=False, primaryjoin=\"ProjectUser.role_id==Role.role_id\"\n    )\n\n    rate: Mapped[Optional[float]] = mapped_column(default=0.0)\n\n    def __init__(\n        self,\n        project: Optional[Project] = None,\n        user: Optional[\"User\"] = None,\n        role: Optional[\"Role\"] = None,\n    ) -> None:\n        self.user = user\n        self.project = project\n        self.role = role\n        if self.user:\n            # don't need to validate rate\n            # as it is already validated on the User side\n            self.rate = user.rate\n\n    @validates(\"user\")\n    def _validate_user(\n        self,\n        key: str,\n        user: Union[None, \"User\"],\n    ) -> Union[None, \"User\"]:\n        \"\"\"Validate the given user value.\n\n        Args:\n            key (str): The name of the validated column.\n            user (User): The user value to be validated.\n\n        Raises:\n            TypeError: If the given user is not a User instance.\n\n        Returns:\n            User: The validated user value.\n        \"\"\"\n        if user is not None:\n            from stalker.models.auth import User\n\n            if not isinstance(user, User):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.user should be a \"\n                    \"stalker.models.auth.User instance, \"\n                    f\"not {user.__class__.__name__}: '{user}'\"\n                )\n\n            # also update rate attribute\n            from stalker.db.session import DBSession\n\n            with DBSession.no_autoflush:\n                self.rate = user.rate\n\n        return user\n\n    @validates(\"project\")\n    def _validate_project(\n        self, key: str, project: Union[None, Project]\n    ) -> Union[None, Project]:\n        \"\"\"Validate the given project value.\n\n        Args:\n            key (str): The name of the validated column.\n            project (Union[None, Project]): The project value to be validated.\n\n        Raises:\n            TypeError: If the project is not a Project instance.\n\n        Returns:\n            Union[None, Project]: The validated project value.\n        \"\"\"\n        if project is not None:\n            # check if it is instance of Project object\n            if not isinstance(project, Project):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.project should be a \"\n                    \"stalker.models.project.Project instance, \"\n                    f\"not {project.__class__.__name__}: '{project}'\"\n                )\n        return project\n\n    @validates(\"role\")\n    def _validate_role(self, key: str, role: Union[None, \"Role\"]):\n        \"\"\"Validate the given role instance.\n\n        Args:\n            key (str): The name of the validated column.\n            role (Union[None, \"Role\"]): The role value to be validated.\n\n        Raises:\n            TypeError: If the given role is not a Role instance.\n\n        Returns:\n            Union[None, \"Role\"]: The validated role value.\n        \"\"\"\n        if role is None:\n            return role\n\n        from stalker import Role\n\n        if not isinstance(role, Role):\n            raise TypeError(\n                f\"{self.__class__.__name__}.role should be a \"\n                \"stalker.models.auth.Role instance, \"\n                f\"not {role.__class__.__name__}: '{role}'\"\n            )\n\n        return role\n\n\nclass ProjectClient(Base):\n    \"\"\"The association object used in Client-to-Project relation.\n\n    Args:\n        project (Project): The project.\n        client (Client): The client.\n        role (Role): The client role in this project.\n    \"\"\"\n\n    __tablename__ = \"Project_Clients\"\n\n    client_id: Mapped[int] = mapped_column(ForeignKey(\"Clients.id\"), primary_key=True)\n\n    client: Mapped[\"Client\"] = relationship(\n        back_populates=\"project_role\",\n        cascade_backrefs=False,\n        primaryjoin=\"Project_Clients.c.client_id==Clients.c.id\",\n    )\n\n    project_id: Mapped[int] = mapped_column(ForeignKey(\"Projects.id\"), primary_key=True)\n\n    project: Mapped[Project] = relationship(\n        back_populates=\"client_role\",\n        cascade_backrefs=False,\n        primaryjoin=\"ProjectClient.project_id==Project.project_id\",\n    )\n\n    role_id: Mapped[Optional[int]] = mapped_column(\n        \"rid\", ForeignKey(\"Roles.id\"), nullable=True\n    )\n\n    role: Mapped[Optional[\"Role\"]] = relationship(\n        cascade_backrefs=False,\n        primaryjoin=\"ProjectClient.role_id==Role.role_id\",\n    )\n\n    def __init__(\n        self,\n        project: Optional[Project] = None,\n        client: Optional[\"Client\"] = None,\n        role: Optional[\"Role\"] = None,\n    ) -> None:\n        self.client = client\n        self.project = project\n        self.role = role\n\n    @validates(\"client\")\n    def _validate_client(\n        self,\n        key: str,\n        client: Union[None, \"Client\"],\n    ) -> Union[None, \"Client\"]:\n        \"\"\"Validate the given client value.\n\n        Args:\n            key (str): The name of the validated column.\n            client (Union[None, Client]): The client value to be validated.\n\n        Raises:\n            TypeError: If the given client arg value is not a Client instance.\n\n        Returns:\n            Client: The validated client value.\n        \"\"\"\n        if client is None:\n            return client\n\n        from stalker.models.client import Client\n\n        if not isinstance(client, Client):\n            raise TypeError(\n                f\"{self.__class__.__name__}.client should be an instance of \"\n                \"stalker.models.auth.Client, \"\n                f\"not {client.__class__.__name__}: '{client}'\"\n            )\n\n        return client\n\n    @validates(\"project\")\n    def _validate_project(\n        self, key: str, project: Union[None, Project]\n    ) -> Union[None, Project]:\n        \"\"\"Validate the given project value.\n\n        Args:\n            key (str): The name of the validated column.\n            project (Project): The project value to be validated.\n\n        Raises:\n            TypeError: If the given project value is not a Project instance.\n\n        Returns:\n            Project: The validated project value.\n        \"\"\"\n        if project is None:\n            return project\n\n        # check if it is instance of Project object\n        if not isinstance(project, Project):\n            raise TypeError(\n                f\"{self.__class__.__name__}.project should be a \"\n                \"stalker.models.project.Project instance, \"\n                f\"not {project.__class__.__name__}: '{project}'\"\n            )\n\n        return project\n\n    @validates(\"role\")\n    def _validate_role(\n        self,\n        key: str,\n        role: Union[None, \"Role\"],\n    ) -> Union[None, \"Role\"]:\n        \"\"\"Validate the given role instance.\n\n        Args:\n            key (str): The name of the validated column.\n            role (Union[None, Role]): The role value to be validated.\n\n        Raises:\n            TypeError: If the given role value is not a Role instance.\n\n        Returns:\n            Union[None, Role]: The validated role value.\n        \"\"\"\n        if role is None:\n            return role\n\n        from stalker import Role\n\n        if not isinstance(role, Role):\n            raise TypeError(\n                f\"{self.__class__.__name__}.role should be a \"\n                \"stalker.models.auth.Role instance, \"\n                f\"not {role.__class__.__name__}: '{role}'\"\n            )\n\n        return role\n\n\ndef create_project_client(project: Project) -> ProjectClient:\n    \"\"\"Create ProjectClient instance on association proxy.\n\n    Args:\n        project (Project): The :class:`.Project` instance to be used to create the\n            :class:`.ProjectClient` instance.\n\n    Returns:\n        ProjectClient: The :class:`.ProjectClient` instance.\n    \"\"\"\n    return ProjectClient(project=project)\n"
  },
  {
    "path": "src/stalker/models/repository.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Repository related functionality is situated here.\"\"\"\nimport os\nimport platform\nfrom typing import Any, Dict, Optional, TYPE_CHECKING\n\nfrom sqlalchemy import ForeignKey, String, event\nfrom sqlalchemy.orm import Mapped, mapped_column, validates\n\nfrom stalker import defaults\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity\nfrom stalker.models.mixins import CodeMixin\n\nif TYPE_CHECKING:  # pragma: no cover\n    from sqlalchemy.orm import Mapper\n    from sqlalchemy.engine import Connection\n\nlogger = get_logger(__name__)\n\n\nclass Repository(Entity, CodeMixin):\n    r\"\"\"Manage fileserver/repository related data.\n\n    A repository is a network share that all users have access to.\n\n    A studio can create several repositories, for example, one for movie\n    projects and one for commercial projects.\n\n    A repository also defines the default paths for linux, windows and mac\n    foreshores.\n\n    The path separator in the repository is always forward slashes (\"/\").\n    Setting a path that contains backward slashes (\"\\\"), will be converted to\n    a path with forward slashes.\n\n    .. versionadded:: 0.2.24\n       Code attribute\n\n       Starting with v0.2.24 Repository instances have a new :attr:`.code`\n       attribute whose value is used by the\n       :class:`stalker.models.studio.Studio` to generate environment variables\n       that contains the path of this\n       :class:`stalker.models.repository.Repository` (i.e.\n       $REPOCP/path/to/asset.ma ``CP`` here is the ``Repository.code``) so that\n       instead of using absolute full paths one can use the\n       :attr:`.make_relative`` path to generate a universal path that can be\n       used across OSes and different installations of Stalker.\n\n    Args:\n        code (str): The code of the :class:`stalker.models.repository.Repository`.\n            This attribute value is used by the :class:`stalker.models.studio.Studio`\n            to generate environment variables that contains the path of this\n            ``Repository`` (i.e. $REPOCP/path/to/asset.ma) so that instead of\n            using absolute full paths one can use the ``repository_relative``\n            path to generate a universal path that can be used across OSes and\n            different installations of Stalker.\n        linux_path (str): shows the linux path of the repository root, should be\n            a string\n        macos_path (str): shows the macOS path of the repository root, should be\n            a string.\n        windows_path (str): shows the windows path of the repository root, should\n            be a string\n    \"\"\"\n\n    #\n    # TODO: Add OpenLDAP support.\n    #\n    # In an OpenLDAP Server + AutoFS setup Stalker can create new entries to\n    # OpenLDAP server.\n    #\n    # The AutoFS can be installed to any linux system easily or it is already\n    # installed. macOS has it already. I know nothing about Windows.\n    #\n    # AutoFS can be setup to listen for new mount points from an OpenLDAP\n    # server. Thus it is heavily related with the users system, Stalker\n    # cannot do anything about that. The IT should setup workstations.\n    #\n    # But Stalker can connect to the OpenLDAP server and create new entries.\n    #\n\n    __auto_name__ = False\n    __tablename__ = \"Repositories\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Repository\"}\n    repository_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n    linux_path: Mapped[Optional[str]] = mapped_column(String(256))\n    windows_path: Mapped[Optional[str]] = mapped_column(String(256))\n    macos_path: Mapped[Optional[str]] = mapped_column(String(256))\n\n    def __init__(\n        self,\n        code: str = \"\",\n        linux_path: str = \"\",\n        windows_path: str = \"\",\n        macos_path: str = \"\",\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        kwargs[\"code\"] = code\n        super(Repository, self).__init__(**kwargs)\n        CodeMixin.__init__(self, **kwargs)\n\n        self.linux_path = linux_path\n        self.windows_path = windows_path\n        self.macos_path = macos_path\n\n    @validates(\"linux_path\")\n    def _validate_linux_path(self, key: str, linux_path: str) -> str:\n        \"\"\"Validate the given Linux path.\n\n        Args:\n            key (str): The name of the validated column.\n            linux_path (str): The Linux path to validated.\n\n        Raises:\n            TypeError: If the given Linux path is not a str.\n\n        Returns:\n            str: The validated Linux path.\n        \"\"\"\n        if not isinstance(linux_path, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.linux_path should be an instance of \"\n                f\"string, not {linux_path.__class__.__name__}: '{linux_path}'\"\n            )\n\n        linux_path = os.path.normpath(linux_path) + \"/\"\n\n        linux_path = linux_path.replace(\"\\\\\", \"/\")\n\n        if self.code is not None and platform.system() == \"Linux\":\n            # update the environment variable\n            os.environ[defaults.repo_env_var_template.format(code=self.code)] = (\n                linux_path\n            )\n\n        return linux_path\n\n    @validates(\"macos_path\")\n    def _validate_macos_path(self, key: str, macos_path: str) -> str:\n        \"\"\"Validate the given macOS path.\n\n        Args:\n            key (str): The name of the validated column.\n            macos_path (str): The macOS path to validate.\n\n        Raises:\n            TypeError: If the given macOS path is not a str.\n\n        Returns:\n            str: The validated macOS path.\n        \"\"\"\n        if not isinstance(macos_path, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.macos_path should be an instance of \"\n                f\"string, not {macos_path.__class__.__name__}: '{macos_path}'\"\n            )\n\n        macos_path = os.path.normpath(macos_path) + \"/\"\n        macos_path = macos_path.replace(\"\\\\\", \"/\")\n        if self.code is not None and platform.system() == \"Darwin\":\n            # update the environment variable\n            rendered_env_var = defaults.repo_env_var_template.format(code=self.code)\n            os.environ[rendered_env_var] = macos_path\n\n        return macos_path\n\n    @validates(\"windows_path\")\n    def _validate_windows_path(self, key: str, windows_path: str) -> str:\n        \"\"\"Validate the given Windows path.\n\n        Args:\n            key (str): The name of the validated column.\n            windows_path (str): The Windows path to validate.\n\n        Raises:\n            TypeError: If the given Windows path is not a str.\n\n        Returns:\n            str: The validated Windows path.\n        \"\"\"\n        if not isinstance(windows_path, str):\n            raise TypeError(\n                f\"{self.__class__.__name__}.windows_path should be an instance of \"\n                f\"string, not {windows_path.__class__.__name__}: '{windows_path}'\"\n            )\n\n        windows_path = os.path.normpath(windows_path)\n        windows_path = windows_path.replace(\"\\\\\", \"/\")\n\n        if not windows_path.endswith(\"/\"):\n            windows_path += \"/\"\n\n        if self.code is not None and platform.system() == \"Windows\":\n            # update the environment variable\n            os.environ[defaults.repo_env_var_template.format(code=self.code)] = (\n                windows_path\n            )\n\n        return windows_path\n\n    @property\n    def path(self) -> str:\n        \"\"\"Return the repository path for the current OS.\n\n        Returns:\n            str: The repository path for the current OS.\n        \"\"\"\n        # return the proper value according to the current os\n        platform_system = platform.system()\n\n        if platform_system == \"Linux\":\n            return self.linux_path\n        elif platform_system == \"Windows\":\n            return self.windows_path\n        elif platform_system == \"Darwin\":\n            return self.macos_path\n\n    @path.setter\n    def path(self, path: str) -> None:\n        \"\"\"Set the path for the current OS.\n\n        Args:\n            path (str): The path.\n        \"\"\"\n        # return the proper value according to the current os\n        platform_system = platform.system()\n\n        if platform_system == \"Linux\":\n            self.linux_path = path\n        elif platform_system == \"Windows\":\n            self.windows_path = path\n        elif platform_system == \"Darwin\":\n            self.macos_path = path\n\n    def is_in_repo(self, path: str) -> bool:\n        \"\"\"Return True or False depending on the given is in this repo or not.\n\n        Args:\n            path: The path to be investigated.\n\n        Returns:\n            bool: Return True if the given path is in this repository.\n        \"\"\"\n        path = path.replace(\"\\\\\", \"/\")\n        return (\n            path.lower().startswith(self.windows_path.lower())\n            or path.startswith(self.linux_path)\n            or path.startswith(self.macos_path)\n        )\n\n    def _to_path(self, path: str, replace_with: str) -> str:\n        \"\"\"Return the path replacing the OS related part with the given str.\n\n        Args:\n            path (str): The input path.\n            replace_with (str): replace_with path\n\n        Raises:\n            TypeError: When the given path is not a str.\n\n        Returns:\n            str: The converted path.\n        \"\"\"\n        if not isinstance(path, str):\n            raise TypeError(\n                \"path should be a string containing a file path, \"\n                f\"not {path.__class__.__name__}: '{path}'\"\n            )\n\n        if not isinstance(replace_with, str):\n            raise TypeError(\n                \"replace_with should be a string containing a file path, \"\n                f\"not {replace_with.__class__.__name__}: '{replace_with}'\"\n            )\n\n        # expand all variables\n        path = os.path.normpath(os.path.expandvars(os.path.expanduser(path))).replace(\n            \"\\\\\", \"/\"\n        )\n\n        if path.startswith(self.windows_path):\n            return path.replace(self.windows_path, replace_with)\n        elif path.startswith(self.linux_path):\n            return path.replace(self.linux_path, replace_with)\n        elif path.startswith(self.macos_path):\n            return path.replace(self.macos_path, replace_with)\n\n        return path\n\n    def to_linux_path(self, path: str) -> str:\n        \"\"\"Return the Linux version of the given path.\n\n        Args:\n            path (str): The path that needs to be converted to Linux path.\n\n        Returns:\n            str: The Linux path.\n        \"\"\"\n        return self._to_path(path, self.linux_path)\n\n    def to_windows_path(self, path: str) -> str:\n        \"\"\"Return the Windows version of the given path.\n\n        Args:\n            path (str): The path that needs to be converted to windows path.\n\n        Returns:\n            str: The Windows path.\n        \"\"\"\n        return self._to_path(path, self.windows_path)\n\n    def to_macos_path(self, path: str) -> str:\n        \"\"\"Return the macOS version of the given path.\n\n        Args:\n            path (str): The path that needs to be converted to macOS path.\n\n        Returns:\n            str: The macOS path.\n        \"\"\"\n        return self._to_path(path, self.macos_path)\n\n    def to_native_path(self, path: str) -> str:\n        \"\"\"Return the native version of the given path.\n\n        Args:\n            path (str): The path that needs to be converted to native path.\n\n        Returns:\n            str: The native path.\n        \"\"\"\n        return self._to_path(path, self.path)\n\n    def make_relative(self, path: str) -> str:\n        \"\"\"Make the given path relative to the repository root.\n\n        Args:\n            path (str): The path to be made relative.\n\n        Returns:\n            str: The relative path.\n        \"\"\"\n        path = self.to_native_path(path)\n        return os.path.relpath(path, self.path).replace(\"\\\\\", \"/\")\n\n    @classmethod\n    def find_repo(cls, path: str) -> \"Repository\":\n        \"\"\"Return the repository from the given path.\n\n        Args:\n            path (str): Path in a repository.\n\n        Returns:\n            Repository:\n        \"\"\"\n        logger.debug(f\"Looking for a repo for path: {path}\")\n        # path could be using environment variables so expand them\n        path = os.path.expandvars(path)\n        logger.debug(f\"path after expanding vars  : {path}\")\n\n        # first find the repository\n        repos = Repository.query.all()\n        found_repo = None\n        for repo in repos:\n            if (\n                path.startswith(repo.path)\n                or path.lower().startswith(repo.windows_path.lower())\n                or path.startswith(repo.linux_path)\n                or path.startswith(repo.macos_path)\n            ):\n                found_repo = repo\n                break\n\n        if found_repo is None:\n            logger.debug(f\"Couldn't find a repo for path: {path}\")\n\n        return found_repo\n\n    @classmethod\n    def to_os_independent_path(cls, path: str) -> str:\n        \"\"\"Replace the part of the given path with repository environment var.\n\n         This makes the given path OS independent.\n\n        Args:\n            path (str): path to make OS independent.\n\n        Returns:\n            str: OS independent path.\n        \"\"\"\n        # find the related repo\n        repo = cls.find_repo(path)\n\n        if repo:\n            logger.debug(\"Found repo for path: {}\".format(repo))\n            return \"${}/{}\".format(repo.env_var, repo.make_relative(path))\n        else:\n            logger.debug(\"Can't find repo for path: {}\".format(path))\n            return path\n\n    @property\n    def env_var(self) -> str:\n        \"\"\"Return the env var of this repo.\n\n        Returns:\n            str: The env_var corresponding to this repo.\n        \"\"\"\n        return defaults.repo_env_var_template.format(code=self.code)\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is equal to this one as an Entity, is a\n                Repository instance and has the same linux_path, macos_path,\n                windows_path.\n        \"\"\"\n        return (\n            super(Repository, self).__eq__(other)\n            and isinstance(other, Repository)\n            and self.linux_path == other.linux_path\n            and self.macos_path == other.macos_path\n            and self.windows_path == other.windows_path\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Repository, self).__hash__()\n\n\n@event.listens_for(Repository, \"after_insert\")\ndef receive_after_insert(\n    mapper: \"Mapper\",\n    connection: \"Connection\",\n    repo: \"Repository\",\n) -> None:\n    \"\"\"Listen for the 'after_insert' event and update environment variables.\n\n    This is a mapper event to update the environment variables with the newly inserted\n    Repository data.\n\n    Args:\n        mapper (sqlalchemy.orm.Mapper): The mapper object.\n        connection (sqlalchemy.engine.Connection): The connection object.\n        repo (Repository): The Repository instance that is just inserted to the DB.\n    \"\"\"\n    logger.debug(\"auto creating env var for Repository: {}\".format(repo.name))\n    os.environ[defaults.repo_env_var_template.format(code=repo.code)] = repo.path\n"
  },
  {
    "path": "src/stalker/models/review.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Review related classes and functions are situated here.\"\"\"\n\nfrom typing import Any, Dict, List, Optional, TYPE_CHECKING, Union\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.ext.associationproxy import association_proxy\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, synonym, validates\n\nfrom stalker.db.declarative import Base\nfrom stalker.db.session import DBSession\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity, SimpleEntity\nfrom stalker.models.enum import DependencyTarget, TimeUnit, TraversalDirection\nfrom stalker.models.file import File\nfrom stalker.models.mixins import (\n    ProjectMixin,\n    ScheduleMixin,\n    StatusMixin,\n)\nfrom stalker.models.status import Status\nfrom stalker.utils import walk_hierarchy\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.auth import User\n    from stalker.models.task import Task\n    from stalker.models.version import Version\n\nlogger = get_logger(__name__)\n\n\nclass Review(SimpleEntity, ScheduleMixin, StatusMixin):\n    \"\"\"Manages the Task Review Workflow.\n\n    This class represents a very important part of the review workflow. For\n    more information about the workflow please read the documentation about the\n    `Stalker Task Review Workflow`_.\n\n    .. _`Stalker Task Review Workflow`: task_review_workflow_top_level\n\n    According to the workflow, Review instances holds information about what\n    have the responsible of the task requested about the task when the resource\n    requested a review from the responsible.\n\n    Each Review instance with the same :attr:`.review_number` for a\n    :class:`.Task` represents a set of reviews.\n\n    .. version-added:: 1.0.0\n\n      Review -> Version relation\n\n      Versions can now be attached to reviews.\n\n    Review instances, alongside the :class:`.Task` can also optionally hold a\n    :class:`.Version` instance. This allows the information of which\n    :class:`.Version` instance has been reviewed as a part of the review\n    process to be much cleaner, and when the Review history is investigated,\n    it will be much easier to identify which :class:`.Version` the review was\n    about.\n\n    Args:\n        task (Task): A :class:`.Task` instance that this review is related to.\n            It can be skipped if a :class:`.Version` instance has been given.\n\n        version (Version): A :class:`.Version` instance that this review\n            instance is related to. The :class:`.Version` and the\n            :class:`.Task` should be related, a ``ValueError`` will be raised\n            if they are not.\n\n        review_number (int): This number represents the revision set id that\n            this Review instance belongs to.\n\n        reviewer (User): One of the responsible of the related Task. There will\n            be only one Review instances with the same review_number for every\n            responsible of the same Task.\n\n        schedule_timing (int): Holds the timing value of this review. It is a\n            float value. Only useful if it is a review which ends up requesting\n            a revision.\n\n        schedule_unit (Union[str, TimeUnit]): Holds the timing unit of this\n            review. Only useful if it is a review which ends up requesting a\n            revision.\n\n        schedule_model (str): It holds the schedule model of this review. Only\n            useful if it is a review which ends up requesting a revision.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"Reviews\"\n    __table_args__ = {\"extend_existing\": True}\n\n    __mapper_args__ = {\"polymorphic_identity\": \"Review\"}\n\n    review_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    task_id: Mapped[int] = mapped_column(\n        ForeignKey(\"Tasks.id\"),\n        nullable=False,\n        doc=\"The id of the related task.\",\n    )\n\n    task: Mapped[\"Task\"] = relationship(\n        primaryjoin=\"Reviews.c.task_id==Tasks.c.id\",\n        uselist=False,\n        back_populates=\"reviews\",\n        doc=\"The :class:`.Task` instance that this Review is created for\",\n    )\n\n    version_id: Mapped[Optional[int]] = mapped_column(\n        \"version_id\", ForeignKey(\"Versions.id\")\n    )\n\n    version: Mapped[Optional[\"Version\"]] = relationship(\n        primaryjoin=\"Reviews.c.version_id==Versions.c.id\",\n        uselist=False,\n        back_populates=\"reviews\",\n    )\n\n    reviewer_id: Mapped[int] = mapped_column(\n        ForeignKey(\"Users.id\"),\n        nullable=False,\n        doc=\"The User which does the review, also on of the responsible of \"\n        \"the related Task\",\n    )\n\n    reviewer: Mapped[\"User\"] = relationship(\n        primaryjoin=\"Reviews.c.reviewer_id==Users.c.id\"\n    )\n\n    _review_number: Mapped[Optional[int]] = mapped_column(\"review_number\", default=1)\n\n    def __init__(\n        self,\n        task: Optional[\"Task\"] = None,\n        version: Optional[\"Version\"] = None,\n        reviewer: Optional[\"User\"] = None,\n        description: str = \"\",\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        kwargs[\"description\"] = description\n        SimpleEntity.__init__(self, **kwargs)\n        ScheduleMixin.__init__(self, **kwargs)\n        StatusMixin.__init__(self, **kwargs)\n\n        self.task = task\n        self.version = version\n        self.reviewer = reviewer\n\n        # set the status to NEW\n        with DBSession.no_autoflush:\n            new = Status.query.filter_by(code=\"NEW\").first()\n        self.status = new\n\n        # set the review_number\n        self._review_number = self.task.review_number + 1\n\n    @validates(\"task\")\n    def _validate_task(\n        self, key: str, task: Union[None, \"Task\"]\n    ) -> Union[None, \"Task\"]:\n        \"\"\"Validate the given task value.\n\n        Args:\n            key (str): The name of the validated column.\n            task (Union[None, Task]): The task value to be validated.\n\n        Raises:\n            TypeError: If the given task value is not a Task instance.\n            ValueError: If the given task is not a leaf task.\n\n        Returns:\n            Union[None, Task]: The validated Task instance.\n        \"\"\"\n        if task is None:\n            return task\n\n        from stalker.models.task import Task\n\n        if not isinstance(task, Task):\n            raise TypeError(\n                f\"{self.__class__.__name__}.task should be an instance of \"\n                f\"stalker.models.task.Task, not {task.__class__.__name__}: '{task}'\"\n            )\n\n        # is it a leaf task\n        if not task.is_leaf:\n            raise ValueError(\n                \"It is only possible to create a review for a leaf tasks, \"\n                f\"and {task} is not a leaf task.\"\n            )\n\n        # set the review_number of this review instance\n        self._review_number = task.review_number + 1\n\n        return task\n\n    @validates(\"version\")\n    def _validate_version(\n        self, key: str, version: Union[None, \"Version\"]\n    ) -> Union[None, \"Version\"]:\n        \"\"\"Validate the given version value.\n\n        Args:\n            key (str): The name of the validated column.\n            version (Union[None, Version]): The version value to be validated.\n\n        Raises:\n            TypeError: If version is not a Version instance.\n            ValueError: If the version.task and the self.task is not matching.\n\n        Returns:\n            Union[None, Version]: The validated version value.\n        \"\"\"\n        if version is None:\n            return version\n\n        from stalker.models.version import Version\n\n        if not isinstance(version, Version):\n            raise TypeError(\n                f\"{self.__class__.__name__}.version should be a Version \"\n                f\"instance, not {version.__class__.__name__}: '{version}'\"\n            )\n\n        if self.task is not None:\n            if version.task != self.task:\n                raise ValueError(\n                    f\"{self.__class__.__name__}.version should be a Version \"\n                    f\"instance related to this Task: {version}\"\n                )\n        else:\n            self.task = version.task\n\n        return version\n\n    @validates(\"reviewer\")\n    def _validate_reviewer(self, key: str, reviewer: \"User\") -> \"User\":\n        \"\"\"Validate the given reviewer value.\n\n        Args:\n            key (str): The name of the validated column.\n            reviewer (User): The reviewer value to validate.\n\n        Raises:\n            TypeError: If the given reviewer is not a User instance.\n\n        Returns:\n            User: The validated reviewer value.\n        \"\"\"\n        from stalker.models.auth import User\n\n        if not isinstance(reviewer, User):\n            raise TypeError(\n                f\"{self.__class__.__name__}.reviewer should be set to a \"\n                \"stalker.models.auth.User instance, \"\n                f\"not {reviewer.__class__.__name__}: '{reviewer}'\"\n            )\n        return reviewer\n\n    def _review_number_getter(self) -> int:\n        \"\"\"Return the review number value.\n\n        Returns:\n            int: The review_number value.\n        \"\"\"\n        return self._review_number\n\n    review_number: Mapped[Optional[int]] = synonym(\n        \"_review_number\",\n        descriptor=property(_review_number_getter),\n        doc=\"returns the _review_number attribute value\",\n    )\n\n    @property\n    def review_set(self) -> List[\"Review\"]:\n        \"\"\"Return all the reviews in the same review set with this one.\n\n        Returns:\n            List[Review]: The Review instances in the same review set with this one.\n        \"\"\"\n        logger.debug(\n            f\"finding revisions with the same review_number of: {self.review_number}\"\n        )\n        with DBSession.no_autoflush:\n            logger.debug(\"using raw Python to get review set\")\n            reviews = []\n            rev_num = self.review_number\n            for review in self.task.reviews:\n                if review.review_number == rev_num:\n                    reviews.append(review)\n\n        return reviews\n\n    def is_finalized(self) -> bool:\n        \"\"\"Check if all reviews in the same set with this one are finalized.\n\n        Returns:\n            bool: True if all the reviews in the same review set with this one are\n                finalized, False otherwise.\n        \"\"\"\n        return all([review.status.code != \"NEW\" for review in self.review_set])\n\n    def request_revision(\n        self,\n        schedule_timing: Union[float, int] = 1,\n        schedule_unit: Union[str, TimeUnit] = TimeUnit.Hour,\n        description: str = \"\",\n    ) -> None:\n        \"\"\"Finalize the review by requesting a revision.\n\n        Args:\n            schedule_timing (Union[float, int]): The schedule timing value for\n                this Review instance.\n            schedule_unit (Union[str, TimeUnit]): The schedule unit value for\n                this Review instance.\n            description (str): The description for this Review instance.\n        \"\"\"\n        # set self timing values\n        self.schedule_timing = schedule_timing\n        self.schedule_unit = schedule_unit\n        self.description = description\n\n        # set self status to RREV\n        with DBSession.no_autoflush:\n            rrev = Status.query.filter_by(code=\"RREV\").first()\n\n            # set self status to RREV\n            self.status = rrev\n\n        # call finalize_review_set\n        self.finalize_review_set()\n\n    def approve(self):\n        \"\"\"Finalize the review by approving the task.\"\"\"\n        # set self status to APP\n        with DBSession.no_autoflush:\n            app = Status.query.filter_by(code=\"APP\").first()\n            self.status = app\n\n        # call finalize review_set\n        self.finalize_review_set()\n\n    def finalize_review_set(self) -> None:\n        \"\"\"Finalize the current review set Review decisions.\"\"\"\n        with DBSession.no_autoflush:\n            hrev = Status.query.filter_by(code=\"HREV\").first()\n            cmpl = Status.query.filter_by(code=\"CMPL\").first()\n\n        # check if all the reviews are finalized\n        if not self.is_finalized():\n            logger.debug(\"not all reviews are finalized yet!\")\n            return\n\n        logger.debug(\"all reviews are finalized\")\n\n        # check if there are any RREV reviews\n        revise_task = False\n\n        # now we can extend the timing of the task\n        total_seconds = self.task.total_logged_seconds\n        for review in self.review_set:\n            if review.status.code == \"RREV\":\n                total_seconds += review.schedule_seconds\n                revise_task = True\n\n        timing, unit = self.least_meaningful_time_unit(total_seconds)\n        self.task._review_number += 1\n        if revise_task:\n            # revise the task timing if the task needs more time\n            if total_seconds > self.task.schedule_seconds:\n                logger.debug(f\"total_seconds including reviews: {total_seconds}\")\n\n                self.task.schedule_timing = timing\n                self.task.schedule_unit = unit\n            self.task.status = hrev\n        else:\n            # approve the task\n            self.task.status = cmpl\n\n            # also clamp the schedule timing\n            self.task.schedule_timing = timing\n            self.task.schedule_unit = unit\n\n        # update task parent statuses\n        self.task.update_parent_statuses()\n\n        from stalker import TaskDependency\n\n        # update dependent task statuses\n        for dependency in walk_hierarchy(\n            self.task, \"dependent_of\", method=TraversalDirection.BreadthFirst\n        ):\n            logger.debug(f\"current TaskDependency object: {dependency}\")\n            dependency.update_status_with_dependent_statuses()\n            if dependency.status.code in [\"HREV\", \"PREV\", \"DREV\", \"OH\", \"STOP\"]:\n                # for tasks that are still be able to continue to work,\n                # change the dependency_target to DependencyTarget.OnStart\n                # to allow the two of the tasks to work together and still let\n                # the TJ to be able to schedule the tasks correctly\n                with DBSession.no_autoflush:\n                    task_dependencies = TaskDependency.query.filter_by(\n                        depends_on=dependency\n                    ).all()\n                for task_dependency in task_dependencies:\n                    task_dependency.dependency_target = DependencyTarget.OnStart\n\n            # also update the status of parents of dependencies\n            dependency.update_parent_statuses()\n\n\nclass Daily(Entity, StatusMixin, ProjectMixin):\n    \"\"\"Manages data related to **Dailies**.\n\n    Dailies are sessions where outputs of a group of tasks are reviewed all\n    together by the resources and responsible of those tasks.\n\n    The main purpose of a ``Daily`` is to gather a group of :class:`.File`\n    instances and introduce a simple way of presenting them as a group.\n\n    :class:`.Note` s created during a Daily session can be directly stored\n    both in the :class:`.File` and the :class:`.Daily` instances and a *join*\n    will reveal which :class:`.Note` is created in which :class:`.Daily`.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Dailies\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Daily\"}\n\n    daily_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    files: Mapped[Optional[List[File]]] = association_proxy(\n        \"file_relations\", \"file\", creator=lambda n: DailyFile(file=n)\n    )\n\n    file_relations: Mapped[Optional[List[\"DailyFile\"]]] = relationship(\n        back_populates=\"daily\",\n        cascade=\"all, delete-orphan\",\n        primaryjoin=\"Dailies.c.id==Daily_Files.c.daily_id\",\n    )\n\n    def __init__(\n        self,\n        files: Optional[List[File]] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        super(Daily, self).__init__(**kwargs)\n        StatusMixin.__init__(self, **kwargs)\n        ProjectMixin.__init__(self, **kwargs)\n\n        if files is None:\n            files = []\n\n        self.files = files\n\n    @property\n    def versions(self) -> List[\"Version\"]:\n        \"\"\"Return the Version instances related to this Daily.\n\n        Returns:\n            List[Task]: A list of :class:`.Version` instances that this Daily\n                is related to (through the files attribute of the versions).\n        \"\"\"\n        from stalker.models.version import Version\n\n        return (\n            Version.query.join(Version.files)\n            .join(DailyFile)\n            .join(Daily)\n            .filter(Daily.id == self.id)\n            .all()\n        )\n\n    @property\n    def tasks(self) -> List[\"Task\"]:\n        \"\"\"Return the Task's related this Daily instance.\n\n        Returns:\n            List[Task]: A list of :class:`.Task` instances that this Daily is\n                related to (through the files attribute of the versions).\n        \"\"\"\n        from stalker.models.version import Version\n        from stalker.models.task import Task\n\n        return (\n            Task.query.join(Task.versions)\n            .join(Version.files)\n            .join(DailyFile)\n            .join(Daily)\n            .filter(Daily.id == self.id)\n            .all()\n        )\n\n\nclass DailyFile(Base):\n    \"\"\"The association object used in Daily-to-File relation.\"\"\"\n\n    __tablename__ = \"Daily_Files\"\n\n    daily_id: Mapped[int] = mapped_column(\n        ForeignKey(\"Dailies.id\"),\n        primary_key=True,\n    )\n    daily: Mapped[Daily] = relationship(\n        back_populates=\"file_relations\",\n        primaryjoin=\"DailyFile.daily_id==Daily.daily_id\",\n    )\n\n    file_id: Mapped[int] = mapped_column(\n        ForeignKey(\"Files.id\"),\n        primary_key=True,\n    )\n    file: Mapped[File] = relationship(\n        primaryjoin=\"DailyFile.file_id==File.file_id\",\n        doc=\"\"\"stalker.models.file.File instances related to the Daily instance.\n\n        Attach the same :class:`.File` instances that are linked as an output\n        to a certain :class:`.Version` s instance to this attribute.\n\n        This attribute is an **association_proxy** so and the real attribute\n        that the data is related to is the :attr:`.file_relations` attribute.\n\n        You can use the :attr:`.file_relations` attribute to change the\n        ``rank`` attribute of the :class:`.DailyFile` instance (which is the\n        returned data), thus change the order of the ``Files``.\n\n        This is done in that way to be able to store the order of the files in\n        this Daily instance.\n        \"\"\",\n    )\n\n    # may used for sorting\n    rank: Mapped[Optional[int]] = mapped_column(default=0)\n\n    def __init__(\n        self, daily: Optional[Daily] = None, file: Optional[File] = None, rank: int = 0\n    ) -> None:\n        super(DailyFile, self).__init__()\n\n        self.daily = daily\n        self.file = file\n        self.rank = rank\n\n    @validates(\"file\")\n    def _validate_file(self, key: str, file: Union[None, File]) -> Union[None, File]:\n        \"\"\"Validate the given file instance.\n\n        Args:\n            key (str): The name of the validated column.\n            file (Union[None, File]): The like value to be validated.\n\n        Raises:\n            TypeError: When the given like value is not a File instance.\n\n        Returns:\n            Union[None, File]: The validated File instance.\n        \"\"\"\n        from stalker import File\n\n        if file is not None and not isinstance(file, File):\n            raise TypeError(\n                f\"{self.__class__.__name__}.file should be an instance of \"\n                \"stalker.models.file.File instance, \"\n                f\"not {file.__class__.__name__}: '{file}'\"\n            )\n\n        return file\n\n    @validates(\"daily\")\n    def _validate_daily(\n        self, key: str, daily: Union[None, Daily]\n    ) -> Union[None, Daily]:\n        \"\"\"Validate the given daily instance.\n\n        Args:\n            key (str): The name of the validated column.\n            daily (Union[None, Daily]): The daily value to be validated.\n\n        Raises:\n            TypeError: If the given daily value is not a Daily instance.\n\n        Returns:\n            Union[None, Daily]: The validated daily instance.\n        \"\"\"\n        if daily is not None:\n            if not isinstance(daily, Daily):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.daily should be an instance of \"\n                    \"stalker.models.review.Daily instance, \"\n                    f\"not {daily.__class__.__name__}: '{daily}'\"\n                )\n\n        return daily\n"
  },
  {
    "path": "src/stalker/models/scene.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Scene related classes and functions are situated here.\"\"\"\n\nfrom typing import Any, Dict, List, Optional, TYPE_CHECKING\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, validates\n\nfrom stalker.log import get_logger\nfrom stalker.models.mixins import CodeMixin\nfrom stalker.models.task import Task\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.shot import Shot\n\nlogger = get_logger(__name__)\n\n\nclass Scene(Task, CodeMixin):\n    \"\"\"Stores data about Scenes.\n\n    Scenes are grouping the Shots according to their view to the world, that is\n    shots taking place in the same set configuration can be grouped together by\n    using Scenes.\n\n    You cannot replace :class:`.Sequence` s with Scenes, because Scene\n    instances doesn't have some key features that :class:`.Sequence` s have.\n\n    A Scene needs to be tied to a :class:`.Project`\n    instance, so it is not possible to create a Scene without a one.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Scenes\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Scene\"}\n    scene_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Tasks.id\"),\n        primary_key=True,\n    )\n\n    shots: Mapped[Optional[List[\"Shot\"]]] = relationship(\n        primaryjoin=\"Shots.c.scene_id==Scenes.c.id\",\n        back_populates=\"scene\",\n        doc=\"\"\"The :class:`.Shot` s that is related with this Scene.\n\n        It is a list of :class:`.Shot` instances.\n        \"\"\",\n    )\n\n    def __init__(self, shots: Optional[List[\"Shot\"]] = None, **kwargs: Dict[str, Any]):\n        super(Scene, self).__init__(**kwargs)\n\n        # call the mixin __init__ methods\n        CodeMixin.__init__(self, **kwargs)\n\n        if shots is None:\n            shots = []\n\n        self.shots = shots\n\n    @validates(\"shots\")\n    def _validate_shots(self, key: str, shot: \"Shot\") -> \"Shot\":\n        \"\"\"Validate the given shot value.\n\n        Args:\n            key (str): The name of the validated column.\n            shot (Shot): The shot instance.\n\n        Raises:\n            TypeError: If the shot is not a Shot instance.\n\n        Returns:\n            Shot: Return the validated Shot instance.\n        \"\"\"\n        from stalker.models.shot import Shot\n\n        if not isinstance(shot, Shot):\n            raise TypeError(\n                f\"{self.__class__.__name__}.shots should only contain \"\n                \"instances of stalker.models.shot.Shot, \"\n                f\"not {shot.__class__.__name__}: '{shot}'\"\n            )\n        return shot\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality with the other object.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is equal to this object.\n        \"\"\"\n        return isinstance(other, Scene) and super(Scene, self).__eq__(other)\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Scene, self).__hash__()\n"
  },
  {
    "path": "src/stalker/models/schedulers.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Scheduler related function and classes are situated here.\"\"\"\n\nimport csv\nimport datetime\nimport json\nimport os\nimport subprocess\nimport sys\nimport tempfile\nimport time\nfrom typing import List, Optional, TYPE_CHECKING, Union\n\nfrom jinja2 import Template\n\nimport pytz\n\nfrom sqlalchemy import bindparam, text\n\nfrom stalker import defaults\nfrom stalker.db.session import DBSession\nfrom stalker.log import get_logger\nfrom stalker.models.project import Project\nfrom stalker.models.task import Task, Task_Computed_Resources\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.studio import Studio\n\nlogger = get_logger(__name__)\n\n\nclass SchedulerBase(object):\n    \"\"\"This is the base class for schedulers.\n\n    All the schedulers should be derived from this class.\n    \"\"\"\n\n    def __init__(self, studio: Optional[\"Studio\"] = None) -> None:\n        self._studio = None\n        self.studio = studio\n\n    def _validate_studio(self, studio: Union[None, \"Studio\"]) -> Union[None, \"Studio\"]:\n        \"\"\"Validate the given studio value.\n\n        Args:\n            studio (Union[None, Studio]): The Studio instance to set the studio\n                attribute to.\n\n        Raises:\n            TypeError: If the given value is not a Studio instance.\n\n        Returns:\n            Union[None, Studio]: The validated Studio instance.\n        \"\"\"\n        if studio is not None:\n            from stalker.models.studio import Studio\n\n            if not isinstance(studio, Studio):\n                raise TypeError(\n                    f\"{self.__class__.__name__}.studio should be an instance of \"\n                    \"stalker.models.studio.Studio, \"\n                    f\"not {studio.__class__.__name__}: '{studio}'\"\n                )\n        return studio\n\n    @property\n    def studio(self) -> Union[None, \"Studio\"]:\n        \"\"\"Return studio attribute value.\n\n        Returns:\n            Studio: The studio attribute value.\n        \"\"\"\n        return self._studio\n\n    @studio.setter\n    def studio(self, studio: Union[None, \"Studio\"]) -> None:\n        \"\"\"Set studio attribute.\n\n        Args:\n            studio (Studio): The Studio instance to set the studio attribute to.\n        \"\"\"\n        self._studio = self._validate_studio(studio)\n\n    def schedule(self) -> None:\n        \"\"\"Schedule function that needs to be implemented in the derivatives.\n\n        Raises:\n            NotImplementedError: If this is not implemented in the derived class.\n        \"\"\"\n        raise NotImplementedError\n\n\nclass TaskJugglerScheduler(SchedulerBase):\n    \"\"\"This is the main scheduler for Stalker right now.\n\n    This class prepares the data for TaskJuggler and let it solve the\n    scheduling problem, and then retrieves the solved date and resource data\n    back.\n\n    TaskJugglerScheduler needs a :class:`.Studio` instance to work with, it\n    will create a .tjp file and then solve the tasks and restore the\n    computed_start and computed_end dates and the computed_resources\n    attributes for each task.\n\n    Stalker will pass all its data to TaskJuggler by creating a tjp file that\n    TaskJuggler can parse. This tjp file has all the Projects, Tasks, Users,\n    Departments, TimeLogs, Vacations and everything that TJ need for solving\n    the tasks. With every new version of it, Stalker tries to cover more and\n    more TaskJuggler directives.\n\n    .. note::\n       .. versionadded:: 0.2.5\n          Alternative Resources\n\n       Stalker is now able to pass alternative resources to TaskJuggler.\n       Although, per resource alternatives are not yet possible, it will be\n       implemented in future versions of Stalker.\n\n    .. note::\n       .. versionadded:: 0.2.5\n          Task Dependency Relation Attributes\n\n       Stalker now can use 'gapduration', 'gaplength', 'onstart' and 'onend'\n       TaskJuggler directives for each dependent task of a task. Use the\n       TaskDependency instance in Task.task_dependency attribute to control how\n       a particular task is depending on another task.\n\n    .. warning::\n       **Task.computed_resources Attribute Content**\n\n       After the scheduling is finished, TaskJuggler will create a ``csv``\n       report that TaskJugglerScheduler will parse. This csv file contains the\n       ``id``, ``start date``, ``end date`` and ``resources`` data. The\n       resources reported back by TJ will be stored in\n       :attr:`.Task.computed_resources` attribute.\n\n       TaskJuggler will put all the resources who may have entered a\n       :class:`.TimeLog` previously to the csv file. But the resources from the\n       csv file may not be in :attr:`.Task.resources` or\n       :attr:`.Task.alternative_resources` anymore. Because of that,\n       TaskJugglerScheduler will only store the resources those are both in csv\n       file and in :attr:`.Task.resources` or\n       :attr:`.Task.alternative_resources` attributes.\n\n    Stalker will export each Project to tjp as the highest task in the\n    hierarchy and all the projects will be combined in to the same tjp file.\n    Combining all the Projects in one tjp file has a very nice side effect,\n    projects using the same resources will respect their allocations to the\n    resource. So that when a TaskJugglerScheduler instance is used to schedule\n    the project, all projects are scheduled together.\n\n    The following table shows which Stalker data type is converted to which\n    TaskJuggler type:\n\n      +------------+-------------+\n      | Stalker    | TaskJuggler |\n      +============+=============+\n      | Studio     | Project     |\n      +------------+-------------+\n      | Project    | Task        |\n      +------------+-------------+\n      | Task       | Task        |\n      +------------+-------------+\n      | Asset      | Task        |\n      +------------+-------------+\n      | Shot       | Task        |\n      +------------+-------------+\n      | Sequence   | Task        |\n      +------------+-------------+\n      | Departmemt | Resource    |\n      +------------+-------------+\n      | User       | Resource    |\n      +------------+-------------+\n      | TimeLog    | Booking     |\n      +------------+-------------+\n      | Vacation   | Vacation    |\n      +------------+-------------+\n\n    Args:\n        compute_resources (bool): When set to True it will also consider\n            :attr:`.Task.alternative_resources` attribute and will fill\n            :attr:`.Task.computed_resources` attribute for each Task. With\n            :class:`.TaskJugglerScheduler` when the total number of Task is around\n            15k it will take around 7 minutes to generate this data, so by default\n            it is False.\n        parsing_method (int): Choose between SQL (0) or Pure Python (1) parsing.\n            The default is SQL.\n    \"\"\"\n\n    def __init__(\n        self,\n        studio: Optional[\"Studio\"] = None,\n        compute_resources: Optional[bool] = False,\n        parsing_method: Optional[int] = 0,\n        projects: Optional[Project] = None,\n    ) -> None:\n        super(TaskJugglerScheduler, self).__init__(studio)\n\n        self.tjp_content = \"\"\n\n        self.temp_file_full_path = None\n        self.temp_file_path = None\n        self.temp_file_name = None\n\n        self.tjp_file_full_path = None\n        self.tjp_file = None\n\n        self.csv_file_full_path = None\n        self.csv_file = None\n\n        self.compute_resources = compute_resources\n        self.parsing_method = parsing_method\n\n        self._projects = []\n        self.projects = projects\n\n    def _create_tjp_file(self) -> None:\n        \"\"\"Create the tjp file.\"\"\"\n        self.temp_file_full_path = tempfile.mktemp(prefix=\"Stalker_\")\n        self.temp_file_path = os.path.dirname(self.temp_file_full_path)\n        self.temp_file_name = os.path.basename(self.temp_file_full_path)\n        self.tjp_file_full_path = f\"{self.temp_file_full_path}.tjp\"\n        self.csv_file_full_path = f\"{self.temp_file_full_path}.csv\"\n\n    def _create_tjp_file_content(self) -> None:  # noqa: C901\n        \"\"\"Create the tjp file content.\"\"\"\n        start = time.time()\n\n        # use new way of doing it, it will just work with PostgreSQL\n        template = Template(defaults.tjp_main_template2)\n\n        if not self.projects:\n            project_ids = (\n                DBSession.connection()\n                .execute(text('select id, code from \"Projects\"'))\n                .fetchall()\n            )\n        else:\n            project_ids = [[project.id] for project in self.projects]\n\n        sql_query = \"\"\"select\n    \"Tasks\".id,\n    tasks.path,\n    coalesce(\"Tasks\".parent_id, \"Tasks\".project_id) as parent_id,\n    tasks.entity_type,\n    tasks.name,\n    \"Tasks\".priority,\n    \"Tasks\".schedule_timing,\n    \"Tasks\".schedule_unit,\n    \"Tasks\".schedule_model,\n    \"Tasks\".allocation_strategy,\n    \"Tasks\".persistent_allocation,\n    tasks.depth,\n    task_resources.resource_ids,\n    task_alternative_resources.resource_ids as alternative_resource_ids,\n    time_logs.time_log_array,\n    task_dependencies.dependency_info,\n    not exists (\n       select 1\n        from \"Tasks\" as \"Child_Tasks\"\n        where \"Child_Tasks\".parent_id = \"Tasks\".id\n    ) as is_leaf\nfrom \"Tasks\"\njoin (\n    with recursive recursive_task(id, parent_id, path_as_text, path, depth) as (\n        select\n            id,\n            parent_id,\n            id::text as path_as_text,\n            array[project_id] as path,\n            0\n        from \"Tasks\"\n        where parent_id is NULL and project_id = :id\n    union all\n        select\n            task.id,\n            task.parent_id,\n            (parent.path_as_text || '-' || task.id) as path_as_text,\n            (parent.path || task.parent_id) as path,\n            parent.depth + 1 as depth\n        from \"Tasks\" as task\n        join recursive_task as parent on task.parent_id = parent.id\n    ) select\n        recursive_task.id,\n        recursive_task.parent_id,\n        recursive_task.path_as_text,\n        recursive_task.path,\n        \"SimpleEntities\".name as name,\n        \"SimpleEntities\".entity_type,\n        recursive_task.depth\n    from recursive_task\n    join \"SimpleEntities\" on recursive_task.id = \"SimpleEntities\".id\n    --order by path_as_text\n) as tasks on \"Tasks\".id = tasks.id\n\n-- resources\nleft outer join (\n    select\n        task_id,\n        array_agg(resource_id order by resource_id) as resource_ids\n    from \"Task_Resources\"\n    group by task_id\n) as task_resources on \"Tasks\".id = task_resources.task_id\n\n-- alternative resources\nleft outer join (\n    select\n        task_id,\n        array_agg(resource_id order by resource_id) as resource_ids\n    from \"Task_Alternative_Resources\"\n    group by task_id\n) as task_alternative_resources on \"Tasks\".id = task_alternative_resources.task_id\n\n-- time logs\nleft outer join (\n    select\n        \"TimeLogs\".task_id,\n        array_agg(('User_' || \"TimeLogs\".resource_id, to_char(cast(\"TimeLogs\".start at time zone 'utc' as timestamp), 'YYYY-MM-DD-HH24:MI:00'), to_char(cast(\"TimeLogs\".end at time zone 'utc' as timestamp), 'YYYY-MM-DD-HH24:MI:00'))) as time_log_array\n    from \"TimeLogs\"\n    group by task_id\n) as time_logs on \"Tasks\".id = time_logs.task_id\n\n-- dependencies\nleft outer join (\n    select\n        task_id,\n        array_agg((tasks.alt_path, dependency_target, gap_timing, gap_unit, gap_model)) dependency_info\n    from \"Task_Dependencies\"\n    join (\n        with recursive recursive_task(id, parent_id, alt_path) as (\n            select\n                id,\n                parent_id,\n                project_id::text as alt_path\n            from \"Tasks\"\n            where parent_id is NULL\n        union all\n            select\n                task.id,\n                task.parent_id,\n                (parent.alt_path || '-' || task.parent_id) as alt_path\n            from \"Tasks\" as task\n            join recursive_task as parent on task.parent_id = parent.id\n        ) select\n            recursive_task.id,\n            recursive_task.parent_id,\n            recursive_task.alt_path || '-' || recursive_task.id as alt_path\n        from recursive_task\n        join \"SimpleEntities\" on recursive_task.id = \"SimpleEntities\".id\n    ) as tasks on \"Task_Dependencies\".depends_on_id = tasks.id\n    group by task_id\n) as task_dependencies on \"Tasks\".id = task_dependencies.task_id\n\n--order by \"Tasks\".id\norder by path_as_text\"\"\"  # noqa: B950\n\n        result_buffer = []\n        num_of_records = 0\n\n        # run it per project\n        for pr in project_ids:\n            p_id = pr[0]\n            # p_code = pr[1]\n\n            result = DBSession.connection().execute(text(sql_query), {\"id\": p_id})\n\n            # start by adding the project first\n            result_buffer.append(f'task Project_{p_id} \"Project_{p_id}\" {{')\n\n            # now start jumping around\n            previous_level = 0\n            for r in result.fetchall():\n                # start by appending task tjp id first\n                task_id = r[0]\n                # path = r[1]\n                # parent_id = r[2]\n                # entity_type = r[3]\n                # name = r[4]\n                priority = r[5]\n                schedule_timing = r[6]\n                schedule_unit = r[7]\n                schedule_model = r[8]\n                allocation_strategy = r[9]\n                persistent_allocation = r[10]\n                depth = r[11] + 1\n                resource_ids = r[12]\n                alternative_resource_ids = r[13]\n                time_log_array = r[14]\n                dependency_info = r[15]\n                is_leaf = r[16]\n\n                tab = \"  \" * depth\n\n                # close the previous level if necessary\n                for i in range(previous_level - depth + 1):\n                    i_tab = \"  \" * (previous_level - i)\n                    result_buffer.append(f\"{i_tab}}}\")\n\n                result_buffer.append(\n                    f\"\"\"{tab}task Task_{task_id} \"Task_{task_id}\" {{\"\"\"\n                )\n\n                # append priority if it is different then 500\n                if priority != 500:\n                    result_buffer.append(f\"{tab}  priority {priority}\")\n\n                # append dependency information\n                if dependency_info:\n                    dep_buffer = [f\"{tab}  depends \"]\n\n                    json_data = json.loads(\n                        dependency_info.replace(\"{\", \"[\")\n                        .replace(\"}\", \"]\")\n                        .replace(\"(\", \"\")\n                        .replace(\")\", \"\")\n                    )  # it is an array of string\n\n                    for i, dependency in enumerate(json_data):\n                        if i > 0:\n                            dep_buffer.append(\", \")\n\n                        (\n                            dep_full_ids,\n                            dependency_target,\n                            gap_timing,\n                            gap_unit,\n                            gap_model,\n                        ) = dependency.split(\",\")\n\n                        dep_full_path = \".\".join(\n                            map(lambda x: f\"Task_{x}\", dep_full_ids.split(\"-\"))\n                        )\n                        # fix for Project id\n                        dep_full_path = f\"Project_{dep_full_path[5:]}\"\n\n                        dep_string = f\"{dep_full_path} {{{dependency_target}}}\"\n\n                        dep_buffer.append(dep_string)\n\n                    result_buffer.append(\"\".join(dep_buffer))\n\n                # append schedule model and timing information\n                # if this is a leaf task and has resources\n                if is_leaf and resource_ids:\n                    result_buffer.append(\n                        f\"{tab}  {schedule_model} {schedule_timing}{schedule_unit}\"\n                    )\n\n                    resource_buffer = [f\"{tab}  allocate \"]\n                    for i, resource_id in enumerate(resource_ids):\n                        if i > 0:\n                            resource_buffer.append(\", \")\n                        resource_buffer.append(f\"User_{resource_id}\")\n\n                        # now go through alternatives\n                        if alternative_resource_ids:\n                            resource_buffer.append(\" { alternative \")\n                            for j, alt_resource_id in enumerate(\n                                alternative_resource_ids\n                            ):\n                                if j > 0:\n                                    resource_buffer.append(\", \")\n                                resource_buffer.append(f\"User_{alt_resource_id}\")\n\n                            # set the allocation strategy\n                            resource_buffer.append(f\" select {allocation_strategy}\")\n\n                            # is is persistent\n                            if persistent_allocation:\n                                resource_buffer.append(\" persistent\")\n                            resource_buffer.append(\" }\")\n\n                    result_buffer.append(\"\".join(resource_buffer))\n\n                    # append any time log information\n                    if time_log_array:\n                        json_data = json.loads(\n                            time_log_array.replace(\"{\", \"[\")\n                            .replace(\"}\", \"]\")\n                            .replace(\"(\", \"\")\n                            .replace(\")\", \"\")\n                        )  # it is an array of string\n\n                        for tlog in json_data:\n                            user_id, t_start, t_end = tlog.split(\",\")\n                            result_buffer.append(\n                                f\"{tab}  booking {user_id} {t_start} - {t_end} \"\n                                \"{ overtime 2 }\"\n                            )\n\n                previous_level = depth\n                num_of_records += 1\n\n            # and close the brackets per project\n            depth = 0  # current depth is 0 (Project)\n            # previous_level is the last task\n            for i in range(previous_level - depth + 1):\n                i_tab = \"  \" * (previous_level - i)\n                result_buffer.append(f\"{i_tab}}}\")\n\n        tasks_buffer = \"\\n\".join(result_buffer)\n\n        import stalker\n\n        self.tjp_content = template.render(\n            {\n                \"stalker\": stalker,\n                \"studio\": self.studio,\n                \"csv_file_name\": self.temp_file_name,\n                \"csv_file_full_path\": self.temp_file_full_path,\n                \"compute_resources\": self.compute_resources,\n                \"tasks_buffer\": tasks_buffer,\n            },\n            trim_blocks=True,\n            lstrip_blocks=True,\n        )\n\n        logger.debug(f\"total number of records: {num_of_records}\")\n\n        end = time.time()\n        logger.debug(\n            \"rendering the whole tjp file took: {:0.3f} seconds\".format(end - start)\n        )\n\n    def _fill_tjp_file(self) -> None:\n        \"\"\"Fill the tjp file with content.\"\"\"\n        with open(self.tjp_file_full_path, \"w+\") as self.tjp_file:\n            self.tjp_file.write(self.tjp_content)\n\n    def _delete_tjp_file(self) -> None:\n        \"\"\"Delete the temp tjp file.\"\"\"\n        try:\n            os.remove(self.tjp_file_full_path)\n        except OSError:\n            pass\n\n    def _delete_csv_file(self) -> None:\n        \"\"\"Delete the temp csv file.\"\"\"\n        try:\n            os.remove(self.csv_file_full_path)\n        except OSError:\n            pass\n\n    def _clean_up(self) -> None:\n        \"\"\"Remove the temp files.\"\"\"\n        self._delete_tjp_file()\n        self._delete_csv_file()\n\n    def _parse_csv_file(self) -> None:\n        \"\"\"Parse the csv file and set the Task.computes_start and Task.computed_end.\"\"\"\n        parsing_start = time.time()\n\n        logger.debug(f\"csv_file_full_path : {self.csv_file_full_path}\")\n        if not os.path.exists(self.csv_file_full_path):\n            logger.debug(\"could not find CSV file, returning without updating db!\")\n            return\n\n        entity_ids = []\n        update_data = []\n        update_user_data = []\n\n        with open(self.csv_file_full_path, \"r\") as self.csv_file:\n            csv_content = csv.reader(self.csv_file, delimiter=\";\")\n\n            lines = [line for line in csv_content]\n            lines.pop(0)\n\n        for data in lines:\n            id_line = data[0]\n\n            entity_id = int(id_line.split(\".\")[-1].split(\"_\")[-1])\n            if not entity_id:\n                continue\n\n            entity_ids.append(entity_id)\n            start_date = datetime.datetime.strptime(data[1], \"%Y-%m-%d-%H:%M\")\n            end_date = datetime.datetime.strptime(data[2], \"%Y-%m-%d-%H:%M\")\n\n            # implement time zone info\n            start_date = start_date.replace(tzinfo=pytz.utc)\n            end_date = end_date.replace(tzinfo=pytz.utc)\n\n            # computed_resources\n            if self.compute_resources and data[3] != \"\":\n                resources_data = map(\n                    lambda x: x.split(\"_\")[-1].split(\")\")[0], data[3].split(\",\")\n                )\n                for rid in resources_data:\n                    update_user_data.append({\"task_id\": entity_id, \"resource_id\": rid})\n\n            update_data.append(\n                {\n                    \"b_id\": entity_id,\n                    \"start\": start_date,\n                    \"end\": end_date,\n                    \"computed_start\": start_date,\n                    \"computed_end\": end_date,\n                }\n            )\n\n        # update date values\n        update_statement = (\n            Task.__table__.update()\n            .where(Task.__table__.c.id == bindparam(\"b_id\"))\n            .values(\n                start=bindparam(\"start\"),\n                end=bindparam(\"end\"),\n                computed_start=bindparam(\"computed_start\"),\n                computed_end=bindparam(\"computed_end\"),\n            )\n        )\n        DBSession.connection().execute(update_statement, update_data)\n\n        # update project dates\n        update_project_statement = (\n            Project.__table__.update()\n            .where(Project.__table__.c.id == bindparam(\"b_id\"))\n            .values(\n                start=bindparam(\"start\"),\n                end=bindparam(\"end\"),\n                computed_start=bindparam(\"computed_start\"),\n                computed_end=bindparam(\"computed_end\"),\n            )\n        )\n        DBSession.connection().execute(update_project_statement, update_data)\n\n        # update computed resources data\n        # first delete everything\n        if self.compute_resources:\n            delete_resources_statement = Task_Computed_Resources.delete()\n\n            update_resources_statement = Task_Computed_Resources.insert().values(\n                task_id=bindparam(\"task_id\"), resource_id=bindparam(\"resource_id\")\n            )\n\n            DBSession.connection().execute(delete_resources_statement)\n            DBSession.connection().execute(update_resources_statement, update_user_data)\n\n        parsing_end = time.time()\n        logger.debug(\n            \"completed parsing csv file in (SQL): {} seconds\".format(\n                parsing_end - parsing_start\n            )\n        )\n\n    def schedule(self) -> str:\n        \"\"\"Schedule the project or all projects in the Studio.\n\n        Raises:\n            TypeError: If the self.studio is not a Studio instance.\n            RuntimeError: If the tj3 command returns an error.\n\n        Returns:\n            str: The tj3 command output.\n        \"\"\"\n        # check the studio attribute\n        from stalker.models.studio import Studio\n\n        if not isinstance(self.studio, Studio):\n            raise TypeError(\n                f\"{self.__class__.__name__}.studio should be an instance of \"\n                \"stalker.models.studio.Studio, \"\n                f\"not {self.studio.__class__.__name__}: '{self.studio}'\"\n            )\n\n        # create a tjp file\n        self._create_tjp_file()\n\n        # create tjp file content\n        self._create_tjp_file_content()\n\n        # fill it with data\n        self._fill_tjp_file()\n\n        logger.debug(f\"tjp_file_full_path: {self.tjp_file_full_path}\")\n\n        # pass it to tj3\n        if sys.platform == \"win32\":\n            logger.debug(\"tj3 using fallback mode for Windows!\")\n            command = \"{} {} -o {}\".format(\n                defaults.tj_command,\n                self.tjp_file_full_path,\n                self.temp_file_path,\n            )\n            logger.debug(f\"tj3 command: {command}\")\n            return_code = os.system(command)\n            stderr_buffer = \"\"\n        else:\n            process = subprocess.Popen(\n                [\n                    defaults.tj_command,\n                    self.tjp_file_full_path,\n                    \"-o\",\n                    self.temp_file_path,\n                ],\n                stderr=subprocess.PIPE,\n            )\n\n            # loop until process finishes and capture stderr output\n            stderr_buffer = []\n            while True:\n                stderr = process.stderr.readline()\n\n                if stderr == b\"\" and process.poll() is not None:\n                    break\n\n                if stderr != b\"\":\n                    stderr = stderr.decode(\"utf-8\").strip()\n                    stderr_buffer.append(stderr)\n                    logger.debug(stderr)\n\n            # flatten the buffer\n            stderr_buffer = \"\\n\".join(stderr_buffer)\n\n            return_code = process.returncode\n\n        if return_code:\n            # there is an error\n            raise RuntimeError(stderr_buffer)\n\n        # read back the csv file\n        self._parse_csv_file()\n\n        logger.debug(f\"tj3 return code: {return_code}\")\n\n        # remove the tjp file\n        self._clean_up()\n\n        return stderr_buffer\n\n    def _validate_projects(self, projects: List[Project]) -> List[Project]:\n        \"\"\"Validate the given projects value.\n\n        Args:\n            projects (List[Project]): List of Project instances.\n\n        Raises:\n            TypeError: If the projects is not a list or if any of the items in the\n                projects list is not a Project instance.\n\n        Returns:\n            List[Project]: List of validated Project instances.\n        \"\"\"\n        if projects is None:\n            projects = []\n\n        msg = (\n            \"{cls}.projects should only contain instances of \"\n            \"stalker.models.project.Project, not \"\n            \"{projects_class}: '{projects}'\"\n        )\n\n        if not isinstance(projects, list):\n            raise TypeError(\n                msg.format(\n                    cls=self.__class__.__name__,\n                    projects_class=projects.__class__.__name__,\n                    projects=projects,\n                )\n            )\n\n        for item in projects:\n            if not isinstance(item, Project):\n                raise TypeError(\n                    msg.format(\n                        cls=self.__class__.__name__,\n                        projects_class=item.__class__.__name__,\n                        projects=item,\n                    )\n                )\n\n        return projects\n\n    @property\n    def projects(self) -> List[Project]:\n        \"\"\"Return the projects attribute value.\n\n        Returns:\n            List[Project]: List of Project instances.\n        \"\"\"\n        return self._projects\n\n    @projects.setter\n    def projects(self, projects: List[Project]) -> None:\n        \"\"\"Set the projects attribute.\n\n        Args:\n            projects (List[Project]): List of Project instances.\n        \"\"\"\n        self._projects = self._validate_projects(projects)\n"
  },
  {
    "path": "src/stalker/models/sequence.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Sequence related function and classes are situated here.\"\"\"\n\nfrom typing import Any, Dict, List, Optional, TYPE_CHECKING\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, validates\n\nfrom stalker.log import get_logger\nfrom stalker.models.mixins import CodeMixin, ReferenceMixin\nfrom stalker.models.task import Task\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.shot import Shot\n\nlogger = get_logger(__name__)\n\n\nclass Sequence(Task, CodeMixin):\n    \"\"\"Stores data about Sequences.\n\n    Sequences are a way of grouping the Shots according to their temporal\n    position to each other.\n\n    **Initialization**\n\n    .. warning::\n\n       .. deprecated:: 0.2.0\n\n       Sequences do not have a lead anymore. Use the :class:`.Task.responsible`\n       attribute of the super (:class:`.Task`).\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Sequences\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Sequence\"}\n    sequence_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Tasks.id\"),\n        primary_key=True,\n    )\n\n    shots: Mapped[Optional[List[\"Shot\"]]] = relationship(\n        primaryjoin=\"Shots.c.sequence_id==Sequences.c.id\",\n        back_populates=\"sequence\",\n        doc=\"\"\"The :class:`.Shot` s assigned to this Sequence.\n\n        It is a list of :class:`.Shot` instances.\n        \"\"\",\n    )\n\n    def __init__(self, **kwargs: Dict[str, Any]) -> None:\n        super(Sequence, self).__init__(**kwargs)\n\n        # call the mixin __init__ methods\n        ReferenceMixin.__init__(self, **kwargs)\n        CodeMixin.__init__(self, **kwargs)\n        self.shots = []\n\n    @validates(\"shots\")\n    def _validate_shots(self, key: str, shot: \"Shot\") -> \"Shot\":\n        \"\"\"Validate the given shot value.\n\n        Args:\n            key (str): The name of the validated column.\n            shot (Shot): The Shot instance to validate.\n\n        Raises:\n            TypeError: If the given shot is not a Shot instance.\n\n        Returns:\n            Shot: The validated shot value.\n        \"\"\"\n        from stalker.models.shot import Shot\n\n        if not isinstance(shot, Shot):\n            raise TypeError(\n                f\"{self.__class__.__name__}.shots should only contain \"\n                \"instances of stalker.models.shot.Shot, \"\n                f\"not {shot.__class__.__name__}: '{shot}'\"\n            )\n        return shot\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a Sequence instance and has the same\n                attributes.\n        \"\"\"\n        return isinstance(other, Sequence) and super(Sequence, self).__eq__(other)\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Sequence, self).__hash__()\n"
  },
  {
    "path": "src/stalker/models/shot.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Shot related functions and classes are situated here.\"\"\"\n\nfrom typing import Any, Dict, Optional, TYPE_CHECKING, Union\n\nfrom sqlalchemy import Float, ForeignKey\nfrom sqlalchemy.exc import OperationalError, UnboundExecutionError\nfrom sqlalchemy.orm import (\n    Mapped,\n    mapped_column,\n    reconstructor,\n    relationship,\n    synonym,\n    validates,\n)\n\nfrom stalker.db.session import DBSession\nfrom stalker.log import get_logger\nfrom stalker.models.format import ImageFormat\nfrom stalker.models.mixins import CodeMixin, ReferenceMixin, StatusMixin\nfrom stalker.models.task import Task\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.project import Project\n    from stalker.models.scene import Scene\n    from stalker.models.sequence import Sequence\n\nlogger = get_logger(__name__)\n\n\nclass Shot(Task, CodeMixin):\n    \"\"\"Manages Shot related data.\n\n    A shot is a continuous, unbroken sequence of images that makes up a single\n    part of a film. Shots are organized into :class:`.Sequence` s and\n    :class:`.Scene` s :class:`.Sequence` s group shots together based on time,\n    such as montage or flashback. :class:`.Scene` s group shots together based\n    on location and narrative, marking where and when a specific story event\n    occurs.\n\n    .. warning::\n\n       .. deprecated:: 0.1.2\n\n       Because most of the shots in different projects may going to have\n       the same name, which is a kind of a code like SH001, SH012A etc., and\n       in Stalker you cannot have two entities with the same name if their\n       types are also matching, to guarantee all the shots are going to have\n       different names the :attr:`.name` attribute of the Shot instances are\n       automatically set to a randomly generated **uuid4** sequence.\n\n    .. note::\n\n       .. versionadded:: 0.1.2\n\n       The name of the shot can be freely set without worrying about clashing\n       names.\n\n    .. note::\n\n       .. versionadded:: 0.2.0\n\n       Shot instances now can have their own image format. So you can set up\n       different resolutions per shot.\n\n    .. note::\n\n       .. versionadded:: 0.2.0\n\n       Shot instances can now be created with a Project instance only, without\n       needing a Sequence instance. Sequences are now a kind of a grouping\n       attribute for the Shots. And Shots can have more than one Sequence.\n\n    .. note::\n\n       .. versionadded:: 1.0.0\n\n       Shot instances can only be connected to a single Sequence instance via\n       the `Shot.sequence` attribute. Previously, Shots could have multiple\n       Sequences, the initial purpose of that was to allow the very very rare\n       case of having the same shot appear in two different sequences which\n       proved itself being very useless and making things unnecessarily\n       complicated. So, it has been removed and Shots can only be connected to\n       a single Sequence (Shot <-> Scene relation will follow this in later\n       versions/commits).\n\n    .. note::\n\n       .. versionadded:: 1.0.0\n\n       Shot and Scene relation is now many-to-one, meaning a Shot can only be\n       connected to a single Scene instance through the `Shot.scene` attribute.\n\n    Two shots with the same :attr:`.code` cannot be assigned to the same\n    :class:`.Sequence`.\n\n    .. note::\n\n       .. versionadded:: 0.2.10\n\n       Simplified the implementation of :attr:`.cut_in`, :attr:`.cut_out` and\n       :attr:`.cut_duration` attributes. The :attr:`.cut_duration` is always\n       the difference between :attr:`.cut_in` and :attr:`.cut_out` and its\n       value is only be calculated when it is requested. This greatly\n       simplifies the implementation of :attr:`.cut_in` and :attr:`.cut_out`\n       attributes.\n\n    The :attr:`.cut_out` and :attr:`.cut_duration` attributes effects each\n    other. Setting the :attr:`.cut_out` will change the :attr:`.cut_duration`\n    and setting the :attr:`.cut_duration` will change the :attr:`.cut_out`\n    value. The default value of the :attr:`.cut_duration` attribute is\n    calculated from the :attr:`.cut_in` and :attr:`.cut_out` attributes. If\n    both :attr:`.cut_out` and :attr:`.cut_duration` arguments are set to None,\n    the :attr:`.cut_duration` defaults to 1 and :attr:`.cut_out` will be set to\n    :attr:`.cut_in` + :attr:`.cut_duration`. So the priority of the attributes\n    are as follows:\n\n      :attr:`.cut_in` >\n      :attr:`.cut_out` >\n      :attr:`.cut_duration`\n\n    .. note::\n\n       .. versionadded:: 0.2.4\n\n       :attr:`.handles_at_start` and :attr:`.handles_at_end` attributes.\n\n    .. note::\n\n       .. versionadded:: 0.2.17.2\n\n       Per shot FPS values. It is now possible to change the shot fps by\n       setting its :attr:`.fps` attribute. The default values is same with the\n       :class:`.Project` .\n\n    Args:\n        project (Project): This is the :class:`.Project` instance that this\n            shot belongs to. A Shot cannot be created without a Project\n            instance.\n\n        sequence (Sequence): This is a :class:`.Sequence` that this shot is\n            assigned to. A Shot can be created without having a Sequence\n            instance.\n\n        cut_in (int): The in frame number that this shot starts. The default\n            value is 1. When the ``cut_in`` is bigger then ``cut_out``, the\n            :attr:`.cut_out` attribute is set to :attr:`.cut_in` + 1.\n\n        cut_duration (int): The duration of this shot in frames. It should be\n            zero or a positive integer value (natural number?) or . The default\n            value is None.\n\n        cut_out (int): The out frame number that this shot ends. If it is given\n            as a value lower then the ``cut_in`` parameter, then the\n            :attr:`.cut_out` will be recalculated from the existent\n            :attr:`.cut_in` :attr:`.cut_duration` attributes. Can be skipped.\n            The default value is None.\n\n        image_format (ImageFormat): The image format of this shot. This is an\n            optional variable to differentiate the image format per shot. The\n            default value is the same with the Project that this Shot belongs\n            to.\n\n        fps (float): The FPS of this shot. Default value is the same with the\n            :class:`.Project` .\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"Shots\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Shot\"}\n\n    shot_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Tasks.id\"),\n        primary_key=True,\n    )\n\n    sequence_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Sequences.id\"))\n\n    sequence: Mapped[Optional[\"Sequence\"]] = relationship(\n        primaryjoin=\"Shots.c.sequence_id==Sequences.c.id\",\n        back_populates=\"shots\",\n    )\n\n    scene_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Scenes.id\"))\n\n    scene: Mapped[Optional[\"Scene\"]] = relationship(\n        primaryjoin=\"Shots.c.scene_id==Scenes.c.id\",\n        back_populates=\"shots\",\n    )\n\n    image_format_id: Mapped[Optional[int]] = mapped_column(\n        ForeignKey(\"ImageFormats.id\")\n    )\n    _image_format: Mapped[Optional[ImageFormat]] = relationship(\n        \"ImageFormat\",\n        primaryjoin=\"Shots.c.image_format_id==ImageFormats.c.id\",\n        doc=\"\"\"The :class:`.ImageFormat` of this shot.\n\n        This value defines the output image format of this shot, should be an\n        instance of :class:`.ImageFormat`.\n        \"\"\",\n    )\n\n    # the cut_duration attribute is not going to be stored in the database,\n    # only the cut_in and cut_out will be enough to calculate the cut_duration\n    cut_in: Mapped[Optional[int]] = mapped_column(\n        doc=\"The start frame of this shot. It is the start frame of the \"\n        \"playback range in the application (Maya, Nuke etc.).\",\n        default=1,\n    )\n    cut_out: Mapped[Optional[int]] = mapped_column(\n        doc=\"The end frame of this shot. It is the end frame of the \"\n        \"playback range in the application (Maya, Nuke etc.).\",\n        default=1,\n    )\n\n    source_in: Mapped[Optional[int]] = mapped_column(\n        doc=\"The start frame of the used range, should be in between\"\n        \":attr:`.cut_in` and :attr:`.cut_out`\",\n    )\n    source_out: Mapped[Optional[int]] = mapped_column(\n        doc=\"The end frame of the used range, should be in between\"\n        \":attr:`.cut_in and :attr:`.cut_out`\",\n    )\n    record_in: Mapped[Optional[int]] = mapped_column(\n        doc=\"The start frame in the Editors timeline specifying the start \"\n        \"frame general placement of this shot.\",\n    )\n\n    _fps: Mapped[Optional[float]] = mapped_column(\n        \"fps\",\n        Float(precision=3),\n        doc=\"\"\"The fps of the project.\n\n        It is a float value, any other types will be converted to float. The\n        default value is equal to :attr:`stalker.models.project..Project.fps`.\n        \"\"\",\n    )\n\n    def __init__(\n        self,\n        code: Optional[str] = None,\n        project: Optional[\"Project\"] = None,\n        sequence: Optional[\"Sequence\"] = None,\n        scene: Optional[\"Scene\"] = None,\n        cut_in: Optional[int] = None,\n        cut_out: Optional[int] = None,\n        source_in: Optional[int] = None,\n        source_out: Optional[int] = None,\n        record_in: Optional[int] = None,\n        image_format: Optional[ImageFormat] = None,\n        fps: Optional[float] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        kwargs[\"project\"] = project\n        kwargs[\"code\"] = code\n\n        self._updating_cut_in_cut_out = False\n\n        super(Shot, self).__init__(**kwargs)\n        ReferenceMixin.__init__(self, **kwargs)\n        StatusMixin.__init__(self, **kwargs)\n        CodeMixin.__init__(self, **kwargs)\n\n        self.sequence = sequence\n        self.scene = scene\n        self.image_format = image_format\n\n        if cut_in is None:\n            if cut_out is not None:\n                cut_in = cut_out\n\n        if cut_out is None:\n            if cut_in is not None:\n                cut_out = cut_in\n\n        # if both are None set them to default values\n        if cut_in is None and cut_out is None:\n            cut_in = 1\n            cut_out = 1\n\n        self.cut_in = cut_in\n        self.cut_out = cut_out\n\n        if source_in is None:\n            source_in = self.cut_in\n\n        if source_out is None:\n            source_out = self.cut_out\n\n        self.source_in = source_in\n        self.source_out = source_out\n        self.record_in = record_in\n\n        self.fps = fps\n\n    @reconstructor\n    def __init_on_load__(self) -> None:\n        \"\"\"Initialize on DB load.\"\"\"\n        super(Shot, self).__init_on_load__()\n        self._updating_cut_in_cut_out = False\n\n    def __repr__(self) -> str:\n        \"\"\"Return the string representation of this Shot instance.\n\n        Returns:\n            str: The string representation of this Shot instance.\n        \"\"\"\n        return f\"<{self.entity_type} ({self.name}, {self.code})>\"\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a Shot instance and has the same code and\n                project.\n        \"\"\"\n        return (\n            isinstance(other, Shot)\n            and self.code == other.code\n            and self.project == other.project\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Shot, self).__hash__()\n\n    @classmethod\n    def _check_code_availability(cls, code: str, project: \"Project\") -> bool:\n        \"\"\"Check if the given code is available in the given project.\n\n        Args:\n            code (str): The code to check the availability of.\n            project (Project): The stalker.models.project.Project instance that this\n                shot is a part of.\n\n        Raises:\n            TypeError: If the code is not a str.\n\n        Returns:\n            bool: True if the given code is available, False otherwise.\n        \"\"\"\n        if not project or not code:\n            return True\n\n        if not isinstance(code, str):\n            raise TypeError(\n                \"code should be a string containing a shot code, \"\n                f\"not {code.__class__.__name__}: '{code}'\"\n            )\n\n        from stalker import Project\n\n        if not isinstance(project, Project):\n            raise TypeError(\n                \"project should be a Project instance, \"\n                f\"not {project.__class__.__name__}: '{project}'\"\n            )\n\n        try:\n            logger.debug(\"Try checking Shot.code with SQL expression.\")\n            with DBSession.no_autoflush:\n                return (\n                    Shot.query.filter(Shot.project == project)\n                    .filter(Shot.code == code)\n                    .first()\n                    is None\n                )\n        except (UnboundExecutionError, OperationalError):\n            # Fallback to Python\n            logger.debug(\"SQL expression failed, falling back to Python!\")\n            for t in project.tasks:\n                if isinstance(t, Shot) and t.code == code:\n                    return False\n        return True\n\n    def _fps_getter(self) -> float:\n        \"\"\"Return the fps value either from the Project or from the _fps attribute.\n\n        Returns:\n            float: The fps attribute value.\n        \"\"\"\n        if self._fps is None:\n            return self.project.fps\n        else:\n            return self._fps\n\n    def _fps_setter(self, fps: float) -> None:\n        \"\"\"Set the fps value.\n\n        Args:\n            fps (float): The fps value to set the fps attribute to.\n        \"\"\"\n        self._fps = self._validate_fps(fps)\n\n    fps: Mapped[Optional[float]] = synonym(\n        \"_fps\",\n        descriptor=property(_fps_getter, _fps_setter),\n        doc=\"The fps of this shot.\",\n    )\n\n    def _validate_fps(self, fps: Union[int, float]) -> float:\n        \"\"\"Validate the given fps value.\n\n        Args:\n            fps (Union[int, float]): Either an integer or float value to used as the\n                fps.\n\n        Raises:\n            TypeError: If the given `fps` value is not an integer or float.\n            ValueError: If the `fps` value is smaller or equal to 0.\n\n        Returns:\n            float: The validated fps value.\n        \"\"\"\n        if fps is None:\n            # fps = self.project.fps\n            return None\n\n        if not isinstance(fps, (int, float)):\n            raise TypeError(\n                \"{}.fps should be a positive float or int, not {}: '{}'\".format(\n                    self.__class__.__name__, fps.__class__.__name__, fps\n                )\n            )\n\n        fps = float(fps)\n        if fps <= 0:\n            raise ValueError(\n                \"{}.fps should be a positive float or int, not {}\".format(\n                    self.__class__.__name__, fps\n                )\n            )\n        return float(fps)\n\n    @validates(\"cut_in\")\n    def _validate_cut_in(self, key: str, cut_in: int) -> int:\n        \"\"\"Validate the cut_in value.\n\n        Args:\n            key (str): The name of the validated column.\n            cut_in (int): The `cut_in` value to be validated.\n\n        Raises:\n            TypeError: If the given `cut_in` value is not an integer.\n\n        Returns:\n            int: The validated `cut_in` value.\n        \"\"\"\n        if not isinstance(cut_in, int):\n            raise TypeError(\n                f\"{self.__class__.__name__}.cut_in should be an int, \"\n                f\"not {cut_in.__class__.__name__}: '{cut_in}'\"\n            )\n\n        if self.cut_out is not None and not self._updating_cut_in_cut_out:\n            if cut_in > self.cut_out:\n                # lock the attribute update\n                self._updating_cut_in_cut_out = True\n                self.cut_out = cut_in\n                self._updating_cut_in_cut_out = False\n\n        return cut_in\n\n    @validates(\"cut_out\")\n    def _validate_cut_out(self, key: str, cut_out: int) -> int:\n        \"\"\"Validate the cut_out value.\n\n        Args:\n            key (str): The name of the validated column.\n            cut_out (int): The `cut_out` value to be validated.\n\n        Raises:\n            TypeError: If the `cut_out` value is not an integer.\n\n        Returns:\n            int: The validated `cut_out` value.\n        \"\"\"\n        if not isinstance(cut_out, int):\n            raise TypeError(\n                f\"{self.__class__.__name__}.cut_out should be an int, \"\n                f\"not {cut_out.__class__.__name__}: '{cut_out}'\"\n            )\n\n        if (\n            self.cut_in is not None\n            and not self._updating_cut_in_cut_out\n            and cut_out < self.cut_in\n        ):\n            # lock the attribute update\n            self._updating_cut_in_cut_out = True\n            self.cut_in = cut_out\n            self._updating_cut_in_cut_out = False\n\n        return cut_out\n\n    @validates(\"source_in\")\n    def _validate_source_in(self, key: str, source_in: int) -> int:\n        \"\"\"Validate the source_in value.\n\n        Args:\n            key (str): The name of the validated column.\n            source_in (int): The `source_in` value to be validated.\n\n        Raises:\n            TypeError: If the `source_in` value is not an int.\n            ValueError: If the given `source_in` value is smaller than the `cut_in`\n                attribute value.\n            ValueError: If the given `source_in` value is larger than the `cut_out`\n                attribute value.\n            ValueError: If a `source_out` is given before and the `source_in` value is\n                larger than the `source_out` attribute value.\n\n        Returns:\n            int: The validated `source_in` value.\n        \"\"\"\n        if not isinstance(source_in, int):\n            raise TypeError(\n                f\"{self.__class__.__name__}.source_in should be an int, \"\n                f\"not {source_in.__class__.__name__}: '{source_in}'\"\n            )\n\n        if source_in < self.cut_in:\n            raise ValueError(\n                \"{cls}.source_in cannot be smaller than \"\n                \"{cls}.cut_in, cut_in: {cut_in} where as \"\n                \"source_in: {source_in}\".format(\n                    cls=self.__class__.__name__,\n                    cut_in=self.cut_in,\n                    source_in=source_in,\n                )\n            )\n\n        if source_in > self.cut_out:\n            raise ValueError(\n                \"{cls}.source_in cannot be bigger than \"\n                \"{cls}.cut_out, cut_out: {cut_out} where as \"\n                \"source_in: {source_in}\".format(\n                    cls=self.__class__.__name__,\n                    cut_out=self.cut_out,\n                    source_in=source_in,\n                )\n            )\n\n        if self.source_out and source_in > self.source_out:\n            raise ValueError(\n                \"{cls}.source_in cannot be bigger than \"\n                \"{cls}.source_out, source_in: {source_in} where \"\n                \"as source_out: {source_out}\".format(\n                    cls=self.__class__.__name__,\n                    source_out=self.source_out,\n                    source_in=source_in,\n                )\n            )\n\n        return source_in\n\n    @validates(\"source_out\")\n    def _validate_source_out(self, key: str, source_out: int) -> int:\n        \"\"\"Validate the source_out value.\n\n        Args:\n            key (str): The name of the validated column.\n            source_out (int): The source_out value to be validated.\n\n        Raises:\n            TypeError: If the source_out is not an integer.\n            ValueError: If the source_out is smaller than the cut_in attribute value.\n            ValueError: If the source_out is larger than the cut_out attribute value.\n            ValueError: If the source_in is not None and source_out is smaller than the\n                source_in value.\n\n        Returns:\n            int: The validated source_out value.\n        \"\"\"\n        if not isinstance(source_out, int):\n            raise TypeError(\n                f\"{self.__class__.__name__}.source_out should be an int, \"\n                f\"not {source_out.__class__.__name__}: '{source_out}'\"\n            )\n\n        if source_out < self.cut_in:\n            raise ValueError(\n                \"{cls}.source_out cannot be smaller than \"\n                \"{cls}.cut_in, cut_in: {cut_in} where as \"\n                \"source_out: {source_out}\".format(\n                    cls=self.__class__.__name__,\n                    cut_in=self.cut_in,\n                    source_out=source_out,\n                )\n            )\n\n        if source_out > self.cut_out:\n            raise ValueError(\n                \"{cls}.source_out cannot be bigger than \"\n                \"{cls}.cut_out, cut_out: {cut_out} where as \"\n                \"source_out: {source_out}\".format(\n                    cls=self.__class__.__name__,\n                    cut_out=self.cut_out,\n                    source_out=source_out,\n                )\n            )\n\n        if self.source_in and source_out < self.source_in:\n            raise ValueError(\n                \"{cls}.source_out cannot be smaller than \"\n                \"{cls}.source_in, source_in: {source_in} where \"\n                \"as source_out: {source_out}\".format(\n                    cls=self.__class__.__name__,\n                    source_in=self.source_in,\n                    source_out=source_out,\n                )\n            )\n\n        return source_out\n\n    # @validates('record_in')\n    # def _validate_record_in(self, key, record_in):\n    #     \"\"\"validates the given record_in value\n    #     \"\"\"\n    #     # we don't really care about the record in value right now.\n    #     # it can be set to anything\n    #     return record_in\n\n    @property\n    def cut_duration(self) -> int:\n        \"\"\"Return the cut_duration property value.\n\n        Returns:\n            int: The cut_duration property value.\n        \"\"\"\n        return self.cut_out - self.cut_in + 1\n\n    @cut_duration.setter\n    def cut_duration(self, cut_duration: int) -> None:\n        \"\"\"Set the cut_duration attribute.\n\n        Args:\n            cut_duration (int): The cut_duration value to be validated.\n\n        Raises:\n            TypeError: If the given cut_duration value is not an integer.\n            ValueError: If the given cut_duration value is not a positive integer.\n        \"\"\"\n        if not isinstance(cut_duration, int):\n            raise TypeError(\n                f\"{self.__class__.__name__}.cut_duration should be a positive \"\n                \"integer value, \"\n                f\"not {cut_duration.__class__.__name__}: '{cut_duration}'\"\n            )\n\n        if cut_duration < 1:\n            raise ValueError(\n                f\"{self.__class__.__name__}.cut_duration cannot be set to \"\n                \"zero or a negative value\"\n            )\n\n        # always extend or contract the shot from end\n        self.cut_out = self.cut_in + cut_duration - 1\n\n    @validates(\"sequence\")\n    def _validate_sequence(self, key: str, sequence: \"Sequence\") -> \"Sequence\":\n        \"\"\"Validate the given sequence value.\n\n        Args:\n            key (str): The name of the validated column.\n            sequence (Sequence): The sequence value to validate.\n\n        Raises:\n            TypeError: If the given sequence value is not a Sequence instance.\n\n        Returns:\n            Sequence: The validated Sequence instance.\n        \"\"\"\n        from stalker.models.sequence import Sequence\n\n        if sequence is not None and not isinstance(sequence, Sequence):\n            raise TypeError(\n                f\"{self.__class__.__name__}.sequence should be a \"\n                \"stalker.models.sequence.Sequence instance, \"\n                f\"not {sequence.__class__.__name__}: '{sequence}'\"\n            )\n        return sequence\n\n    @validates(\"scene\")\n    def _validate_scene(self, key: str, scene: \"Scene\") -> \"Scene\":\n        \"\"\"Validate the given scene value.\n\n        Args:\n            key (str): The name of the validated column.\n            scene (Scene): The scene value to validate.\n\n        Raises:\n            TypeError: If the given scene is not a Scene instance.\n\n        Returns:\n            Scene: The validated Scene instance.\n        \"\"\"\n        from stalker.models.scene import Scene\n\n        if scene is not None and not isinstance(scene, Scene):\n            raise TypeError(\n                f\"{self.__class__.__name__}.scene should be a \"\n                \"stalker.models.scene.Scene instance, \"\n                f\"not {scene.__class__.__name__}: '{scene}'\"\n            )\n        return scene\n\n    def _image_format_getter(self) -> ImageFormat:\n        \"\"\"Return image_format value from the Project or from the _image_format attr.\n\n        Returns:\n            ImageFormat: The ImageFormat instance from image_format attribute or from\n                the related Project's image_format attribute.\n        \"\"\"\n        if self._image_format is None:\n            return self.project.image_format\n        else:\n            return self._image_format\n\n    def _image_format_setter(self, imf: ImageFormat) -> None:\n        \"\"\"Set the image_format value.\n\n        Args:\n            imf (ImageFormat): The ImageFormat instance to set the image_format\n                attribute value.\n        \"\"\"\n        self._image_format = self._validate_image_format(imf)\n\n    image_format: Mapped[Optional[ImageFormat]] = synonym(\n        \"_image_format\",\n        descriptor=property(_image_format_getter, _image_format_setter),\n        doc=\"The image_format of this shot. Set it to None to re-sync with \"\n        \"Project.image_format.\",\n    )\n\n    def _validate_image_format(\n        self, imf: Union[None, ImageFormat]\n    ) -> Union[None, ImageFormat]:\n        \"\"\"Validate the given imf value.\n\n        Args:\n            imf (ImageFormat): The ImageFormat instance to validate.\n\n        Raises:\n            TypeError: If the given imf value is not an ImageFormat instance.\n\n        Returns:\n            ImageFormat: The validated ImageFormat instance.\n        \"\"\"\n        if imf is None:\n            # do not set it to anything it will automatically use the project\n            # image format\n            return None\n\n        if not isinstance(imf, ImageFormat):\n            raise TypeError(\n                f\"{self.__class__.__name__}.image_format should be an instance of \"\n                \"stalker.models.format.ImageFormat, \"\n                f\"not {imf.__class__.__name__}: '{imf}'\"\n            )\n\n        return imf\n\n    @validates(\"code\")\n    def _validate_code(self, key: str, code: str) -> str:\n        \"\"\"Validate the given code value.\n\n        Args:\n            key (str): The name of the validated column.\n            code (str): The code to validate.\n\n        Raises:\n            ValueError: If the code is not available.\n\n        Returns:\n            str: The validated code value.\n        \"\"\"\n        code = super(Shot, self)._validate_code(key, code)\n\n        # check code uniqueness\n        if code != self.code and not self._check_code_availability(code, self.project):\n            raise ValueError(f\"There is a Shot with the same code: {code}\")\n\n        return code\n"
  },
  {
    "path": "src/stalker/models/status.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Status and StatusList related functions and classes are situated here.\"\"\"\nfrom typing import Any, Dict, List, Optional, Type, Union\n\nfrom sqlalchemy import Column, ForeignKey, Integer, Table\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, validates\n\nfrom stalker.db.declarative import Base\nfrom stalker.db.session import DBSession\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity\nfrom stalker.models.mixins import CodeMixin, TargetEntityTypeMixin\n\nlogger = get_logger(__name__)\n\n\nclass Status(Entity, CodeMixin):\n    \"\"\"Defines object statutes.\n\n    No extra parameters, use the *code* attribute to give a short name for the\n    status.\n\n    A Status object can be compared with a string value and it will return if\n    the lower case name or lower case code of the status matches the lower case\n    form of the given string::\n\n    .. code-block:: Python\n\n        >>> from stalker import Status\n        >>> a_status = Status(name=\"On Hold\", code=\"OH\")\n        >>> a_status == \"on hold\"\n        True\n        >>> a_status != \"complete\"\n        True\n        >>> a_status == \"oh\"\n        True\n        >>> a_status == \"another status\"\n        False\n\n    Args:\n        name (str): The name long name of this Status.\n        code (str): The code of this Status, its generally the short version of\n            the name attribute.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Statuses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Status\"}\n    status_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    def __init__(\n        self,\n        name: Optional[str] = None,\n        code: Optional[str] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        kwargs[\"name\"] = name\n        kwargs[\"code\"] = code\n\n        super(Status, self).__init__(**kwargs)\n        self.code = code\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a Status instance and has the same\n                attributes.\n        \"\"\"\n        if isinstance(other, str):\n            return (\n                self.name.lower() == other.lower() or self.code.lower() == other.lower()\n            )\n        else:\n            return super(Status, self).__eq__(other) and isinstance(other, Status)\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Status, self).__hash__()\n\n\nclass StatusList(Entity, TargetEntityTypeMixin):\n    \"\"\"Type specific list of :class:`.Status` instances.\n\n    Holds multiple :class:`.Status` instances to be used as a choice list for several\n    other classes.\n\n    A StatusList can only be assigned to only one entity type. So a\n    :class:`.Project` can only have one suitable StatusList object which is\n    designed for :class:`.Project` entities.\n\n    The list of statuses in StatusList can be accessed by using a list like\n    indexing and it also supports string indexes only for getting the item,\n    you cannot set an item with string indices:\n\n    .. code-block:: Python\n\n        >>> from stalker import Status, StatusList\n        >>> status1 = Status(name=\"Complete\", code=\"CMPLT\")\n        >>> status2 = Status(name=\"Work in Progress\", code=\"WIP\")\n        >>> status3 = Status(name=\"Pending Review\", code=\"PRev\")\n        >>> a_status_list = StatusList(name=\"Asset Status List\",\n                                    statuses=[status1, status2, status3],\n                                    target_entity_type=\"Asset\")\n        >>> a_status_list[0]\n        <Status (Complete, CMPLT)>\n        >>> a_status_list[\"complete\"]\n        <Status (Complete, CMPLT)>\n        >>> a_status_list[\"WIP\"]\n        <Status (Work in Progress, WIP)>\n\n    Args:\n        statuses (List[Status]): This is a list of :class:`.Status` instances,\n            so you can prepare different StatusLists for different kind of\n            entities using the same pool of :class:`.Status` instances.\n\n        target_entity_type (str): use this parameter to specify the target entity\n            type that this StatusList is designed for. It accepts classes or names\n            of classes.\n\n        For example:\n\n        .. code-block:: Python\n\n            from stalker import Status, StatusList, Project\n\n            status_list = [\n                Status(name=\"Waiting To Start\", code=\"WTS\"),\n                Status(name=\"On Hold\", code=\"OH\"),\n                Status(name=\"In Progress\", code=\"WIP\"),\n                Status(name=\"Waiting Review\", code=\"WREV\"),\n                Status(name=\"Approved\", code=\"APP\"),\n                Status(name=\"Completed\", code=\"CMPLT\"),\n            ]\n\n            project_status_list = StatusList(\n                name=\"Project Status List\",\n                statuses=status_list,\n                target_entity_type=\"Project\"\n            )\n\n            # or\n            project_status_list = StatusList(\n                name=\"Project Status List\",\n                statuses=status_list,\n                target_entity_type=Project\n            )\n\n        now with the code above you cannot assign the ``project_status_list``\n        object to any other class than a ``Project`` object.\n\n        The StatusList instance can be empty, means it may not have anything in\n        its :attr:`.StatusList.statuses`. But it is useless. The validation for\n        empty statuses list is left to the SOM user.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"StatusLists\"\n    __mapper_args__ = {\"polymorphic_identity\": \"StatusList\"}\n\n    __unique_target__ = True\n\n    status_list_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    statuses: Mapped[Optional[List[Status]]] = relationship(\n        secondary=\"StatusList_Statuses\",\n        doc=\"List of :class:`.Status` objects, showing the possible statuses\",\n    )\n\n    def __init__(\n        self,\n        statuses: Optional[List[Status]] = None,\n        target_entity_type: Optional[Union[Type, str]] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        super(StatusList, self).__init__(**kwargs)\n        TargetEntityTypeMixin.__init__(self, target_entity_type, **kwargs)\n\n        if statuses is None:\n            statuses = []\n        self.statuses = statuses\n\n    @validates(\"statuses\")\n    def _validate_statuses(self, key: str, status: Status) -> Status:\n        \"\"\"Validate the given status value.\n\n        Args:\n            key (str): The name of the validated column.\n            status (Status): The status value to be validated.\n\n        Raises:\n            TypeError: If the status value is not a Status instance.\n\n        Returns:\n            Status: The validated status value.\n        \"\"\"\n        if not isinstance(status, Status):\n            raise TypeError(\n                f\"All of the elements in {self.__class__.__name__}.statuses must be an \"\n                \"instance of stalker.models.status.Status, \"\n                f\"not {status.__class__.__name__}: '{status}'\"\n            )\n        return status\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a StatusList instance and has the same\n                statuses, target_entity_type.\n        \"\"\"\n        return (\n            super(StatusList, self).__eq__(other)\n            and isinstance(other, StatusList)\n            and self.statuses == other.statuses\n            and self.target_entity_type == other.target_entity_type\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(StatusList, self).__hash__()\n\n    def __getitem__(self, key: int) -> Status:\n        \"\"\"Return the Status at the given key.\n\n        Args:\n            key (int): The index to return the value of.\n\n        Returns:\n            Status: The Status instance at the given index.\n        \"\"\"\n        return_item = None\n        with DBSession.no_autoflush:\n            if isinstance(key, str):\n                for item in self.statuses:\n                    if item == key:\n                        return_item = item\n                        break\n            else:\n                return_item = self.statuses[key]\n\n        return return_item\n\n    def __setitem__(self, key: int, value: Status) -> None:\n        \"\"\"Set the value at the given index.\n\n        Args:\n            key (int): The index to set the item value to.\n            value (Status): The Status instance to set at the given index.\n        \"\"\"\n        self.statuses[key] = value\n\n    def __delitem__(self, key: int) -> None:\n        \"\"\"Delete the item with the given key.\n\n        Args:\n            key (int): Remove the Status at the given index.\n        \"\"\"\n        del self.statuses[key]\n\n    def __len__(self) -> int:\n        \"\"\"Return the  number of Statuses in this StatusList.\n\n        Returns:\n            int: The number of Statuses in this StatusList.\n        \"\"\"\n        return len(self.statuses)\n\n\n# StatusList_Statuses Table\nStatusList_Statuses = Table(\n    \"StatusList_Statuses\",\n    Base.metadata,\n    Column(\"status_list_id\", Integer, ForeignKey(\"StatusLists.id\"), primary_key=True),\n    Column(\"status_id\", Integer, ForeignKey(\"Statuses.id\"), primary_key=True),\n)\n"
  },
  {
    "path": "src/stalker/models/structure.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Structure related functions and classes are situated here.\"\"\"\nfrom typing import Any, Dict, List, Optional, Union\n\nfrom sqlalchemy import Column, ForeignKey, Integer, Table, Text\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, validates\n\nfrom stalker.db.declarative import Base\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity\nfrom stalker.models.template import FilenameTemplate\n\nlogger = get_logger(__name__)\n\n\nclass Structure(Entity):\n    \"\"\"Defines folder structures for :class:`.Projects`.\n\n    Structures are generally owned by :class:`.Project` objects. Whenever a\n    :class:`.Project` is physically created, project folders are created by\n    looking at :attr:`.Structure.custom_template` of the :class:`.Structure`,\n    the :class:`.Project` object is generally given to the :class:`.Structure`.\n    So it is possible to use a variable like \"{{project}}\" or derived variables\n    like::\n\n      {% for seq in project.sequences %}\n          {{do something here}}\n\n    Every line of this rendered template will represent a folder and Stalker\n    will create these folders on the attached :class:`.Repository`.\n\n    Args:\n        templates (List[FilenameTemplate]): A list of :class:`.FilenameTemplate`\n            instances which defines a specific template for the given\n            :attr:`.FilenameTemplate.target_entity_type` values.\n\n        custom_template (str): A string containing several lines of folder\n            names. The folders are relative to the :class:`.Project` root. It\n            can also contain a Jinja2 Template code. Which will be rendered to\n            show the list of folders to be created with the project. The Jinja2\n            Template is going to have the {{project}} variable. The important\n            point to be careful about is to list all the custom folders of the\n            project in a new line in this string. For example a\n            :class:`.Structure` for a :class:`.Project` can have the following\n            :attr:`.Structure.custom_template`::\n\n            .. code-block:: Jinja\n\n                ASSETS\n                {% for asset in project.assets %}\n                    {% set asset_root = 'ASSETS/' + asset.code %}\n                    {{asset_root}}\n\n                    {% for task in asset.tasks %}\n                        {% set task_root = asset_root + '/' + task.code %}\n                        {{task_root}}\n\n                SEQUENCES\n                {% for seq in project.sequences %}}\n                    {% set seq_root = 'SEQUENCES/' + {{seq.code}} %}\n                    {{seq_root}}/Edit\n                    {{seq_root}}/Edit/Export\n                    {{seq_root}}/Storyboard\n\n                    {% for shot in seq.shots %}\n                        {% set shot_root = seq_root + '/SHOTS/' + shot.code %}\n                        {{shot_root}}\n\n                        {% for task in shot.tasks %}\n                            {% set task_root = shot_root + '/' + task.code %}\n                            {{task_root}}\n\n            The above example has gone far beyond deep than it is needed, where\n            it started to define paths for :class:`.Asset` s. Even it is\n            possible to create a :class:`.Project` structure like that, in\n            general it is unnecessary. Because the above folders are going to\n            be created but they are probably going to be empty for a while,\n            because the :class:`.Asset` s are not created yet (or in fact no\n            :class:`.Version` instances are created for the :class:`.Task` s).\n            Anyway, it is much suitable and desired to create this details by\n            using :class:`.FilenameTemplate` objects. Which are specific to\n            certain :attr:`.FilenameTemplate.target_entity_type` s. And by\n            using the :attr:`.Structure.custom_template` attribute, Stalker\n            cannot place any source or output file of a :class:`.Version` in\n            the :class:`.Repository` where as it can by using\n            :class:`.FilenameTemplate` s.\n\n            But for certain types of :class:`.Task` s it is may be good to\n            previously create the folder structure just because in certain\n            environments (programs) it is not possible to run a Python code\n            that will place the file in to the Repository like in Photoshop.\n\n            The ``custom_template`` parameter can be None or an empty string if\n            it is not needed.\n\n            A :class:`.Structure` cannot be created without a ``type``\n            (__strictly_typed__ = True). By giving a ``type`` to the\n            :class:`.Structure`, you can create one structure for\n            **Commercials** and another project structure for **Movies** and\n            another one for **Print** projects etc. and can reuse them with new\n            :class:`.Project` s.\n    \"\"\"\n\n    # __strictly_typed__ = True\n    __auto_name__ = False\n    __tablename__ = \"Structures\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Structure\"}\n\n    structure_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    templates: Mapped[Optional[List[FilenameTemplate]]] = relationship(\n        secondary=\"Structure_FilenameTemplates\"\n    )\n\n    custom_template: Mapped[Optional[str]] = mapped_column(\"custom_template\", Text)\n\n    def __init__(\n        self,\n        templates: Optional[List[FilenameTemplate]] = None,\n        custom_template: Optional[str] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        super(Structure, self).__init__(**kwargs)\n\n        if templates is None:\n            templates = []\n\n        self.templates = templates\n        self.custom_template = custom_template\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a Structure instance and has the same\n                templates, custom_template.\n        \"\"\"\n        return (\n            super(Structure, self).__eq__(other)\n            and isinstance(other, Structure)\n            and self.templates == other.templates\n            and self.custom_template == other.custom_template\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Structure, self).__hash__()\n\n    @validates(\"custom_template\")\n    def _validate_custom_template(\n        self, key: str, custom_template: Union[None, str]\n    ) -> str:\n        \"\"\"Validate the given custom_template value.\n\n        Args:\n            key (str): The name of the validated column.\n            custom_template (Union[None, str]): The custom template value to be\n                validated.\n\n        Raises:\n            TypeError: If the given custom_template value is not a str.\n\n        Returns:\n            str: The validated `custom_template` value.\n        \"\"\"\n        if custom_template is None:\n            custom_template = \"\"\n\n        if not isinstance(custom_template, str):\n            raise TypeError(\n                \"{}.custom_template should be a string, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    custom_template.__class__.__name__,\n                    custom_template,\n                )\n            )\n        return custom_template\n\n    @validates(\"templates\")\n    def _validate_templates(\n        self, key: str, template: FilenameTemplate\n    ) -> FilenameTemplate:\n        \"\"\"Validate the given template value.\n\n        Args:\n            key (str): The name of the validated column.\n            template (FilenameTemplate): The validated template value.\n\n        Raises:\n            TypeError: If the given template value is not a FilenameTemplate\n                instance.\n\n        Returns:\n            FilenameTemplate: Return the validated template value.\n        \"\"\"\n        if not isinstance(template, FilenameTemplate):\n            raise TypeError(\n                f\"{self.__class__.__name__}.templates should only contain \"\n                \"instances of stalker.models.template.FilenameTemplate, \"\n                f\"not {template.__class__.__name__}: '{template}'\"\n            )\n\n        return template\n\n\n# Structure_FilenameTemplates Table\nStructure_FilenameTemplates = Table(\n    \"Structure_FilenameTemplates\",\n    Base.metadata,\n    Column(\"structure_id\", Integer, ForeignKey(\"Structures.id\"), primary_key=True),\n    Column(\n        \"filenametemplate_id\",\n        Integer,\n        ForeignKey(\"FilenameTemplates.id\"),\n        primary_key=True,\n    ),\n)\n"
  },
  {
    "path": "src/stalker/models/studio.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Studio, WorkingHours and Vacation related functions and classes are situated here.\"\"\"\n\nimport copy\nimport datetime\nimport time\nfrom math import ceil\nfrom typing import Any, Dict, List, Optional, Union\n\nimport pytz\n\nfrom sqlalchemy import ForeignKey, Interval, Text\nfrom sqlalchemy.orm import (\n    Mapped,\n    mapped_column,\n    reconstructor,\n    relationship,\n    synonym,\n    validates,\n)\n\nfrom stalker import defaults, log\nfrom stalker.db.session import DBSession\nfrom stalker.db.types import GenericDateTime, GenericJSON\nfrom stalker.models.auth import User\nfrom stalker.models.department import Department\nfrom stalker.models.entity import Entity, SimpleEntity\nfrom stalker.models.mixins import DateRangeMixin, WorkingHoursMixin\nfrom stalker.models.project import Project\nfrom stalker.models.schedulers import SchedulerBase\nfrom stalker.models.status import Status\n\n\nlogger = log.get_logger(__name__)\n\n\nclass Studio(Entity, DateRangeMixin, WorkingHoursMixin):\n    \"\"\"Manage all the studio information at once.\n\n    With Stalker, you can manage all you Studio data by using this class. Studio\n    knows all the projects, all the departments, all the users and every thing\n    about the studio. But the most important part of the Studio is that it can\n    schedule all the Projects by using TaskJuggler.\n\n    Studio class is kind of the database itself::\n\n      studio = Studio()\n\n      # simple data\n      studio.projects\n      studio.active_projects\n      studio.inactive_projects\n      studio.departments\n      studio.users\n\n      # project management\n      studio.to_tjp          # a tjp representation of the studio with all\n                             # its projects, departments and resources etc.\n\n      studio.schedule() # schedules all the active projects at once\n\n    **Working Hours**\n\n    In Stalker, Studio class also manages the working hours of the studio.\n    Allowing project tasks to be scheduled to be scheduled in those hours.\n\n    **Vacations**\n\n    Studio wide vacations are managed by the Studio class.\n\n    **Scheduling**\n\n    .. versionadded: 0.2.5\n       Schedule Info Attributes\n\n    There are a couple of attributes those become pretty interesting when used\n    together with the Studio instance while using the scheduling part of the\n    Studio. Please refer to the attribute documentation for each attribute:\n\n      :attr:`.is_scheduling`\n      :attr:`.last_scheduled_at`\n      :attr:`.last_scheduled_by`\n      :attr:`.last_schedule_message`\n\n    Args:\n        daily_working_hours (int): An integer specifying the daily working\n            hours for the studio. It is another critical value attribute which\n            TaskJuggler uses mainly converting working day values to working hours\n            (1d = 10h kind of thing).\n        now (datetime.datetime): The now attribute overrides the TaskJugglers ``now``\n            attribute allowing the user to schedule the projects as if the scheduling is\n            done on that date. The default value is the rounded value of\n            datetime.datetime.now(pytz.utc).\n        timing_resolution (datetime.timedelta): The timing_resolution of the\n            datetime.datetime object in datetime.timedelta. Uses ``timing_resolution``\n            settings in the :class:`stalker.config.Config` class which defaults to 1\n            hour. Setting the timing_resolution to less then 5 minutes is not suggested\n            because it is a limit for TaskJuggler.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Studios\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Studio\"}\n\n    studio_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    _timing_resolution: Mapped[Optional[datetime.timedelta]] = mapped_column(\n        \"timing_resolution\", Interval\n    )\n\n    is_scheduling: Mapped[Optional[bool]] = mapped_column(default=False)\n    is_scheduling_by_id: Mapped[Optional[int]] = mapped_column(\n        ForeignKey(\"Users.id\"),\n        doc=\"The id of the user who is scheduling the Studio projects right \" \"now\",\n    )\n    is_scheduling_by: Mapped[Optional[User]] = relationship(\n        primaryjoin=\"Studios.c.is_scheduling_by_id==Users.c.id\",\n        doc=\"The User who is scheduling the Studio projects right now\",\n    )\n    scheduling_started_at: Mapped[Optional[datetime.datetime]] = mapped_column(\n        GenericDateTime,\n        doc=\"Stores when the current scheduling is started at, it is a good \"\n        \"measure for measuring if the last schedule is not correctly \"\n        \"finished\",\n    )\n    last_scheduled_at: Mapped[Optional[datetime.datetime]] = mapped_column(\n        GenericDateTime, doc=\"Stores the last schedule date\"\n    )\n    last_scheduled_by_id: Mapped[Optional[int]] = mapped_column(\n        ForeignKey(\"Users.id\"),\n        doc=\"The id of the user who has last scheduled the Studio projects\",\n    )\n    last_scheduled_by: Mapped[Optional[User]] = relationship(\n        primaryjoin=\"Studios.c.last_scheduled_by_id==Users.c.id\",\n        doc=\"The User who has last scheduled the Studio projects\",\n    )\n    last_schedule_message: Mapped[Optional[str]] = mapped_column(\n        Text,\n        doc=\"Holds the last schedule message, generally coming generated by \"\n        \"TaskJuggler\",\n    )\n\n    def __init__(\n        self,\n        daily_working_hours: Optional[int] = None,\n        now: Optional[datetime.datetime] = None,\n        timing_resolution: Optional[datetime.timedelta] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        super(Studio, self).__init__(**kwargs)\n        DateRangeMixin.__init__(self, **kwargs)\n        WorkingHoursMixin.__init__(self, **kwargs)\n        self.timing_resolution = timing_resolution\n        self.daily_working_hours = daily_working_hours\n        self._now = None\n        self.now = self._validate_now(now)\n        self._scheduler = None\n\n        # update defaults\n        self.update_defaults()\n\n    @property\n    def daily_working_hours(self) -> int:\n        \"\"\"Return the Studio.working_hours.daily_working_hours.\n\n        Returns:\n            int: The daily working hours of this Studio.\n        \"\"\"\n        return self.working_hours.daily_working_hours\n\n    @daily_working_hours.setter\n    def daily_working_hours(self, daily_working_hours: int) -> None:\n        \"\"\"Set the Studio.working_hours.daily_working_hours.\n\n        Args:\n            daily_working_hours (int): The daily working hours in this studio.\n        \"\"\"\n        self.working_hours.daily_working_hours = daily_working_hours\n\n    def update_defaults(self) -> None:\n        \"\"\"Update the default values with the studio.\"\"\"\n        # TODO: add update_defaults() to attribute edit/update methods,\n        #       so we will always have an up to date info about the working\n        #       hours.\n\n        logger.debug(\"updating defaults with Studio instance\")\n        logger.debug(\"defaults: {}\".format(defaults))\n        logger.debug(\"id(defaults): {}\".format(id(defaults)))\n\n        defaults[\"daily_working_hours\"] = self.daily_working_hours\n        logger.debug(\n            \"updated defaults.daily_working_hours: {}\".format(\n                defaults.daily_working_hours\n            )\n        )\n\n        defaults[\"weekly_working_days\"] = self.weekly_working_days\n        logger.debug(\n            f\"updated defaults.weekly_working_days: {defaults.weekly_working_days}\"\n        )\n\n        defaults[\"weekly_working_hours\"] = self.weekly_working_hours\n        logger.debug(\n            \"updated defaults.weekly_working_hours: {}\".format(\n                defaults.weekly_working_hours\n            )\n        )\n\n        defaults[\"yearly_working_days\"] = self.yearly_working_days\n        logger.debug(\n            f\"updated defaults.yearly_working_days: {defaults.yearly_working_days}\"\n        )\n\n        defaults[\"timing_resolution\"] = self.timing_resolution\n        logger.debug(\n            f\"updated defaults.timing_resolution: {defaults.timing_resolution}\"\n        )\n\n        logger.debug(\n            \"\"\"done updating defaults:\n        daily_working_hours  : {daily_working_hours}\n        weekly_working_days  : {weekly_working_days}\n        weekly_working_hours : {weekly_working_hours}\n        yearly_working_days  : {yearly_working_days}\n        timing_resolution    : {timing_resolution}\n        \"\"\".format(\n                daily_working_hours=defaults.daily_working_hours,\n                weekly_working_days=defaults.weekly_working_days,\n                weekly_working_hours=defaults.weekly_working_hours,\n                yearly_working_days=defaults.yearly_working_days,\n                timing_resolution=defaults.timing_resolution,\n            )\n        )\n\n    @reconstructor\n    def __init_on_load__(self) -> None:\n        \"\"\"Update defaults on load.\"\"\"\n        self.update_defaults()\n\n    def _validate_now(self, now: datetime.datetime) -> datetime.datetime:\n        \"\"\"Validate the given now value.\n\n        Args:\n            now (Union[None, datetime.datetime]): Either None in which the current\n                date and time will be used or a datetime.datetime instance.\n\n        Raises:\n            TypeError: If the now value is not None and not a datetime.datetime\n                instance.\n\n        Returns:\n            datetime.datetime: The validated datetime.datetime value.\n        \"\"\"\n        if now is None:\n            now = datetime.datetime.now(pytz.utc)\n\n        if not isinstance(now, datetime.datetime):\n            raise TypeError(\n                \"{}.now attribute should be an instance of datetime.datetime, \"\n                \"not {}: '{}'\".format(\n                    self.__class__.__name__, now.__class__.__name__, now\n                )\n            )\n\n        return self.round_time(now)\n\n    @property\n    def now(self) -> datetime.datetime:\n        \"\"\"Return the currently stored now value.\n\n        Returns:\n            datetime.datetime: Return the currently stored now value if there is any,\n                return the current date and time otherwise.\n        \"\"\"\n        if self._now is None:\n            self._now = self.round_time(datetime.datetime.now(pytz.utc))\n        return self._now\n\n    @now.setter\n    def now(self, now: datetime.datetime) -> None:\n        \"\"\"Set the current date and time.\n\n        Args:\n            now (datetime.datetime): The datetime.datetime instance showing the current\n                date and time, useful for project management purposes before\n                scheduling.\n        \"\"\"\n        self._now = self._validate_now(now)\n\n    def _validate_scheduler(\n        self, scheduler: Union[None, SchedulerBase]\n    ) -> Union[None, SchedulerBase]:\n        \"\"\"Validate the given scheduler value.\n\n        Args:\n            scheduler (Union[None, SchedulerBase]): The scheduler to be used to schedule\n                the projects in this Studio instance. Can be set to None to disable the\n                scheduling abilities.\n\n        Raises:\n            TypeError: If the given scheduler value is not None and is not a\n                SchedulerBase instance.\n\n        Returns:\n            Union[None, SchedulerBase]: The validated scheduler value.\n        \"\"\"\n        if scheduler is not None and not isinstance(scheduler, SchedulerBase):\n            raise TypeError(\n                \"{}.scheduler should be an instance of \"\n                \"stalker.models.scheduler.SchedulerBase, not {}: '{}'\".format(\n                    self.__class__.__name__, scheduler.__class__.__name__, scheduler\n                )\n            )\n        return scheduler\n\n    @property\n    def scheduler(self) -> Union[None, SchedulerBase]:\n        \"\"\"Return the scheduler.\n\n        Returns:\n            Union[None, SchedulerBase]: The scheduler of this Studio.\n        \"\"\"\n        return self._scheduler\n\n    @scheduler.setter\n    def scheduler(self, scheduler: Union[None, SchedulerBase]):\n        \"\"\"Set the scheduler.\n\n        Args:\n            scheduler (Union[None, SchedulerBase]): The SchedulerBase derivative as\n                the scheduler.\n        \"\"\"\n        self._scheduler = self._validate_scheduler(scheduler)\n\n    @property\n    def to_tjp(self) -> str:\n        \"\"\"Convert the studio to a tjp representation.\n\n        Returns:\n            str: The TaskJuggler representation of this Studio.\n        \"\"\"\n        start = time.time()\n\n        tab = \"    \"\n        indent = tab\n        now = self.round_time(self.now).astimezone(pytz.utc).strftime(\"%Y-%m-%d-%H:%M\")\n        tjp = (\n            f'project {self.tjp_id} \"{self.tjp_id}\" '\n            f\"{self.start.date()} - {self.end.date()} {{\"\n        )\n        timing_resolution = (\n            self.timing_resolution.days * 86400 + self.timing_resolution.seconds // 60\n        )\n        tjp += f\"\\n{indent}timingresolution {timing_resolution}min\"\n        tjp += f\"\\n{indent}now {now}\"\n        tjp += f\"\\n{indent}dailyworkinghours {self.daily_working_hours}\"\n        tjp += f\"\\n{indent}weekstartsmonday\"\n\n        # working hours\n        tjp += \"\\n\"\n        tjp += \"\\n\".join(\n            f\"{indent}{line}\" for line in self.working_hours.to_tjp.split(\"\\n\")\n        )\n\n        tjp += f'\\n{indent}timeformat \"%Y-%m-%d\"'\n        tjp += f'\\n{indent}scenario plan \"Plan\"'\n        tjp += f\"\\n{indent}trackingscenario plan\"\n        tjp += \"\\n}\"\n\n        end = time.time()\n        logger.debug(\"render studio to tjp took: {:0.3f} seconds\".format(end - start))\n        return tjp\n\n    @property\n    def projects(self) -> List[Project]:\n        \"\"\"Returns all the projects in the studio.\n\n        Returns:\n            List[Project]: List of all the Project instances in this Studio.\n        \"\"\"\n        return Project.query.all()\n\n    @property\n    def active_projects(self) -> List[Project]:\n        \"\"\"Return all the active projects in the studio.\n\n        Returns:\n            List[Project]: List of active Project instances in this studio.\n        \"\"\"\n        with DBSession.no_autoflush:\n            wip = Status.query.filter_by(code=\"WIP\").first()\n        return Project.query.filter(Project.status == wip).all()\n\n    @property\n    def inactive_projects(self) -> List[Project]:\n        \"\"\"Return all the inactive projects in the studio.\n\n        Returns:\n            List[Project]: List of inactive Project instances in this studio.\n        \"\"\"\n        with DBSession.no_autoflush:\n            wip = Status.query.filter_by(code=\"WIP\").first()\n        return Project.query.filter(Project.status != wip).all()\n\n    @property\n    def departments(self) -> List[Department]:\n        \"\"\"Return all the departments in the studio.\n\n        Returns:\n            List[Department]: The list of Department instances in this Studio.\n        \"\"\"\n        return Department.query.all()\n\n    @property\n    def users(self) -> List[User]:\n        \"\"\"Return all the users in the studio.\n\n        Returns:\n            List[User]: List of User instances in the studio.\n        \"\"\"\n        return User.query.all()\n\n    @property\n    def vacations(self) -> List[\"Vacation\"]:\n        \"\"\"Return all Vacations which doesn't have a User defined.\n\n        Returns:\n            List[Vacation]: List of Vacation instances.\n        \"\"\"\n        return Vacation.query.filter(Vacation.user == None).all()  # noqa: E711\n\n    def schedule(self, scheduled_by: Optional[User] = None) -> str:\n        \"\"\"Schedule all the active projects in the studio.\n\n        Needs a Scheduler, so before calling it set a scheduler by using the\n        :attr:`.scheduler` attribute.\n\n        Args:\n            scheduled_by (stalker.models.auth.User): A User instance who is doing the\n                scheduling.\n\n        Raises:\n            RuntimeError: If the `self.scheduler` is None or it is not a `SchedulerBase`\n                instance.\n\n        Returns:\n            str: The result of the scheduling process.\n        \"\"\"\n        # check the scheduler first\n        if self.scheduler is None or not isinstance(self.scheduler, SchedulerBase):\n            raise RuntimeError(\n                \"There is no scheduler for this {cls}, please assign a scheduler to \"\n                \"the {cls}.scheduler attribute, before calling {cls}.schedule()\".format(\n                    cls=self.__class__.__name__\n                )\n            )\n\n        with DBSession.no_autoflush:\n            self.scheduling_started_at = datetime.datetime.now(pytz.utc)\n\n            # run the scheduler\n            self.scheduler.studio = self\n        start = time.time()\n\n        # commit before scheduling\n        # DBSession.commit()\n\n        result = None\n        try:\n            result = self.scheduler.schedule()\n        finally:\n            # in any case set is_scheduling to False\n            with DBSession.no_autoflush:\n                self.is_scheduling = False\n                self.is_scheduling_by = None\n\n                # also store the result\n                # if result:\n                self.last_schedule_message = result\n\n                # And the date the schedule is completed\n                self.last_scheduled_at = datetime.datetime.now(pytz.utc)\n\n                # and who has done the scheduling\n                if scheduled_by:\n                    logger.debug(f\"setting last_scheduled_by to : {scheduled_by}\")\n                    self.last_scheduled_by = scheduled_by\n\n        end = time.time()\n        logger.debug(\"scheduling took {:0.3f} seconds\".format(end - start))\n        return result\n\n    @property\n    def weekly_working_hours(self) -> int:\n        \"\"\"Return the WorkingHours.weekly_working_hours value.\n\n        Returns:\n            int: The weekly working hours value stored in the working hours\n                configuration of this Studio instance.\n        \"\"\"\n        return self.working_hours.weekly_working_hours\n\n    @property\n    def weekly_working_days(self) -> int:\n        \"\"\"Return the WorkingHours.weekly_working_hours value.\n\n        Returns:\n            int: The weekly working days value stored in the working hours\n                configuration of this Studio instance.\n        \"\"\"\n        return self.working_hours.weekly_working_days\n\n    @property\n    def yearly_working_days(self) -> int:\n        \"\"\"Return the WorkingHours.yearly_working_days value.\n\n        Returns:\n            int: The yearly working days in the working hours configuration of this\n                Studio instance.\n        \"\"\"\n        return self.working_hours.yearly_working_days\n\n    def to_unit(\n        self,\n        from_timing: int,\n        from_unit: str,\n        to_unit: str,\n        working_hours: bool = True,\n    ) -> int:\n        \"\"\"Convert the given timing and unit to the desired unit.\n\n        If working_hours=True then the given timing is considered as working hours.\n\n        Args:\n            from_timing (int): The timing value.\n            from_unit (str): The timing unit.\n            to_unit (str): The other timing unit to convert the given timing unit to.\n            working_hours (bool): True to consider the given from timing as a working\n                hour. Default is True.\n\n        Raises:\n            NotImplementedError: Unless it is implemented.\n        \"\"\"\n        raise NotImplementedError(\"this is not implemented yet\")\n\n    def _timing_resolution_getter(self) -> datetime.timedelta:\n        \"\"\"Return the timing_resolution value.\n\n        Returns:\n            datetime.timedelta: The timing resolution stored in this Studio instance.\n        \"\"\"\n        return self._timing_resolution\n\n    def _timing_resolution_setter(self, timing_resolution: datetime.timedelta) -> None:\n        \"\"\"Set the timing_resolution.\n\n        Args:\n            timing_resolution (datetime.timedelta): The `timing_resolution` instance to\n                validate.\n        \"\"\"\n        self._timing_resolution = self._validate_timing_resolution(timing_resolution)\n        logger.debug(f\"self._timing_resolution: {self._timing_resolution}\")\n        # update date values\n        if self.start and self.end and self.duration:\n            self._start, self._end, self._duration = self._validate_dates(\n                self.round_time(self.start), self.round_time(self.end), None\n            )\n\n    timing_resolution: Mapped[Optional[datetime.timedelta]] = synonym(\n        \"_timing_resolution\",\n        descriptor=property(\n            _timing_resolution_getter,\n            _timing_resolution_setter,\n            doc=\"\"\"The timing_resolution of this object.\n\n            Can be set to any value that is representable with\n            datetime.timedelta. The default value is 1 hour. Whenever it is\n            changed the start, end and duration values will be updated.\n            \"\"\",\n        ),\n    )\n\n    def _validate_timing_resolution(\n        self, timing_resolution: datetime.timedelta\n    ) -> datetime.timedelta:\n        \"\"\"Validate the given timing_resolution value.\n\n        Args:\n            timing_resolution (datetime.timedelta): The timing resolution value as a\n                `datetime.timedelta` instance.\n\n        Raises:\n            TypeError: If the given `timing_resolution` is not a `datetime.timedelta`\n                instance.\n\n        Returns:\n            datetime.timedelta: The validated timing resolution instance.\n        \"\"\"\n        if timing_resolution is None:\n            timing_resolution = defaults.timing_resolution\n\n        if not isinstance(timing_resolution, datetime.timedelta):\n            raise TypeError(\n                \"{}.timing_resolution should be an instance of \"\n                \"datetime.timedelta, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    timing_resolution.__class__.__name__,\n                    timing_resolution,\n                )\n            )\n\n        return timing_resolution\n\n\nclass WorkingHours(Entity):\n    \"\"\"A helper class to manage Studio working hours.\n\n    Working hours is a data class to store the weekly working hours pattern of\n    the studio.\n\n    The data stored as a dictionary with the short day names are used as the\n    key and the value is a list of two integers showing the working hours\n    interval as the minutes after midnight. This is done in that way to ease\n    the data transfer to TaskJuggler. The default value is defined in\n    :class:`stalker.config.Config` ::\n\n      wh = WorkingHours()\n      wh.working_hours = {\n          'mon': [[540, 720], [820, 1080]], # 9:00 - 12:00, 13:00 - 18:00\n          'tue': [[540, 720], [820, 1080]], # 9:00 - 12:00, 13:00 - 18:00\n          'wed': [[540, 720], [820, 1080]], # 9:00 - 12:00, 13:00 - 18:00\n          'thu': [[540, 720], [820, 1080]], # 9:00 - 12:00, 13:00 - 18:00\n          'fri': [[540, 720], [820, 1080]], # 9:00 - 12:00, 13:00 - 18:00\n          'sat': [], # saturday off\n          'sun': [], # sunday off\n      }\n\n    The default value is 9:00 - 18:00 from Monday to Friday and Saturday and\n    Sunday are off.\n\n    The working hours can be updated by the user supplied dictionary. If the\n    user supplied dictionary doesn't have all the days then the default values\n    will be used for those days.\n\n    It is possible to use day index and day short names as a key value to reach\n    the data::\n\n      from stalker import config\n      defaults = config.Config()\n\n      wh = WorkingHours()\n\n      # this is same by doing wh.working_hours['sun']\n      assert wh['sun'] == defaults.working_hours['sun']\n\n      # you can reach the data using the weekday number as index\n      assert wh[0] == defaults.working_hours['mon']\n\n      # working hours of sunday if defaults are used or any other day defined\n      # by the stalker.config.Config.day_order\n      assert wh[0] == defaults.working_hours[defaults.day_order[0]]\n\n    Args:\n        working_hours (Union[None, dict]): The dictionary that shows the\n            working hours. The keys of the dictionary should be one of ['mon',\n            'tue', 'wed', 'thu', 'fri', 'sat', 'sun']. And the values should be\n            a list of two integers like [[int, int], [int, int], ...] format,\n            showing the minutes after midnight. For missing days the default\n            value will be used. If skipped the default value is going to be\n            used.\n        daily_working_hours (Union[None, int]): The daily working hours value.\n            If given None the default value will be used.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"WorkingHours\"\n    __mapper_args__ = {\"polymorphic_identity\": \"WorkingHours\"}\n\n    working_hours_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    working_hours: Mapped[Optional[Dict[str, List]]] = mapped_column(GenericJSON)\n    daily_working_hours: Mapped[Optional[int]] = mapped_column(\n        default=defaults.daily_working_hours\n    )\n\n    def __init__(\n        self,\n        working_hours: Optional[Dict[str, List]] = None,\n        daily_working_hours=None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        super(WorkingHours, self).__init__(**kwargs)\n        if working_hours is None:\n            working_hours = defaults.working_hours\n        self.working_hours = working_hours\n        self.daily_working_hours = daily_working_hours\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a WorkingHours instance and has\n                the same working_hours.\n        \"\"\"\n        return (\n            isinstance(other, WorkingHours)\n            and other.working_hours == self.working_hours\n        )\n\n    def __getitem__(self, index: Union[int, str]) -> Optional[List]:\n        \"\"\"Return the item at the given index.\n\n        Args:\n            index (Union[int, str]): Either an integer representing the weekday starting\n                from Monday:0 or a string value of a shorthand day name one of\n                [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\", \"sun\"].\n\n        Returns:\n            List[int, int]: The daily working hour arranged in a list where the first\n                item is the minute from the midnight of the start of the working hour\n                and the second item is the minute from the midnight of the end of the\n                daily working hour. As in [540, 1080] represents 9am to 6pm.\n        \"\"\"\n        if isinstance(index, int):\n            return self.working_hours[defaults.day_order[index]]\n        elif isinstance(index, str):\n            return self.working_hours[index]\n\n    def __setitem__(self, key: Union[int, str], value: List[List]) -> None:\n        \"\"\"Set the item value at the given index.\n\n        Args:\n            key (Union[int, str]): The index to set the value of or the day name.\n            value (List[List[int, int]]): The working hours data arranged in a list of\n                lists of two integers.\n\n        Raises:\n            KeyError: If the given key value is not one of the day names.\n        \"\"\"\n        self._validate_working_hours_value(value)\n        if isinstance(key, int):\n            self.working_hours[defaults.day_order[key]] = value\n        elif isinstance(key, str):\n            # check if key is in\n            if key not in defaults.day_order:\n                raise KeyError(\n                    \"{} accepts only {} as key, not '{}'\".format(\n                        self.__class__.__name__, defaults.day_order, key\n                    )\n                )\n            self.working_hours[key] = value\n\n    @validates(\"working_hours\")\n    def _validate_working_hours(self, key: str, working_hours: Dict[str, List]) -> dict:\n        \"\"\"Validate the given working hours value.\n\n        Args:\n            key (str): The name of the validated column.\n            working_hours (dict): The working hours value to be validated.\n\n        Raises:\n            TypeError: If the given working hours is not a dictionary.\n            TypeError: If the values in the working hours dictionary are not lists.\n\n        Returns:\n            dict: The validated working hours dictionary.\n        \"\"\"\n        if not isinstance(working_hours, dict):\n            raise TypeError(\n                \"{}.working_hours should be a dictionary, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    working_hours.__class__.__name__,\n                    working_hours,\n                )\n            )\n\n        for day in working_hours:\n            if not isinstance(working_hours[day], list):\n                raise TypeError(\n                    '{}.working_hours should be a dictionary with keys \"mon, '\n                    'tue, wed, thu, fri, sat, sun\" and the values should a '\n                    \"list of lists of two integers like [[540, 720], [800, \"\n                    \"1080]], not {}: '{}'\".format(\n                        self.__class__.__name__,\n                        working_hours[day].__class__.__name__,\n                        working_hours[day],\n                    )\n                )\n\n            # validate item values\n            self._validate_working_hours_value(working_hours[day])\n\n        # update the default values with the supplied working_hour dictionary\n        # copy the defaults\n        wh_def = copy.copy(defaults.working_hours)\n        # update them\n        wh_def.update(working_hours)\n\n        return wh_def\n\n    def is_working_hour(self, check_for_date: datetime.datetime) -> bool:\n        \"\"\"Check if the given datetime is in working hours.\n\n        Args:\n            check_for_date (datetime.datetime): The time value to check if it\n                is a working hour.\n\n        Returns:\n            bool: True if the given datetime coincides to a working hour, False\n                otherwise.\n        \"\"\"\n        weekday_nr = check_for_date.weekday()\n        hour = check_for_date.hour\n        minute = check_for_date.minute\n\n        time_from_midnight = hour * 60 + minute\n\n        # check if the hour is inside the working hour ranges\n        logger.debug(f\"checking for: {time_from_midnight}\")\n        logger.debug(f\"self[weekday_nr]: {self[weekday_nr]}\")\n        for working_hour_groups in self[weekday_nr]:\n            start = working_hour_groups[0]\n            end = working_hour_groups[1]\n            logger.debug(f\"start       : {start}\")\n            logger.debug(f\"end         : {end}\")\n            if start <= time_from_midnight < end:\n                return True\n\n        return False\n\n    def _validate_working_hours_value(self, value: List) -> List:\n        \"\"\"Validate the working hour value.\n\n        The given value should follow the following format:\n\n        .. code-block:: python\n\n            working_hours = [\n                [540, 1080],  # Working hour in minutes for Monday\n                [540, 1080],  # Working hour in minutes for Tuesday\n                [540, 1080],  # Working hour in minutes for Wednesday\n                [540, 1080],  # Working hour in minutes for Thursday\n                [540, 1080],  # Working hour in minutes for Friday\n                [0, 0],  # Working hour in minutes for Saturday\n                [0, 0],  # Working hour in minutes for Sunday\n            ]\n\n        Args:\n            value (List): The validated working hour value.\n\n        Raises:\n            TypeError: If the given value is not a list.\n            TypeError: If the immediate items in the list is not a list.\n            TypeError: If the length of the items in the given list is not 2.\n            TypeError: If the items in the lists inside the list are not integers.\n            ValueError: If the integer values in the secondary lists are smaller than 0\n                or larger than 1440 (which is 24 * 60).\n\n        Returns:\n            List[List[int, int]]\n        \"\"\"\n        err = (\n            \"{}.working_hours value should be a list of lists of two \"\n            \"integers and the range of integers should be between 0-1440, \"\n            \"not {}: '{}'\".format(\n                self.__class__.__name__, value.__class__.__name__, value\n            )\n        )\n\n        if not isinstance(value, list):\n            raise TypeError(err)\n\n        for i in value:\n            if not isinstance(i, list):\n                raise TypeError(err)\n\n            # check list length\n            if len(i) != 2:\n                raise ValueError(err)\n\n            # check type\n            for j in range(2):\n                if not isinstance(i[j], int):\n                    raise TypeError(err)\n\n                # check range\n                if i[j] < 0 or i[j] > 1440:\n                    raise ValueError(err)\n\n        return value\n\n    @property\n    def to_tjp(self) -> str:\n        \"\"\"Return TaskJuggler representation of this object.\n\n        Returns:\n            str: The TaskJuggler representation.\n        \"\"\"\n        tjp = \"\"\n        for i, day in enumerate([\"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\", \"sun\"]):\n            if i != 0:\n                tjp += \"\\n\"\n            tjp += f\"workinghours {day} \"\n            if self[day]:\n                for i, part in enumerate(self[day]):\n                    start_hour, end_hour = part\n                    if i != 0:\n                        tjp += \", \"\n                    tjp += (\n                        f\"{start_hour // 60:02d}:{start_hour % 60:02d} - \"\n                        f\"{end_hour // 60:02d}:{end_hour % 60:02d}\"\n                    )\n            else:\n                tjp += \"off\"\n        return tjp\n\n    @property\n    def weekly_working_hours(self) -> int:\n        \"\"\"Return the total working hours in a week.\n\n        Returns:\n            int: The calculated weekly working hours.\n        \"\"\"\n        weekly_working_hours = 0\n        for i in range(0, 7):\n            for start, end in self[i]:\n                weekly_working_hours += end - start\n        return weekly_working_hours / 60.0\n\n    @property\n    def weekly_working_days(self) -> int:\n        \"\"\"Return the weekly working days by looking at the working hours settings.\n\n        Returns:\n            int: The weekly working days value.\n        \"\"\"\n        wwd = 0\n        for i in range(0, 7):\n            if len(self[i]):\n                wwd += 1\n        return wwd\n\n    @property\n    def yearly_working_days(self) -> int:\n        \"\"\"Return the total working days in a year.\n\n        Returns:\n            int: The calculated yearly_working_days value.\n        \"\"\"\n        return int(ceil(self.weekly_working_days * 52.1428))\n\n    @validates(\"daily_working_hours\")\n    def _validate_daily_working_hours(self, key: str, daily_working_hours: int) -> int:\n        \"\"\"Validate the given daily working hours value.\n\n        Args:\n            key (str): The name of the validated column.\n            daily_working_hours (int): The daily working hours to be validated.\n\n        Raises:\n            TypeError: If the `daily_working_hours` value is not an integer.\n            ValueError: If the `daily_working_hours` is smaller thane 0 or\n                bigger than 24.\n\n        Returns:\n            int: The validated daily working hours value.\n        \"\"\"\n        if daily_working_hours is None:\n            daily_working_hours = defaults.daily_working_hours\n\n        if not isinstance(daily_working_hours, int):\n            raise TypeError(\n                \"{}.daily_working_hours should be an integer, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    daily_working_hours.__class__.__name__,\n                    daily_working_hours,\n                )\n            )\n\n        if daily_working_hours <= 0 or daily_working_hours > 24:\n            raise ValueError(\n                f\"{self.__class__.__name__}.daily_working_hours should be a positive \"\n                \"integer value greater than 0 and smaller than or equal to 24\"\n            )\n        return daily_working_hours\n\n    def split_in_to_working_hours(\n        self, start: datetime.datetime, end: datetime.datetime\n    ) -> List[datetime.datetime]:\n        \"\"\"Split the given start and end datetime objects in to working hours.\n\n        Args:\n            start (datetime.datetime): The start date and time.\n            end (datetime.datetime): The end date and time.\n\n        Raises:\n            NotImplementedError: Unless this is implemented.\n        \"\"\"\n        raise NotImplementedError()\n\n\nclass Vacation(SimpleEntity, DateRangeMixin):\n    \"\"\"Vacation is the way to manage the User vacations.\n\n    Args:\n        user (User): The user of this vacation. Should be an instance of\n            :class:`.User` if skipped or given as None the Vacation is considered\n            as a Studio vacation and applies to all Users.\n\n        start (datetime.datetime): The start datetime of the vacation. Is is an\n            datetime.datetime instance. When skipped it will be set to the rounded\n            value of.\n\n        end (datetime.datetime): The end datetime of the vacation. It is an\n            datetime.datetime instance.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"Vacations\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Vacation\"}\n\n    __strictly_typed__ = False\n\n    vacation_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    user_id: Mapped[Optional[int]] = mapped_column(\"user_id\", ForeignKey(\"Users.id\"))\n\n    user: Mapped[User] = relationship(\n        primaryjoin=\"Vacations.c.user_id==Users.c.id\",\n        back_populates=\"vacations\",\n        doc=\"\"\"The User of this Vacation.\n\n        Accepts :class:`.User` instance.\n        \"\"\",\n    )\n\n    def __init__(\n        self,\n        user: Optional[User] = None,\n        start: Optional[datetime.datetime] = None,\n        end: Optional[datetime.datetime] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        kwargs[\"start\"] = start\n        kwargs[\"end\"] = end\n        super(Vacation, self).__init__(**kwargs)\n        DateRangeMixin.__init__(self, **kwargs)\n        self.user = user\n\n    @validates(\"user\")\n    def _validate_user(self, key: str, user: User) -> User:\n        \"\"\"Validate the given user instance.\n\n        Args:\n            key (str): The name of the validated column.\n            user (User): The user value to be validated.\n\n        Raises:\n            TypeError: If the user value is not None and not a\n                :class:`stalker.models.auth.User` instance.\n\n        Returns:\n            User: The validated user value.\n        \"\"\"\n        if user is not None and not isinstance(user, User):\n            raise TypeError(\n                \"{}.user should be an instance of stalker.models.auth.User, \"\n                \"not {}: '{}'\".format(\n                    self.__class__.__name__, user.__class__.__name__, user\n                )\n            )\n        return user\n\n    @property\n    def to_tjp(self) -> str:\n        \"\"\"Override the to_tjp method.\n\n        Returns:\n            str: The rendered tjp template.\n        \"\"\"\n        tjp = \"vacation \"\n        tjp += f\"{self.start.astimezone(pytz.utc).strftime('%Y-%m-%d-%H:%M:%S')} - \"\n        tjp += f\"{self.end.astimezone(pytz.utc).strftime('%Y-%m-%d-%H:%M:%S')}\"\n        return tjp\n"
  },
  {
    "path": "src/stalker/models/tag.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tag related functions and classes are situated here.\"\"\"\nfrom typing import Any, Dict\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker.log import get_logger\nfrom stalker.models.entity import SimpleEntity\n\nlogger = get_logger(__name__)\n\n\nclass Tag(SimpleEntity):\n    \"\"\"Use it to create tags for any object available in SOM.\n\n    Doesn't have any other attribute than what is inherited from\n    :class:`.SimpleEntity`\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Tags\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Tag\"}\n    tag_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs: Dict[str, Any]) -> None:\n        super(Tag, self).__init__(**kwargs)\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a Tag instance and has the same\n                attributes.\n        \"\"\"\n        return super(Tag, self).__eq__(other) and isinstance(other, Tag)\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value for this Tag instance.\n\n        Returns:\n            int: The hash of this Tag.\n        \"\"\"\n        return super(Tag, self).__hash__()\n"
  },
  {
    "path": "src/stalker/models/task.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Task related functions and classes are situated here.\"\"\"\nimport copy\nimport datetime\nimport os\nfrom typing import Any, Dict, Generator, List, Optional, TYPE_CHECKING, Union\n\nfrom jinja2 import Template\n\nimport pytz\n\nimport sqlalchemy\nfrom sqlalchemy import (\n    CheckConstraint,\n    Column,\n    DDL,\n    Enum,\n    ForeignKey,\n    Integer,\n    Table,\n    event,\n    text,\n)\nfrom sqlalchemy.exc import (\n    InternalError,\n    InvalidRequestError,\n    OperationalError,\n    ProgrammingError,\n    UnboundExecutionError,\n)\nfrom sqlalchemy.ext.associationproxy import association_proxy\nfrom sqlalchemy.orm import (\n    Mapped,\n    mapped_column,\n    reconstructor,\n    relationship,\n    synonym,\n    validates,\n)\nfrom sqlalchemy.orm.attributes import AttributeEvent\n\nfrom stalker.db.declarative import Base\nfrom stalker.db.session import DBSession\nfrom stalker.exceptions import (\n    CircularDependencyError,\n    DependencyViolationError,\n    OverBookedError,\n    StatusError,\n)\nfrom stalker.log import get_logger\nfrom stalker.models.auth import User\nfrom stalker.models.budget import Good\nfrom stalker.models.entity import Entity\nfrom stalker.models.enum import (\n    DependencyTarget,\n    DependencyTargetDecorator,\n    ScheduleConstraint,\n    ScheduleModel,\n    TimeUnit,\n    TimeUnitDecorator,\n    TraversalDirection,\n)\nfrom stalker.models.mixins import (\n    DAGMixin,\n    DateRangeMixin,\n    ReferenceMixin,\n    ScheduleMixin,\n    StatusMixin,\n)\nfrom stalker.models.review import Review\nfrom stalker.models.status import Status\nfrom stalker.models.ticket import Ticket\nfrom stalker.utils import check_circular_dependency, walk_hierarchy\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.project import Project\n    from stalker.models.version import Version\n\nlogger = get_logger(__name__)\n\n\nBINARY_STATUS_VALUES = {\n    \"WFD\": 0b100000000,\n    \"RTS\": 0b010000000,\n    \"WIP\": 0b001000000,\n    \"PREV\": 0b000100000,\n    \"HREV\": 0b000010000,\n    \"DREV\": 0b000001000,\n    \"OH\": 0b000000100,\n    \"STOP\": 0b000000010,\n    \"CMPL\": 0b000000001,\n}\n\"\"\"\n  +--------- WFD\n  |+-------- RTS\n  ||+------- WIP\n  |||+------ PREV\n  ||||+----- HREV\n  |||||+---- DREV\n  ||||||+--- OH\n  |||||||+-- STOP\n  ||||||||+- CMPL\n  |||||||||\n0b000000000\n\"\"\"  # noqa: SC100\n\n\nCHILDREN_TO_PARENT_STATUSES_MAP = {\n    0b000000000: 0,\n    0b000000001: 3,\n    0b000000010: 3,\n    0b000000011: 3,\n    0b010000000: 1,\n    0b010000010: 1,\n    0b100000000: 0,\n    0b100000010: 0,\n    0b110000000: 1,\n    0b110000010: 1,\n}\n\"\"\"Although the  dictionary seems cryptic, it shows the final status index in\nparent_statuses_map[] list.\n\nSo by using the cumulative statuses of children we got an index from the following\ntable, and use the found element (integer) as the index for the parent_statuses_map[]\nlist, and we find the desired status.\n\nWe are doing it in this way for a couple of reasons:\n\n  1. We shouldn't hold the statuses in the following list,\n  2. We are using a sparse dictionary which is more efficient than storing\n     all the data in a single list.\n\"\"\"\n\n\nclass TimeLog(Entity, DateRangeMixin):\n    \"\"\"Time entry for the time spent on a :class:`.Task` by a specific :class:`.User`.\n\n    It is so important to note that the TimeLog reports the **uninterrupted**\n    time interval that is spent for a Task. Thus it doesn't care about the\n    working time attributes like daily working hours, weekly working days or\n    anything else. Again it is the uninterrupted time which is spent for a\n    task.\n\n    Entering a time log for 2 days will book the resource for 48 hours and not,\n    2 * daily working hours.\n\n    TimeLogs are created per resource. It means, you need to record all the\n    works separately for each resource. So there is only one resource in a\n    TimeLog instance.\n\n    A :class:`.TimeLog` instance needs to be initialized with a :class:`.Task`\n    and a :class:`.User` instances.\n\n    Adding overlapping time log for a :class:`.User` will raise a\n    :class:`.OverBookedError`.\n\n    .. ::\n      TimeLog instances automatically extends the :attr:`.Task.schedule_timing`\n      of the assigned Task if the :attr:`.Task.total_logged_seconds` is getting\n      bigger than the :attr:`.Task.schedule_timing` after this TimeLog.\n\n    Args:\n        task (Task): The :class:`.Task` instance that this time log belongs to.\n        resource (User): The :class:`.User` instance that this time log is created\n            for.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"TimeLogs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"TimeLog\"}\n\n    __table_args__ = (\n        CheckConstraint('\"end\" > start'),  # this will be ignored in SQLite3\n    )\n\n    time_log_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n    task_id: Mapped[int] = mapped_column(\n        ForeignKey(\"Tasks.id\"),\n        nullable=False,\n        doc=\"\"\"The id of the related task.\"\"\",\n    )\n    task: Mapped[\"Task\"] = relationship(\n        primaryjoin=\"TimeLogs.c.task_id==Tasks.c.id\",\n        uselist=False,\n        back_populates=\"time_logs\",\n        doc=\"\"\"The :class:`.Task` instance that this time log is created for\"\"\",\n    )\n\n    resource_id: Mapped[int] = mapped_column(\n        ForeignKey(\"Users.id\"),\n        nullable=False,\n    )\n    resource: Mapped[User] = relationship(\n        primaryjoin=\"TimeLogs.c.resource_id==Users.c.id\",\n        uselist=False,\n        back_populates=\"time_logs\",\n        doc=\"\"\"The :class:`.User` instance that this time_log is created for\"\"\",\n    )\n\n    def __init__(\n        self,\n        task: Optional[\"Task\"] = None,\n        resource: Optional[User] = None,\n        start: Optional[datetime.datetime] = None,\n        end: Optional[datetime.datetime] = None,\n        duration: Optional[datetime.timedelta] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        super(TimeLog, self).__init__(**kwargs)\n        kwargs[\"start\"] = start\n        kwargs[\"end\"] = end\n        kwargs[\"duration\"] = duration\n        DateRangeMixin.__init__(self, **kwargs)\n        self.task = task\n        self.resource = resource\n\n    @validates(\"task\")\n    def _validate_task(self, key: str, task: \"Task\") -> \"Task\":\n        \"\"\"Validate the given task value.\n\n        Args:\n            key (str): The name of the validated column.\n            task (Task): The Task instance to be validated.\n\n        Raises:\n            TypeError: If the given task is not a Task instance.\n            ValueError: If this is a container task.\n            StatusError: If the Task.status is one of [WDF, OH, STOP, CMPL] where it is\n                not allowed to entry any further TimeLog information.\n            DependencyViolationError: If this TimeLog overlaps with one of the\n                dependencies start and end time, essentially forcing this Task to start\n                or end before its dependencies start or end.\n\n        Returns:\n            Task: The validated task value.\n        \"\"\"\n        if not isinstance(task, Task):\n            raise TypeError(\n                \"{}.task should be an instance of stalker.models.task.Task, \"\n                \"not {}: '{}'\".format(\n                    self.__class__.__name__, task.__class__.__name__, task\n                )\n            )\n\n        if task.is_container:\n            raise ValueError(\n                f\"{task.name} (id: {task.id}) is a container task, and it is not \"\n                \"allowed to create TimeLogs for a container task\"\n            )\n\n        # check status\n        logger.debug(\"checking task status!\")\n        with DBSession.no_autoflush:\n            task_status_list = task.status_list\n            WFD = task_status_list[\"WFD\"]\n            RTS = task_status_list[\"RTS\"]\n            WIP = task_status_list[\"WIP\"]\n            # PREV = task_status_list[\"PREV\"]\n            HREV = task_status_list[\"HREV\"]\n            # DREV = task_status_list[\"DREV\"]\n            OH = task_status_list[\"OH\"]\n            STOP = task_status_list[\"STOP\"]\n            CMPL = task_status_list[\"CMPL\"]\n\n            if task.status in [WFD, OH, STOP, CMPL]:\n                raise StatusError(\n                    f\"{task.name} is a {task.status.code} task, and it is not allowed \"\n                    f\"to create TimeLogs for a {task.status.code} task, please supply \"\n                    \"a RTS, WIP, HREV or DREV task!\"\n                )\n            elif task.status in [RTS, HREV]:\n                # update task status\n                logger.debug(\"updating task status to WIP!\")\n                task.status = WIP\n\n            # check dependent tasks\n            logger.debug(\"checking dependent task statuses\")\n            for task_dependencies in task.task_depends_on:\n                dep_task = task_dependencies.depends_on\n                dependency_target = task_dependencies.dependency_target\n                raise_violation_error = False\n                violation_date = None\n\n                if dependency_target == DependencyTarget.OnEnd:\n                    # time log cannot start before the end date of this task\n                    if self.start < dep_task.end:\n                        raise_violation_error = True\n                        violation_date = dep_task.end\n                elif dependency_target == DependencyTarget.OnStart:\n                    if self.start < dep_task.start:\n                        raise_violation_error = True\n                        violation_date = dep_task.start\n\n                if raise_violation_error:\n                    raise DependencyViolationError(\n                        \"It is not possible to create a TimeLog before \"\n                        f\"{violation_date}, which violates the dependency relation of \"\n                        f'\"{task.name}\" to \"{dep_task.name}\"'\n                    )\n\n        # this may need to be in an external event, it needs to trigger a flush\n        # to correctly function\n        task.update_parent_statuses()\n\n        return task\n\n    @validates(\"resource\")\n    def _validate_resource(self, key: str, resource: User) -> User:\n        \"\"\"Validate the given resource value.\n\n        Args:\n            key (str): The name of the validated column.\n            resource (User): The User instance as the resource of this TimeLog.\n\n        raises:\n            TypeError: If the resource is None or is not a User instance.\n            OverBookedError: If the resource has already a clashing TimeLog.\n\n        Returns:\n            User: The validated User instance.\n        \"\"\"\n        if resource is None:\n            raise TypeError(f\"{self.__class__.__name__}.resource cannot be None\")\n\n        if not isinstance(resource, User):\n            raise TypeError(\n                \"{}.resource should be a stalker.models.auth.User instance, \"\n                \"not {}: '{}'\".format(\n                    self.__class__.__name__, resource.__class__.__name__, resource\n                )\n            )\n\n        # check for overbooking\n        clashing_time_log_data = None\n        with DBSession.no_autoflush:\n            try:\n                from sqlalchemy import or_, and_\n\n                clashing_time_log_data = (\n                    DBSession.query(TimeLog.start, TimeLog.end)\n                    .filter(TimeLog.id != self.id)\n                    .filter(TimeLog.resource_id == resource.id)\n                    .filter(\n                        or_(\n                            and_(TimeLog.start <= self.start, self.start < TimeLog.end),\n                            and_(TimeLog.start < self.end, self.end <= TimeLog.end),\n                        )\n                    )\n                    .first()\n                )\n\n            except (UnboundExecutionError, OperationalError):\n                # fallback to Python\n                for time_log in resource.time_logs:\n                    if time_log == self:\n                        continue\n\n                    if (\n                        time_log.start == self.start\n                        or time_log.end == self.end\n                        or time_log.start < self.end < time_log.end\n                        or time_log.start < self.start < time_log.end\n                    ):\n                        clashing_time_log_data = [time_log.start, time_log.end]\n                        break\n\n            if clashing_time_log_data:\n                import tzlocal\n\n                local_tz = tzlocal.get_localzone()\n                raise OverBookedError(\n                    \"The resource has another TimeLog between {} and {}\".format(\n                        clashing_time_log_data[0].astimezone(local_tz),\n                        clashing_time_log_data[1].astimezone(local_tz),\n                    )\n                )\n\n        return resource\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a TimeLog instance and has the same task,\n                resource, start, end and name.\n        \"\"\"\n        return (\n            isinstance(other, TimeLog)\n            and self.task is other.task\n            and self.resource is other.resource\n            and self.start == other.start\n            and self.end == other.end\n            and self.name == other.name\n        )\n\n\n# TODO: Consider contracting a Task with TimeLogs, what will happen when the\n#       task has logged in time\n# TODO: Check, what happens when a task has TimeLogs and will have child task\n#       later on, will it be ok with TJ\n\n\nclass Task(\n    Entity, StatusMixin, DateRangeMixin, ReferenceMixin, ScheduleMixin, DAGMixin\n):\n    \"\"\"Manages Task related data.\n\n    **Introduction**\n\n    Tasks are the smallest unit of work that should be accomplished to complete\n    a :class:`.Project`. Tasks define a certain amount of time needed to be\n    spent for a purpose. They also define a complex hierarchy of relation.\n\n    Stalker follows and enhances the concepts stated in TaskJuggler_.\n\n    .. _TaskJuggler : http://www.taskjuggler.org/\n\n    .. note::\n       .. versionadded:: 0.2.0\n          References in Tasks\n\n       Tasks can now have References.\n\n    **Initialization**\n\n    Tasks are a part of a bigger Project, that's way a Task needs to be created\n    with a :class:`.Project` instance. It is possible to create a task without\n    a project, if it is created to be a child of another task. And it is also\n    possible to pass both a project and a parent task.\n\n    But because passing both a project and a parent task may create an\n    ambiguity, Stalker will raise a RuntimeWarning, if both project and task\n    are given and the owner project of the given parent task is different then\n    the supplied project instance. But again Stalker will warn the user but\n    will continue to use the task as the parent and will correctly use the\n    project of the given task as the project of the newly created task.\n\n    The following codes are a couple of examples for creating Task instances::\n\n      # with a project instance\n      >>> from stalker import Project\n      >>> project1 = Project(name='Test Project 1')  # simplified\n      >>> task1 = Task(name='Schedule', project=project1)\n\n      # with a parent task\n      >>> task2 = Task(name='Documentation', parent=task1)\n\n      # or both\n      >>> task3 = Task(name='Test', project=project1, parent=task1)\n\n      # this will create a RuntimeWarning\n      >>> project2 = Project(name='Test Project 2')\n      >>> task4 = Task(name='Test', project=project2, parent=task1)\n      # task1 is not a # task of proj2\n\n      >>> assert task4.project == project1\n      # Stalker uses the task1.project for task4\n\n      # this will also create a RuntimeError\n      >>> task3 = Task(name='Failure 2') # no project no parent, this is an\n                                         # orphan task.\n\n    Also initially Stalker will pin point the :attr:`.start` value and then\n    will calculate proper :attr:`.end` and :attr:`.duration` values by using\n    the :attr:`.schedule_timing` and :attr:`.schedule_unit` attributes. But\n    these values (start, end and duration) are temporary values for an\n    unscheduled task. The final date values will be calculated by TaskJuggler\n    in the `auto scheduling` phase.\n\n    **Auto Scheduling**\n\n    Stalker uses TaskJuggler for task scheduling. After defining all the tasks,\n    Stalker will convert them to a single tjp file along with the recorded\n    :class:`.TimeLog` s :class:`.Vacation` s etc. and let TaskJuggler to\n    solve the scheduling problem.\n\n    During the auto scheduling (with TaskJuggler), the calculation of task\n    duration, start and end dates are effected by the working hours setting of\n    the :class:`.Studio`, the effort that needs to spend for that task and the\n    availability of the resources assigned to the task.\n\n    A good practice for creating a project plan is to supply the parent/child\n    and dependency relation between tasks and the effort and resource\n    information per task and leave the start and end date calculation to\n    TaskJuggler.\n\n    The default :attr:`.schedule_model` for Stalker tasks is\n    `ScheduleModel.Effort`, the default :attr:`TimeUnit.Hour` and the default\n    value of :attr:`.schedule_timing` is defined by the\n    :attr:`stalker.config.Config.timing_resolution`. So for a config where the\n    ``timing_resolution`` is set to 1 hour the schedule_timing is 1.\n\n    It is also possible to use :attr:`.ScheduleModel.Length`` or\n    :attr:`.ScheduleModel.duration` values for\n    :attr:`.schedule_model` (set it to :attr:`ScheduleModel.Effort`,\n    :attr:`.ScheduleModel.Length` or :attr:`.ScheduleModel.Duration` to get the\n    desired scheduling model).\n\n    To convert a Task instance to a TaskJuggler compatible string use the\n    :attr:`.to_tjp`` attribute. It will try to create a good representation of\n    the Task by using the resources, schedule_model, schedule_timing and\n    schedule_constraint attributes.\n\n    ** Alternative Resources**\n\n    .. versionadded:: 0.2.5\n       Alternative Resources\n\n    Stalker now supports alternative resources per task. You can specify\n    alternative resources by using the :attr:`.alternative_resources`\n    attribute. The number of resources and the number of alternative resources\n    are not related. So you can define only 1 resource and more than one\n    alternative resources, or you can define 2 resources and only one\n    alternative resource.\n\n    .. warning::\n\n       As opposed to TaskJuggler alternative resources are not per resource\n       based. So Stalker will use the alternatives list for all of the\n       resources in the resources list. Per resource alternative will be\n       supported in future versions of Stalker.\n\n    Stalker will pass the data to TaskJuggler and TJ will compute a list of\n    resources that are assigned to the task in the report time frame and\n    Stalker will store the resultant list of users in\n    :attr:`.computed_resources` attribute.\n\n    .. warning::\n\n       When TaskJuggler computes the resources, the returned list may contain\n       resources which are not in the :attr:`.resources` nor in\n       :attr:`.alternative_resources` list anymore. Stalker will silently\n       filter those resources and will only store resources (in\n       :attr:`.computed_resources`) those are still available as a direct or\n       alternative resource to that particular task.\n\n    The selection strategy of the alternative resource is defined by the\n    :attr:`.allocation_strategy` attribute. The `allocation_strategy`\n    attribute value should be one of [minallocated, maxloaded, minloaded,\n    order, random]. The following description is from TaskJuggler\n    documentation:\n\n      +--------------+--------------------------------------------------------+\n      | minallocated | Pick the resource that has the smallest allocation     |\n      |              | factor. The allocation factor is calculated from the   |\n      |              | various allocations of the resource across the tasks.  |\n      |              | This is the default setting.)                          |\n      +--------------+--------------------------------------------------------+\n      | maxloaded    | Pick the available resource that has been used the     |\n      |              | most so far.                                           |\n      +--------------+--------------------------------------------------------+\n      | minloaded    | Pick the available resource that has been used the     |\n      |              | least so far.                                          |\n      +--------------+--------------------------------------------------------+\n      | order        | Pick the first available resource from the list.       |\n      +--------------+--------------------------------------------------------+\n      | random       | Pick a random resource from the list.                  |\n      +--------------+--------------------------------------------------------+\n\n    As in TaskJuggler the default for :attr:`.allocation_strategy` attribute is\n    \"minallocated\".\n\n    Also the allocation of the resources are effected by the\n    :attr:`.persistent_allocation` attribute. The persistent_allocation\n    attribute refers to the ``persistent`` attribute in TJ. The documentation\n    of ``persistent`` in TJ is as follows:\n\n      Specifies that once a resource is picked from the list of alternatives\n      this resource is used for the whole task. This is useful when several\n      alternative resources have been specified. Normally the selected resource\n      can change after each break. A break is an interval of at least one\n      time slot where no resources were available.\n\n    :attr:`.persistent_allocation` attribute is True by default.\n\n    For a not yet scheduled task the :attr:`.computed_resources` attribute will\n    be the same as the :attr:`.resources` list. After the task is scheduled the\n    content of the :attr:`.computed_resources` will purely come from TJ.\n\n    Updating the resources list will not update the :attr:`.computed_resources`\n    list if the task :attr:`.is_scheduled`.\n\n    **Task to Task Relation**\n\n    .. note::\n       .. versionadded:: 0.2.0\n       Task to Task Relation\n\n    Tasks can have child Tasks. So you can create complex relations of Tasks to\n    comply with your project needs.\n\n    A Task is called a ``container task`` if it has at least one child Task.\n    And it is called a ``leaf task`` if it doesn't have any children Tasks.\n    Tasks which doesn't have a parent called ``root_task``.\n\n    As opposed to TaskJuggler where the resource information is passed through\n    parent to child, in Stalker the resources in a container task is\n    meaningless, cause the resources are defined by the child tasks.\n\n    Although the values are not very important after TaskJuggler schedules a\n    task, the :attr:`~.start` and :attr:`~.end` values for a container\n    task is gathered from the child tasks. The start is equal to the earliest\n    start value of the children tasks, and the end is equal to the latest end\n    value of the children tasks. Of course, these values are going to be\n    ignored by TaskJuggler, but for interactive gantt charts these are good toy\n    attributes to play with.\n\n    Stalker will check if there will be a cycle if one wants to parent a Task\n    to a child Task of its own or the dependency relation creates a cycle.\n\n    In Gantt Charts the ``computed_start``, ``computed_end`` and\n    ``computed_resources`` attributes will be used if the task\n    :attr:`.is_scheduled`.\n\n    **Task Responsible**\n\n    .. note::\n       .. versionadded:: 0.2.0\n          Task Responsible\n\n    .. note::\n       .. versionadded:: 0.2.5\n          Multiple Responsible Per Task\n\n    Tasks have a **responsible** which is a list of :class:`.User` instances\n    who are responsible of the assigned task and all the hierarchy under it.\n\n    If a task doesn't have any responsible, it will start looking to its\n    parent tasks and will return a copy of the responsible of its parent and it\n    will be an empty list if non of its parents has a responsible.\n\n    You can create complex responsibility chains for different branches of\n    Tasks.\n\n    **Percent Complete Calculation** .. versionadded:: 0.2.0\n\n    Tasks can calculate how much it is completed based on the\n    :attr:`.schedule_seconds` and :attr:`.total_logged_seconds` attributes.\n    For a parent task, the calculation is based on the total\n    :attr:`.schedule_seconds` and :attr:`.total_logged_seconds` attributes of\n    their children.\n\n    .. versionadded:: 0.2.14\n       Because duration tasks do not need time logs there is no way to\n       calculate the percent complete by using the time logs. And Percent\n       Complete on a duration task is calculated directly from the\n       :attr:`.start` and :attr:`.end` and ``datetime.datetime.now(pytz.utc)``.\n\n    .. versionadded:: 0.2.26\n       For parent tasks that have both effort based and duration based children\n       tasks the percent complete is calculated as if the\n       :attr:`.total_logged_seconds` is properly filled for duration based\n       tasks proportional to the elapsed time from the :attr:`.start` attr\n       value.\n\n    Even tough, the percent_complete attribute of a task is\n    100% the task may not be considered as completed, because it may not be\n    reviewed and approved by the responsible yet.\n\n    **Task Review Workflow**\n\n    .. versionadded:: 0.2.5\n       Task Review Workflow\n\n    Starting with Stalker v0.2.5 tasks are reviewed by their responsible users.\n    The reviews done by responsible users will set the task status according to\n    the supplied reviews. Please see the :class:`.Review` class documentation\n    for more details.\n\n    **Task Status Workflow**\n\n    .. note::\n       .. versionadded:: 0.2.5\n          Task Status Workflow\n\n    Task statuses now follow a workflow called \"Task Status Workflow\".\n\n    The \"Task Status Workflow\" defines the different statuses that a Task will\n    have along its normal life cycle. Container and leaf tasks will have\n    different workflow using nearly the same set of statuses (container tasks\n    have only 4 statuses where as leaf tasks have 9).\n\n    The following diagram shows the status workflow for leaf tasks:\n\n    .. image:: ../../../docs/source/_static/images/Task_Status_Workflow.png\n          :width: 637 px\n          :height: 611 px\n          :align: center\n\n    The workflow defines the following statuses at described situations:\n\n      +-----------------------------------------------------------------------+\n      | LEAF TASK STATUS WORKFLOW                                             |\n      +------------------+----------------------------------------------------+\n      | Status Name      | Description                                        |\n      +------------------+----------------------------------------------------+\n      | Waiting For      | If a task has uncompleted dependencies then it     |\n      | Dependency (WFD) | will have its status to set to WFD. A WFD Task can |\n      |                  | not have a TimeLog or a review request cannot be  |\n      |                  | made for it.                                       |\n      +------------------+----------------------------------------------------+\n      | Ready To Start   | A task is set to RTS when there are no             |\n      | (RTS)            | dependencies or all of its dependencies are        |\n      |                  | completed, so there is nothing preventing it to be |\n      |                  | started. An RTS Task can have new TimeLogs. A      |\n      |                  | review cannot be requested at this stage cause no |\n      |                  | work is done yet.                                  |\n      +------------------+----------------------------------------------------+\n      | Work In Progress | A task is set to WIP when a TimeLog has been       |\n      | (WIP)            | created for that task. A WIP task can have new     |\n      |                  | TimeLogs and a review can be requested for that    |\n      |                  | task.                                              |\n      +------------------+----------------------------------------------------+\n      | Pending Review   | A task is set to PREV when a new set of Review     |\n      | (PREV)           | instances created for it by using the              |\n      |                  | :meth:`.Task.request_review` method. And it is     |\n      |                  | possible to request a review only for a task with  |\n      |                  | status WIP. A PREV task cannot have new TimeLogs  |\n      |                  | nor a new request can be made because it is in     |\n      |                  | already in review.                                 |\n      +------------------+----------------------------------------------------+\n      | Has Revision     | A task is set to HREV when one of its Reviews      |\n      | (HREV)           | completed by requesting a review by using the      |\n      |                  | :meth:`.Review.request_review` method. A HREV Task |\n      |                  | can have new TimeLogs, and it will be converted to |\n      |                  | a WIP or DREV depending on its dependency task     |\n      |                  | statuses.                                          |\n      +------------------+----------------------------------------------------+\n      | Dependency Has   | If the dependent task of a WIP, PREV, HREV, DREV   |\n      | Revision (DREV)  | or CMPL task has a revision then the statuses of   |\n      |                  | the tasks are set to DREV which means both of the  |\n      |                  | dependee and the dependent tasks can work at the   |\n      |                  | same time. For a DREV task a review request can    |\n      |                  | not be made until it is set to WIP again by        |\n      |                  | setting the depending task to CMPL again.          |\n      +------------------+----------------------------------------------------+\n      | On Hold (OH)     | A task is set to OH when the resource needs to     |\n      |                  | work for another task, and the :meth:`Task.hold`   |\n      |                  | is called. An OH Task can be resumed by calling    |\n      |                  | :meth:`.Task.resume` method and depending on its   |\n      |                  | :attr:`.Task.time_logs` attribute it will have its |\n      |                  | status set to RTS or WIP.                          |\n      +------------------+----------------------------------------------------+\n      | Stopped (STOP)   | A task is set to STOP when no more work needs to   |\n      |                  | done for that task and it will not be used         |\n      |                  | anymore. Call :meth:`.Task.stop` method to do it   |\n      |                  | properly. Only applicable to WIP tasks.            |\n      |                  |                                                    |\n      |                  | The schedule values of the task will be capped to  |\n      |                  | current time spent on it, so Task Juggler will not |\n      |                  | reserve any more resources for it.                 |\n      |                  |                                                    |\n      |                  | Also STOP tasks are treated as if they are dead.   |\n      +------------------+----------------------------------------------------+\n      | Completed (CMPL) | A task is set to CMPL when all of the Reviews are  |\n      |                  | completed by approving the task. It is not         |\n      |                  | possible to create any new TimeLogs and no new     |\n      |                  | review can be requested for a CMPL Task.           |\n      +------------------+----------------------------------------------------+\n\n    Container \"Task Status Workflow\" defines a set of statuses where the\n    container task status will only change according to its children task\n    statuses:\n\n      +-----------------------------------------------------------------------+\n      |                    CONTAINER TASK STATUS WORKFLOW                     |\n      +------------------+----------------------------------------------------+\n      | Status Name      | Description                                        |\n      +------------------+----------------------------------------------------+\n      | Waiting For      | If all of the child tasks are in WFD status then   |\n      | Dependency (WFD) | the container task is also WFD.                    |\n      +------------------+----------------------------------------------------+\n      | Ready To Start   | A container task is set to RTS when children tasks |\n      | (RTS)            | have statuses of only WFD and RTS.                 |\n      +------------------+----------------------------------------------------+\n      | Work In Progress | A container task is set to WIP when one of its     |\n      | (WIP)            | children tasks have any of the statuses of RTS,    |\n      |                  | WIP, PREV, HREV, DREV or CMPL.                     |\n      +------------------+----------------------------------------------------+\n      | Completed (CMPL) | A container task is set to CMPL when all of its    |\n      |                  | children tasks are CMPL.                           |\n      +------------------+----------------------------------------------------+\n\n    Even though, users are encouraged to use the actions (like\n    :meth:`.Task.create_time_log`, :meth:`.Task.hold`, :meth:`.Task.stop`,\n    :meth:`.Task.resume`, :meth:`.Task.request_revision`,\n    :meth:`.Task.request_review`, :meth:`.Task.approve`) to update the task\n    statuses , setting the :attr:`.Task.status` will also update the dependent\n    tasks or will check the new status against dependencies or the current\n    status of the task.\n\n    Thus in some situations setting the :attr:`.Task.status` will not change\n    the status of the task. For example, setting the task status to WFD when\n    there are no dependencies will not update the task status to WFD,\n    also updating a PREV task status to STOP or HOLD or RTS is not possible.\n    And it is not possible to set a task to WIP if there are no TimeLogs\n    entered for that task.\n\n    So the task will strictly follow the Task Status Workflow diagram above.\n\n    .. warning::\n       **Dependency Relation in Task Status Workflow**\n\n       Because the Task Status Workflow heavily effected by the dependent task\n       statuses, and the main reason of having dependency relation is to let\n       TaskJuggler to schedule the tasks correctly, and any task status other\n       than WFD or RTS means that a TimeLog has been created for a task (which\n       means that you cannot change the :attr:`.computed_start` anymore), it\n       is only allowed to change the dependencies of a WFD and RTS tasks.\n\n    .. warning::\n       **Resuming a STOP Task**\n\n       Resuming a STOP Task will be treated as if a revision has been made to\n       that task, and all the statuses of the tasks depending on this\n       particular task will be updated accordingly.\n\n    .. warning::\n       **Initial Status of a Task**\n\n       .. versionadded:: 0.2.5\n\n       Because of the Task Status Workflow, supplying a status with the\n       **status** argument may not set the status of the Task to the desired\n       value. A Task starts with WFD status by default, and updated to RTS if\n       it doesn't have any dependencies or all of the dependencies are STOP or\n       CMPL.\n\n    .. note::\n       .. versionadded:: 0.2.5.2\n          Task.path and Task.absolute_path properties\n\n          Task instances now have two new properties called :attr:`.path` and\n          :attr:`.absolute_path` . The value of these attributes are the\n          rendered version of the related :class:`.FilenameTemplate` which\n          has its target_entity_type attribute set to \"Task\" (or \"Asset\",\n          \"Shot\" or \"Sequence\" or anything matching to the derived class name,\n          so it can be used in :class:`.Asset`, :class:`.Shot` and\n          :class:`.Sequences` or anything that is derived from Task class) in\n          the :class:`.Project` that this task belongs to. This property has\n          been added to make it easier to write custom template codes for\n          Project :class:`.Structure` s.\n\n          The :attr:`.path` attribute is a repository relative path, where as\n          the :attr:`.absolute_path` is the full path and includes the OS\n          dependent repository path.\n\n    .. versionadded: 0.2.13\n\n       Task to :class:`.Good` relation. It is now possible to define the\n       related Good to this task, to be able to filter tasks that are related\n       to the same :class:`.Good`.\n\n       Its main purpose of existence is to be able to generate\n       :class:`.BudgetEntry` instances from the tasks that are related to the\n       same :class:`.Good` and because the Goods are defining the cost and MSRP\n       of different things, it is possible to create BudgetEntries and thus\n       :class;`.Budget` s with this information.\n\n    Args:\n        project (Project): A Task which doesn't have a parent (a root task)\n            should be created with a :class:`.Project` instance. If it is\n            skipped an no :attr:`.parent` is given then Stalker will raise a\n            RuntimeError. If both the ``project`` and the :attr:`.parent`\n            argument contains data and the project of the Task instance given\n            with parent argument is different than the Project instance given\n            with the ``project`` argument then a RuntimeWarning will be raised\n            and the project of the parent task will be used.\n        parent (Task): The parent Task or Project of this Task. Every Task in\n            Stalker should be related with a :class:`.Project` instance. So if\n            no parent task is desired, at least a Project instance should be\n            passed as the parent of the created Task or the Task will be an\n            orphan task and Stalker will raise a RuntimeError.\n        depends_on (List[Task]): A list of :class:`.Task` s that this\n            :class:`.Task` is depending on. A Task cannot depend on itself or\n            any other Task which are already depending on this one in anyway or\n            a CircularDependency error will be raised.\n        resources (List[User]): The :class:`.User` s assigned to this\n            :class:`.Task`. A :class:`.Task` without any resource cannot be\n            scheduled.\n        responsible (List[User]): A list of :class:`.User` instances that is\n            responsible of this task.\n        watchers (List[User]): A list of :class:`.User` those are added this\n            Task instance to their watch list.\n        start (datetime.datetime): The start date and time of this task\n            instance. It is only used if the :attr:`.schedule_constraint`\n            attribute is set to :attr:`.ScheduleConstraint.Start` or\n            :attr:`.ScheduleConstraint.Both`. The default value is\n            `datetime.datetime.now(pytz.utc)`.\n        end (datetime.datetime): The end date and time of this task instance.\n            It is only used if the :attr:`.schedule_constraint` attribute is\n            set to :attr:`.CONSTRAIN_END` or :attr:`.CONSTRAIN_BOTH`. The\n            default value is `datetime.datetime.now(pytz.utc)`.\n        schedule_timing (int): The value of the schedule timing.\n        schedule_unit (str): The unit value of the schedule timing. Should be\n            one of 'min', 'h', 'd', 'w', 'm', 'y'.\n        schedule_constraint (ScheduleConstraint): The\n            :class:`.ScheduleConstraint` value. The default is\n            `ScheduleConstraint.NONE`.\n        bid_timing (int): The initial bid for this Task. It can be used in\n            measuring how accurate the initial guess was. It will be compared\n            against the total amount of effort spend doing this task. Can be\n            set to None, which will be set to the schedule_timing_day argument\n            value if there is one or 0.\n        bid_unit (str): The unit of the bid value for this Task. Should be one\n            of the 'min', 'h', 'd', 'w', 'm', 'y'.\n        is_milestone (bool): A bool (True or False) value showing if this task\n            is a milestone which doesn't need any resource and effort.\n        priority (int): It is a number between 0 to 1000 which defines the\n            priority of the :class:`.Task`. The higher the value the higher its\n            priority. The default value is 500. Mainly used by TaskJuggler.\n\n            Higher priority tasks will be scheduled to an early date or at\n            least will tried to be scheduled to an early date then a lower\n            priority task (a task that is using the same resources).\n\n            In complex projects, a task with a lower priority task may steal\n            resources from a higher priority task, this is due to the internals\n            of TaskJuggler, it tries to increase the resource utilization by\n            letting the lower priority task to be completed earlier than the\n            higher priority task. This is done in that way if the lower\n            priority task is dependent of more important tasks (tasks in\n            critical path or tasks with critical resources). Read TaskJuggler\n            documentation for more information on how TaskJuggler schedules\n            tasks.\n        allocation_strategy (str): Defines the allocation strategy for\n            resources of a task with alternative resources. Should be one of\n            ['minallocated', 'maxloaded', 'minloaded', 'order', 'random'] and\n            the default value is 'minallocated'. For more information read the\n            :class:`.Task` class documentation.\n        persistent_allocation (bool): Specifies that once a resource is picked\n            from the list of alternatives this resource is used for the whole\n            task. The default value is True. For more information read the\n            :class:`.Task` class documentation.\n        good (stalker.models.budget.Good): It is possible to attach a good to\n            this Task to be able to filter and group them later on.\n    \"\"\"\n\n    from stalker import defaults\n\n    __auto_name__ = False\n    __tablename__ = \"Tasks\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Task\"}\n    task_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n        doc=\"\"\"The ``primary_key`` attribute for the ``Tasks`` table used by\n        SQLAlchemy to map this Task in relationships.\n        \"\"\",\n    )\n    __id_column__ = \"task_id\"\n\n    project_id: Mapped[Optional[int]] = mapped_column(\n        ForeignKey(\"Projects.id\"),\n        doc=\"\"\"The id of the owner :class:`.Project` of this Task. This\n        attribute is mainly used by **SQLAlchemy** to map a :class:`.Project`\n        instance to a Task.\n        \"\"\",\n    )\n    _project: Mapped[Optional[\"Project\"]] = relationship(\n        primaryjoin=\"Tasks.c.project_id==Projects.c.id\",\n        back_populates=\"tasks\",\n        uselist=False,\n        post_update=True,\n    )\n\n    tasks: Mapped[Optional[List[\"Task\"]]] = synonym(\n        \"children\",\n        doc=\"\"\"A synonym for the :attr:`.children` attribute used by the\n        descendants of the :class:`Task` class (currently :class:`.Asset`,\n        :class:`.Shot` and :class:`.Sequence` classes).\n        \"\"\",\n    )\n\n    is_milestone: Mapped[Optional[bool]] = mapped_column(\n        doc=\"\"\"Specifies if this Task is a milestone.\n\n        Milestones doesn't need any duration, any effort and any resources. It\n        is used to create meaningful dependencies between the critical stages\n        of the project.\n        \"\"\",\n    )\n\n    depends_on = association_proxy(\n        \"task_depends_on\", \"depends_on\", creator=lambda n: TaskDependency(depends_on=n)\n    )\n\n    dependent_of = association_proxy(\n        \"task_dependent_of\", \"task\", creator=lambda n: TaskDependency(task=n)\n    )\n\n    task_depends_on: Mapped[Optional[List[\"TaskDependency\"]]] = relationship(\n        back_populates=\"task\",\n        cascade=\"all, delete-orphan\",\n        primaryjoin=\"Tasks.c.id==Task_Dependencies.c.task_id\",\n        doc=\"\"\"A list of :class:`.Task` s that this one is depending on.\n\n        A CircularDependencyError will be raised when the task dependency\n        creates a circular dependency which means it is not allowed to create\n        a dependency for this Task which is depending on another one which in\n        some way depends on this one again.\"\"\",\n    )\n\n    task_dependent_of: Mapped[Optional[List[\"TaskDependency\"]]] = relationship(\n        back_populates=\"depends_on\",\n        cascade=\"all, delete-orphan\",\n        primaryjoin=\"Tasks.c.id==Task_Dependencies.c.depends_on_id\",\n        doc=\"\"\"A list of :class:`.Task` s that this one is being depended by.\n\n        A CircularDependencyError will be raised when the task dependency\n        creates a circular dependency which means it is not allowed to create\n        a dependency for this Task which is depending on another one which in\n        some way depends on this one again.\n        \"\"\",\n    )\n\n    resources: Mapped[Optional[List[User]]] = relationship(\n        secondary=\"Task_Resources\",\n        primaryjoin=\"Tasks.c.id==Task_Resources.c.task_id\",\n        secondaryjoin=\"Task_Resources.c.resource_id==Users.c.id\",\n        back_populates=\"tasks\",\n        doc=\"The list of :class:`.User` s assigned to this Task.\",\n    )\n\n    alternative_resources: Mapped[Optional[List[User]]] = relationship(\n        secondary=\"Task_Alternative_Resources\",\n        primaryjoin=\"Tasks.c.id==Task_Alternative_Resources.c.task_id\",\n        secondaryjoin=\"Task_Alternative_Resources.c.resource_id==Users.c.id\",\n        backref=\"alternative_resource_in_tasks\",\n        doc=\"The list of :class:`.User` s assigned to this Task as an \"\n        \"alternative resource.\",\n    )\n\n    allocation_strategy: Mapped[str] = mapped_column(\n        Enum(*defaults.allocation_strategy, name=\"ResourceAllocationStrategy\"),\n        default=defaults.allocation_strategy[0],\n        doc=\"Please read :class:`.Task` class documentation for details.\",\n    )\n\n    persistent_allocation: Mapped[bool] = mapped_column(\n        default=True,\n        doc=\"Please read :class:`.Task` class documentation for details.\",\n    )\n\n    watchers: Mapped[Optional[List[User]]] = relationship(\n        secondary=\"Task_Watchers\",\n        primaryjoin=\"Tasks.c.id==Task_Watchers.c.task_id\",\n        secondaryjoin=\"Task_Watchers.c.watcher_id==Users.c.id\",\n        back_populates=\"watching\",\n        doc=\"The list of :class:`.User` s watching this Task.\",\n    )\n\n    _responsible: Mapped[Optional[List[User]]] = relationship(\n        secondary=\"Task_Responsible\",\n        primaryjoin=\"Tasks.c.id==Task_Responsible.c.task_id\",\n        secondaryjoin=\"Task_Responsible.c.responsible_id==Users.c.id\",\n        back_populates=\"responsible_of\",\n        doc=\"The list of :class:`.User` s responsible from this Task.\",\n    )\n\n    priority: Mapped[Optional[int]] = mapped_column(\n        doc=\"\"\"An integer number between 0 and 1000 used by TaskJuggler to\n        determine the priority of this Task. The default value is 500.\"\"\",\n        default=500,\n    )\n\n    time_logs: Mapped[Optional[List[TimeLog]]] = relationship(\n        primaryjoin=\"TimeLogs.c.task_id==Tasks.c.id\",\n        back_populates=\"task\",\n        cascade=\"all, delete-orphan\",\n        doc=\"\"\"A list of :class:`.TimeLog` instances showing who and when has\n        spent how much effort on this task.\"\"\",\n    )\n\n    versions: Mapped[Optional[List[\"Version\"]]] = relationship(\n        primaryjoin=\"Versions.c.task_id==Tasks.c.id\",\n        back_populates=\"task\",\n        cascade=\"all, delete-orphan\",\n        doc=\"\"\"A list of :class:`.Version` instances showing the files created\n        for this task.\n        \"\"\",\n    )\n\n    _computed_resources: Mapped[Optional[List[User]]] = relationship(\n        secondary=\"Task_Computed_Resources\",\n        primaryjoin=\"Tasks.c.id==Task_Computed_Resources.c.task_id\",\n        secondaryjoin=\"Task_Computed_Resources.c.resource_id==Users.c.id\",\n        backref=\"computed_resource_in_tasks\",\n        doc=\"A list of :class:`.User` s computed by TaskJuggler. It is the \"\n        \"result of scheduling.\",\n    )\n\n    bid_timing: Mapped[Optional[float]] = mapped_column(\n        default=0,\n        doc=\"\"\"The value of the initial bid of this Task. It is an integer or\n        a float.\n        \"\"\",\n    )\n\n    bid_unit: Mapped[Optional[TimeUnit]] = mapped_column(\n        TimeUnitDecorator,\n        doc=\"\"\"The unit of the initial bid of this Task. It is a string value.\n        And should be one of 'min', 'h', 'd', 'w', 'm', 'y'.\n        \"\"\",\n    )\n\n    _schedule_seconds: Mapped[Optional[int]] = mapped_column(\n        \"schedule_seconds\",\n        Integer,\n        nullable=True,\n        doc=\"cache column for schedule_seconds\",\n    )\n\n    _total_logged_seconds: Mapped[Optional[int]] = mapped_column(\n        \"total_logged_seconds\",\n        doc=\"cache column for total_logged_seconds\",\n    )\n\n    reviews: Mapped[Optional[List[Review]]] = relationship(\n        primaryjoin=\"Reviews.c.task_id==Tasks.c.id\",\n        back_populates=\"task\",\n        cascade=\"all, delete-orphan\",\n        doc=\"\"\"A list of :class:`.Review` holding the details about the reviews\n        created for this task.\"\"\",\n    )\n\n    _review_number: Mapped[Optional[int]] = mapped_column(\"review_number\", default=0)\n\n    good_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Goods.id\"))\n\n    good: Mapped[Optional[Good]] = relationship(\n        primaryjoin=\"Tasks.c.good_id==Goods.c.id\",\n        uselist=False,\n        post_update=True,\n    )\n\n    # TODO: Add ``unmanaged`` attribute for Asset management only tasks.\n    #\n    # Some tasks are created for asset management purposes only and doesn't\n    # need TimeLogs to be entered. Create an attribute called ``unmanaged`` and\n    # and set it to False by default, and if its True don't include it in the\n    # TaskJuggler project. And do not track its status.\n\n    def __init__(\n        self,\n        project: Optional[\"Project\"] = None,\n        parent: Optional[\"Task\"] = None,\n        depends_on: Optional[List[\"Task\"]] = None,\n        resources: Optional[List[User]] = None,\n        alternative_resources: Optional[List[User]] = None,\n        responsible: Optional[List[User]] = None,\n        watchers: Optional[List[User]] = None,\n        start: Optional[datetime.datetime] = None,\n        end: Optional[datetime.datetime] = None,\n        schedule_timing: float = 1.0,\n        schedule_unit: TimeUnit = TimeUnit.Hour,\n        schedule_model: Optional[ScheduleModel] = None,\n        schedule_constraint: Optional[ScheduleConstraint] = ScheduleConstraint.NONE,\n        bid_timing: Optional[Union[int, float]] = None,\n        bid_unit: Optional[TimeUnit] = None,\n        is_milestone: bool = False,\n        priority: int = defaults.task_priority,\n        allocation_strategy: str = defaults.allocation_strategy[0],\n        persistent_allocation: bool = True,\n        good: Optional[Good] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        # temp attribute for remove event\n        self._previously_removed_dependent_tasks = []\n\n        # update kwargs with extras\n        kwargs[\"start\"] = start\n        kwargs[\"end\"] = end\n\n        kwargs[\"schedule_timing\"] = schedule_timing\n        kwargs[\"schedule_unit\"] = schedule_unit\n        kwargs[\"schedule_model\"] = schedule_model\n        kwargs[\"schedule_constraint\"] = schedule_constraint\n\n        super(Task, self).__init__(**kwargs)\n\n        # call the mixin __init__ methods\n        StatusMixin.__init__(self, **kwargs)\n        DateRangeMixin.__init__(self, **kwargs)\n        ScheduleMixin.__init__(self, **kwargs)\n\n        kwargs[\"parent\"] = parent\n        DAGMixin.__init__(self, **kwargs)\n\n        self._review_number = 0\n\n        # self.parent = parent\n        self._project = project\n\n        self.time_logs = []\n        self.versions = []\n\n        self.is_milestone = is_milestone\n\n        # update the status\n        with DBSession.no_autoflush:\n            self.status = self.status_list[\"WFD\"]\n\n        if depends_on is None:\n            depends_on = []\n\n        self.depends_on = depends_on\n\n        if self.is_milestone:\n            resources = None\n\n        if resources is None:\n            resources = []\n        self.resources = resources\n\n        if alternative_resources is None:\n            alternative_resources = []\n        self.alternative_resources = alternative_resources\n\n        # for newly created tasks set the computed_resources to resources\n        self.computed_resources = self.resources\n\n        if watchers is None:\n            watchers = []\n        self.watchers = watchers\n\n        if bid_timing is None:\n            bid_timing = self.schedule_timing\n\n        if bid_unit is None:\n            bid_unit = self.schedule_unit\n\n        self.bid_timing = bid_timing\n        self.bid_unit = bid_unit\n        self.priority = priority\n        if responsible is None:\n            responsible = []\n        self.responsible = responsible\n\n        self.allocation_strategy = allocation_strategy\n        self.persistent_allocation = persistent_allocation\n\n        self.update_status_with_dependent_statuses()\n\n        self.good = good\n\n    @reconstructor\n    def __init_on_load__(self) -> None:\n        \"\"\"Update defaults on load.\"\"\"\n        # temp attribute for remove event\n        self._previously_removed_dependent_tasks = []\n\n    def __eq__(self, other: Any) -> None:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a Task instance and has the same project,\n                parent, depends_on, start and end value and resources.\n        \"\"\"\n        return (\n            super(Task, self).__eq__(other)\n            and isinstance(other, Task)\n            and self.project == other.project\n            and self.parent == other.parent\n            and self.depends_on == other.depends_on\n            and self.start == other.start\n            and self.end == other.end\n            and self.resources == other.resources\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Task, self).__hash__()\n\n    @validates(\"time_logs\")\n    def _validate_time_logs(self, key: str, time_log: TimeLog) -> TimeLog:\n        \"\"\"Validate the given time_log value.\n\n        Args:\n            key (str): The name of the validated column.\n            time_log (TimeLog): A TimeLog instance to validate.\n\n        Raises:\n            TypeError: If the given time_log value is not a :class:`.TimeLog` instance.\n\n        Returns:\n            TimeLog: The validated TimeLog instance.\n        \"\"\"\n        if not isinstance(time_log, TimeLog):\n            raise TypeError(\n                \"{}.time_logs should only contain instances of \"\n                \"stalker.models.task.TimeLog, not {}: '{}'\".format(\n                    self.__class__.__name__, time_log.__class__.__name__, time_log\n                )\n            )\n\n        # TODO: convert this to an event\n        # update parents total_logged_second attribute\n        with DBSession.no_autoflush:\n            if self.parent:\n                self.parent.total_logged_seconds += time_log.total_seconds\n\n        return time_log\n\n    @validates(\"reviews\")\n    def _validate_reviews(self, key: str, review: Review) -> Review:\n        \"\"\"Validate the given review value.\n\n        Args:\n            key (str): The name of the validated column.\n            review (Review): The validated review value.\n\n        Raises:\n            TypeError: If the review is not a :class:`stalker.models.review.Review`\n                instance.\n\n        Returns:\n            Review: The validated review instance.\n        \"\"\"\n        if not isinstance(review, Review):\n            raise TypeError(\n                \"{}.reviews should only contain instances of \"\n                \"stalker.models.review.Review, not {}: '{}'\".format(\n                    self.__class__.__name__, review.__class__.__name__, review\n                )\n            )\n        return review\n\n    @validates(\"task_depends_on\")\n    def _validate_task_depends_on(self, key: str, task_depends_on: \"Task\") -> \"Task\":\n        \"\"\"Validate the given task_depends_on value.\n\n        Args:\n            key (str): The name of the validated column.\n            task_depends_on (Task): The Task instance that this Task is depending on.\n\n        Raises:\n            TypeError: If the `task_depends_on.depends_on` is not a Task instance.\n            StatusError: If the status of the current task is one of WIP, PREV, HREV,\n                OH, STOP or CMPL as this means the Task has been started to be worked on\n                and it is not allowed to change the dependency chain of an already\n                started task.\n            StatusError: If the current task is a container and its status is CMPL.\n            CircularDependencyError: If the given task is in circular relation with this\n                Task instance.\n\n        Returns:\n            Task: The validated task_depends_on value.\n        \"\"\"\n        if not isinstance(task_depends_on, TaskDependency):\n            raise TypeError(\n                \"{}.task_depends_on should only contain instances of \"\n                \"TaskDependency, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    task_depends_on.__class__.__name__,\n                    task_depends_on,\n                )\n            )\n\n        depends_on = task_depends_on.depends_on\n        if not depends_on:\n            # the relation is still not setup yet\n            # trust to the TaskDependency class for checking the\n            # depends_on attribute\n            return task_depends_on\n\n        # check the status of the current task\n        with DBSession.no_autoflush:\n            wfd = self.status_list[\"WFD\"]\n            rts = self.status_list[\"RTS\"]\n            wip = self.status_list[\"WIP\"]\n            prev = self.status_list[\"PREV\"]\n            hrev = self.status_list[\"HREV\"]\n            drev = self.status_list[\"DREV\"]\n            oh = self.status_list[\"OH\"]\n            stop = self.status_list[\"STOP\"]\n            cmpl = self.status_list[\"CMPL\"]\n\n            if self.status in [wip, prev, hrev, drev, oh, stop, cmpl]:\n                raise StatusError(\n                    f\"This is a {self.status.code} task and it is not allowed to \"\n                    f\"change the dependencies of a {self.status.code} task\"\n                )\n\n        # check for the circular dependency\n        with DBSession.no_autoflush:\n            check_circular_dependency(depends_on, self, \"depends_on\")\n            check_circular_dependency(depends_on, self, \"children\")\n\n        # check for circular dependency toward the parent, non of the parents\n        # should be depending on the given depends_on_task\n        with DBSession.no_autoflush:\n            parent = self.parent\n            while parent:\n                if parent in depends_on.depends_on:\n                    raise CircularDependencyError(\n                        f\"One of the parents of {self} is depending on {depends_on}\"\n                    )\n                parent = parent.parent\n\n        # update status with the new dependency\n        # update towards more constrained situation\n        #\n        # Do not update for example to RTS if the current dependent task is\n        # CMPL or STOP, this will be done by the approve or stop action in the\n        # dependent task it self\n\n        if self.status == rts:\n            with DBSession.no_autoflush:\n                do_update_status = False\n                if depends_on.status in [wfd, rts, wip, oh, prev, hrev, drev, oh]:\n                    do_update_status = True\n\n            if do_update_status:\n                self.status = wfd\n\n        return task_depends_on\n\n    @validates(\"schedule_timing\")\n    def _validate_schedule_timing(\n        self, key: str, schedule_timing: Union[int, float]\n    ) -> Union[int, float]:\n        \"\"\"Validate the given schedule_timing value.\n\n        Args:\n            key (str): The name of the validated column.\n            schedule_timing (Union[int, float]): The schedule_timing value to be\n                validated.\n\n        Returns:\n            float: The validated schedule_timing value.\n        \"\"\"\n        schedule_timing = ScheduleMixin._validate_schedule_timing(\n            self, key, schedule_timing\n        )\n\n        # reschedule\n        self._reschedule(schedule_timing, self.schedule_unit)\n\n        return schedule_timing\n\n    @validates(\"schedule_unit\")\n    def _validate_schedule_unit(\n        self, key: str, schedule_unit: Union[str, TimeUnit]\n    ) -> TimeUnit:\n        \"\"\"Validate the given schedule_unit value.\n\n        Args:\n            key (str): The name of the validated column.\n            schedule_unit (Union[str, TimeUnit]): The schedule_unit value to be\n                validated.\n\n        Returns:\n            TimeUnit: The validated schedule_unit value.\n        \"\"\"\n        schedule_unit = ScheduleMixin._validate_schedule_unit(self, key, schedule_unit)\n\n        if self.schedule_timing:\n            self._reschedule(self.schedule_timing, schedule_unit)\n\n        return schedule_unit\n\n    def _reschedule(\n        self, schedule_timing: Union[int, float], schedule_unit: Union[str, TimeUnit]\n    ) -> None:\n        \"\"\"Update the start and end date with schedule_timing and schedule_unit values.\n\n        Args:\n            schedule_timing (Union[int, float]): An integer or float value showing the\n                value of the schedule timing.\n            schedule_unit (Union[str, TimeUnit]): One of 'min', 'h', 'd',\n                'w', 'm', 'y' or a TimeUnit enum value.\n        \"\"\"\n        # update end date value by using the start and calculated duration\n        if not self.is_leaf:\n            return\n\n        from stalker import defaults\n\n        schedule_unit_value = None\n        if schedule_unit is not None:\n            schedule_unit = TimeUnit.to_unit(schedule_unit)\n            schedule_unit_value = schedule_unit.value\n\n        unit = defaults.datetime_units_to_timedelta_kwargs.get(schedule_unit_value)\n        if not unit:  # we are in a pre flushing state do not do anything\n            return\n\n        kwargs = {unit[\"name\"]: schedule_timing * unit[\"multiplier\"]}\n        calculated_duration = datetime.timedelta(**kwargs)\n        if (\n            self.schedule_constraint == ScheduleConstraint.NONE\n            or self.schedule_constraint == ScheduleConstraint.Start\n        ):\n            # get end\n            self._start, self._end, self._duration = self._validate_dates(\n                self.start, None, calculated_duration\n            )\n        elif self.schedule_constraint == ScheduleConstraint.End:\n            # get start\n            self._start, self._end, self._duration = self._validate_dates(\n                None, self.end, calculated_duration\n            )\n        elif self.schedule_constraint == ScheduleConstraint.Both:\n            # restore duration\n            self._start, self._end, self._duration = self._validate_dates(\n                self.start, self.end, None\n            )\n\n        # also update cached _schedule_seconds value\n        self._schedule_seconds = self.schedule_seconds\n\n    @validates(\"is_milestone\")\n    def _validate_is_milestone(self, key: str, is_milestone: Union[None, bool]) -> bool:\n        \"\"\"Validate the given is_milestone value.\n\n        Args:\n            key (str): The name of the validated column.\n            is_milestone (Union[None, bool]): The value to validated.\n\n        Raises:\n            TypeError: If the is_milestone value is not None and not a bool value.\n\n        Returns:\n            bool: The validated value.\n        \"\"\"\n        if is_milestone is None:\n            is_milestone = False\n\n        if not isinstance(is_milestone, bool):\n            raise TypeError(\n                \"{}.is_milestone should be a bool value (True or False), \"\n                \"not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    is_milestone.__class__.__name__,\n                    is_milestone,\n                )\n            )\n\n        if is_milestone:\n            self.resources = []\n\n        return bool(is_milestone)\n\n    @validates(\"parent\")\n    def _validate_parent(self, key: str, parent: \"Task\") -> \"Task\":\n        \"\"\"Validate the given parent value.\n\n        Args:\n            key (str): The name of the validated column.\n            parent (Task): The parent value to be validated.\n\n        Raises:\n            TypeError: If the parent value is not None and not a :class:`.Task`\n                instance.\n\n        Returns:\n            Task: The validated parent value.\n        \"\"\"\n        if parent is not None:\n            if not isinstance(parent, Task):\n                raise TypeError(\n                    \"{}.parent should be an instance of \"\n                    \"stalker.models.task.Task, not {}: '{}'\".format(\n                        self.__class__.__name__, parent.__class__.__name__, parent\n                    )\n                )\n\n            # check for cycle\n            check_circular_dependency(self, parent, \"children\")\n            check_circular_dependency(self, parent, \"depends_on\")\n\n        old_parent = self.parent\n        new_parent = parent\n\n        if old_parent:\n            old_parent.schedule_seconds -= self.schedule_seconds\n            old_parent.total_logged_seconds -= self.total_logged_seconds\n\n        # update the new parent\n        if new_parent:\n            # if the new parent was a leaf task before this attachment\n            # set schedule_seconds to 0\n            if new_parent.is_leaf:\n                new_parent.schedule_seconds = self.schedule_seconds\n                new_parent.total_logged_seconds = self.total_logged_seconds\n            else:\n                new_parent.schedule_seconds += self.schedule_seconds\n                new_parent.total_logged_seconds += self.total_logged_seconds\n\n        return parent\n\n    @validates(\"_project\")\n    def _validate_project(self, key: str, project: \"Project\") -> \"Project\":\n        \"\"\"Validate the given project value.\n\n        Args:\n            key (str): The name of the validated column.\n            project (stalker.models.project.Project): The project value to validate.\n\n        Raises:\n            TypeError: If the project is None and a\n                :class:`stalker.models.project.Project` cannot be found through the\n                parents of this current task.\n            TypeError: If the project is not a :class:`stalker.models.project.Project`\n                instance.\n\n        Returns:\n            stalker.models.project.Project: The validated\n                :class:`stalker.models.project.Project` instance.\n        \"\"\"\n        if project is None:\n            # check if there is a parent defined\n            if self.parent:\n                # use its project as the project\n\n                # to prevent prematurely flush the parent\n                with DBSession.no_autoflush:\n                    project = self.parent.project\n\n            else:\n                # no project, no task, go mad again!!!\n                raise TypeError(\n                    \"{}.project should be an instance of \"\n                    \"stalker.models.project.Project, not {}: '{}'.\\n\\nOr please supply \"\n                    \"a stalker.models.task.Task with the parent argument, so \"\n                    \"Stalker can use the project of the supplied parent task\".format(\n                        self.__class__.__name__, project.__class__.__name__, project\n                    )\n                )\n\n        from stalker.models.project import Project\n\n        if not isinstance(project, Project):\n            # go mad again it is not a project instance\n            raise TypeError(\n                \"{}.project should be an instance of stalker.models.project.Project, \"\n                \"not {}: '{}'\".format(\n                    self.__class__.__name__, project.__class__.__name__, project\n                )\n            )\n\n        # check if there is a parent\n        if not self.parent:\n            return project\n\n        # check if given project is matching the parent.project\n        with DBSession.no_autoflush:\n            if self.parent.project != project:\n                # don't go mad again, but warn the user that there is\n                # an ambiguity!!!\n                import warnings\n\n                message = (\n                    \"The supplied parent and the project is not matching in \"\n                    \"{}, Stalker will use the parent's project ({}) as the \"\n                    \"parent of this {}\".format(\n                        self, self.parent.project, self.__class__.__name__\n                    )\n                )\n\n                warnings.warn(message, RuntimeWarning, stacklevel=2)\n\n                # use the parent.project\n                project = self.parent.project\n\n        return project\n\n    @validates(\"priority\")\n    def _validate_priority(self, key: str, priority: Union[int, float]) -> int:\n        \"\"\"Validate the given priority value.\n\n        Args:\n            key (str): The name of the validated column.\n            priority (int): The priority value to be validated. It should be a float or\n                integer value between 0 and 1000, any other value will be clamped to\n                this range.\n\n        Raises:\n            TypeError: If the given priority value is not an integer or float.\n\n        Returns:\n            int: The validated priority value.\n        \"\"\"\n        if priority is None:\n            from stalker import defaults\n\n            priority = defaults.task_priority\n\n        if not isinstance(priority, (int, float)):\n            raise TypeError(\n                \"{}.priority should be an integer value between 0 and 1000, \"\n                \"not {}: '{}'\".format(\n                    self.__class__.__name__, priority.__class__.__name__, priority\n                )\n            )\n\n        if priority < 0:\n            priority = 0\n        elif priority > 1000:\n            priority = 1000\n\n        return int(priority)\n\n    @validates(\"children\")\n    def _validate_children(self, key: str, child: \"Task\") -> \"Task\":\n        \"\"\"Validate the given child value.\n\n        Args:\n            key (str): The name of the validated column.\n            child (Task): The child Task to be validated.\n\n        Returns:\n            Task: The validated child Task instance.\n        \"\"\"\n        # just empty the resources list\n        # do it without a flush\n        with DBSession.no_autoflush:\n            self.resources = []\n\n            # if this is the first ever child we receive\n            # set total_scheduled_seconds to child's total_logged_seconds\n            # and set schedule_seconds to child's schedule_seconds\n            if self.is_leaf:\n                # remove info from parent\n                old_schedule_seconds = self.schedule_seconds\n                self._total_logged_seconds = child.total_logged_seconds\n                self._schedule_seconds = child.schedule_seconds\n                # got a parent ?\n                if self.parent:\n                    # update schedule_seconds\n                    self.parent._schedule_seconds -= old_schedule_seconds\n                    self.parent._schedule_seconds += child.schedule_seconds\n\n                # it was a leaf but now a parent, so set the start to max and\n                # end to min\n                self._start = datetime.datetime.max.replace(tzinfo=pytz.utc)\n                self._end = datetime.datetime.min.replace(tzinfo=pytz.utc)\n\n            # extend start and end dates\n            self._expand_dates(self, child.start, child.end)\n\n        return child\n\n    @validates(\"resources\")\n    def _validate_resources(self, key: str, resource: User) -> User:\n        \"\"\"Validate the given resources value.\n\n        Args:\n            key (str): The name of the validated column.\n            resource (User): The value to validate.\n\n        Raises:\n            TypeError: If the given resource value is not a\n                :class:`stalker.models.auth.User` instance.\n\n        Returns:\n            User: The validated :class:`stalker.models.auth.User` instance.\n        \"\"\"\n        if not isinstance(resource, User):\n            raise TypeError(\n                \"{}.resources should only contain instances of \"\n                \"stalker.models.auth.User, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    resource.__class__.__name__,\n                    resource,\n                )\n            )\n        return resource\n\n    @validates(\"alternative_resources\")\n    def _validate_alternative_resources(self, key: str, resource: User) -> User:\n        \"\"\"Validate the given resource value.\n\n        Args:\n            key (str): The name of the validated column.\n            resource (User): The value to validate.\n\n        Raises:\n            TypeError: If the given resource value is not a\n                :class:`stalker.models.auth.User` instance.\n\n        Returns:\n            User: The validated User instance.\n        \"\"\"\n        if not isinstance(resource, User):\n            raise TypeError(\n                \"{}.alternative_resources should only contain instances of \"\n                \"stalker.models.auth.User, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    resource.__class__.__name__,\n                    resource,\n                )\n            )\n        return resource\n\n    @validates(\"_computed_resources\")\n    def _validate_computed_resources(self, key: str, resource: User) -> User:\n        \"\"\"Validate the computed resources value.\n\n        Args:\n            key (str): The name of the validated column.\n            resource (User): The value to validate.\n\n        Raises:\n            TypeError: If the given resource value is not a\n                :class:`stalker.models.auth.User` instance.\n\n        Returns:\n            User: The validated User instance.\n        \"\"\"\n        if not isinstance(resource, User):\n            raise TypeError(\n                \"{}.computed_resources should only contain instances of \"\n                \"stalker.models.auth.User, not {}: '{}'\".format(\n                    self.__class__.__name__, resource.__class__.__name__, resource\n                )\n            )\n        return resource\n\n    def _computed_resources_getter(self):\n        \"\"\"Return the _computed_resources attribute value.\n\n        Returns:\n            User: The computed_user attribute value if there are any else the\n                same content of the resources attribute.\n        \"\"\"\n        if not self.is_scheduled:\n            self._computed_resources = self.resources\n        return self._computed_resources\n\n    def _computed_resources_setter(self, resources: List[User]) -> None:\n        \"\"\"Set the _computed_resources attribute value.\n\n        Args:\n            resources (List[User]): List of User instances to set the computed\n                resources too.\n        \"\"\"\n        self._computed_resources = resources\n\n    computed_resources: Mapped[Optional[List[\"User\"]]] = synonym(\n        \"_computed_resources\",\n        descriptor=property(_computed_resources_getter, _computed_resources_setter),\n    )\n\n    @validates(\"allocation_strategy\")\n    def _validate_allocation_strategy(self, key: str, strategy: str) -> str:\n        \"\"\"Validate the given allocation_strategy value.\n\n        Args:\n            key (str): The name of the validated column.\n            strategy (str): The allocation strategy value to validate.\n\n        Raises:\n            TypeError: If the given allocation strategy value is not a string.\n            ValueError: If the given allocation strategy value is not one of\n                [ \"minallocated\", \"maxloaded\", \"minloaded\", \"order\", \"random\"].\n\n        Returns:\n            str: The validated allocation strategy value.\n        \"\"\"\n        from stalker import defaults\n\n        if strategy is None:\n            strategy = defaults.allocation_strategy[0]\n\n        if not isinstance(strategy, str):\n            raise TypeError(\n                \"{}.allocation_strategy should be one of {}, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    defaults.allocation_strategy,\n                    strategy.__class__.__name__,\n                    strategy,\n                )\n            )\n\n        if strategy not in defaults.allocation_strategy:\n            raise ValueError(\n                \"{}.allocation_strategy should be one of {}, not '{}'\".format(\n                    self.__class__.__name__,\n                    defaults.allocation_strategy,\n                    strategy,\n                )\n            )\n\n        return strategy\n\n    @validates(\"persistent_allocation\")\n    def _validate_persistent_allocation(\n        self, key: str, persistent_allocation: bool\n    ) -> bool:\n        \"\"\"Validate the given persistent_allocation value.\n\n        Args:\n            key (str): The name of the validate column.\n            persistent_allocation (bool): The persistent allocation value to be\n                validated.\n\n        Returns:\n            bool: The validated persistent allocation value.\n        \"\"\"\n        if persistent_allocation is None:\n            from stalker import defaults\n\n            persistent_allocation = defaults.persistent_allocation\n\n        return bool(persistent_allocation)\n\n    @validates(\"watchers\")\n    def _validate_watchers(self, key: str, watcher: User) -> User:\n        \"\"\"Validate the given watcher value.\n\n        Args:\n            key (str): The name of the validated column.\n            watcher (User): The watcher value to be validated.\n\n        Raises:\n            TypeError: If the watcher is not a :class:`stalker.models.auth.User`\n                instance.\n\n        Returns:\n            User: The validated :class:`stalker.models.auth.User` instance.\n        \"\"\"\n        if not isinstance(watcher, User):\n            raise TypeError(\n                \"{}.watchers should only contain instances of \"\n                \"stalker.models.auth.User, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    watcher.__class__.__name__,\n                    watcher,\n                )\n            )\n        return watcher\n\n    @validates(\"versions\")\n    def _validate_versions(self, key: str, version: \"Version\"):\n        \"\"\"Validate the given version value.\n\n        Args:\n            key (str): The name of the validated column.\n            version (stalker.models.version.Version): The version value to be\n                validated.\n\n        Raises:\n            TypeError: If the version is not an Version instance.\n\n        Returns:\n            stalker.models.version.Version: The validated\n                :class:`stalker.models.version.Version` value.\n        \"\"\"\n        from stalker.models.version import Version\n\n        if not isinstance(version, Version):\n            raise TypeError(\n                \"{}.versions should only contain instances of \"\n                \"stalker.models.version.Version, and not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    version.__class__.__name__,\n                    version,\n                )\n            )\n\n        return version\n\n    @validates(\"bid_timing\")\n    def _validate_bid_timing(\n        self, key: str, bid_timing: Union[None, int, float]\n    ) -> Union[None, int, float]:\n        \"\"\"Validate the given bid_timing value.\n\n        Args:\n            key (str): The name of the validated column.\n            bid_timing (Union[int, float]): The bid_timing value to be validated.\n\n        Raises:\n            TypeError: If the bid_timing is not None and not an integer or float.\n\n        Returns:\n            Union[int, float]: The validated bid_timing value.\n        \"\"\"\n        if bid_timing is not None:\n            if not isinstance(bid_timing, (int, float)):\n                raise TypeError(\n                    \"{}.bid_timing should be an integer or float showing the value of \"\n                    \"the initial bid for this {}, not {}: '{}'\".format(\n                        self.__class__.__name__,\n                        self.__class__.__name__,\n                        bid_timing.__class__.__name__,\n                        bid_timing,\n                    )\n                )\n        return bid_timing\n\n    @validates(\"bid_unit\")\n    def _validate_bid_unit(self, key: str, bid_unit: Union[str, TimeUnit]) -> str:\n        \"\"\"Validate the given bid_unit value.\n\n        Args:\n            key (str): The name of the validated column.\n            bid_unit (Union[str, TimeUnit]): The timing unit of the bid\n                value, should be a TimeUnit enum value or one of [\"min\", \"h\",\n                \"d\", \"w\", \"m\", \"y\", \"Minute\", \"Hour\", \"Day\", \"Week\", \"Month\",\n                \"Year\"].\n\n        Returns:\n            str: The validated bid_unit value.\n        \"\"\"\n        if bid_unit is None:\n            bid_unit = TimeUnit.Hour\n\n        bid_unit = TimeUnit.to_unit(bid_unit)\n\n        return bid_unit\n\n    @classmethod\n    def _expand_dates(\n        cls, task: \"Task\", start: datetime.datetime, end: datetime.datetime\n    ) -> None:\n        \"\"\"Extend the given tasks date values with the given start and end values.\n\n        Args:\n            task (Task): The Task instance.\n            start (datetime.datetime): The start datetime.datetime instance.\n            end (datetime.datetime): The end datetime.datetime instance.\n        \"\"\"\n        # update parents start and end date\n        if task:\n            if task.start > start:\n                task.start = start\n            if task.end < end:\n                task.end = end\n\n    # TODO: Why these methods are not in the DateRangeMixin class.\n    @validates(\"computed_start\")\n    def _validate_computed_start(\n        self, key: str, computed_start: datetime.datetime\n    ) -> datetime.datetime:\n        \"\"\"Validate the given computed_start value.\n\n        Args:\n            key (str): The name of the validated column.\n            computed_start (datetime.datetime): The computed start as a\n                datetime.datetime instance.\n\n        Returns:\n            datetime.datetime: The validated computed start value.\n        \"\"\"\n        self.start = computed_start\n        return computed_start\n\n    @validates(\"computed_end\")\n    def _validate_computed_end(\n        self, key: str, computed_end: datetime.datetime\n    ) -> datetime.datetime:\n        \"\"\"Validate the given computed_end value.\n\n        Args:\n            key (str): The name of the validated column.\n            computed_end (datetime.datetime): The computed start as a\n                datetime.datetime instance.\n\n        Returns:\n            datetime.datetime: The validated computed end value.\n        \"\"\"\n        self.end = computed_end\n        return computed_end\n\n    def _start_getter(self) -> datetime.datetime:\n        \"\"\"Return the start value.\n\n        Returns:\n            datetime.datetime: The start date and time value.\n        \"\"\"\n        return self._start\n\n    def _start_setter(self, start: datetime.datetime) -> None:\n        \"\"\"Set the start value.\n\n        Args:\n            start (datetime.datetime): The start date and time value to be validated.\n        \"\"\"\n        self._start, self._end, self._duration = self._validate_dates(\n            start, self._end, self._duration\n        )\n        self._expand_dates(self.parent, self.start, self.end)\n\n    def _end_getter(self) -> datetime.datetime:\n        \"\"\"Return the end value.\n\n        Returns:\n            datetime.datetime: The end date and time value.\n        \"\"\"\n        return self._end\n\n    def _end_setter(self, end: datetime.datetime) -> None:\n        \"\"\"Set the end value.\n\n        Args:\n            end (datetime.datetime): The end date and time value to be validated.\n        \"\"\"\n        # update the end only if this is not a container task\n        self._start, self._end, self._duration = self._validate_dates(\n            self.start, end, self.duration\n        )\n        self._expand_dates(self.parent, self.start, self.end)\n\n    def _project_getter(self) -> \"Project\":\n        \"\"\"Return the project value.\n\n        Returns:\n            stalker.models.project.Project: The :class:`stalker.models.project.Project`\n                instance.\n        \"\"\"\n        return self._project\n\n    project: Mapped[Optional[\"Project\"]] = synonym(\n        \"_project\",\n        descriptor=property(_project_getter),\n        doc=\"\"\"The owner Project of this task.\n\n        It is a read-only attribute. It is not possible to change the owner\n        Project of a Task it is defined when the Task is created.\n        \"\"\",\n    )\n\n    @property\n    def tjp_abs_id(self) -> str:\n        \"\"\"Return the calculated absolute id of this task.\n\n        Returns:\n            str: The calculated absolute id of this task.\n        \"\"\"\n        abs_id = self.parent.tjp_abs_id if self.parent else self.project.tjp_id\n        return f\"{abs_id}.{self.tjp_id}\"\n\n    @property\n    def to_tjp(self) -> str:\n        \"\"\"Return the TaskJuggler representation of this task.\n\n        Returns:\n            str: The TaskJuggler representation of this task.\n        \"\"\"\n        tab = \"    \"\n        indent = tab * len(self.parents)\n        has_inner_data = False\n        tjp = f'{indent}task {self.tjp_id} \"{self.tjp_id}\" {{'\n        if self.priority != 500:\n            has_inner_data = True\n            tjp += f\"\\n{indent}{tab}priority {self.priority}\"\n        if self.task_depends_on:\n            has_inner_data = True\n            tjp += f\"\\n{indent}{tab}depends \"\n            for i, depends_on in enumerate(self.task_depends_on):\n                if i != 0:\n                    tjp += \", \"\n                tjp += depends_on.to_tjp\n        if self.is_container:\n            has_inner_data = True\n            for child_task in self.children:\n                tjp += \"\\n\"\n                tjp += child_task.to_tjp\n        if self.resources:\n            has_inner_data = True\n            if self.schedule_constraint:\n                if self.schedule_constraint in [1, 3]:\n                    tjp += f\"\\n{indent}{tab}start {self.start.astimezone(pytz.utc).strftime('%Y-%m-%d-%H:%M')}\"  # noqa: B950\n                if self.schedule_constraint in [2, 3]:\n                    tjp += f\"\\n{indent}{tab}end {self.end.astimezone(pytz.utc).strftime('%Y-%m-%d-%H:%M')}\"  # noqa: B950\n            tjp += f\"\\n{indent}{tab}{self.schedule_model} {self.schedule_timing}{self.schedule_unit}\"  # noqa: B950\n            tjp += f\"\\n{indent}{tab}allocate \"\n            for i, resource in enumerate(sorted(self.resources, key=lambda x: x.id)):\n                if i != 0:\n                    tjp += \", \"\n                tjp += resource.tjp_id\n                if not self.alternative_resources:\n                    continue\n                tjp += f\" {{\\n{indent}{tab}{tab}alternative\\n{indent}{tab}{tab}\"\n                for i, alt_res in enumerate(\n                    sorted(self.alternative_resources, key=lambda x: x.id)\n                ):\n                    if i != 0:\n                        tjp += \", \"\n                    tjp += alt_res.tjp_id\n                tjp += f\" select {self.allocation_strategy}\"\n                if self.persistent_allocation:\n                    tjp += f\"\\n{indent}{tab}{tab}persistent\"\n                tjp += f\"\\n{indent}{tab}}}\"\n        for time_log in self.time_logs:\n            has_inner_data = True\n            tjp += (\n                f\"\\n{indent}{tab}booking \"\n                f\"{time_log.resource.tjp_id} \"\n                f\"{time_log.start.astimezone(pytz.utc).strftime('%Y-%m-%d-%H:%M:%S')} \"\n                f\"- \"\n                f\"{time_log.end.astimezone(pytz.utc).strftime('%Y-%m-%d-%H:%M:%S')} \"\n                \"{ overtime 2 }\"\n            )\n        tjp += f\"\\n{indent}}}\" if has_inner_data else \"}\"\n        return tjp\n\n    @property\n    def level(self) -> int:\n        \"\"\"Returns the hierarchical level of this task.\n\n        It is a temporary property and will be useless when Stalker has its own\n        implementation of a proper Gantt Chart. Right now it is used by the jQueryGantt.\n\n        Returns:\n            int: The hierarchical level of this task.\n        \"\"\"\n        i = 0\n        current_task = self\n        while current_task:\n            i += 1\n            current_task = current_task.parent\n        return i\n\n    @property\n    def is_scheduled(self) -> bool:\n        \"\"\"Return if this task has both a computed_start and computed_end values.\n\n        Returns:\n            bool: Return True if this task has both a computed_start and computed_end\n                values.\n        \"\"\"\n        return self.computed_start is not None and self.computed_end is not None\n\n    def _total_logged_seconds_getter(self) -> int:\n        \"\"\"Return the total effort spent for this Task.\n\n        It is the sum of all the TimeLogs recorded for this task as seconds.\n\n        Returns:\n            int: An integer showing the total seconds spent.\n        \"\"\"\n        with DBSession.no_autoflush:\n            if not self.is_leaf:\n                if self._total_logged_seconds is None:\n                    self.update_schedule_info()\n                return self._total_logged_seconds\n\n            if self.schedule_model == ScheduleModel.Effort:\n                logger.debug(\"effort based task detected!\")\n                try:\n                    sql = \"\"\"\n                    select\n                        extract(epoch from sum(\"TimeLogs\".end - \"TimeLogs\".start))::int\n                    from \"TimeLogs\"\n                    where \"TimeLogs\".task_id = :task_id\n                    \"\"\"\n                    connection = DBSession.connection()\n                    result = connection.execute(\n                        text(sql), {\"task_id\": self.id}\n                    ).fetchone()\n                    return result[0] if result[0] else 0\n                except (UnboundExecutionError, OperationalError, ProgrammingError):\n                    # no database connection\n                    # fallback to Python\n                    logger.debug(\"No session found! Falling back to Python\")\n                    seconds = 0\n                    for time_log in self.time_logs:\n                        seconds += time_log.total_seconds\n                    return seconds\n            else:\n                now = datetime.datetime.now(pytz.utc)\n                if self.schedule_model == ScheduleModel.Duration:\n                    # directly return the difference between\n                    # min(now - start, end - start)\n                    logger.debug(\n                        \"duration based task detected!, \"\n                        \"calculating schedule_info from duration of the task\"\n                    )\n                    daily_working_hours = 86400.0\n                elif self.schedule_model == ScheduleModel.Length:\n                    # directly return the difference between\n                    # min(now - start, end - start)\n                    # but use working days\n                    logger.debug(\n                        \"length based task detected!, \"\n                        \"calculating schedule_info from duration of the task\"\n                    )\n                    from stalker import defaults\n\n                    daily_working_hours = defaults.daily_working_hours\n\n                if self.end <= now:\n                    seconds = (\n                        self.duration.days * daily_working_hours + self.duration.seconds\n                    )\n                elif self.start >= now:\n                    seconds = 0\n                else:\n                    past = now - self.start\n                    past_as_seconds = past.days * daily_working_hours + past.seconds\n                    logger.debug(f\"past_as_seconds: {past_as_seconds}\")\n                    seconds = past_as_seconds\n                return seconds\n\n    def _total_logged_seconds_setter(self, seconds: int) -> None:\n        \"\"\"Set the total_logged_seconds value.\n\n        This is mainly used for container tasks, to cache the child logged_seconds\n\n        Args:\n            seconds (int): An integer value for the seconds.\n        \"\"\"\n        # only set for container tasks\n        if self.is_container:\n            # update parent\n            old_value = 0\n            if self._total_logged_seconds:\n                old_value = self._total_logged_seconds\n            self._total_logged_seconds = seconds\n            if self.parent:\n                self.parent.total_logged_seconds = (\n                    self.parent.total_logged_seconds - old_value + seconds\n                )\n\n    total_logged_seconds: Mapped[Optional[int]] = synonym(\n        \"_total_logged_seconds\",\n        descriptor=property(_total_logged_seconds_getter, _total_logged_seconds_setter),\n    )\n\n    def _schedule_seconds_getter(self) -> int:\n        \"\"\"Return the total effort, length or duration in seconds.\n\n        This is used for calculating the percent_complete value.\n\n        Returns:\n            int: The total effort, length or duration in seconds.\n        \"\"\"\n        # for container tasks use the children schedule_seconds attribute\n        if self.is_container:\n            if self._schedule_seconds is None or self._schedule_seconds < 0:\n                self.update_schedule_info()\n            return self._schedule_seconds\n        else:\n            return self.to_seconds(\n                self.schedule_timing, self.schedule_unit, self.schedule_model\n            )\n\n    def _schedule_seconds_setter(self, seconds: int) -> None:\n        \"\"\"Set the schedule_seconds of this task.\n\n        Mainly used for container tasks.\n\n        Args:\n            seconds (int): An integer value of schedule_seconds for this task.\n        \"\"\"\n        # do it only for container tasks\n        if self.is_container:\n            # also update the parents\n            with DBSession.no_autoflush:\n                if self.parent:\n                    current_value = 0\n                    if self._schedule_seconds:\n                        current_value = self._schedule_seconds\n                    self.parent.schedule_seconds = (\n                        self.parent.schedule_seconds - current_value + seconds\n                    )\n                self._schedule_seconds = seconds\n\n    schedule_seconds: Mapped[Optional[int]] = synonym(\n        \"_schedule_seconds\",\n        descriptor=property(_schedule_seconds_getter, _schedule_seconds_setter),\n    )\n\n    def update_schedule_info(self) -> None:\n        \"\"\"Update the total_logged_seconds and schedule_seconds attributes.\n\n        This updates the total_logged_seconds and schedule_seconds attributes by using\n        the children info and triggers an update on every children.\n        \"\"\"\n        if self.is_container:\n            total_logged_seconds = 0\n            schedule_seconds = 0\n            logger.debug(f\"updating schedule info for: {self.name}\")\n            for child in self.children:\n                # update children if they are a container task\n                if child.is_container:\n                    child.update_schedule_info()\n                    if child.schedule_seconds:\n                        schedule_seconds += child.schedule_seconds\n                    if child.total_logged_seconds:\n                        total_logged_seconds += child.total_logged_seconds\n                else:\n                    # DRY please!!!!\n                    schedule_seconds += (\n                        child.schedule_seconds if child.schedule_seconds else 0\n                    )\n                    total_logged_seconds += (\n                        child.total_logged_seconds if child.total_logged_seconds else 0\n                    )\n\n            self._schedule_seconds = schedule_seconds\n            self._total_logged_seconds = total_logged_seconds\n        else:\n            self._schedule_seconds = self.schedule_seconds\n            self._total_logged_seconds = self.total_logged_seconds\n\n    @property\n    def percent_complete(self) -> float:\n        \"\"\"Calculate and return the percent_complete value.\n\n        The percent_complete value is based on the total_logged_seconds and\n        schedule_seconds of the task.\n\n        Container tasks will use info from their children.\n\n        Returns:\n            float: The percent complete value between 0 and 1.\n        \"\"\"\n        if self.is_container and (\n            self._total_logged_seconds is None or self._schedule_seconds is None\n        ):\n            self.update_schedule_info()\n\n        return self.total_logged_seconds / float(self.schedule_seconds) * 100.0\n\n    @property\n    def remaining_seconds(self) -> int:\n        \"\"\"Return the remaining amount of effort, length or duration left as seconds.\n\n        Returns:\n            int: The remaining amount of effort, length or duration in seconds.\n        \"\"\"\n        # for effort based tasks use the time_logs\n        return self.schedule_seconds - self.total_logged_seconds\n\n    def _responsible_getter(self) -> List[User]:\n        \"\"\"Return the current responsible of this task.\n\n        Returns:\n            List[User]: The list of :class:`stalker.models.auth.User` instances that are\n                the responsible of this task. If no stored value is found the same list\n                of Users from the parents will be returned.\n        \"\"\"\n        if not self._responsible:\n            # traverse parents\n            for parent in reversed(self.parents):\n                if parent.responsible:\n                    self._responsible = copy.copy(parent.responsible)\n                    break\n\n        # so parents do not have a responsible\n        return self._responsible\n\n    def _responsible_setter(self, responsible: List[User]) -> None:\n        \"\"\"Set the responsible attribute.\n\n        Args:\n            responsible (List[User]): A list of :class:`.User` instances to be the\n                responsible of this Task.\n        \"\"\"\n        self._responsible = responsible\n\n    @validates(\"_responsible\")\n    def _validate_responsible(self, key, responsible: User) -> User:\n        \"\"\"Validate the given responsible value (each responsible).\n\n        Args:\n            key (str): The name of the validated column.\n            responsible (User): A :class:`stalker.models.auth.User` instance to be\n                validated.\n\n        Raises:\n            TypeError: If the given responsible value is not a User instance.\n\n        Returns:\n            User: The validated :class:`stalker.models.auth.User` instance.\n        \"\"\"\n        if not isinstance(responsible, User):\n            raise TypeError(\n                \"{}.responsible should only contain instances of \"\n                \"stalker.models.auth.User, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    responsible.__class__.__name__,\n                    responsible,\n                )\n            )\n        return responsible\n\n    responsible: Mapped[Optional[List[User]]] = synonym(\n        \"_responsible\",\n        descriptor=property(\n            _responsible_getter,\n            _responsible_setter,\n            doc=\"\"\"The responsible of this task.\n\n            This attribute will return the responsible of this task which is a\n            list of :class:`.User` instances. If there is no responsible set\n            for this task, then it will try to find a responsible in its\n            parents.\n            \"\"\",\n        ),\n    )\n\n    @property\n    def tickets(self) -> List[Ticket]:\n        \"\"\"Return the tickets referencing this Task in their links attribute.\n\n        Returns:\n            List[Ticket]: List of :class:`stalker.models.ticket.Ticket` instances that\n                are referencing this Task in their links attribute.\n        \"\"\"\n        return Ticket.query.filter(Ticket.links.contains(self)).all()\n\n    @property\n    def open_tickets(self) -> List[Ticket]:\n        \"\"\"Return the open tickets referencing this task in their links attribute.\n\n        Returns:\n            List[Ticket]: List of open :class:`stalker.models.ticket.Ticket` instances\n                that are referencing this Task in their links attribute.\n        \"\"\"\n        status_closed = Status.query.filter(Status.name == \"Closed\").first()\n        return (\n            Ticket.query.filter(Ticket.links.contains(self))\n            .filter(Ticket.status != status_closed)\n            .all()\n        )\n\n    def walk_dependencies(\n        self,\n        method: Union[int, str, TraversalDirection] = TraversalDirection.BreadthFirst,\n    ) -> Generator[None, \"Task\", None]:\n        \"\"\"Walk the dependencies of this task.\n\n        Args:\n            method (Union[int, str, TraversalDirection]): The walk method\n                defined by the :class:`.TraversalDirection` enum value. Default\n                is :attr:`.TraversalDirection.BreadthFirst`.\n\n        Yields:\n            Task: Yields Task instances.\n        \"\"\"\n        for t in walk_hierarchy(self, \"depends_on\", method=method):\n            yield t\n\n    @validates(\"good\")\n    def _validate_good(self, key: str, good: Good) -> Good:\n        \"\"\"Validate the given good value.\n\n        Args:\n            key (str): The name of the validated column.\n            good (Good): The validated good value.\n\n        Raises:\n            TypeError: If the given good is not None and not a Good instance.\n\n        Returns:\n            Good: The validated good value.\n        \"\"\"\n        if good is not None and not isinstance(good, Good):\n            raise TypeError(\n                \"{}.good should be a stalker.models.budget.Good instance, \"\n                \"not {}: '{}'\".format(\n                    self.__class__.__name__, good.__class__.__name__, good\n                )\n            )\n        return good\n\n    # =============\n    # ** ACTIONS **\n    # =============\n    def create_time_log(\n        self, resource: User, start: datetime.datetime, end: datetime.datetime\n    ) -> TimeLog:\n        \"\"\"Create a TimeLog with the given information.\n\n        This will ease creating TimeLog instances for task.\n\n        Args:\n            resource (User): The :class:`stalker.models.auth.User` instance as\n                the resource for the TimeLog.\n            start (datetime.datetime): The start date and time.\n            end (datetime.datetime): The end date and time.\n\n        Returns:\n            TimeLog: The created TimeLog instance.\n        \"\"\"\n        # all the status checks are now part of TimeLog._validate_task\n        # create a TimeLog\n        return TimeLog(task=self, resource=resource, start=start, end=end)\n        # also updating parent statuses are done in TimeLog._validate_task\n\n    def request_review(self, version: Optional[\"Version\"] = None) -> List[Review]:\n        \"\"\"Create and return Review instances for each of the responsible of this task.\n\n        Also set the task status to PREV.\n\n        .. versionadded:: 0.2.0\n           Request review will not cap the timing of this task anymore.\n\n        Only applicable to leaf tasks.\n\n        Args:\n            version (Optional[Version]): An optional :class:`.Version` instance\n                can also be passed. The :class:`.Version` should be related to\n                this :class:`.Task`.\n\n        Raises:\n            StatusError: If the current task status is not WIP a StatusError\n                will be raised as the task has either not been started on being\n                worked yet, it is already on review, a dependency might be\n                under review or this is stopped, hold or completed.\n\n        Returns:\n            List[Review]: The list of :class:`stalker.models.review.Review`\n                instances created.\n        \"\"\"\n        # check task status\n        with DBSession.no_autoflush:\n            wip = self.status_list[\"WIP\"]\n            prev = self.status_list[\"PREV\"]\n\n        if self.status != wip:\n            raise StatusError(\n                \"{task} (id:{id}) is a {status} task, and it is not suitable for \"\n                \"requesting a review, please supply a WIP task instead.\".format(\n                    task=self.name, id=self.id, status=self.status.code\n                )\n            )\n\n        # create Review instances for each Responsible of this task\n        reviews = []\n        for responsible in self.responsible:\n            reviews.append(Review(task=self, version=version, reviewer=responsible))\n\n        # update the status to PREV\n        self.status = prev\n\n        # no need to update parent or dependent task statuses\n        return reviews\n\n    def request_revision(\n        self,\n        reviewer: Optional[User] = None,\n        description: str = \"\",\n        schedule_timing: int = 1,\n        schedule_unit: Union[str, TimeUnit] = TimeUnit.Hour,\n    ) -> Review:\n        \"\"\"Request revision.\n\n        Applicable to PREV or CMPL leaf tasks. This method will expand the\n        schedule timings of the task according to the supplied arguments.\n\n        When request_revision is called on a PREV task, the other NEW Review\n        instances (those created when request_review on a WIP task is called\n        and still waiting a review) will be deleted.\n\n        This method at the end will return a new Review instance with correct\n        attributes (reviewer, description, schedule_timing, schedule_unit and\n        review_number attributes).\n\n        Args:\n            reviewer (User): This is the user that requested the revision. They\n                don't need to be the responsible, anybody that has a Permission\n                to create a Review instance can request a revision.\n            description (str): The description of the requested revision.\n            schedule_timing (int): The timing value of the requested revision.\n                The task will be extended this much of duration. Works along\n                with the ``schedule_unit`` parameter. The default value is 1.\n            schedule_unit (Union[str, TimeUnit]): The timing unit value of the\n                requested revision. The task will be extended this much of\n                duration. Works along with the ``schedule_timing`` parameter.\n                The default value is `TimeUnit.Hour`.\n\n        Raises:\n            StatusError: If the status of the current task is not PREV or CMPL.\n\n        Returns:\n            Review: The newly created :class:`stalker.models.review.Review`\n                instance.\n        \"\"\"\n        # check status\n        with DBSession.no_autoflush:\n            prev = self.status_list[\"PREV\"]\n            cmpl = self.status_list[\"CMPL\"]\n\n        if self.status not in [prev, cmpl]:\n            raise StatusError(\n                \"{task} (id: {id}) is a {status} task, and it is not suitable \"\n                \"for requesting a revision, please supply a PREV or CMPL \"\n                \"task\".format(task=self.name, id=self.id, status=self.status.code)\n            )\n\n        # *********************************************************************\n        # TODO: I don't like this part, find another way to delete them\n        #       directly\n        # find other NEW Reviews and delete them\n        reviews_to_be_deleted = []\n        for r in self.reviews:\n            if r.status.code == \"NEW\":\n                reviews_to_be_deleted.append(r)\n\n        for r in reviews_to_be_deleted:\n            logger.debug(f\"removing {r} from task.reviews\")\n            self.reviews.remove(r)\n            r.task = None\n            try:\n                DBSession.delete(r)\n            except InvalidRequestError:\n                # not persisted yet\n                # do nothing\n                pass\n        # *********************************************************************\n\n        # create a Review instance with the given data\n        review = Review(reviewer=reviewer, task=self)\n        # and call request_revision in the Review instance\n        review.request_revision(\n            schedule_timing=schedule_timing,\n            schedule_unit=schedule_unit,\n            description=description,\n        )\n        return review\n\n    def hold(self) -> None:\n        \"\"\"Pause the execution of this task by setting its status to OH.\n\n        Only applicable to RTS and WIP tasks, any task with other statuses will raise a\n        StatusError. Also sets the priority to 0.\n\n        Raises:\n            StatusError: If the status of the task is not RTS or WIP.\n        \"\"\"\n        # check if status is WIP\n        with DBSession.no_autoflush:\n            wip = self.status_list[\"WIP\"]\n            drev = self.status_list[\"DREV\"]\n            oh = self.status_list[\"OH\"]\n\n        if self.status not in [wip, drev, oh]:\n            raise StatusError(\n                \"{task} (id:{id}) is a {status} task, only WIP or DREV tasks can be \"\n                \"set to On Hold\".format(\n                    task=self.name, id=self.id, status=self.status.code\n                )\n            )\n        # update the status to OH\n        self.status = oh\n\n        # set the priority to 0\n        self.priority = 0\n\n        # no need to update the status of dependencies nor parents\n\n    def stop(self) -> None:\n        \"\"\"Stop this task.\n\n        It is nearly equivalent to deleting this task. But this will at least preserve\n        the TimeLogs entered for this task. It is only possible to stop WIP tasks.\n\n        You can use :meth:`.resume` to resume the task.\n\n        The only difference between :meth:`.hold` (other than setting the task to\n        different statuses) is the schedule info, while the :meth:`.hold` method will\n        preserve the schedule info, stop() will set the schedule info to the current\n        effort.\n\n        So if 2 days of effort has been entered for a 4 days task, when stopped the task\n        effort will be capped to 2 days, thus TaskJuggler will not try to reserve any\n        resource for this task anymore.\n\n        Also, STOP tasks will be ignored in dependency relations.\n\n        Raises:\n            StatusError: If the task status is not WIP, DREV or STOP.\n        \"\"\"\n        # check the status\n        with DBSession.no_autoflush:\n            wip = self.status_list[\"WIP\"]\n            drev = self.status_list[\"DREV\"]\n            stop = self.status_list[\"STOP\"]\n\n        if self.status not in [wip, drev, stop]:\n            raise StatusError(\n                \"{task} (id:{id})is a {status} task and it is not possible to stop a \"\n                \"{status} task.\".format(\n                    task=self.name, id=self.id, status=self.status.code\n                )\n            )\n\n        # set the status\n        self.status = stop\n\n        # clamp schedule values\n        self.schedule_timing, self.schedule_unit = self.least_meaningful_time_unit(\n            self.total_logged_seconds\n        )\n\n        # update parent statuses\n        self.update_parent_statuses()\n\n        # update dependent task statuses\n        for dependency in self.dependent_of:\n            dependency.update_status_with_dependent_statuses()\n\n    def resume(self) -> None:\n        \"\"\"Resume the execution of this task.\n\n        Resume the task by setting its status to RTS or WIP depending on its time_logs\n        attribute, so if it has TimeLogs then it will resume as WIP and if it doesn't\n        then it will resume as RTS. Only applicable to Tasks with status OH.\n\n        Raises:\n            StatusError: If the task status is not OH or STOP.\n        \"\"\"\n        # check status\n        with DBSession.no_autoflush:\n            wip = self.status_list[\"WIP\"]\n            oh = self.status_list[\"OH\"]\n            stop = self.status_list[\"STOP\"]\n\n        if self.status not in [oh, stop]:\n            raise StatusError(\n                \"{task} (id:{id}) is a {status} task, and it is not suitable to be \"\n                \"resumed, please supply an OH or STOP task\".format(\n                    task=self.name, id=self.id, status=self.status.code\n                )\n            )\n        else:\n            # set to WIP\n            self.status = wip\n\n        # now update the status with dependencies\n        self.update_status_with_dependent_statuses()\n\n        # and update parents statuses\n        self.update_parent_statuses()\n\n    def review_set(self, review_number: Union[None, int] = None) -> List[Review]:\n        \"\"\"Return the reviews with the given review_number.\n\n        Args:\n            review_number (Union[None, int]): The review number. If\n                review_number is skipped it will return the latest set of\n                reviews.\n\n        Raises:\n            TypeError: If the review_number is not None and not an integer.\n            ValueError: If the review_number is less than 0.\n\n        Returns:\n            List[Review]: The reviews with the given review number or the\n                latest set of :class:`stalker.models.review.Review` instances\n                if the review number is is skipped or None.\n        \"\"\"\n        review_set = []\n        if review_number is None:\n            if self.status.code == \"PREV\":\n                review_number = self.review_number + 1\n            else:\n                review_number = self.review_number\n\n        if not isinstance(review_number, int):\n            raise TypeError(\n                \"review_number argument in {}.review_set should be a positive \"\n                \"integer, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    review_number.__class__.__name__,\n                    review_number,\n                )\n            )\n\n        if review_number < 1:\n            raise ValueError(\n                \"review_number argument in {}.review_set should be a positive \"\n                \"integer, not {}\".format(self.__class__.__name__, review_number)\n            )\n\n        for review in self.reviews:\n            if review.review_number == review_number:\n                review_set.append(review)\n\n        return review_set\n\n    def update_status_with_dependent_statuses(\n        self,\n        removing: Optional[\"Task\"] = None,\n    ) -> None:  # noqa: C901\n        \"\"\"Update the status by looking at the dependent tasks.\n\n        Args:\n            removing (Task): The item that is being removed right now, used for\n                the remove event to overcome the update issue.\n        \"\"\"\n        if self.is_container:\n            # do nothing, its status will be decided by its children\n            return\n\n        # in case there is no database\n        # try to find the statuses from the status_list attribute\n        with DBSession.no_autoflush:\n            wfd = self.status_list[\"WFD\"]\n            rts = self.status_list[\"RTS\"]\n            wip = self.status_list[\"WIP\"]\n            hrev = self.status_list[\"HREV\"]\n            drev = self.status_list[\"DREV\"]\n            cmpl = self.status_list[\"CMPL\"]\n\n        if removing:\n            self._previously_removed_dependent_tasks.append(removing)\n        else:\n            self._previously_removed_dependent_tasks = []\n\n        # create a new list from depends_on and skip_list\n        dependency_list = []\n        for dependency in self.depends_on:\n            if dependency not in self._previously_removed_dependent_tasks:\n                dependency_list.append(dependency)\n\n        logger.debug(f\"self           : {self}\")\n        logger.debug(f\"self.depends_on: {self.depends_on}\")\n        logger.debug(f\"dependency_list: {dependency_list}\")\n\n        # if not self.depends_on:\n        if not dependency_list:\n            # doesn't have any dependency\n            # convert its status from WFD to RTS if necessary\n            if self.status == wfd:\n                self.status = rts\n            elif self.status in [wip, drev]:\n                if len(self.time_logs):\n                    self.status = wip\n                else:\n                    # doesn't have any TimeLogs return back to rts\n                    self.status = rts\n            return\n\n        # Keep this part for future reference\n        # if self.id:\n        #     # use pure sql\n        #     logger.debug('using pure SQL to query dependency statuses')\n        #     sql_query = \"\"\"\n        #         select\n        #             \"Statuses\".code\n        #         from \"Tasks\"\n        #             join \"Task_Dependencies\"\n        #                 on \"Tasks\".id = \"Task_Dependencies\".task_id\n        #             join \"Tasks\" as \"Dependent_Tasks\"\n        #                 on \"Task_Dependencies\".depends_on_id = \"Dependent_Tasks\".id\n        #             join \"Statuses\" on \"Dependent_Tasks\".status_id = \"Statuses\".id\n        #         where \"Tasks\".id = {}\n        #         group by \"Statuses\".code\n        #     \"\"\".format(self.id)\n        #\n        #     result = DBSession.connection().execute(sql_query)\n        #\n        #     # convert to a binary value\n        #     binary_status = reduce(\n        #         lambda x, y: x+y,\n        #         map(lambda x: binary_status_codes[x[0]], result.fetchall()),\n        #         0\n        #     )\n        #\n        # else:\n        # task is not committed yet, use Python version\n        logger.debug(\"using pure Python to query dependency statuses\")\n        binary_status = 0\n        dep_statuses = []\n        # with DBSession.no_autoflush:\n        logger.debug(\n            \"self.depends_on in update_status_with_dependent_statuses: \"\n            f\"{self.depends_on}\"\n        )\n        for dependency in dependency_list:\n            # consider every status only once\n            if dependency.status not in dep_statuses:\n                dep_statuses.append(dependency.status)\n                binary_status += BINARY_STATUS_VALUES[dependency.status.code]\n\n        logger.debug(f\"status of the task                   : {self.status.code}\")\n        logger.debug(f\"binary status for dependency statuses: {binary_status}\")\n\n        work_alone = binary_status < 4\n        status = self.status\n        if work_alone:\n            if self.status == wfd:\n                status = rts\n            elif self.status == drev:\n                status = hrev\n                # Expand task timing with the timing resolution if there is no\n                # time left for this task\n                if self.total_logged_seconds == self.schedule_seconds:\n                    from stalker import defaults\n\n                    total_seconds = (\n                        self.schedule_seconds + defaults.timing_resolution.seconds\n                    )\n                    timing, unit = self.least_meaningful_time_unit(total_seconds)\n                    self.schedule_timing = timing\n                    self.schedule_unit = unit\n\n        else:\n            if self.status == rts:\n                status = wfd\n            elif self.status == wip:\n                status = drev\n            elif self.status == hrev:\n                status = drev\n            elif self.status == cmpl:\n                status = drev\n\n        logger.debug(f\"setting status from {self.status} to {status}: \")\n        self.status = status\n\n        # also update parent statuses\n        self.update_parent_statuses()\n\n        # # also update dependent tasks\n        # for dependency in dependency_list:\n        #     dependency.update_status_with_dependent_statuses()\n\n    def update_parent_statuses(self) -> None:\n        \"\"\"Update the parent statuses of this task if any.\"\"\"\n        # prevent query-invoked auto-flush\n        with DBSession.no_autoflush:\n            if self.parent:\n                self.parent.update_status_with_children_statuses()\n\n    def update_status_with_children_statuses(self) -> None:\n        \"\"\"Update the task status according to its children statuses.\"\"\"\n        logger.debug(f\"setting statuses with child statuses for: {self.name}\")\n\n        if not self.is_container:\n            # do nothing\n            logger.debug(\"not a container returning!\")\n            return\n\n        with DBSession.no_autoflush:\n            wfd = self.status_list[\"WFD\"]\n            rts = self.status_list[\"RTS\"]\n            wip = self.status_list[\"WIP\"]\n            cmpl = self.status_list[\"CMPL\"]\n\n        parent_statuses_map = [wfd, rts, wip, cmpl]\n\n        # use Python\n        logger.debug(\"using pure Python to query children statuses\")\n        binary_status = 0\n        children_statuses = []\n        for child in self.children:\n            # consider every status only once\n            if child.status not in children_statuses:\n                children_statuses.append(child.status)\n                binary_status += BINARY_STATUS_VALUES[child.status.code]\n\n        # any condition not listed above should return the status_index of \"2=WIP\"\n        status_index = CHILDREN_TO_PARENT_STATUSES_MAP.get(binary_status, 2)\n        status = parent_statuses_map[status_index]\n\n        logger.debug(f\"binary statuses value : {binary_status}\")\n        logger.debug(f\"setting status to     : {status.code}\")\n\n        self.status = status\n\n        # # update dependent task statuses\n        # for dependent in self.dependent_of:\n        #     dependent.update_status_with_dependent_statuses()\n\n        # go to parents\n        self.update_parent_statuses()\n\n    def _review_number_getter(self) -> None:\n        \"\"\"Return the revision number value.\n\n        Returns:\n            int: The current revision number.\n        \"\"\"\n        return self._review_number\n\n    review_number: Mapped[Optional[int]] = synonym(\n        \"_review_number\",\n        descriptor=property(_review_number_getter),\n        doc=\"returns the _review_number attribute value\",\n    )\n\n    def _template_variables(self) -> dict:\n        \"\"\"Return variables used in rendering the filename template.\n\n        Returns:\n            dict: The template variables.\n        \"\"\"\n        # TODO: add test for this template variables\n        from stalker import Asset, Shot\n\n        asset = None\n        sequence = None\n        scene = None\n        shot = None\n        if isinstance(self, Shot):\n            shot = self\n            sequence = self.sequence\n            scene = self.scene\n        elif isinstance(self, Asset):\n            asset = self\n        else:\n            # Look for shots in parents\n            for parent in self.parents:\n                if isinstance(parent, Shot):\n                    sequence = parent.sequence\n                    scene = parent.scene\n                    break\n                elif isinstance(parent, Asset):\n                    asset = parent\n                    break\n\n        # get the parent tasks\n        task = self\n        parent_tasks = task.parents\n        parent_tasks.append(task)\n\n        return {\n            \"project\": self.project,\n            \"sequence\": sequence,\n            \"scene\": scene,\n            \"shot\": shot,\n            \"asset\": asset,\n            \"task\": self,\n            \"parent_tasks\": parent_tasks,\n            \"type\": self.type,\n        }\n\n    @property\n    def path(self) -> str:\n        \"\"\"Return the rendered file path of this Task.\n\n        The path attribute will generate a path suitable for placing the files under it.\n        It will use the :class:`.FilenameTemplate` class related to the\n        :class:`.Project` :class:`.Structure` with the ``target_entity_type`` is set to\n        the type of this instance.\n\n        Raises:\n            RuntimeError: If no :class:`stalker.models.template.FilenameTemplate`\n                instance found in the :class:`stalker.models.structure.Structure` of the\n                related :class:`stalker.models.project.Project`.\n\n        Returns:\n            str: The rendered file path of this Task.\n        \"\"\"\n        # get a suitable FilenameTemplate\n        structure = self.project.structure\n\n        task_template = None\n        if structure:\n            for template in structure.templates:\n                if template.target_entity_type == self.entity_type:\n                    task_template = template\n                    break\n\n        if not task_template:\n            raise RuntimeError(\n                \"There are no suitable FilenameTemplate \"\n                \"(target_entity_type == '{entity_type}') defined in the \"\n                \"Structure of the related Project instance, please create a \"\n                \"new stalker.models.template.FilenameTemplate instance with \"\n                \"its 'target_entity_type' attribute is set to '{entity_type}' \"\n                \"and assign it to the `templates` attribute of the structure \"\n                \"of the project\".format(entity_type=self.entity_type)\n            )\n\n        return os.path.normpath(\n            Template(task_template.path).render(\n                **self._template_variables(),\n                trim_blocks=True,\n                lstrip_blocks=True,\n            )\n        ).replace(\"\\\\\", \"/\")\n\n    @property\n    def absolute_path(self) -> str:\n        \"\"\"Return the absolute file path of this task.\n\n        This is the absolute version of the :attr:`.Task.path` attribute and\n        depends on the :class:`stalker.models.template.FilenameTemplate` found\n        in the :class:`stalker.models.structure.Structure` instance of the\n        related :class:`stalker.models.project.Project` instance.\n\n        Returns:\n            str: The rendered absolute file path of this task.\n        \"\"\"\n        return os.path.normpath(os.path.expandvars(self.path)).replace(\"\\\\\", \"/\")\n\n\nclass TaskDependency(Base, ScheduleMixin):\n    \"\"\"The association object used in Task-to-Task dependency relation.\"\"\"\n\n    from stalker import defaults\n\n    __default_schedule_attr_name__ = \"gap\"  # used in docstring of ScheduleMixin\n    __default_schedule_model__ = ScheduleModel.Length\n    __default_schedule_timing__ = 0\n    __default_schedule_unit__ = TimeUnit.Hour\n\n    __tablename__ = \"Task_Dependencies\"\n\n    # depends_on_id\n    depends_on_id: Mapped[int] = mapped_column(\n        ForeignKey(\"Tasks.id\"),\n        primary_key=True,\n    )\n\n    # depends_on\n    depends_on: Mapped[Task] = relationship(\n        back_populates=\"task_dependent_of\",\n        primaryjoin=\"Task.task_id==TaskDependency.depends_on_id\",\n    )\n\n    # task_id\n    task_id: Mapped[int] = mapped_column(ForeignKey(\"Tasks.id\"), primary_key=True)\n\n    # task\n    task: Mapped[Task] = relationship(\n        back_populates=\"task_depends_on\",\n        primaryjoin=\"Task.task_id==TaskDependency.task_id\",\n    )\n\n    dependency_target: Mapped[DependencyTarget] = mapped_column(\n        DependencyTargetDecorator(),\n        nullable=False,\n        doc=\"\"\"The dependency target of the relation. The default value is\n        \"onend\", which will create a dependency between two tasks so that the\n        depending task will start after the task that it is depending on is\n        finished.\n\n        The dependency_target attribute is updated to\n        :attr:`.DependencyTarget.OnStart` when a task has a revision and needs\n        to work together with its depending tasks.\n        \"\"\",\n        default=DependencyTarget.OnStart,\n    )\n\n    gap_timing: Mapped[Optional[float]] = synonym(\n        \"schedule_timing\",\n        doc=\"\"\"A positive float value showing the desired gap between the\n        dependent and dependee tasks. The meaning of the gap value, either is\n        it *work time* or *calendar time* is defined by the :attr:`.gap_model`\n        attribute. So when the gap model is \"duration\" then the value of `gap`\n        is in calendar time, if `gap` is \"length\" then it is considered as work\n        time.\n        \"\"\",\n    )\n\n    gap_unit: Mapped[Optional[str]] = synonym(\"schedule_unit\")\n\n    gap_model: Mapped[str] = synonym(\n        \"schedule_model\",\n        doc=\"\"\"An enumeration value one of [:attr:`.ScheduleModel.Length`,\n        :attr:`.ScheduleModel.Duration`]. The value of this attribute defines\n        if the :attr:`.gap` value is in *Work Time* or *Calendar Time*. The\n        default value is :attr:`.ScheduleModel.Length` so the gap value defines\n        a time interval in work time.\n        \"\"\",\n    )\n\n    def __init__(\n        self,\n        task: Optional[\"Task\"] = None,\n        depends_on: Optional[\"Task\"] = None,\n        dependency_target: Optional[str] = None,\n        gap_timing: Optional[Union[float, int]] = 0,\n        gap_unit: Optional[TimeUnit] = TimeUnit.Hour,\n        gap_model: Optional[ScheduleModel] = ScheduleModel.Length,\n    ) -> None:\n        ScheduleMixin.__init__(\n            self,\n            schedule_timing=gap_timing,\n            schedule_unit=gap_unit,\n            schedule_model=gap_model,\n        )\n\n        self.task = task\n        self.depends_on = depends_on\n        self.dependency_target = dependency_target\n\n    @validates(\"task\")\n    def _validate_task(self, key: str, task: Task) -> Task:\n        \"\"\"Validate the task value.\n\n        Args:\n            key (str): The name of the validated column.\n            task (Task): The task value to be validated.\n\n        Raises:\n            TypeError: If the given task value is not None and not a\n                :class:`stalker.models.task.Task` instance.\n\n        Returns:\n            Task: The validated task value.\n        \"\"\"\n        # trust to the session for checking the task\n        if task is not None and not isinstance(task, Task):\n            raise TypeError(\n                \"{}.task should be and instance of stalker.models.task.Task, \"\n                \"not {}: '{}'\".format(\n                    self.__class__.__name__, task.__class__.__name__, task\n                )\n            )\n        return task\n\n    @validates(\"depends_on\")\n    def _validate_depends_on(self, key: str, dependency: Task) -> Task:\n        \"\"\"Validate the task value.\n\n        Args:\n            key (str): The name of the validated column.\n            dependency (Task): The depends_on value to be validated.\n\n        Raises:\n            TypeError: If the given depends_on value is not None and not a\n                :class:`stalker.models.task.Task` instance.\n\n        Returns:\n            Task: The validated depends_on value.\n        \"\"\"\n        # trust to the session for checking the depends_on attribute\n        if dependency is not None and not isinstance(dependency, Task):\n            raise TypeError(\n                \"{}.depends_on should be and instance of stalker.models.task.Task, \"\n                \"not {}: '{}'\".format(\n                    self.__class__.__name__, dependency.__class__.__name__, dependency\n                )\n            )\n        return dependency\n\n    @validates(\"dependency_target\")\n    def _validate_dependency_target(\n        self, key: str, dependency_target: Union[None, str, DependencyTarget]\n    ) -> DependencyTarget:\n        \"\"\"Validate the given dependency_target value.\n\n        Args:\n            key (str): The name of the validated column.\n            dependency_target (Union[None, str, DependencyTarget]): The\n                dependency_target value to be validated.\n\n        Returns:\n            DependencyTarget: The validated dependency_target value.\n        \"\"\"\n        from stalker import defaults\n\n        if dependency_target is None:\n            dependency_target = defaults.task_dependency_targets[0]\n\n        dependency_target = DependencyTarget.to_target(dependency_target)\n\n        return dependency_target\n\n    @property\n    def to_tjp(self) -> str:\n        \"\"\"Return the TaskJuggler representation of this TaskDependency.\n\n        Returns:\n            str: The TaskJuggler representation of this TaskDependency.\n        \"\"\"\n        tjp = f\"{self.depends_on.tjp_abs_id} {{{self.dependency_target}\"\n        if self.gap_timing:\n            tjp += f\" gap{self.gap_model} {self.gap_timing}{self.gap_unit}\"\n        tjp += \"}\"\n        return tjp\n\n\n# TASK_RESOURCES\nTask_Resources = Table(\n    \"Task_Resources\",\n    Base.metadata,\n    Column(\"task_id\", Integer, ForeignKey(\"Tasks.id\"), primary_key=True),\n    Column(\"resource_id\", Integer, ForeignKey(\"Users.id\"), primary_key=True),\n)\n\n# TASK_ALTERNATIVE_RESOURCES\nTask_Alternative_Resources = Table(\n    \"Task_Alternative_Resources\",\n    Base.metadata,\n    Column(\"task_id\", Integer, ForeignKey(\"Tasks.id\"), primary_key=True),\n    Column(\"resource_id\", Integer, ForeignKey(\"Users.id\"), primary_key=True),\n)\n\n# TASK_COMPUTED_RESOURCES\nTask_Computed_Resources = Table(\n    \"Task_Computed_Resources\",\n    Base.metadata,\n    Column(\"task_id\", Integer, ForeignKey(\"Tasks.id\"), primary_key=True),\n    Column(\"resource_id\", Integer, ForeignKey(\"Users.id\"), primary_key=True),\n)\n\n# TASK_WATCHERS\nTask_Watchers = Table(\n    \"Task_Watchers\",\n    Base.metadata,\n    Column(\"task_id\", Integer, ForeignKey(\"Tasks.id\"), primary_key=True),\n    Column(\"watcher_id\", Integer, ForeignKey(\"Users.id\"), primary_key=True),\n)\n\n# TASK_RESPONSIBLE\nTask_Responsible = Table(\n    \"Task_Responsible\",\n    Base.metadata,\n    Column(\"task_id\", Integer, ForeignKey(\"Tasks.id\"), primary_key=True),\n    Column(\"responsible_id\", Integer, ForeignKey(\"Users.id\"), primary_key=True),\n)\n\n# *****************************************************************************\n# Register Events\n# *****************************************************************************\n\n\n# *****************************************************************************\n# TimeLog updates the owner tasks parents total_logged_seconds attribute\n# with new duration\n@event.listens_for(TimeLog._start, \"set\")\ndef update_time_log_task_parents_for_start(\n    timelog: TimeLog,\n    new_start: datetime.datetime,\n    old_start: datetime.datetime,\n    initiator: AttributeEvent,\n) -> None:\n    \"\"\"Update the parent task of the TimeLog.task if the new_start value is changed.\n\n    Args:\n        timelog (TimeLog): The TimeLog instance.\n        new_start (datetime.datetime): The datetime.datetime instance showing the new\n            value.\n        old_start (datetime.datetime): The datetime.datetime instance showing the old\n            value.\n        initiator (AttributeEvent): Currently not used.\n    \"\"\"\n    logger.debug(f\"Received set event for new_start in target : {timelog}\")\n    if timelog.end and old_start and new_start:\n        old_duration = timelog.end - old_start\n        new_duration = timelog.end - new_start\n        __update_total_logged_seconds__(timelog, new_duration, old_duration)\n\n\n@event.listens_for(TimeLog._end, \"set\")\ndef update_time_log_task_parents_for_end(\n    timelog: TimeLog,\n    new_end: datetime.datetime,\n    old_end: datetime.datetime,\n    initiator: sqlalchemy.orm.attributes.AttributeEvent,\n) -> None:\n    \"\"\"Update the parent task of the TimeLog.task if the new_end value is changed.\n\n    Args:\n        timelog (TimeLog): The TimeLog instance.\n        new_end (datetime.datetime): The datetime.datetime instance showing the new\n            value.\n        old_end (datetime.datetime): The datetime.datetime instance showing the old\n            value.\n        initiator (sqlalchemy.orm.attributes.AttributeEvent): Currently not used.\n    \"\"\"\n    logger.debug(f\"Received set event for new_end in target: {timelog}\")\n    if (\n        timelog.start\n        and isinstance(old_end, datetime.datetime)\n        and isinstance(new_end, datetime.datetime)\n    ):\n        old_duration = old_end - timelog.start\n        new_duration = new_end - timelog.start\n        __update_total_logged_seconds__(timelog, new_duration, old_duration)\n\n\ndef __update_total_logged_seconds__(\n    time_log: TimeLog,\n    new_duration: datetime.timedelta,\n    old_duration: datetime.timedelta,\n) -> None:\n    \"\"\"Update the given parent tasks total_logged_seconds attr with the new duration.\n\n    Args:\n        time_log (TimeLog): A :class:`.Task` instance which is the parent of the.\n        new_duration (datetime.timedelta): The new duration value.\n        old_duration (datetime.timedelta): The old duration value.\n    \"\"\"\n    # if not time_log.task:\n    #     logger.debug(f\"TimeLog doesn't have a task yet: {time_log}\")\n    #     return\n\n    logger.debug(f\"TimeLog has a task: {time_log.task}\")\n    parent = time_log.task.parent\n    if not parent:\n        logger.debug(\"TimeLog.task doesn't have a parent!\")\n        return\n\n    logger.debug(f\"TImeLog.task has a parent: {parent}\")\n\n    logger.debug(f\"old_duration: {old_duration}\")\n    logger.debug(f\"new_duration: {new_duration}\")\n\n    old_total_seconds = old_duration.days * 86400 + old_duration.seconds\n    new_total_seconds = new_duration.days * 86400 + new_duration.seconds\n\n    parent.total_logged_seconds = (\n        parent.total_logged_seconds - old_total_seconds + new_total_seconds\n    )\n\n\n# *****************************************************************************\n# Task.schedule_timing updates Task.parent.schedule_seconds attribute\n# *****************************************************************************\n@event.listens_for(Task.schedule_timing, \"set\", propagate=True)\ndef update_parents_schedule_seconds_with_schedule_timing(\n    task: Task,\n    new_schedule_timing: int,\n    old_schedule_timing: int,\n    initiator: sqlalchemy.orm.attributes.AttributeEvent,\n) -> None:\n    \"\"\"Update parent task's schedule_seconds attr if schedule_timing attr is updated.\n\n    Args:\n        task (Task): The base task.\n        new_schedule_timing (int): An integer showing the schedule_timing of the task.\n        old_schedule_timing (int): The old value of schedule_timing.\n        initiator (sqlalchemy.orm.attribute.AttributeEvent): Currently not used.\n    \"\"\"\n    logger.debug(f\"Received set event for new_schedule_timing in target: {task}\")\n    # update parents schedule_seconds attribute\n    if not task.parent:\n        return\n\n    old_schedule_seconds = task.to_seconds(\n        old_schedule_timing, task.schedule_unit, task.schedule_model\n    )\n    new_schedule_seconds = task.to_seconds(\n        new_schedule_timing, task.schedule_unit, task.schedule_model\n    )\n    # remove the old and add the new one\n    task.parent.schedule_seconds = (\n        task.parent.schedule_seconds - old_schedule_seconds + new_schedule_seconds\n    )\n\n\n# *****************************************************************************\n# Task.schedule_unit updates Task.parent.schedule_seconds attribute\n# *****************************************************************************\n@event.listens_for(Task.schedule_unit, \"set\", propagate=True)\ndef update_parents_schedule_seconds_with_schedule_unit(\n    task: Task,\n    new_schedule_unit: str,\n    old_schedule_unit: str,\n    initiator: sqlalchemy.orm.attributes.AttributeEvent,\n) -> None:\n    \"\"\"Update parent task's schedule_seconds attr if new_schedule_unit attr is updated.\n\n    Args:\n        task (Task): The base task that the schedule unit is updated of.\n        new_schedule_unit (str): A string with a value of 'min', 'h', 'd', 'w', 'm' or\n            'y' showing the timing unit.\n        old_schedule_unit (str): The old value of new_schedule_unit.\n        initiator (sqlalchemy.orm.attribute.AttributeEvent): Currently not used.\n    \"\"\"\n    logger.debug(f\"Received set event for new_schedule_unit in target: {task}\")\n    # update parents schedule_seconds attribute\n    if not task.parent:\n        return\n\n    schedule_timing = 0\n    if task.schedule_timing:\n        schedule_timing = task.schedule_timing\n    old_schedule_seconds = task.to_seconds(\n        schedule_timing, old_schedule_unit, task.schedule_model\n    )\n    new_schedule_seconds = task.to_seconds(\n        schedule_timing, new_schedule_unit, task.schedule_model\n    )\n    # remove the old and add the new one\n    parent_schedule_seconds = 0\n    if task.parent.schedule_seconds:\n        parent_schedule_seconds = task.parent.schedule_seconds\n    task.parent.schedule_seconds = (\n        parent_schedule_seconds - old_schedule_seconds + new_schedule_seconds\n    )\n\n\n# *****************************************************************************\n# Task.children removed\n# *****************************************************************************\n@event.listens_for(Task.children, \"remove\", propagate=True)\ndef update_task_date_values(\n    task: Task, removed_child: Task, initiator: sqlalchemy.orm.attributes.AttributeEvent\n) -> None:\n    \"\"\"Run when a child is removed from parent.\n\n    Args:\n        task (Task): The task that a child is removed from.\n        removed_child (Task): The removed child.\n        initiator (sqlalchemy.orm.attribute.AttributeEvent): Currently not used.\n    \"\"\"\n    # update start and end date values of the task\n    with DBSession.no_autoflush:\n        start = datetime.datetime.max.replace(tzinfo=pytz.utc)\n        end = datetime.datetime.min.replace(tzinfo=pytz.utc)\n        for child in task.children:\n            if child is not removed_child:\n                if child.start < start:\n                    start = child.start\n                if child.end > end:\n                    end = child.end\n\n        max_date = datetime.datetime.max.replace(tzinfo=pytz.utc)\n        min_date = datetime.datetime.min.replace(tzinfo=pytz.utc)\n        if start != max_date and end != min_date:\n            task.start = start\n            task.end = end\n        else:\n            # no child left\n            # set it to now\n            task.start = datetime.datetime.now(pytz.utc)\n            # this will also update end\n\n\n# *****************************************************************************\n# Task.depends_on set\n# *****************************************************************************\n@event.listens_for(Task.task_depends_on, \"remove\", propagate=True)\ndef removed_a_dependency(\n    task: Task,\n    task_dependency: TaskDependency,\n    initiator: sqlalchemy.orm.attributes.AttributeEvent,\n) -> None:\n    \"\"\"Update statuses when a task is removed from another tasks dependency list.\n\n    Args:\n        task (Task): The task that a dependent is being removed from.\n        task_dependency (TaskDependency): The association object that has the\n            relation.\n        initiator (sqlalchemy.orm.attributes.AttributeEvent): Currently not used.\n    \"\"\"\n    # update task status with dependencies\n    task.update_status_with_dependent_statuses(removing=task_dependency.depends_on)\n\n\n@event.listens_for(TimeLog.__table__, \"after_create\")\ndef add_exclude_constraint(\n    table: sqlalchemy.sql.schema.Table,\n    connection: sqlalchemy.engine.base.Connection,\n    **kwargs,\n) -> None:\n    \"\"\"Add the PostgreSQL specific ExcludeConstraint.\n\n    Args:\n        table (sqlalchemy.sql.schema.Table): The table that this event is triggered on.\n        connection (sqlalchemy.engine.base.Connection): The connection instance.\n        **kwargs (Any): Extra kwargs that are passed to the event.\n    \"\"\"\n    if connection.engine.dialect.name != \"postgresql\":\n        logger.debug(\"it is not a PostgreSQL database not creating Exclude Constraint\")\n        return\n\n    logger.debug(\"add_exclude_constraint is Running!\")\n    # try to create the extension first\n    create_extension = DDL(\"CREATE EXTENSION btree_gist;\")\n    try:\n        logger.debug('running \"btree_gist\" extension creation!')\n        connection.execute(create_extension)\n        logger.debug('successfully created \"btree_gist\" extension!')\n    except (ProgrammingError, InternalError) as e:\n        logger.debug(f\"add_exclude_constraint: {e}\")\n\n    # create the ts_to_box sql function\n    ts_to_box = DDL(\n        \"\"\"CREATE FUNCTION ts_to_box(TIMESTAMPTZ, TIMESTAMPTZ)\nRETURNS BOX\nAS\n$$\n    SELECT  BOX(\n      POINT(DATE_PART('epoch', $1), 0),\n      POINT(DATE_PART('epoch', $2 - interval '1 minute'), 1)\n    )\n$$\nLANGUAGE 'sql'\nIMMUTABLE;\n\"\"\"\n    )\n    try:\n        logger.debug(\"creating ts_to_box function!\")\n        connection.execute(ts_to_box)\n        logger.debug(\"successfully created ts_to_box function\")\n    except (ProgrammingError, InternalError) as e:\n        logger.debug(f\"failed creating ts_to_box function!: {e}\")\n\n    # create exclude constraint\n    exclude_constraint = DDL(\n        \"\"\"ALTER TABLE \"TimeLogs\" ADD CONSTRAINT\n        overlapping_time_logs EXCLUDE USING GIST (\n            resource_id WITH =,\n            ts_to_box(start, \"end\") WITH &&\n        )\"\"\"\n    )\n    try:\n        logger.debug('running ExcludeConstraint for \"TimeLogs\" table creation!')\n        connection.execute(exclude_constraint)\n        logger.debug('successfully created ExcludeConstraint for \"TimeLogs\" table!')\n    except (ProgrammingError, InternalError) as e:\n        logger.debug(f\"failed creating ExcludeConstraint for TimeLogs table!: {e}\")\n"
  },
  {
    "path": "src/stalker/models/template.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"FilenameTemplate related functions and classes are situated here.\"\"\"\nfrom typing import Any, Dict, Optional, Union\n\nfrom sqlalchemy import ForeignKey, Text\nfrom sqlalchemy.orm import Mapped, mapped_column, validates\n\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity\nfrom stalker.models.mixins import TargetEntityTypeMixin\n\nlogger = get_logger(__name__)\n\n\nclass FilenameTemplate(Entity, TargetEntityTypeMixin):\n    \"\"\"Holds templates for filename and path conventions.\n\n    FilenameTemplate objects help to specify where to place a :class:`.Version`\n    related file.\n\n    Although, it is mainly used by Stalker to define :class:`.Version` related\n    file paths and file names to place them in to proper places inside a\n    :class:`.Project`'s :attr:`.Project.structure`, the idea behind is open to\n    endless possibilities.\n\n    Here is an example::\n\n        p1 = Project(name=\"Test Project\") # shortened for this example\n\n        # shortened for this example\n        s1 = Structure(name=\"Commercial Project Structure\")\n\n        # this is going to be used by Stalker to decide the :stalker:`.File`\n        # :stalker:`.File.filename` and :stalker:`.File.path` (which is the way\n        # Stalker links external files to Version instances)\n        f1 = FilenameTemplate(\n            name=\"Asset Version Template\",\n            target_entity_type=\"Asset\",\n            path='$REPO{{project.repository.id}}/{{project.code}}/{%- for parent_task in parent_tasks -%}{{parent_task.nice_name}}/{%- endfor -%}\",\n            filename=\"{{version.nice_name}}_v{{\"%03d\"|format(version.version_number)}}\"\n        )\n\n        s1.templates.append(f1)\n        p1.structure = s1\n\n        # now because we have defined a FilenameTemplate for Assets,\n        # Stalker is now able to produce a path and a filename for any Version\n        # related to an asset in this project.\n\n    Args:\n        target_entity_type (str): The class name that this FilenameTemplate is designed\n            for. You can also pass the class itself. So both of the examples below can\n            work::\n\n                new_filename_template1 = FilenameTemplate(target_entity_type=\"Asset\")\n                new_filename_template2 = FilenameTemplate(target_entity_type=Asset)\n\n            A TypeError will be raised when it is skipped or it is None and a\n            ValueError will be raised when it is given as and empty string.\n\n        path (str): A `Jinja2`_ template code which specifies the path of the given\n            item. It is relative to the repository root. A typical example  could be::\n\n            '$REPO{{project.repository.id}}/{{project.code}}/{%- for parent_task in parent_tasks -%}{{parent_task.nice_name}}/{%- endfor -%}'\n\n        filename (str): A `Jinja2`_ template code which specifies the file name of the\n            given item. It is relative to the :attr:`.FilenameTemplate.path`. A typical\n            example could be::\n\n            '{{version.nice_name}}_v{{\"%03d\"|format(version.version_number)}}'\n\n            Could be set to an empty string or None, the default value is None. It can\n            be None, or an empty string, or it can be skipped.\n\n    .. _Jinja2: http://jinja.pocoo.org/docs/\n    \"\"\"  # noqa: B950\n\n    __auto_name__ = False\n    __strictly_typed__ = False\n    __tablename__ = \"FilenameTemplates\"\n    __mapper_args__ = {\"polymorphic_identity\": \"FilenameTemplate\"}\n    filenameTemplate_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    path: Mapped[Optional[str]] = mapped_column(\n        Text, doc=\"\"\"The template code for the path of this FilenameTemplate.\"\"\"\n    )\n\n    filename: Mapped[Optional[str]] = mapped_column(\n        Text, doc=\"\"\"The template code for the file part of the FilenameTemplate.\"\"\"\n    )\n\n    def __init__(\n        self,\n        target_entity_type: Optional[str] = None,\n        path: Optional[str] = None,\n        filename: Optional[str] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        super(FilenameTemplate, self).__init__(**kwargs)\n        TargetEntityTypeMixin.__init__(self, target_entity_type, **kwargs)\n        self.path = path\n        self.filename = filename\n\n    @validates(\"path\")\n    def _validate_path(self, key: str, path: Union[None, str]) -> str:\n        \"\"\"Validate the given path value.\n\n        Args:\n            key (str): The name of the validated column.\n            path (Union[None, str]): The path value to be validated.\n\n        Raises:\n            TypeError: If the given path value is not None and not a string.\n\n        Returns:\n            str: The validated path value.\n        \"\"\"\n        # check if it is None\n        if path is None:\n            path = \"\"\n\n        if not isinstance(path, str):\n            raise TypeError(\n                \"{}.path attribute should be string, not {}: '{}'\".format(\n                    self.__class__.__name__, path.__class__.__name__, path\n                )\n            )\n\n        return path\n\n    @validates(\"filename\")\n    def _validate_filename(self, key: str, filename: Union[None, str]) -> str:\n        \"\"\"Validate the given filename value.\n\n        Args:\n            key (str): The name of the validated column.\n            filename (Union[None, str]): The filename value to be validated.\n\n        Raises:\n            TypeError: If the given filename value is not None and not a string.\n\n        Returns:\n            str: The validated filename value.\n        \"\"\"\n        # check if it is None\n        if filename is None:\n            filename = \"\"\n\n        if not isinstance(filename, str):\n            raise TypeError(\n                \"{}.filename attribute should be string, not {}: '{}'\".format(\n                    self.__class__.__name__, filename.__class__.__name__, filename\n                )\n            )\n\n        return filename\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a FilenameTemplate instance and has the\n                same target_entity_type, path and filename.\n        \"\"\"\n        return (\n            super(FilenameTemplate, self).__eq__(other)\n            and isinstance(other, FilenameTemplate)\n            and self.target_entity_type == other.target_entity_type\n            and self.path == other.path\n            and self.filename == other.filename\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(FilenameTemplate, self).__hash__()\n"
  },
  {
    "path": "src/stalker/models/ticket.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Ticket related functions and classes are situated here.\"\"\"\nimport uuid\nfrom typing import Any, Dict, List, Optional, Union\n\nfrom sqlalchemy import Column, Integer, String, Text\nfrom sqlalchemy.exc import OperationalError, UnboundExecutionError\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, synonym\nfrom sqlalchemy.orm.mapper import validates\nfrom sqlalchemy.schema import ForeignKey, Table\nfrom sqlalchemy.types import Enum\n\nfrom stalker.db.declarative import Base\nfrom stalker.db.session import DBSession\nfrom stalker.exceptions import CircularDependencyError\nfrom stalker.log import get_logger\nfrom stalker.models.auth import User\nfrom stalker.models.entity import Entity, SimpleEntity\nfrom stalker.models.mixins import StatusMixin\nfrom stalker.models.note import Note\nfrom stalker.models.project import Project\nfrom stalker.models.status import Status\n\nlogger = get_logger(__name__)\n\n# RESOLUTIONS\nFIXED = \"fixed\"\nINVALID = \"invalid\"\nWONTFIX = \"wontfix\"\nDUPLICATE = \"duplicate\"\nWORKSFORME = \"worksforme\"\nCANTFIX = \"cantfix\"\n\n\nclass Ticket(Entity, StatusMixin):\n    \"\"\"Tickets are the way of reporting errors or asking for changes.\n\n    The Stalker Ticketing system is based on Trac Basic Workflow. For more\n    information please visit `Trac Workflow`_\n\n    _`Trac Workflow`:: http://trac.edgewall.org/wiki/TracWorkflow\n\n    Stalker Ticket system is very flexible, to customize the workflow please\n    update the :class:`.Config.ticket_workflow` dictionary.\n\n    In the default setup, there are four actions available; ``accept``,\n    ``resolve``, ``reopen``, ``reassign``, and five statuses available ``New``,\n    ``Assigned``, ``Accepted``, ``Reopened``, ``Closed``.\n\n    Args:\n        project (Project): The Project that this Ticket is assigned to. A Ticket in\n            Stalker must be assigned to a Project. ``project`` argument cannot be\n            skipped or cannot be None.\n        summary (str): A string which contains the title or a short\n            description of this Ticket.\n        priority (str): The priority of the Ticket which is an enum value.\n            Possible values are:\n\n            +--------------+-------------------------------------------------+\n            | 0 / TRIVIAL  | defect with little or no impact / cosmetic      |\n            |              | enhancement                                     |\n            +--------------+-------------------------------------------------+\n            | 1 / MINOR    | defect with minor impact / small enhancement    |\n            +--------------+-------------------------------------------------+\n            | 2 / MAJOR    | defect with major impact / big enhancement      |\n            +--------------+-------------------------------------------------+\n            | 3 / CRITICAL | severe loss of data due to the defect or highly |\n            |              | needed enhancement                              |\n            +--------------+-------------------------------------------------+\n            | 4 / BLOCKER  | basic functionality is not available until this |\n            |              | is fixed                                        |\n            +--------------+-------------------------------------------------+\n\n        reported_by (User): A :class:`.User` instance who created this Ticket. It is\n            basically a synonym for the :attr:`.SimpleEntity.created_by` attribute.\n\n    Changing the :attr`.Ticket.status` will create a new :class:`.TicketLog` instance\n    showing the previous operation.\n\n    Even though Tickets needs statuses they don't need to be supplied a\n    :class:`.StatusList` nor :class:`.Status` for the Tickets. It will be\n    automatically filled accordingly. For newly created Tickets the status of\n    the ticket is ``NEW`` and can be changed to other statuses as follows:\n\n        Status   -> Action   -> New Status\n\n        NEW      -> resolve  -> CLOSED\n        NEW      -> accept   -> ACCEPTED\n        NEW      -> reassign -> ASSIGNED\n\n        ASSIGNED -> resolve  -> CLOSED\n        ASSIGNED -> accept   -> ACCEPTED\n        ASSIGNED -> reassign -> ASSIGNED\n\n        ACCEPTED -> resolve  -> CLOSED\n        ACCEPTED -> accept   -> ACCEPTED\n        ACCEPTED -> reassign -> ASSIGNED\n\n        REOPENED -> resolve  -> CLOSED\n        REOPENED -> accept   -> ACCEPTED\n        REOPENED -> reassign -> ASSIGNED\n\n        CLOSED   -> reopen   -> REOPENED\n\n        actions available:\n        resolve\n        reassign\n        accept\n        reopen\n\n    The :attr:`.Ticket.name` is automatically generated by using the\n    ``stalker.config.Config.ticket_label`` attribute and\n    :attr:`.Ticket.ticket_number` . So if defaults are used the first ticket\n    name will be \"Ticket#1\" and the second \"Ticket#2\" and so on. For every\n    project the number will restart from 1.\n\n    Use the :meth:`.Ticket.resolve`, :meth:`.Ticket.reassign`,\n    :meth:`.Ticket.accept`, :meth:`.Ticket.reopen` methods to change the status\n    of the current Ticket.\n\n    Changing the status of the Ticket will create :class:`.TicketLog` entries\n    reflecting the change made.\n    \"\"\"\n\n    # logs attribute\n    __auto_name__ = True\n    __tablename__ = \"Tickets\"\n    # __table_args__ = (\n    #    UniqueConstraint(\"project_id\", 'number'), {}\n    # )\n    __mapper_args__ = {\"polymorphic_identity\": \"Ticket\"}\n\n    ticket_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    # TODO: use ProjectMixin\n    project_id: Mapped[int] = mapped_column(\"project_id\", ForeignKey(\"Projects.id\"))\n\n    _project: Mapped[Project] = relationship(\n        primaryjoin=\"Tickets.c.project_id==Projects.c.id\",\n        back_populates=\"tickets\",\n    )\n\n    _number: Mapped[int] = mapped_column(\n        \"number\",\n        autoincrement=True,\n        default=1,\n        nullable=False,\n        unique=True,\n    )\n\n    related_tickets: Mapped[Optional[List[\"Ticket\"]]] = relationship(\n        secondary=\"Ticket_Related_Tickets\",\n        primaryjoin=\"Tickets.c.id==Ticket_Related_Tickets.c.ticket_id\",\n        secondaryjoin=\"Ticket_Related_Tickets.c.related_ticket_id==\" \"Tickets.c.id\",\n        doc=\"\"\"A list of other Ticket instances which are related\n        to this one. Can be used to related Tickets to point to a common\n        problem. The Ticket itself cannot be assigned to this list\n        \"\"\",\n    )\n\n    summary: Mapped[Optional[str]] = mapped_column(Text)\n\n    logs: Mapped[Optional[List[\"TicketLog\"]]] = relationship(\n        primaryjoin=\"Tickets.c.id==TicketLogs.c.ticket_id\",\n        back_populates=\"ticket\",\n        cascade=\"all, delete-orphan\",\n    )\n\n    links: Mapped[Optional[List[\"SimpleEntity\"]]] = relationship(\n        secondary=\"Ticket_SimpleEntities\"\n    )\n\n    comments: Mapped[Optional[List[\"Note\"]]] = synonym(\n        \"notes\",\n        doc=\"\"\"A list of :class:`.Note` instances showing the comments made for\n        this Ticket instance. It is a synonym for the :attr:`.Ticket.notes`\n        attribute.\n        \"\"\",\n    )\n\n    reported_by: Mapped[Optional[\"User\"]] = synonym(\n        \"created_by\", doc=\"Shows who created this Ticket\"\n    )\n\n    owner_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Users.id\"))\n    owner: Mapped[\"User\"] = relationship(primaryjoin=\"Tickets.c.owner_id==Users.c.id\")\n\n    resolution: Mapped[Optional[str]] = mapped_column(String(128))\n\n    priority: Mapped[Optional[str]] = mapped_column(\n        Enum(\"TRIVIAL\", \"MINOR\", \"MAJOR\", \"CRITICAL\", \"BLOCKER\", name=\"PriorityType\"),\n        default=\"TRIVIAL\",\n        doc=\"\"\"The priority of the Ticket which is an enum value.\n        Possible values are:\n\n          +--------------+-------------------------------------------------+\n          | 0 / TRIVIAL  | defect with little or no impact / cosmetic      |\n          |              | enhancement                                     |\n          +--------------+-------------------------------------------------+\n          | 1 / MINOR    | defect with minor impact / small enhancement    |\n          +--------------+-------------------------------------------------+\n          | 2 / MAJOR    | defect with major impact / big enhancement      |\n          +--------------+-------------------------------------------------+\n          | 3 / CRITICAL | severe loss of data due to the defect or highly |\n          |              | needed enhancement                              |\n          +--------------+-------------------------------------------------+\n          | 4 / BLOCKER  | basic functionality is not available until this |\n          |              | is fixed                                        |\n          +--------------+-------------------------------------------------+\n        \"\"\",\n    )\n\n    def __init__(\n        self,\n        project: Optional[Project] = None,\n        links: Optional[List[SimpleEntity]] = None,\n        priority: str = \"TRIVIAL\",\n        summary: Optional[str] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        # just force auto name generation\n        self._number = self._generate_ticket_number()\n        from stalker import defaults\n\n        kwargs[\"name\"] = \"{:s} #{:d}\".format(defaults.ticket_label, self.number)\n\n        super(Ticket, self).__init__(**kwargs)\n        StatusMixin.__init__(self, **kwargs)\n\n        self._project = project\n\n        self.priority = priority\n        if links is None:\n            links = []\n        self.links = links\n        self.summary = summary\n\n    def _number_getter(self) -> int:\n        \"\"\"Return the number attribute value.\n\n        Returns:\n            int: The number attribute value.\n        \"\"\"\n        return self._number\n\n    number = synonym(\n        \"_number\",\n        descriptor=property(_number_getter),\n        doc=\"\"\"The automatically generated number for the tickets.\n        \"\"\",\n    )\n\n    def _project_getter(self) -> Project:\n        \"\"\"Return the project attribute value.\n\n        Returns:\n            Project: The project attribute value.\n        \"\"\"\n        return self._project\n\n    project = synonym(\"_project\", descriptor=property(_project_getter))\n\n    @classmethod\n    def _maximum_number(cls) -> int:\n        \"\"\"Return the maximum available number from the database.\n\n        Returns:\n            int: The maximum ticket number.\n        \"\"\"\n        try:\n            # do your query\n            with DBSession.no_autoflush:\n                max_ticket = Ticket.query.order_by(Ticket.number.desc()).first()\n        except (UnboundExecutionError, OperationalError):\n            max_ticket = None\n\n        return max_ticket.number if max_ticket is not None else 0\n\n    def _generate_ticket_number(self) -> int:\n        \"\"\"Auto generate a number for the ticket.\n\n        Returns:\n            int: The auto generated ticket number.\n        \"\"\"\n        # TODO: try to make it atomic\n        return self._maximum_number() + 1\n\n    @validates(\"related_tickets\")\n    def _validate_related_tickets(self, key: str, related_ticket: \"Ticket\") -> \"Ticket\":\n        \"\"\"Validate the given related_ticket value.\n\n        Args:\n            key (str): The name of the validated column.\n            related_ticket (Ticket): The related_ticket value to be validated.\n\n        Raises:\n            TypeError: If the related_ticket value is not a Ticket instance.\n            CircularDependencyError: If the related_ticket value is the same with this\n                Ticket.\n\n        Returns:\n            Ticket: The validated related_ticket value.\n        \"\"\"\n        if not isinstance(related_ticket, Ticket):\n            raise TypeError(\n                \"{}.related_ticket should only contain instances of \"\n                \"stalker.models.ticket.Ticket, not {}: '{}'\".format(\n                    self.__class__.__name__,\n                    related_ticket.__class__.__name__,\n                    related_ticket,\n                )\n            )\n\n        if related_ticket is self:\n            raise CircularDependencyError(\n                \"{}.related_ticket attribute cannot have itself in the list\".format(\n                    self.__class__.__name__\n                )\n            )\n\n        return related_ticket\n\n    @validates(\"_project\")\n    def _validate_project(\n        self, key: str, project: Union[None, Project]\n    ) -> Union[None, Project]:\n        \"\"\"Validate the given project value.\n\n        Args:\n            key (str): The name of the validated column.\n            project (Union[None, Project]): The project value to be validated.\n\n        Raises:\n            TypeError: If the given project is not a Project instance.\n\n        Returns:\n            Union[None, Project]: The validated project value.\n        \"\"\"\n        if project is None or not isinstance(project, Project):\n            raise TypeError(\n                \"{}.project should be an instance of \"\n                \"stalker.models.project.Project, not {}: '{}'\".format(\n                    self.__class__.__name__, project.__class__.__name__, project\n                )\n            )\n\n        return project\n\n    @validates(\"summary\")\n    def _validate_summary(self, key: str, summary: Union[None, str]) -> str:\n        \"\"\"Validate the given summary value.\n\n        Args:\n            key (str): The name of the validated column.\n            summary (Union[None, str]): The summary value to be validated.\n\n        Raises:\n            TypeError: If the given summary is not None and not a string.\n\n        Returns:\n            str: The validated summary value.\n        \"\"\"\n        if summary is None:\n            summary = \"\"\n\n        if not isinstance(summary, str):\n            raise TypeError(\n                \"{}.summary should be an instance of str, not {}: '{}'\".format(\n                    self.__class__.__name__, summary.__class__.__name__, summary\n                )\n            )\n        return summary\n\n    def __action__(\n        self, action: str, created_by: User, action_arg: Any = None\n    ) -> \"TicketLog\":\n        \"\"\"Update the ticket status and create a ticket log.\n\n        The log is created according to the Ticket.__available_actions__ dictionary.\n\n        Args:\n            action (str): The name of the action.\n            created_by (User): The User creating this action.\n            action_arg (Any): The argument to pass to the action.\n\n        Returns:\n            TicketLog: The TicketLog instance created.\n        \"\"\"\n        from stalker import defaults\n\n        statuses = defaults.ticket_workflow[action].keys()\n        status = self.status.name\n        return_value = None\n        if status in statuses:\n            action_data = defaults.ticket_workflow[action][status]\n            new_status_code = action_data[\"new_status\"]\n            action_name = action_data[\"action\"]\n\n            # there is an action defined for this status\n            # get the to_status\n            from_status = self.status\n            to_status = self.status_list[new_status_code]\n            self.status = to_status\n\n            # call the action with action_arg\n            func = getattr(self, action_name)\n            func(action_arg)\n\n            ticket_log = TicketLog(\n                self, from_status, to_status, action, created_by=created_by\n            )\n\n            # create log entry\n            self.logs.append(ticket_log)\n            return_value = ticket_log\n        return return_value\n\n    def resolve(\n        self, created_by: Union[None, User] = None, resolution: str = \"\"\n    ) -> \"TicketLog\":\n        \"\"\"Resolve the ticket.\n\n        Args:\n            created_by (User): The User instance who is taking this action.\n            resolution (str): The resolution.\n\n        Returns:\n            TicketLog: The newly created TicketLog instance.\n        \"\"\"\n        return self.__action__(\"resolve\", created_by, resolution)\n\n    def accept(self, created_by: Union[None, User] = None) -> \"TicketLog\":\n        \"\"\"Accept the ticket.\n\n        Args:\n            created_by (User): The User instance who is taking this action.\n\n        Returns:\n            TicketLog: The newly created TicketLog instance.\n        \"\"\"\n        return self.__action__(\"accept\", created_by, created_by)\n\n    def reassign(\n        self, created_by: Union[None, User] = None, assign_to: Union[None, User] = None\n    ) -> \"TicketLog\":\n        \"\"\"Reassign the ticket to another User.\n\n        Args:\n            created_by (User): The User that is doing the action.\n            assign_to (User): The new owner of the ticket.\n\n        Returns:\n            TicketLog: The newly created TicketLog instance.\n        \"\"\"\n        return self.__action__(\"reassign\", created_by, assign_to)\n\n    def reopen(self, created_by: Union[None, User] = None) -> \"TicketLog\":\n        \"\"\"Reopen the ticket.\n\n        Args:\n            created_by (User): The User who is doing the action.\n\n        Returns:\n            TicketLog: The newly created TicketLog instance.\n        \"\"\"\n        return self.__action__(\"reopen\", created_by)\n\n    # actions\n    def set_owner(self, *args) -> None:\n        \"\"\"Set the owner.\n\n        Args:\n            args (Any): Set the owner.\n        \"\"\"\n        self.owner = args[0]\n\n    def set_resolution(self, *args) -> None:\n        \"\"\"Set the timing_resolution.\n\n        Args:\n            args (Any): Any argument passed to this method.\n        \"\"\"\n        self.resolution = args[0]\n\n    def del_resolution(self, *args) -> None:\n        \"\"\"Delete the timing_resolution.\n\n        Args:\n            args (Any): Any arguments passed to this method.\n        \"\"\"\n        self.resolution = \"\"\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is a Ticket instance and has the same name,\n                number, status, logs and priority.\n        \"\"\"\n        return (\n            super(Ticket, self).__eq__(other)\n            and isinstance(other, Ticket)\n            and other.name == self.name\n            and other.number == self.number\n            and other.status == self.status\n            and other.logs == self.logs\n            and other.priority == self.priority\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Ticket, self).__hash__()\n\n\nclass TicketLog(SimpleEntity):\n    \"\"\"Holds :attr:`.Ticket.status` change operations.\n\n    Args:\n        ticket (Ticket): A :class:`.Ticket` instance which is the subject of the\n            operation.\n\n        from_status (Status): Holds a reference to a :class:`.Status` instance which is\n            the previous status of the :class:`.Ticket` .\n\n        to_status (Status): Holds a reference to a :class:`.Status` instance which is\n            the new status of the :class;`.Ticket` .\n\n        action (str): An Enumerator holding the type of the operation should be one of\n            [\"resolve\", \"accept\", \"reassign\", \"reopen\"].\n\n      Operations follow the `Track Workflow`_ ,\n\n      .. image:: http://trac.edgewall.org/chrome/common/guide/original-workflow.png\n          :width: 787 px\n          :height: 509 px\n          :align: left\n\n    .. _Track Workflow: http://trac.edgewall.org/wiki/TracWorkflow\n    \"\"\"\n\n    from stalker import defaults  # need to limit it with a scope\n\n    # TODO: there are no tests for the TicketLog class\n\n    __tablename__ = \"TicketLogs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"TicketLog\"}\n\n    ticket_log_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    from_status_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Statuses.id\"))\n    to_status_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Statuses.id\"))\n\n    from_status: Mapped[Status] = relationship(\n        primaryjoin=\"TicketLogs.c.from_status_id==Statuses.c.id\"\n    )\n\n    to_status: Mapped[Status] = relationship(\n        primaryjoin=\"TicketLogs.c.to_status_id==Statuses.c.id\"\n    )\n\n    action: Mapped[Optional[str]] = mapped_column(\n        Enum(*defaults.ticket_workflow.keys(), name=\"TicketActions\")\n    )\n\n    ticket_id: Mapped[Optional[int]] = mapped_column(ForeignKey(\"Tickets.id\"))\n    ticket: Mapped[Optional[Ticket]] = relationship(\n        primaryjoin=\"TicketLogs.c.ticket_id==Tickets.c.id\",\n        back_populates=\"logs\",\n    )\n\n    def __init__(\n        self,\n        ticket: Optional[Ticket] = None,\n        from_status: Optional[Status] = None,\n        to_status: Optional[Status] = None,\n        action: Optional[str] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        kwargs[\"name\"] = \"TicketLog_\" + uuid.uuid4().hex\n        super(TicketLog, self).__init__(**kwargs)\n        self.ticket = ticket\n        self.from_status = from_status\n        self.to_status = to_status\n        self.action = action\n\n\n# A secondary Table for Ticket to Ticket relations\nTicket_Related_Tickets = Table(\n    \"Ticket_Related_Tickets\",\n    Base.metadata,\n    Column(\"ticket_id\", Integer, ForeignKey(\"Tickets.id\"), primary_key=True),\n    Column(\"related_ticket_id\", Integer, ForeignKey(\"Tickets.id\"), primary_key=True),\n    extend_existing=True,\n)\n\n# Ticket SimpleEntity Relation, link anything to a ticket\nTicket_SimpleEntities = Table(\n    \"Ticket_SimpleEntities\",\n    Base.metadata,\n    Column(\"ticket_id\", Integer, ForeignKey(\"Tickets.id\"), primary_key=True),\n    Column(\n        \"simple_entity_id\", Integer, ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    ),\n)\n"
  },
  {
    "path": "src/stalker/models/type.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Type related functions and classes are situated here.\"\"\"\n\nfrom typing import Any, Dict, Optional\n\nfrom sqlalchemy import ForeignKey, String\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker.db.declarative import Base\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity\nfrom stalker.models.mixins import CodeMixin, TargetEntityTypeMixin\n\nlogger = get_logger(__name__)\n\n\nclass Type(Entity, TargetEntityTypeMixin, CodeMixin):\n    \"\"\"Everything can have a type.\n\n    .. versionadded:: 0.1.1\n      Types\n\n    Type is a generalized version of the previous design that defines types for\n    specific classes.\n\n    The purpose of the :class:`.Type` class is just to define a new type for a\n    specific :class:`.Entity`. For example, you can have a ``Character``\n    :class:`.Asset` or you can have a ``Commercial`` :class:`.Project` or you\n    can define a :class:`.File` as an ``Image`` etc., to create a new\n    :class:`.Type` for various classes:\n\n    ..code-block: Python\n\n        Type(name=\"Character\", target_entity_type=\"Asset\")\n        Type(name=\"Commercial\", target_entity_type=\"Project\")\n        Type(name=\"Image\", target_entity_type=\"File\")\n\n    or:\n\n    ..code-block: Python\n\n        Type(name=\"Character\", target_entity_type=Asset.entity_type)\n        Type(name=\"Commercial\", target_entity_type=Project.entity_type)\n        Type(name=\"Image\", target_entity_type=File.entity_type)\n\n    or even better:\n\n    ..code-block: Python\n\n        Type(name=\"Character\", target_entity_type=Asset)\n        Type(name=\"Commercial\", target_entity_type=Project)\n        Type(name=\"Image\", target_entity_type=File)\n\n    By using :class:`.Type` s, one can able to sort and group same type of\n    entities.\n\n    :class:`.Type` s are generally used in :class:`.Structure` s.\n\n    Args:\n        target_entity_type (str): The string defining the target type of this\n            :class:`.Type`.\n    \"\"\"\n\n    __auto_name__ = False\n    __tablename__ = \"Types\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Type\"}\n    type_id_local: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    def __init__(\n        self,\n        name: Optional[str] = None,\n        code: Optional[str] = None,\n        target_entity_type: Optional[str] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        kwargs[\"name\"] = name\n        kwargs[\"target_entity_type\"] = target_entity_type\n        super(Type, self).__init__(**kwargs)\n        TargetEntityTypeMixin.__init__(self, **kwargs)\n        # CodeMixin.__init__(self, **kwargs)\n        self.code = code\n\n    def __eq__(self, other: Any) -> bool:\n        \"\"\"Check the equality.\n\n        Args:\n            other (Any): The other object.\n\n        Returns:\n            bool: True if the other object is equal to this Type instance as an Entity\n                and has the same target_entity_type.\n        \"\"\"\n        return (\n            super(Type, self).__eq__(other)\n            and isinstance(other, Type)\n            and self.target_entity_type == other.target_entity_type\n        )\n\n    def __hash__(self) -> int:\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Type, self).__hash__()\n\n\nclass EntityType(Base):\n    \"\"\"A simple class just to hold the registered class names in Stalker.\"\"\"\n\n    __tablename__ = \"EntityTypes\"\n    __table_args__ = {\"extend_existing\": True}\n\n    id: Mapped[int] = mapped_column(\"id\", primary_key=True)\n    name: Mapped[str] = mapped_column(\n        String(128),\n        nullable=False,\n        unique=True,\n    )\n    statusable: Mapped[Optional[bool]] = mapped_column(default=False)\n    dateable: Mapped[Optional[bool]] = mapped_column(default=False)\n    schedulable: Mapped[Optional[bool]] = mapped_column(default=False)\n    accepts_references: Mapped[Optional[bool]] = mapped_column(default=False)\n\n    def __init__(\n        self,\n        name: str,\n        statusable: bool = False,\n        schedulable: bool = False,\n        accepts_references: bool = False,\n    ) -> None:\n        self.name = name\n        self.statusable = statusable\n        self.schedulable = schedulable\n        self.accepts_references = accepts_references\n\n        # TODO: add tests for the name attribute\n"
  },
  {
    "path": "src/stalker/models/variant.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Variant related functions and classes are situated here.\"\"\"\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker.models.task import Task\n\n\nclass Variant(Task):\n    \"\"\"A Task derivative to keep track of Variants in a Task hierarchy.\n\n    The basic reason to have the Variant class is to upgrade the variants,\n    into a Task derivative so that it is possible to create dependencies\n    between different variants and being able to review them individually.\n\n    You see, in previous versions of Stalker, the variants were handled as a\n    part of the Version instances with a str attribute. The down side of that\n    design was not being able to distinguish any reviews per variant.\n\n    So, when a Model task is approved, all its variant approved all together,\n    even if one of the variants were still getting worked on.\n\n    The new design prevents that and gives the variant the level of attention\n    they deserved.\n\n    Variants doesn't introduce any new arguments or attributes. They are just\n    initialized like any other Tasks.\n    \"\"\"\n\n    __tablename__ = \"Variants\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Variant\"}\n    variant_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Tasks.id\"),\n        primary_key=True,\n    )\n"
  },
  {
    "path": "src/stalker/models/version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Version related functions and classes are situated here.\"\"\"\n\nimport os\nfrom pathlib import Path\nfrom typing import Any, Dict, List, Optional\n\nimport jinja2\n\nfrom sqlalchemy import Column, ForeignKey, Integer, Table\nfrom sqlalchemy.exc import OperationalError, UnboundExecutionError\nfrom sqlalchemy.orm import Mapped, mapped_column, relationship, synonym, validates\n\nfrom stalker.db.declarative import Base\nfrom stalker.db.session import DBSession\nfrom stalker.log import get_logger\nfrom stalker.models.entity import Entity\nfrom stalker.models.file import File\nfrom stalker.models.mixins import DAGMixin\nfrom stalker.models.review import Review\nfrom stalker.models.task import Task\n\n\nlogger = get_logger(__name__)\n\n\nclass Version(Entity, DAGMixin):\n    \"\"\"Holds information about the versions created for a class:`.Task`.\n\n    A :class:`.Version` instance holds information about the versions created\n    related for a class:`.Task`. This is not directly related to the stored\n    files, but instead holds the information about the incremental change\n    itself (i.e who has created it, when it is created, the revision and\n    version numbers etc.). All the related files are stored in the\n    :attr:`.Version.files` attribute.\n\n    .. versionadded: 0.2.13\n\n       After Stalker 0.2.13 the :attr:`.path` become an absolute path which is\n       not anymore merged with the project repository in anyway.\n\n    .. warning:\n\n       For projects those are created prior to Stalker version 0.2.13 and that\n       has a :class:`.Structure` with :class:`.FilenameTemplate` that doesn't\n       include the repository info, it is suggested to update the related\n       ``FilenameTemplate`` s to include a the repository info manually.\n\n       Example:\n         pre 0.2.13 setup:\n\n         FilenameTemplate with path attribute is set to:\n\n           {{project.code}}/{%- for parent_task in parent_tasks -%}{{parent_task.nice_name}}/{%- endfor -%}\n\n         Update to:\n\n           {{project.repository.path}}/{{project.code}}/{%- for parent_task in parent_tasks -%}{{parent_task.nice_name}}/{%- endfor -%}\n\n         Or, let's have a setup with environment variables:\n\n           $REPO{{project.repository.code}}/{{project.code}}/{%- for parent_task in parent_tasks -%}{{parent_task.nice_name}}/{%- endfor -%}\n\n    .. versionadded: 1.0.0\n\n       Version instances now have an extra numeric counter, preceding the\n       :attr:`.version_number` attribute to allow versions to be better\n       organized alongside revisions or big changes, without relying on the now\n       removed `variant_name` attribute.\n\n    .. versionadded: 1.1.0\n\n       Version class is not deriving from File class anymore. So they are not\n       directly related to any file. And the File relation is stored in the new\n       :attr:`.Version.files` attribute.\n\n    .. versionadded: 1.1.0\n\n       Added the `files` attribute, which replaces the `outputs` attribute and\n       the `inputs` attribute is moved to the :class:`.File` class as the\n       `references` attribute, which makes much more sense as individual files\n       may reference different `Files` so storing the `references` in `Version`\n       doesn't make much sense.\n\n    Args:\n        revision_number (Optional[int]): A positive non-zero integer number\n            holding the major version counter. This can be set with an\n            argument, allowing setting of the revision number as the Version\n            instance is created. So, if a :class:`.Version` is created under\n            the same :class:`Task` before, the newly created :class:`.Version`\n            instances will start from the highest revision number unless it is\n            set to another value. Non-sequential revision numbers can be set.\n            So, one can start with 1 and then can jump to 3 and 10 from there.\n            All the :class:`.Version` instances that have the same\n            :attr:`.revision_number` under the same :class:`.Task` will be\n            considered in the same version stream and version number attribute\n            will be set accordingly. The default is 1.\n        files (List[File]): A list of :class:`.File` instances that are created\n            for this :class:`.Version` instance. This can be different\n            representations (i.e. base, Alembic, USD, ASS, RS etc.) of the same\n            data.\n        task (Task): A :class:`.Task` instance showing the owner of this\n            Version.\n        parent (Version): A :class:`.Version` instance which is the parent of\n            this Version. It is mainly used to see which Version is derived\n            from which in the Version history of a :class:`.Task`.\n    \"\"\"  # noqa: B950\n\n    from stalker import defaults\n\n    __auto_name__ = True\n    __tablename__ = \"Versions\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Version\"}\n\n    __dag_cascade__ = \"save-update, merge\"\n\n    version_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"Entities.id\"), primary_key=True\n    )\n\n    __id_column__ = \"version_id\"\n\n    task_id: Mapped[int] = mapped_column(ForeignKey(\"Tasks.id\"), nullable=False)\n    task: Mapped[Task] = relationship(\n        primaryjoin=\"Versions.c.task_id==Tasks.c.id\",\n        doc=\"The :class:`.Task` instance that this Version is created for.\",\n        uselist=False,\n        back_populates=\"versions\",\n    )\n\n    _revision_number: Mapped[int] = mapped_column(\n        \"revision_number\",\n        default=1,\n        nullable=False,\n    )\n\n    version_number: Mapped[int] = mapped_column(\n        default=1,\n        nullable=False,\n        doc=\"\"\"The :attr:`.version_number` attribute is read-only.\n        Trying to change it will produce an AttributeError.\n        \"\"\",\n    )\n\n    files: Mapped[Optional[List[File]]] = relationship(\n        secondary=\"Version_Files\",\n        primaryjoin=\"Versions.c.id==Version_Files.c.version_id\",\n        secondaryjoin=\"Version_Files.c.file_id==Files.c.id\",\n        doc=\"\"\"The files related to the current version.\n\n        It is a list of :class:`.File` instances.\n        \"\"\",\n    )\n\n    reviews: Mapped[Optional[List[Review]]] = relationship(\n        primaryjoin=\"Reviews.c.version_id==Versions.c.id\"\n    )\n\n    is_published: Mapped[Optional[bool]] = mapped_column(default=False)\n\n    def __init__(\n        self,\n        task: Optional[Task] = None,\n        files: Optional[List[File]] = None,\n        parent: Optional[\"Version\"] = None,\n        full_path: Optional[str] = None,\n        revision_number: Optional[int] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        # call supers __init__\n        kwargs[\"full_path\"] = full_path\n        super(Version, self).__init__(**kwargs)\n\n        DAGMixin.__init__(self, parent=parent)\n\n        self.task = task\n        if revision_number is None:\n            revision_number = 1\n        self.revision_number = revision_number\n        self.version_number = None\n\n        if files is None:\n            files = []\n\n        self.files = files\n        self.is_published = False\n\n    def __repr__(self) -> str:\n        \"\"\"Return the str representation of the Version.\n\n        Returns:\n            str: The string representation of this Version instance.\n        \"\"\"\n        return (\n            \"<{project_code}_{nice_name}_v{version_number:03d} ({entity_type})>\".format(\n                project_code=self.task.project.code,\n                nice_name=self.nice_name,\n                version_number=self.version_number,\n                entity_type=self.entity_type,\n            )\n        )\n\n    def _validate_revision_number(self, revision_number: int) -> int:\n        \"\"\"Validate the given revision_number value.\n\n        Args:\n            revision_number (int): The revision_number value to be validated.\n\n        Raises:\n            TypeError: If the given revision_number value is not an integer.\n            ValueError: If the given revision_number value is not a positive integer.\n\n        Returns:\n            int: The validated revision_number value.\n        \"\"\"\n        error_message = (\n            f\"{self.__class__.__name__}.revision_number should be a \"\n            f\"positive integer, not {revision_number.__class__.__name__}: \"\n            f\"'{revision_number}'\"\n        )\n\n        if not isinstance(revision_number, int):\n            raise TypeError(error_message)\n\n        if revision_number < 1:\n            raise ValueError(error_message)\n\n        return revision_number\n\n    def _revision_number_getter(self) -> int:\n        \"\"\"Return the revision_number value.\n\n        Returns:\n            int: revision_number attribute value\n        \"\"\"\n        return self._revision_number\n\n    def _revision_number_setter(self, revision_number: int):\n        \"\"\"Set the revision attribute value.\n\n        Args:\n            revision_number (int): The new revision number value.\n        \"\"\"\n        revision_number = self._validate_revision_number(revision_number)\n\n        is_updating_revision_number = False\n        if self._revision_number is not None:\n            if revision_number != self._revision_number:\n                logger.debug(\n                    \"Updating revision_number from \"\n                    f\"{self._revision_number} -> {revision_number}\"\n                )\n                is_updating_revision_number = True\n            else:\n                logger.debug(\n                    \"Revision number is the same... \"\n                    f\"{self._revision_number} == {revision_number}\"\n                )\n        else:\n            logger.debug(\"revision_number is being set for the first time!\")\n\n        self._revision_number = revision_number\n\n        if is_updating_revision_number and self.version_number is not None:\n            # if we are updating the revision_number value,\n            # also update reset the version_number\n            logger.debug(\n                \"Updated revision_number! so, let's update version_number too!\"\n            )\n            logger.debug(f\"current version_number is {self.version_number}\")\n            self.version_number = None\n\n    revision_number: Mapped[int] = synonym(\n        \"_revision_number\",\n        descriptor=property(_revision_number_getter, _revision_number_setter),\n    )\n\n    @property\n    def latest_version(self) -> \"Version\":\n        \"\"\"Return the Version instance with the highest version number in this series.\n\n        Returns:\n            Version: The :class:`.Version` instance with the highest version number in\n                this version series.\n        \"\"\"\n        latest_version = None\n        try:\n            with DBSession.no_autoflush:\n                latest_version = (\n                    Version.query.filter(Version.task == self.task)\n                    .filter(Version.revision_number == self.revision_number)\n                    .order_by(Version.version_number.desc())\n                    .first()\n                )\n            return latest_version\n        except (UnboundExecutionError, OperationalError):\n            all_versions = sorted(\n                self.task.versions,\n                key=lambda x: x.version_number if x.version_number else -1,\n            )\n            return all_versions[-1] if all_versions else None\n\n    @property\n    def max_revision_number(self) -> int:\n        \"\"\"Return the maximum revision number for this Version.\n\n        Returns:\n            int: The maximum revision number for this Version.\n        \"\"\"\n        with DBSession.no_autoflush:\n            result = (\n                DBSession.query(Version.revision_number)\n                .filter(Version.task_id == self.task_id)\n                .order_by(Version.revision_number.desc())\n                .first()\n            )\n        return result[0] if result else 1\n\n    @property\n    def max_version_number(self) -> int:\n        \"\"\"Return the maximum version number for this Version.\n\n        Returns:\n            int: The maximum version number for this Version.\n        \"\"\"\n        latest_version = self.latest_version\n        return latest_version.version_number if latest_version else 0\n\n    @validates(\"version_number\")\n    def _validate_version_number(self, key: str, version_number: int) -> int:\n        \"\"\"Validate the given version_number value.\n\n        Args:\n            key (str): The name of the validated column.\n            version_number (int): The version number to be validated.\n\n        Returns:\n            int: The validated version number.\n        \"\"\"\n        max_version_number = self.max_version_number\n\n        logger.debug(f\"max_version_number: {max_version_number}\")\n        logger.debug(f\"given version_number: {version_number}\")\n\n        if version_number is not None and version_number > max_version_number:\n            return version_number\n\n        if self.latest_version == self:\n            if self.version_number is not None:\n                version_number = self.version_number\n            else:\n                version_number = 1\n                logger.debug(\n                    f\"{self.__class__.__name__}.version_number is weirdly 'None', \"\n                    \"no database connection maybe?\"\n                )\n            logger.debug(\n                \"the version is the latest version in database, the \"\n                f\"number will not be changed from {version_number}\"\n            )\n        else:\n            version_number = max_version_number + 1\n            logger.debug(\n                \"given Version.version_number is too low,\"\n                f\"max version_number in the database is {max_version_number}, \"\n                f\"setting the current version_number to {version_number}\"\n            )\n\n        return version_number\n\n    @validates(\"task\")\n    def _validate_task(self, key, task) -> Task:\n        \"\"\"Validate the given task value.\n\n        Args:\n            key (str): The name of the validated column.\n            task (Task): The task value to be validated.\n\n        Raises:\n            TypeError: If the task value is not a :class:`.Task` instance.\n\n        Returns:\n            Task: The validated :class:`.Task` instance.\n        \"\"\"\n        if task is None:\n            raise TypeError(\"{}.task cannot be None\".format(self.__class__.__name__))\n\n        if not isinstance(task, Task):\n            raise TypeError(\n                \"{}.task should be a Task, Asset, Shot, Scene, Sequence or \"\n                \"Variant instance, not {}: '{}'\".format(\n                    self.__class__.__name__, task.__class__.__name__, task\n                )\n            )\n\n        return task\n\n    @validates(\"files\")\n    def _validate_files(self, key: str, file: File) -> File:\n        \"\"\"Validate the given file value.\n\n        Args:\n            key (str): The name of the validated column.\n            file (File): The file value to be validated.\n\n        Raises:\n            TypeError: If the file is not a :class:`.File` instance.\n\n        Returns:\n            File: The validated file value.\n        \"\"\"\n        if not isinstance(file, File):\n            raise TypeError(\n                \"{}.files should only contain instances of \"\n                \"stalker.models.file.File, not {}: '{}'\".format(\n                    self.__class__.__name__, file.__class__.__name__, file\n                )\n            )\n\n        return file\n\n    def _template_variables(self) -> dict:\n        \"\"\"Return the variables used in rendering the filename template.\n\n        Returns:\n            dict: The template variables.\n        \"\"\"\n        version_template_vars = {\n            \"version\": self,\n            # \"extension\": self.extension,\n        }\n        version_template_vars.update(self.task._template_variables())\n        return version_template_vars\n\n    def generate_path(self, extension: Optional[str] = None) -> Path:\n        \"\"\"Generate a Path with the template variables from the parent project.\n\n        Args:\n            extension (Optional[str]): An optional string containing the\n                extension for the resulting Path.\n\n        Raises:\n            TypeError: If the extension is not None and not a str.\n            RuntimeError: If no Version related FilenameTemplate is found in\n                the related `Project.structure`.\n\n        Returns:\n            Path: A `pathlib.Path` object.\n        \"\"\"\n        if extension is not None and not isinstance(extension, str):\n            raise TypeError(\n                \"extension should be a str, \"\n                f\"not {extension.__class__.__name__}: '{extension}'\"\n            )\n        kwargs = self._template_variables()\n\n        # get a suitable FilenameTemplate\n        structure = self.task.project.structure\n\n        vers_template = None\n        if structure:\n            for template in structure.templates:\n                if template.target_entity_type == self.task.entity_type:\n                    vers_template = template\n                    break\n\n        if not vers_template:\n            raise RuntimeError(\n                \"There are no suitable FilenameTemplate \"\n                \"(target_entity_type == '{entity_type}') defined in the Structure of \"\n                \"the related Project instance, please create a new \"\n                \"stalker.models.template.FilenameTemplate instance with its \"\n                \"'target_entity_type' attribute is set to '{entity_type}' and add \"\n                \"it to the `templates` attribute of the structure of the \"\n                \"project\".format(entity_type=self.task.entity_type)\n            )\n\n        path = Path(\n            jinja2.Template(vers_template.path).render(\n                **kwargs, trim_blocks=True, lstrip_blocks=True\n            )\n        ) / Path(\n            jinja2.Template(vers_template.filename).render(\n                **kwargs, trim_blocks=True, lstrip_blocks=True\n            )\n        )\n        if extension is not None:\n            path = path.with_suffix(extension)\n\n        return path\n\n    @property\n    def absolute_full_path(self) -> str:\n        \"\"\"Return the absolute full path of this version.\n\n        This absolute full path includes the repository path of the related project.\n\n        Returns:\n            str: The absolute full path of this Version instance.\n        \"\"\"\n        return Path(\n            os.path.normpath(os.path.expandvars(str(self.generate_path()))).replace(\n                \"\\\\\", \"/\"\n            )\n        )\n\n    @property\n    def absolute_path(self) -> str:\n        \"\"\"Return the absolute path.\n\n        Returns:\n            str: The absolute path.\n        \"\"\"\n        return Path(\n            os.path.normpath(\n                os.path.expandvars(str(self.generate_path().parent))\n            ).replace(\"\\\\\", \"/\")\n        )\n\n    @property\n    def full_path(self) -> Path:\n        \"\"\"Return the full path of this version.\n\n        This full path includes the repository path of the related project as\n        it is.\n\n        Returns:\n            Path: The full path of this Version instance.\n        \"\"\"\n        return self.generate_path()\n\n    @property\n    def path(self) -> Path:\n        \"\"\"Return the path.\n\n        Returns:\n            Path: The path.\n        \"\"\"\n        return self.full_path.parent\n    \n    @property\n    def filename(self) -> str:\n        \"\"\"Return the filename bit of the path.\n        \n        Returns:\n            str: The filename.\n        \"\"\"\n        return self.full_path.name\n\n    def is_latest_published_version(self) -> bool:\n        \"\"\"Return True if this is the latest published Version False otherwise.\n\n        Returns:\n            bool: True if this is the latest published Version, False otherwise.\n        \"\"\"\n        if not self.is_published:\n            return False\n\n        return self == self.latest_published_version\n\n    @property\n    def latest_published_version(self) -> \"Version\":\n        \"\"\"Return the last published version.\n\n        Returns:\n            Version: The last published Version instance.\n        \"\"\"\n        return (\n            Version.query.filter_by(task=self.task)\n            .filter(Version.revision_number == self.revision_number)\n            .filter_by(is_published=True)\n            .order_by(Version.version_number.desc())\n            .first()\n        )\n\n    def __eq__(self, other):\n        \"\"\"Check the equality.\n\n        Args:\n            other (object): The other object.\n\n        Returns:\n            bool: True if the other object is equal to this one as an Entity, is a\n                Version instance, has the same task and same version_number.\n        \"\"\"\n        return (\n            super(Version, self).__eq__(other)\n            and isinstance(other, Version)\n            and self.task == other.task\n            and self.version_number == other.version_number\n        )\n\n    def __hash__(self):\n        \"\"\"Return the hash value of this instance.\n\n        Because the __eq__ is overridden the __hash__ also needs to be overridden.\n\n        Returns:\n            int: The hash value.\n        \"\"\"\n        return super(Version, self).__hash__()\n\n    @property\n    def naming_parents(self) -> List[Task]:\n        \"\"\"Return a list of parents starting from the nearest Asset, Shot or Sequence.\n\n        Returns:\n            List[Task]: List of naming parents.\n        \"\"\"\n        # find a Asset, Shot or Sequence, and store it as the significant\n        # parent, and name the task starting from that entity\n        all_parents = self.task.parents\n        all_parents.append(self.task)\n        naming_parents = []\n        if not all_parents:\n            return naming_parents\n\n        for parent in reversed(all_parents):\n            naming_parents.insert(0, parent)\n            if parent.entity_type in [\"Asset\", \"Shot\", \"Sequence\"]:\n                break\n\n        return naming_parents\n\n    @property\n    def nice_name(self) -> str:\n        \"\"\"Override the nice name method for Version class.\n\n        Returns:\n            str: The nice name.\n        \"\"\"\n        return self._format_nice_name(\n            \"_\".join(map(lambda x: x.nice_name, self.naming_parents))\n        )\n\n    def request_review(self) -> List[Review]:\n        \"\"\"Request a review.\n\n        This is a shortcut to the Task.request_review() method of the related\n        task.\n\n        Returns:\n            List[Review]: The created Review instances.\n        \"\"\"\n        return self.task.request_review(version=self)\n\n\n# VERSION FILES\nVersion_Files = Table(\n    \"Version_Files\",\n    Base.metadata,\n    Column(\"version_id\", Integer, ForeignKey(\"Versions.id\"), primary_key=True),\n    Column(\n        \"file_id\",\n        Integer,\n        ForeignKey(\"Files.id\", onupdate=\"CASCADE\", ondelete=\"CASCADE\"),\n        primary_key=True,\n    ),\n)\n"
  },
  {
    "path": "src/stalker/models/wiki.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Wiki related functions and classes are situated here.\"\"\"\nfrom typing import Any, Dict, Optional, TYPE_CHECKING, Union\n\nfrom sqlalchemy import ForeignKey, Text\nfrom sqlalchemy.orm import Mapped, mapped_column, validates\n\nfrom stalker import Entity, ProjectMixin\n\nif TYPE_CHECKING:  # pragma: no cover\n    from stalker.models.project import Project\n\n\nclass Page(Entity, ProjectMixin):\n    \"\"\"A simple Wiki page implementation.\n\n    Wiki in Stalker are managed per Project. That is, all Wiki pages are\n    related to a Project.\n\n    Stalker wiki pages are very simple in terms of data it holds. It has only\n    one :attr:`.title` and one :attr:`.content` an some usual audit info coming\n    from :class:`.SimpleEntity` and a :attr:`.project` coming from\n    :class:`.ProjectMixin`.\n\n    Args:\n        title (str): The title of this Page.\n        content (str): The content of this page. Can contain any kind of string\n            literals including HTML tags etc.\n    \"\"\"\n\n    __auto_name__ = True\n    __tablename__ = \"Pages\"\n    __mapper_args__ = {\"polymorphic_identity\": \"Page\"}\n    page_id: Mapped[int] = mapped_column(\n        \"id\",\n        ForeignKey(\"Entities.id\"),\n        primary_key=True,\n    )\n\n    title: Mapped[Optional[str]] = mapped_column(Text)\n    content: Mapped[Optional[str]] = mapped_column(Text)\n\n    def __init__(\n        self,\n        title: str = \"\",\n        content: str = \"\",\n        project: Optional[\"Project\"] = None,\n        **kwargs: Dict[str, Any],\n    ) -> None:\n        kwargs[\"project\"] = project\n        super(Page, self).__init__(**kwargs)\n        ProjectMixin.__init__(self, **kwargs)\n\n        self.title = title\n        self.content = content\n\n    @validates(\"title\")\n    def _validate_title(self, key: str, title: str) -> str:\n        \"\"\"Validate the given title value.\n\n        Args:\n            key (str): The name of the validated column.\n            title (str): The title value to be validated.\n\n        Raises:\n            TypeError: If the given title is not a string.\n            ValueError: If the title is an empty string.\n\n        Returns:\n            str: The validated title value.\n        \"\"\"\n        if not isinstance(title, str):\n            raise TypeError(\n                \"{}.title should be a string, not {}: '{}'\".format(\n                    self.__class__.__name__, title.__class__.__name__, title\n                )\n            )\n\n        if not title:\n            raise ValueError(f\"{self.__class__.__name__}.title cannot be empty\")\n\n        return title\n\n    @validates(\"content\")\n    def _validate_content(self, key: str, content: Union[None, str]) -> str:\n        \"\"\"Validate the given content value.\n\n        Args:\n            key (str): The name of the validated column.\n            content (Union[None, str]): The content value to be validated.\n\n        Raises:\n            TypeError: If the content is not None and not str.\n\n        Returns:\n            str: The validated content value.\n        \"\"\"\n        content = \"\" if content is None else content\n        if not isinstance(content, str):\n            raise TypeError(\n                \"{}.content should be a string, not {}: '{}'\".format(\n                    self.__class__.__name__, content.__class__.__name__, content\n                )\n            )\n        return content\n"
  },
  {
    "path": "src/stalker/py.typed",
    "content": ""
  },
  {
    "path": "src/stalker/utils.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Utilities are situated here.\"\"\"\nimport calendar\nfrom datetime import datetime, timedelta\nfrom typing import Any, Generator, Union\n\nimport pytz\n\nfrom stalker.exceptions import CircularDependencyError\nfrom stalker.models.enum import TraversalDirection\n\n\ndef make_plural(name: str) -> str:\n    \"\"\"Return the plural version of the given name argument.\n\n    Args:\n        name (str): The name to make plural.\n\n    Returns:\n        str: The plural version of the given name.\n    \"\"\"\n    plural_name = name + \"s\"\n\n    if name[-1] == \"y\":\n        plural_name = name[:-1] + \"ies\"\n    elif name[-2:] == \"ch\":\n        plural_name = name + \"es\"\n    elif name[-1] == \"f\":\n        plural_name = name[:-1] + \"ves\"\n    elif name[-1] == \"s\":\n        plural_name = name + \"es\"\n\n    return plural_name\n\n\ndef walk_hierarchy(\n    entity: Any,\n    attr: str,\n    method: Union[int, str, TraversalDirection] = TraversalDirection.DepthFirst,\n) -> Generator[Any, Any, Any]:\n    \"\"\"Walk the entity hierarchy over the given attribute and yield the entities found.\n\n    It doesn't check for cycle, so if the attribute is not acyclic then this\n    function will not find an exit point.\n\n    The default method is Depth First Search (DFS), to walk with Breadth First\n    Search (BFS) set the direction to :attr:`.TraversalDirection.BreadthFirst`.\n\n    Args:\n        entity (Any): Starting Entity.\n        attr (str): The attribute name to walk over.\n        method (Union[int, str, TraversalDirection]): Use TraversalDirection\n            enum values, or one of the values listed here [\"DepthFirst\",\n            \"BreadthFirst\", 0, 1]. The default is\n            :attr:`.TraversalDirection.DepthFirst`.\n\n    Yields:\n        Any: List any entities found while traversing the hierarchy.\n    \"\"\"\n    entity_to_visit = [entity]\n    method = TraversalDirection.to_direction(method)\n    if method == TraversalDirection.DepthFirst:\n        while len(entity_to_visit):\n            current_entity = entity_to_visit.pop(0)\n            for child in reversed(getattr(current_entity, attr)):\n                entity_to_visit.insert(0, child)\n            yield current_entity\n    else:  # TraversalDirection.BreadthFirst\n        while len(entity_to_visit):\n            current_entity = entity_to_visit.pop(0)\n            entity_to_visit.extend(getattr(current_entity, attr))\n            yield current_entity\n\n\ndef check_circular_dependency(entity: Any, other_entity: Any, attr_name: str) -> None:\n    \"\"\"Check circular dependency.\n\n    Check if entity and other_entity are in circular dependency over the attr with the\n    name attr_name.\n\n    Args:\n        entity (Any): Any Python object.\n        other_entity (Any): Any Python object.\n        attr_name (str): The name of the attribute to check the circular dependency of.\n\n    Raises:\n        CircularDependencyError: If the entities are in circular dependency over the\n            attr with the name attr_name.\n    \"\"\"\n    for e in walk_hierarchy(entity, attr_name):\n        if e is other_entity:\n            raise CircularDependencyError(\n                \"{entity_name} ({entity_class}) and \"\n                \"{other_entity_name} ({other_entity_class}) are in a \"\n                'circular dependency in their \"{attr_name}\" attribute'.format(\n                    entity_name=entity,\n                    entity_class=entity.__class__.__name__,\n                    other_entity_name=other_entity,\n                    other_entity_class=other_entity.__class__.__name__,\n                    attr_name=attr_name,\n                )\n            )\n\n\ndef utc_to_local(utc_datetime: datetime) -> datetime:\n    \"\"\"Convert utc time to local time.\n\n    Based on the answer of J.F. Sebastian on\n    http://stackoverflow.com/questions/4563272/how-to-convert-a-python-utc-datetime-to-a-local-datetime-using-only-python-stand/13287083#13287083\n\n    Args:\n        utc_datetime (datetime): The UTC datetime instance.\n\n    Returns:\n        datetime: The local datetime instance.\n    \"\"\"\n    # get integer timestamp to avoid precision lost\n    timestamp = calendar.timegm(utc_datetime.timetuple())\n    local_dt = datetime.fromtimestamp(timestamp)\n    return local_dt.replace(microsecond=utc_datetime.microsecond)\n\n\ndef local_to_utc(local_datetime: datetime) -> datetime:\n    \"\"\"Convert local datetime to utc datetime.\n\n    Based on the answer of J.F. Sebastian on\n    http://stackoverflow.com/questions/4563272/how-to-convert-a-python-utc-datetime-to-a-local-datetime-using-only-python-stand/13287083#13287083\n\n    Args:\n        local_datetime (datetime): The local `datetime` instance.\n\n    Returns:\n        datetime: The UTC datetime instance.\n    \"\"\"\n    # get the utc_datetime as if the local_datetime is utc and calculate the timezone\n    # difference and add it to the local datetime object\n    return local_datetime - (utc_to_local(local_datetime) - local_datetime)\n\n\ndef datetime_to_millis(dt: datetime) -> int:\n    \"\"\"Calculate the milliseconds since epoch for the given datetime value.\n\n    This is used as the default JSON serializer for datetime objects.\n\n    Code is based on the answer of Jay Taylor in\n    http://stackoverflow.com/questions/11875770/how-to-overcome-datetime-datetime-not-json-serializable-in-python\n\n    Args:\n        dt (datetime): The ``datetime`` instance.\n\n    Returns:\n        int: The int value of milliseconds since epoch.\n    \"\"\"\n    if isinstance(dt, datetime) and dt.utcoffset() is not None:\n        dt = dt - dt.utcoffset()\n    millis = int(calendar.timegm(dt.timetuple()) * 1000 + dt.microsecond / 1000)\n    return millis\n\n\ndef millis_to_datetime(millis: int) -> datetime:\n    \"\"\"Calculate the datetime from the given milliseconds value.\n\n    Args:\n        millis (int): An int value showing the millis from unix EPOCH\n\n    Returns:\n        datetime: The corresponding ``datetime`` instance to\n            the given milliseconds.\n    \"\"\"\n    epoch = datetime(1970, 1, 1, tzinfo=pytz.utc)\n    return epoch + timedelta(milliseconds=millis)\n"
  },
  {
    "path": "src/stalker/version.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Provides functionality to parse the version number from the VERSION file.\"\"\"\nimport os\nfrom typing import Union\n\nVERSION: Union[None, str] = None\nVERSION_FILE: str = os.path.join(os.path.dirname(__file__), \"VERSION\")\nif os.path.isfile(VERSION_FILE):\n    with open(VERSION_FILE, \"r\") as f:\n        VERSION = f.read().strip()\n__version__ = VERSION or \"0.0.0\"\n\"\"\"str: The version of the package.\"\"\"\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/benchmarks/__init__.py",
    "content": ""
  },
  {
    "path": "tests/benchmarks/task_total_logged_seonds.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Benchmark total logged session computation.\"\"\"\nimport datetime\nimport logging\nimport os\nimport time\n\nimport pytz\nfrom sqlalchemy.orm import close_all_sessions\n\nfrom sqlalchemy.pool import NullPool\n\nimport stalker\nimport stalker.db.setup\nfrom stalker import (\n    Project,\n    Repository,\n    Status,\n    StatusList,\n    Task,\n    TimeLog,\n    Type,\n    User,\n    db,\n    log,\n)\nfrom stalker.config import Config\nfrom stalker.db.declarative import Base\nfrom stalker.db.session import DBSession\n\nfrom stalker.models.enum import TimeUnit\nfrom stalker.models.enum import ScheduleModel\nfrom tests.utils import create_random_db, drop_db, get_server_details_from_url\n\nlog.logging_level = logging.INFO\nlogging.getLogger(\"stalker.models.task\").setLevel(logging.INFO)\n\n\n# create a new database for this test only\ndatabase_url = create_random_db()\n\n# update the config\nconfig = {\"sqlalchemy.url\": database_url, \"sqlalchemy.poolclass\": NullPool}\n\n\ntry:\n    os.environ.pop(Config.env_key)\nexcept KeyError:\n    # already removed\n    pass\n\n# regenerate the defaults\n# stalker.defaults = Config()\nstalker.defaults.config_values = stalker.defaults.default_config_values.copy()\nstalker.defaults[\"timing_resolution\"] = datetime.timedelta(minutes=10)\n\n# init database\nstalker.db.setup.setup(config)\nstalker.db.setup.init()\n\nstatus_wfd = Status.query.filter_by(code=\"WFD\").first()\nstatus_rts = Status.query.filter_by(code=\"RTS\").first()\nstatus_wip = Status.query.filter_by(code=\"WIP\").first()\nstatus_prev = Status.query.filter_by(code=\"PREV\").first()\nstatus_hrev = Status.query.filter_by(code=\"HREV\").first()\nstatus_drev = Status.query.filter_by(code=\"DREV\").first()\nstatus_oh = Status.query.filter_by(code=\"OH\").first()\nstatus_stop = Status.query.filter_by(code=\"STOP\").first()\nstatus_cmpl = Status.query.filter_by(code=\"CMPL\").first()\n\ntask_status_list = StatusList.query.filter_by(target_entity_type=\"Task\").first()\n\ntest_movie_project_type = Type(\n    name=\"Movie Project\",\n    code=\"movie\",\n    target_entity_type=\"Project\",\n)\n\ntest_repository_type = Type(\n    name=\"Test Repository Type\",\n    code=\"test\",\n    target_entity_type=\"Repository\",\n)\n\ntest_repository = Repository(\n    name=\"Test Repository\",\n    code=\"TR\",\n    type=test_repository_type,\n    linux_path=\"/mnt/T/\",\n    windows_path=\"T:/\",\n    macos_path=\"/Volumes/T/\",\n)\n\n\ntest_user1 = User(name=\"User1\", login=\"user1\", email=\"user1@user1.com\", password=\"1234\")\n\ntest_user2 = User(name=\"User2\", login=\"user2\", email=\"user2@user2.com\", password=\"1234\")\n\ntest_user3 = User(name=\"User3\", login=\"user3\", email=\"user3@user3.com\", password=\"1234\")\n\ntest_user4 = User(name=\"User4\", login=\"user4\", email=\"user4@user4.com\", password=\"1234\")\n\ntest_user5 = User(name=\"User5\", login=\"user5\", email=\"user5@user5.com\", password=\"1234\")\n\n\ntest_project1 = Project(\n    name=\"Test Project1\",\n    code=\"tp1\",\n    type=test_movie_project_type,\n    repositories=[test_repository],\n)\n\ntest_dependent_task1 = Task(\n    name=\"Dependent Task1\",\n    project=test_project1,\n    status_list=task_status_list,\n    responsible=[test_user1],\n)\n\ntest_dependent_task2 = Task(\n    name=\"Dependent Task2\",\n    project=test_project1,\n    status_list=task_status_list,\n    responsible=[test_user1],\n)\n\nkwargs = {\n    \"name\": \"Modeling\",\n    \"description\": \"A Modeling Task\",\n    \"project\": test_project1,\n    \"priority\": 500,\n    \"responsible\": [test_user1],\n    \"resources\": [test_user1, test_user2],\n    \"alternative_resources\": [test_user3, test_user4, test_user5],\n    \"allocation_strategy\": \"minloaded\",\n    \"persistent_allocation\": True,\n    \"watchers\": [test_user3],\n    \"bid_timing\": 4,\n    \"bid_unit\": TimeUnit.Day,\n    \"schedule_timing\": 1,\n    \"schedule_unit\": TimeUnit.Day,\n    \"start\": datetime.datetime(2013, 4, 8, 13, 0, tzinfo=pytz.utc),\n    \"end\": datetime.datetime(2013, 4, 8, 18, 0, tzinfo=pytz.utc),\n    \"depends_on\": [test_dependent_task1, test_dependent_task2],\n    \"time_logs\": [],\n    \"versions\": [],\n    \"is_milestone\": False,\n    \"status\": 0,\n    \"status_list\": task_status_list,\n}\n\n\nDBSession.add_all(\n    [\n        test_movie_project_type,\n        test_repository_type,\n        test_repository,\n        test_user1,\n        test_user2,\n        test_user3,\n        test_user4,\n        test_user5,\n        test_project1,\n        test_dependent_task1,\n        test_dependent_task2,\n    ]\n)\nDBSession.commit()\n\n\nkwargs[\"depends_on\"] = []\n\ndt = datetime.datetime\ntd = datetime.timedelta\nnow = dt(2017, 3, 15, 0, 30, tzinfo=pytz.utc)\n\nkwargs[\"schedule_model\"] = ScheduleModel.Effort\n\n# -------------- HOURS --------------\nkwargs[\"schedule_timing\"] = 10\nkwargs[\"schedule_unit\"] = TimeUnit.Hour\nnew_task = Task(**kwargs)\nDBSession.add(new_task)\n\n# create 100000 of 10 minutes of TimeLogs\nbenchmark_start = time.time()\n\ntl_count = 100000\n\nstart = now\nten_minutes = datetime.timedelta(minutes=10)\nresource = kwargs[\"resources\"][0]\nprint(f\"creating {tl_count} TimeLogs\")\nfor i in range(tl_count):\n    end = start + ten_minutes\n    tl = TimeLog(\n        resource=resource,\n        task=new_task,\n        start=start,\n        end=end,\n    )\n    DBSession.add(tl)\n    start = end\n    if i % 1000 == 0:\n        print(f\"i: {i}\")\n        DBSession.flush()\n        DBSession.commit()\nDBSession.flush()\nDBSession.commit()\n\nbenchmark_end = time.time()\nprint(\"data created in: {:0.3f} secs\".format(benchmark_end - benchmark_start))\n\ntask_id = new_task.id\n# del all the TimeLogs\ndel new_task.time_logs\ndel new_task\n\n\n# now get back the task from db\ntask_from_db = DBSession.get(Task, task_id)\n\n# now query the total_logged_seconds\nbenchmark_start = time.time()\ntotal_logged_seconds = task_from_db.total_logged_seconds\nbenchmark_end = time.time()\nprint(\"total_logged_seconds: {:0.3f} sec\".format(total_logged_seconds))\nprint(\"old way worked in: {:0.3f} sec\".format(benchmark_end - benchmark_start))\n\n# now use the new way of doing it\nbenchmark_start = time.time()\nquick_total_logged_seconds = task_from_db.total_logged_seconds\nbenchmark_end = time.time()\nprint(\"quick_total_logged_seconds: {:0.3f} sec\".format(quick_total_logged_seconds))\nprint(\"new way worked in: {:0.3f} sec\".format(benchmark_end - benchmark_start))\nassert total_logged_seconds == quick_total_logged_seconds\n\n# clean up test database\nDBSession.rollback()\nconnection = DBSession.connection()\nengine = connection.engine\nconnection.close()\n\nBase.metadata.drop_all(engine, checkfirst=True)\nDBSession.remove()\n\nstalker.defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n\nclose_all_sessions()\ndrop_db(**get_server_details_from_url(database_url))\n"
  },
  {
    "path": "tests/config/__init__.py",
    "content": ""
  },
  {
    "path": "tests/config/test_config.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"stalker.config module.\"\"\"\nimport datetime\nimport logging\nimport os\nimport shutil\nimport sys\nimport tempfile\n\nimport pytest\n\nfrom stalker import defaults, config\nfrom stalker.db.session import DBSession\nfrom stalker.models.studio import Studio\n\nlogger = logging.getLogger(\"stalker\")\nlogger.setLevel(logging.DEBUG)\n\n\n@pytest.fixture(scope=\"function\")\ndef prepare_config_file():\n    \"\"\"Set up the test.\"\"\"\n    # so we need a temp directory to be specified as our config folder\n    temp_config_folder = tempfile.mkdtemp()\n\n    # we should set the environment variable\n    os.environ[\"STALKER_PATH\"] = temp_config_folder\n\n    config_full_path = os.path.join(temp_config_folder, \"config.py\")\n    yield config_full_path\n    shutil.rmtree(temp_config_folder)\n\n\ndef test_config_variable_updates_with_user_config(prepare_config_file):\n    \"\"\"database_file_name will be updated by the user config.\"\"\"\n    # now create a config.py file and fill it with the desired values\n    # like database_file_name = \"test_value.db\"\n    config_full_path = prepare_config_file\n    test_value = \".test_value.db\"\n    config_file = open(config_full_path, \"w\")\n    config_file.writelines(\n        [\n            \"#-*- coding: utf-8 -*-\\n\",\n            f'database_engine_settings = \"{test_value}\"\\n',\n        ]\n    )\n    config_file.close()\n\n    # now import the config.py and see if it updates the\n    # database_file_name variable\n    conf = config.Config()\n\n    assert test_value == conf.database_engine_settings\n\n\ndef test_config_variable_does_create_new_variables_with_user_config(\n    prepare_config_file,\n):\n    \"\"\"config will be updated by the user config by adding new variables.\"\"\"\n    config_full_path = prepare_config_file\n\n    # now create a config.py file and fill it with the desired values\n    # like database_file_name = \"test_value.db\"\n    test_value = \".test_value.db\"\n    config_file = open(config_full_path, \"w\")\n    config_file.writelines(\n        [\"#-*- coding: utf-8 -*-\\n\", 'test_value = \"' + test_value + '\"\\n']\n    )\n    config_file.close()\n\n    # now import the config.py and see if it updates the\n    # database_file_name variable\n    conf = config.Config()\n\n    assert conf.test_value == test_value\n\n\ndef test_env_variable_with_vars_module_import_with_shortcuts(prepare_config_file):\n    \"\"\"module path has shortcuts like ~ and other env variables.\"\"\"\n    config_full_path = prepare_config_file\n    temp_config_folder = os.path.dirname(config_full_path)\n    splits = os.path.split(temp_config_folder)\n    var1 = splits[0]\n    var2 = os.path.sep.join(splits[1:])\n\n    os.environ[\"var1\"] = var1\n    os.environ[\"var2\"] = var2\n    os.environ[\"STALKER_PATH\"] = \"$var1/$var2\"\n\n    test_value = \"sqlite3:///.test_value.db\"\n    config_file = open(config_full_path, \"w\")\n    config_file.writelines(\n        [\"#-*- coding: utf-8 -*-\\n\", 'database_url = \"' + test_value + '\"\\n']\n    )\n    config_file.close()\n\n    # now import the config.py and see if it updates the\n    # database_file_name variable\n    conf = config.Config()\n\n    assert test_value == conf.database_url\n\n\ndef test_env_variable_with_deep_vars_module_import_with_shortcuts(prepare_config_file):\n    \"\"\"module path has multiple shortcuts like ~ and other env variables.\"\"\"\n    config_full_path = prepare_config_file\n    temp_config_folder = os.path.dirname(config_full_path)\n    splits = os.path.split(temp_config_folder)\n    var1 = splits[0]\n    var2 = os.path.sep.join(splits[1:])\n    var3 = os.path.join(\"$var1\", \"$var2\")\n\n    os.environ[\"var1\"] = var1\n    os.environ[\"var2\"] = var2\n    os.environ[\"var3\"] = var3\n    os.environ[\"STALKER_PATH\"] = \"$var3\"\n\n    test_value = \"sqlite:///.test_value.db\"\n    config_file = open(config_full_path, \"w\")\n    config_file.writelines(\n        [\"#-*- coding: utf-8 -*-\\n\", 'database_url = \"' + test_value + '\"\\n']\n    )\n    config_file.close()\n\n    # now import the config.py and see if it updates the\n    # database_file_name variable\n    conf = config.Config()\n\n    assert test_value == conf.database_url\n\n\ndef test_non_existing_path_in_environment_variable():\n    \"\"\"non-existing path situation will be handled gracefully by warning the user.\"\"\"\n    os.environ[\"STALKER_PATH\"] = \"/tmp/non_existing_path\"\n    config.Config()\n\n\ndef test_syntax_error_in_settings_file(prepare_config_file):\n    \"\"\"RuntimeError will be raised when there are syntax errors in the config.py file.\"\"\"\n    config_full_path = prepare_config_file\n    temp_config_folder = os.path.dirname(config_full_path)\n\n    # now create a config.py file and fill it with the desired values\n    # like database_file_name = \"test_value.db\"\n    # but do a syntax error on purpose, like forgetting the last quote sign\n    test_value = \".test_value.db\"\n    config_file = open(config_full_path, \"w\")\n    config_file.writelines(\n        [\"#-*- coding: utf-8 -*-\\n\", 'database_file_name = \"' + test_value + \"\\n\"]\n    )\n    config_file.close()\n\n    # now import the config.py and see if it updates the\n    # database_file_name variable\n    with pytest.raises(RuntimeError) as cm:\n        config.Config()\n\n    error_message = {\n        8: \"There is a syntax error in your configuration file: \"\n        \"EOL while scanning string literal (<string>, line 2)\",\n        9: \"There is a syntax error in your configuration file: \"\n        \"EOL while scanning string literal (<string>, line 2)\",\n    }.get(\n        sys.version_info.minor,\n        \"There is a syntax error in your configuration file: \"\n        \"unterminated string literal (detected at line 2) (<string>, line 2)\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test___setattr___cannot_set_config_values_directly(prepare_config_file):\n    \"\"\"config.Config.__setattr__() method cannot set config values directly.\"\"\"\n    c = config.Config()\n    test_value = 1\n    c.daily_working_hours = test_value\n    assert c.config_values[\"daily_working_hours\"] != test_value\n\n\ndef test___getattr___is_working_as_expected(prepare_config_file):\n    \"\"\"config.Config.__getattr__() method is working as expected.\"\"\"\n    c = config.Config()\n    assert c.admin_name == \"admin\"\n\n\ndef test___getitem___is_working_as_expected(prepare_config_file):\n    \"\"\"config.Config.__getitem__() method is working as expected.\"\"\"\n    c = config.Config()\n    assert c[\"admin_name\"] == \"admin\"\n\n\ndef test___setitem__is_working_as_expected(prepare_config_file):\n    \"\"\"config.Config.__setitem__() method is working as expected.\"\"\"\n    c = config.Config()\n    test_value = \"administrator\"\n    assert c[\"admin_name\"] != test_value\n    c[\"admin_name\"] = test_value\n    assert c[\"admin_name\"] == test_value\n\n\ndef test___delitem__is_working_as_expected(prepare_config_file):\n    \"\"\"config.Config.__delitem__() method is working as expected.\"\"\"\n    c = config.Config()\n    assert c[\"admin_name\"] is not None\n    del c[\"admin_name\"]\n    assert \"admin_name\" not in c\n\n\ndef test___contains___is_working_as_expected(prepare_config_file):\n    \"\"\"config.Config.__contains__() method is working as expected.\"\"\"\n    c = config.Config()\n    assert \"admin_name\" in c\n\n\ndef test_update_with_studio_is_working_as_expected(setup_postgresql_db):\n    \"\"\"default values are updated with the Studio if there is a DB and a Studio.\"\"\"\n    # check the defaults are still using them self\n    assert defaults.timing_resolution == datetime.timedelta(hours=1)\n\n    studio = Studio(\n        name=\"Test Studio\", timing_resolution=datetime.timedelta(minutes=15)\n    )\n    DBSession.add(studio)\n    DBSession.commit()\n\n    # now check it again\n    assert defaults.timing_resolution == studio.timing_resolution\n\n\ndef test_old_style_repo_env_does_not_exist_anymore():\n    \"\"\"repo_env_var_template_old doesn't exist anymore.\"\"\"\n    assert \"repo_env_var_template_old\" not in defaults.config_values\n\n\ndef test_default_working_hours_is_a_dictionary_with_list_values():\n    \"\"\"default working_hours is a list of lists of two integers.\"\"\"\n    assert isinstance(defaults.working_hours, dict)\n    assert all(\n        day in defaults.working_hours\n        for day in [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\", \"sun\"]\n    )\n    assert all(\n        isinstance(defaults.working_hours[day], list)\n        for day in [\"mon\", \"tue\", \"wed\", \"thu\", \"fri\", \"sat\", \"sun\"]\n    )\n\n\ndef test_default_filename_template_value():\n    \"\"\"default filename_template includes revision_number.\"\"\"\n    assert isinstance(defaults.filename_template, str)\n    assert defaults.filename_template == (\n        \"{{version.nice_name}}\"\n        '_r{{\"%02d\"|format(version.revision_number)}}'\n        '_v{{\"%03d\"|format(version.version_number)}}'\n    )\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Configure tests.\"\"\"\nimport datetime\nimport logging\nimport os\nfrom subprocess import CalledProcessError\n\nimport pytest\n\nfrom sqlalchemy.pool import NullPool\n\nimport stalker\nimport stalker.db.setup\nfrom stalker import db, defaults, log, User\nfrom stalker.config import Config\nfrom stalker.db.session import DBSession\n\nfrom tests.utils import create_random_db, tear_down_db\n\nlogger = logging.getLogger(__name__)\nlog.register_logger(logger)\nlog.set_level(logging.DEBUG)\n\nHERE = os.path.dirname(__file__)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_sqlite3():\n    \"\"\"Set up in memory SQLite3 database for tests.\"\"\"\n    try:\n        os.environ.pop(Config.env_key)\n    except KeyError:\n        # already removed\n        pass\n\n    # regenerate the defaults\n    stalker.defaults.config_values = stalker.defaults.default_config_values.copy()\n    stalker.defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n\n    # Enable Debug logging\n    log.set_level(logging.DEBUG)\n    yield\n    tear_down_db({})\n\n\n@pytest.fixture\ndef get_data_file(request):\n    \"\"\"Request a specific datafile.\n\n    Args:\n        request: pytest.request object.\n\n    Returns:\n        str: Desired data file path.\n    \"\"\"\n    if isinstance(request.param, str):\n        return os.path.join(HERE, \"data\", request.param)\n    elif isinstance(request.param, list):\n        output = []\n        for path in request.param:\n            output.append(os.path.join(HERE, \"data\", path))\n        return output\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_postgresql_db():\n    \"\"\"Set up Postgresql database.\n\n    Yields:\n        dict: Test data storage.\n    \"\"\"\n    data = {\"config\": {}, \"database_url\": None}\n\n    # create a new database for this test only\n    while True:\n        try:\n            data[\"database_url\"] = create_random_db()\n        except CalledProcessError:\n            # in very rare cases the create_random_db generates an already\n            # existing database name\n            # call it again\n            pass\n        else:\n            break\n\n    # update the config\n    data[\"config\"][\"sqlalchemy.url\"] = data[\"database_url\"]\n    data[\"config\"][\"sqlalchemy.poolclass\"] = NullPool\n\n    try:\n        os.environ.pop(Config.env_key)\n    except KeyError:\n        # already removed\n        pass\n\n    # regenerate the defaults\n    stalker.defaults.config_values = stalker.defaults.default_config_values.copy()\n    stalker.defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n\n    # init database\n    # remove anything beforehand\n    stalker.db.setup.setup(data[\"config\"])\n    stalker.db.setup.init()\n\n    yield data\n    tear_down_db(data)\n"
  },
  {
    "path": "tests/data/project_to_tjp_output.jinja2",
    "content": "task Project_{{project.id}} \"Project_{{project.id}}\" {\n    task Sequence_{{sequence1.id}} \"Sequence_{{sequence1.id}}\" {\n        effort 1.0h\n        allocate User_{{user1.id}}\n    }\n    task Sequence_{{sequence2.id}} \"Sequence_{{sequence2.id}}\" {\n        effort 1.0h\n        allocate User_{{user2.id}}\n    }\n    task Sequence_{{sequence3.id}} \"Sequence_{{sequence3.id}}\" {\n        effort 1.0h\n        allocate User_{{user3.id}}\n    }\n    task Sequence_{{sequence4.id}} \"Sequence_{{sequence4.id}}\" {\n        task Task_{{task4.id}} \"Task_{{task4.id}}\" {\n            effort 1.0h\n            allocate User_{{user4.id}}\n        }\n        task Task_{{task5.id}} \"Task_{{task5.id}}\" {\n            effort 1.0h\n            allocate User_{{user5.id}}\n        }\n        task Task_{{task6.id}} \"Task_{{task6.id}}\" {\n            effort 1.0h\n            allocate User_{{user6.id}}\n        }\n    }\n    task Sequence_{{sequence5.id}} \"Sequence_{{sequence5.id}}\" {\n        task Task_{{task7.id}} \"Task_{{task7.id}}\" {\n            effort 1.0h\n            allocate User_{{user7.id}}\n        }\n        task Task_{{task8.id}} \"Task_{{task8.id}}\" {\n            effort 1.0h\n            allocate User_{{user8.id}}\n        }\n        task Task_{{task9.id}} \"Task_{{task9.id}}\" {\n            effort 1.0h\n            allocate User_{{user9.id}}\n        }\n    }\n    task Sequence_{{sequence6.id}} \"Sequence_{{sequence6.id}}\" {}\n    task Shot_{{shot1.id}} \"Shot_{{shot1.id}}\" {\n        task Task_{{task10.id}} \"Task_{{task10.id}}\" {\n            effort 10.0h\n            allocate User_{{user10.id}}\n        }\n        task Task_{{task11.id}} \"Task_{{task11.id}}\" {\n            effort 1.0h\n            allocate User_{{user1.id}}, User_{{user2.id}}\n        }\n        task Task_{{task12.id}} \"Task_{{task12.id}}\" {\n            effort 1.0h\n            allocate User_{{user3.id}}, User_{{user4.id}}\n        }\n    }\n    task Shot_{{shot2.id}} \"Shot_{{shot2.id}}\" {\n        task Task_{{task13.id}} \"Task_{{task13.id}}\" {\n            effort 1.0h\n            allocate User_{{user5.id}}, User_{{user6.id}}\n        }\n        task Task_{{task14.id}} \"Task_{{task14.id}}\" {\n            effort 1.0h\n            allocate User_{{user7.id}}, User_{{user8.id}}\n        }\n        task Task_{{task15.id}} \"Task_{{task15.id}}\" {\n            effort 1.0h\n            allocate User_{{user9.id}}, User_{{user10.id}}\n        }\n    }\n    task Sequence_{{sequence7.id}} \"Sequence_{{sequence7.id}}\" {}\n    task Shot_{{shot3.id}} \"Shot_{{shot3.id}}\" {\n        task Task_{{task16.id}} \"Task_{{task16.id}}\" {\n            effort 1.0h\n            allocate User_{{user1.id}}, User_{{user2.id}}, User_{{user3.id}}\n        }\n        task Task_{{task17.id}} \"Task_{{task17.id}}\" {\n            effort 1.0h\n            allocate User_{{user4.id}}, User_{{user5.id}}, User_{{user6.id}}\n        }\n        task Task_{{task18.id}} \"Task_{{task18.id}}\" {\n            effort 1.0h\n            allocate User_{{user7.id}}, User_{{user8.id}}, User_{{user9.id}}\n        }\n    }\n    task Shot_{{shot4.id}} \"Shot_{{shot4.id}}\" {\n        task Task_{{task19.id}} \"Task_{{task19.id}}\" {\n            effort 1.0h\n            allocate User_{{user1.id}}, User_{{user2.id}}, User_{{user10.id}}\n        }\n        task Task_{{task20.id}} \"Task_{{task20.id}}\" {\n            effort 1.0h\n            allocate User_{{user3.id}}, User_{{user4.id}}, User_{{user5.id}}\n        }\n        task Task_{{task21.id}} \"Task_{{task21.id}}\" {\n            effort 1.0h\n            allocate User_{{user6.id}}, User_{{user7.id}}, User_{{user8.id}}\n        }\n    }\n    task Asset_{{asset1.id}} \"Asset_{{asset1.id}}\" {\n        effort 1.0h\n        allocate User_{{user2.id}}\n    }\n    task Asset_{{asset2.id}} \"Asset_{{asset2.id}}\" {}\n    task Asset_{{asset3.id}} \"Asset_{{asset3.id}}\" {}\n    task Asset_{{asset4.id}} \"Asset_{{asset4.id}}\" {\n        task Task_{{task22.id}} \"Task_{{task22.id}}\" {\n            effort 1.0h\n            allocate User_{{user1.id}}, User_{{user9.id}}, User_{{user10.id}}\n        }\n        task Task_{{task23.id}} \"Task_{{task23.id}}\" {\n            effort 1.0h\n            allocate User_{{user2.id}}, User_{{user3.id}}\n        }\n        task Task_{{task24.id}} \"Task_{{task24.id}}\" {\n            effort 1.0h\n            allocate User_{{user4.id}}, User_{{user5.id}}\n        }\n    }\n    task Asset_{{asset5.id}} \"Asset_{{asset5.id}}\" {\n        task Task_{{task25.id}} \"Task_{{task25.id}}\" {\n            effort 1.0h\n            allocate User_{{user6.id}}, User_{{user7.id}}\n        }\n        task Task_{{task26.id}} \"Task_{{task26.id}}\" {\n            effort 1.0h\n            allocate User_{{user8.id}}, User_{{user9.id}}\n        }\n        task Task_{{task27.id}} \"Task_{{task27.id}}\" {\n            effort 1.0h\n            allocate User_{{user1.id}}, User_{{user10.id}}\n        }\n    }\n    task Task_{{task1.id}} \"Task_{{task1.id}}\" {\n        effort 1.0h\n        allocate User_{{user1.id}}\n    }\n    task Task_{{task2.id}} \"Task_{{task2.id}}\" {\n        effort 1.0h\n        allocate User_{{user2.id}}\n    }\n    task Task_{{task3.id}} \"Task_{{task3.id}}\" {\n        effort 1.0h\n        allocate User_{{user3.id}}\n    }\n}"
  },
  {
    "path": "tests/data/project_to_tjp_output_formatted",
    "content": "task Project_1 \"Project_1\" { task Sequence_2 \"Sequence_2\" { effort 1.0h allocate User_3 } task Sequence_4 \"Sequence_4\" { effort 1.0h allocate User_5 } task Sequence_6 \"Sequence_6\" { effort 1.0h allocate User_7 } task Sequence_8 \"Sequence_8\" { task Task_9 \"Task_9\" { effort 1.0h allocate User_10 } task Task_11 \"Task_11\" { effort 1.0h allocate User_12 } task Task_13 \"Task_13\" { effort 1.0h allocate User_14 } } task Sequence_15 \"Sequence_15\" { task Task_16 \"Task_16\" { effort 1.0h allocate User_17 } task Task_18 \"Task_18\" { effort 1.0h allocate User_19 } task Task_20 \"Task_20\" { effort 1.0h allocate User_21 } } task Sequence_22 \"Sequence_22\" {} task Shot_23 \"Shot_23\" { task Task_24 \"Task_24\" { effort 10.0h allocate User_25 } task Task_26 \"Task_26\" { effort 1.0h allocate User_3, User_5 } task Task_27 \"Task_27\" { effort 1.0h allocate User_7, User_10 } } task Shot_28 \"Shot_28\" { task Task_29 \"Task_29\" { effort 1.0h allocate User_12, User_14 } task Task_30 \"Task_30\" { effort 1.0h allocate User_17, User_19 } task Task_31 \"Task_31\" { effort 1.0h allocate User_21, User_25 } } task Sequence_32 \"Sequence_32\" {} task Shot_33 \"Shot_33\" { task Task_34 \"Task_34\" { effort 1.0h allocate User_3, User_5, User_7 } task Task_35 \"Task_35\" { effort 1.0h allocate User_10, User_12, User_14 } task Task_36 \"Task_36\" { effort 1.0h allocate User_17, User_19, User_21 } } task Shot_37 \"Shot_37\" { task Task_38 \"Task_38\" { effort 1.0h allocate User_3, User_5, User_25 } task Task_39 \"Task_39\" { effort 1.0h allocate User_7, User_10, User_12 } task Task_40 \"Task_40\" { effort 1.0h allocate User_14, User_17, User_19 } } task Asset_41 \"Asset_41\" { effort 1.0h allocate User_5 } task Asset_42 \"Asset_42\" {} task Asset_43 \"Asset_43\" {} task Asset_44 \"Asset_44\" { task Task_45 \"Task_45\" { effort 1.0h allocate User_3, User_21, User_25 } task Task_46 \"Task_46\" { effort 1.0h allocate User_5, User_7 } task Task_47 \"Task_47\" { effort 1.0h allocate User_10, User_12 } } task Asset_48 \"Asset_48\" { task Task_49 \"Task_49\" { effort 1.0h allocate User_14, User_17 } task Task_50 \"Task_50\" { effort 1.0h allocate User_19, User_21 } task Task_51 \"Task_51\" { effort 1.0h allocate User_3, User_25 } } task Task_52 \"Task_52\" { effort 1.0h allocate User_3 } task Task_53 \"Task_53\" { effort 1.0h allocate User_5 } task Task_54 \"Task_54\" { effort 1.0h allocate User_7 } }"
  },
  {
    "path": "tests/data/project_to_tjp_output_rendered",
    "content": "task Project_1 \"Project_1\" {\n    task Sequence_2 \"Sequence_2\" {\n        effort 1.0h\n        allocate User_3\n    }\n    task Sequence_4 \"Sequence_4\" {\n        effort 1.0h\n        allocate User_5\n    }\n    task Sequence_6 \"Sequence_6\" {\n        effort 1.0h\n        allocate User_7\n    }\n    task Sequence_8 \"Sequence_8\" {\n        task Task_9 \"Task_9\" {\n            effort 1.0h\n            allocate User_10\n        }\n        task Task_11 \"Task_11\" {\n            effort 1.0h\n            allocate User_12\n        }\n        task Task_13 \"Task_13\" {\n            effort 1.0h\n            allocate User_14\n        }\n    }\n    task Sequence_15 \"Sequence_15\" {\n        task Task_16 \"Task_16\" {\n            effort 1.0h\n            allocate User_17\n        }\n        task Task_18 \"Task_18\" {\n            effort 1.0h\n            allocate User_19\n        }\n        task Task_20 \"Task_20\" {\n            effort 1.0h\n            allocate User_21\n        }\n    }\n    task Sequence_22 \"Sequence_22\" {}\n    task Shot_23 \"Shot_23\" {\n        task Task_24 \"Task_24\" {\n            effort 10.0h\n            allocate User_25\n        }\n        task Task_26 \"Task_26\" {\n            effort 1.0h\n            allocate User_3, User_5\n        }\n        task Task_27 \"Task_27\" {\n            effort 1.0h\n            allocate User_7, User_10\n        }\n    }\n    task Shot_28 \"Shot_28\" {\n        task Task_29 \"Task_29\" {\n            effort 1.0h\n            allocate User_12, User_14\n        }\n        task Task_30 \"Task_30\" {\n            effort 1.0h\n            allocate User_17, User_19\n        }\n        task Task_31 \"Task_31\" {\n            effort 1.0h\n            allocate User_21, User_25\n        }\n    }\n    task Sequence_32 \"Sequence_32\" {}\n    task Shot_33 \"Shot_33\" {\n        task Task_34 \"Task_34\" {\n            effort 1.0h\n            allocate User_3, User_5, User_7\n        }\n        task Task_35 \"Task_35\" {\n            effort 1.0h\n            allocate User_10, User_12, User_14\n        }\n        task Task_36 \"Task_36\" {\n            effort 1.0h\n            allocate User_17, User_19, User_21\n        }\n    }\n    task Shot_37 \"Shot_37\" {\n        task Task_38 \"Task_38\" {\n            effort 1.0h\n            allocate User_3, User_5, User_25\n        }\n        task Task_39 \"Task_39\" {\n            effort 1.0h\n            allocate User_7, User_10, User_12\n        }\n        task Task_40 \"Task_40\" {\n            effort 1.0h\n            allocate User_14, User_17, User_19\n        }\n    }\n    task Asset_41 \"Asset_41\" {\n        effort 1.0h\n        allocate User_5\n    }\n    task Asset_42 \"Asset_42\" {}\n    task Asset_43 \"Asset_43\" {}\n    task Asset_44 \"Asset_44\" {\n        task Task_45 \"Task_45\" {\n            effort 1.0h\n            allocate User_3, User_21, User_25\n        }\n        task Task_46 \"Task_46\" {\n            effort 1.0h\n            allocate User_5, User_7\n        }\n        task Task_47 \"Task_47\" {\n            effort 1.0h\n            allocate User_10, User_12\n        }\n    }\n    task Asset_48 \"Asset_48\" {\n        task Task_49 \"Task_49\" {\n            effort 1.0h\n            allocate User_14, User_17\n        }\n        task Task_50 \"Task_50\" {\n            effort 1.0h\n            allocate User_19, User_21\n        }\n        task Task_51 \"Task_51\" {\n            effort 1.0h\n            allocate User_3, User_25\n        }\n    }\n    task Task_52 \"Task_52\" {\n        effort 1.0h\n        allocate User_3\n    }\n    task Task_53 \"Task_53\" {\n        effort 1.0h\n        allocate User_5\n    }\n    task Task_54 \"Task_54\" {\n        effort 1.0h\n        allocate User_7\n    }\n}"
  },
  {
    "path": "tests/db/__init__.py",
    "content": ""
  },
  {
    "path": "tests/db/test_db.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Database and connection to the database.\"\"\"\nimport datetime\nimport json\nimport logging\nimport os\n\nimport pytz\nimport pytest\nimport tzlocal\n\nimport stalker\nimport stalker.db.setup\nfrom stalker import defaults, log\nfrom stalker import (\n    Asset,\n    AuthenticationLog,\n    Budget,\n    BudgetEntry,\n    Client,\n    Daily,\n    DailyFile,\n    Department,\n    Entity,\n    EntityGroup,\n    FilenameTemplate,\n    Good,\n    Group,\n    ImageFormat,\n    Invoice,\n    File,\n    Note,\n    Page,\n    Payment,\n    Permission,\n    Project,\n    PriceList,\n    Repository,\n    Review,\n    Scene,\n    Sequence,\n    Shot,\n    SimpleEntity,\n    Status,\n    StatusList,\n    Studio,\n    Structure,\n    Tag,\n    Task,\n    Ticket,\n    TicketLog,\n    TimeLog,\n    Type,\n    User,\n    Vacation,\n    Variant,\n    Version,\n    WorkingHours,\n)\nfrom stalker.config import Config\nfrom stalker.db.setup import create_entity_statuses, alembic_version\nfrom stalker.db.session import DBSession\nfrom stalker.models.auth import LOGIN, LOGOUT\n\nfrom sqlalchemy import text\nfrom sqlalchemy.pool import NullPool\nfrom sqlalchemy.exc import (\n    ArgumentError,\n    IntegrityError,\n    OperationalError,\n    PendingRollbackError,\n    ProgrammingError,\n)\n\nfrom stalker.models.enum import ScheduleConstraint, TimeUnit\nfrom stalker.models.enum import ScheduleModel\nfrom tests.utils import create_random_db, get_admin_user, tear_down_db\n\nlogger = log.get_logger(__name__)\nlog.set_level(logging.DEBUG)\n\n\nCLASS_NAMES = [\n    \"Asset\",\n    \"AuthenticationLog\",\n    \"Budget\",\n    \"BudgetEntry\",\n    \"Client\",\n    \"Good\",\n    \"Group\",\n    \"Permission\",\n    \"User\",\n    \"Department\",\n    \"SimpleEntity\",\n    \"Entity\",\n    \"EntityGroup\",\n    \"ImageFormat\",\n    \"File\",\n    \"Message\",\n    \"Note\",\n    \"Page\",\n    \"Project\",\n    \"PriceList\",\n    \"Repository\",\n    \"Review\",\n    \"Role\",\n    \"Scene\",\n    \"Sequence\",\n    \"Shot\",\n    \"Status\",\n    \"StatusList\",\n    \"Structure\",\n    \"Studio\",\n    \"Tag\",\n    \"TimeLog\",\n    \"Task\",\n    \"FilenameTemplate\",\n    \"Ticket\",\n    \"TicketLog\",\n    \"Type\",\n    \"Vacation\",\n    \"Version\",\n    \"Daily\",\n    \"Invoice\",\n    \"Payment\",\n    \"Variant\",\n]\n\n\n@pytest.fixture(scope=\"function\")\ndef auto_crate_admin_on():\n    \"\"\"Toggle auto create admin value on.\"\"\"\n    # set default admin creation to True\n    default_value = defaults.auto_create_admin\n    defaults[\"auto_create_admin\"] = True\n    yield\n    defaults[\"auto_create_admin\"] = default_value\n\n\n@pytest.fixture(scope=\"function\")\ndef auto_crate_admin_off():\n    \"\"\"Toggle auto create admin value on.\"\"\"\n    # set default admin creation to True\n    default_value = defaults.auto_create_admin\n    defaults[\"auto_create_admin\"] = False\n    yield\n    defaults[\"auto_create_admin\"] = default_value\n\n\ndef test_default_admin_creation(setup_postgresql_db, auto_crate_admin_on):\n    \"\"\"Default admin is created.\"\"\"\n    # check if there is an admin\n    admin_db = User.query.filter(User.name == defaults.admin_name).first()\n    assert admin_db.name == defaults.admin_name\n\n\ndef test_default_admin_for_already_created_databases(\n    setup_postgresql_db, auto_crate_admin_on\n):\n    \"\"\"No extra admin is going to be created for already setup databases.\"\"\"\n    # set default admin creation to True\n    stalker.db.setup.init()\n\n    # try to call the init() for a second time and see if there are more\n    # than one admin\n    stalker.db.setup.init()\n\n    # and get how many admin is created, (it is impossible to create\n    # second one because the tables.simpleEntity.c.nam.unique=True\n    admins = User.query.filter_by(name=defaults.admin_name).all()\n    assert len(admins) == 1\n\n\ndef test_no_default_admin_creation(setup_postgresql_db, auto_crate_admin_off):\n    \"\"\"There is no user if stalker.config.Conf.auto_create_admin is False.\"\"\"\n    data = setup_postgresql_db\n    tear_down_db(data)\n\n    # Setup\n    # update the config\n    data[\"database_url\"] = create_random_db()\n    data[\"config\"][\"sqlalchemy.url\"] = data[\"database_url\"]\n    data[\"config\"][\"sqlalchemy.poolclass\"] = NullPool\n\n    try:\n        os.environ.pop(Config.env_key)\n    except KeyError:\n        # already removed\n        pass\n\n    # regenerate the defaults\n    stalker.defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n\n    # init the db\n    stalker.db.setup.setup(data[\"config\"])\n    stalker.db.setup.init()\n\n    # check if there is a use with name admin\n    assert User.query.filter_by(name=defaults.admin_name).first() is None\n\n    # check if there is an \"admins\" department\n    assert (\n        Department.query.filter_by(name=defaults.admin_department_name).first() is None\n    )\n\n\ndef test_non_unique_names_on_different_entity_type(setup_postgresql_db):\n    \"\"\"There can be non-unique names for different entity types.\"\"\"\n    # try to create a user and an entity with same name\n    # expect Nothing\n    kwargs = {\n        \"name\": \"user1\",\n        # \"created_by\": admin\n    }\n    entity1 = Entity(**kwargs)\n    DBSession.add(entity1)\n    DBSession.commit()\n\n    # let's create the second user\n    kwargs.update(\n        {\n            \"name\": \"User1 Name\",\n            \"login\": \"user1\",\n            \"email\": \"user1@gmail.com\",\n            \"password\": \"user1\",\n        }\n    )\n\n    user1 = User(**kwargs)\n    DBSession.add(user1)\n\n    # expect nothing, this should work without any error\n    DBSession.commit()\n\n\ndef test_ticket_status_list_initialization(setup_postgresql_db):\n    \"\"\"Ticket statuses are correctly created.\"\"\"\n    ticket_status_list = StatusList.query.filter(\n        StatusList.name == \"Ticket Statuses\"\n    ).first()\n\n    assert isinstance(ticket_status_list, StatusList)\n\n    expected_status_names = [\"New\", \"Reopened\", \"Closed\", \"Accepted\", \"Assigned\"]\n\n    assert len(ticket_status_list.statuses) == len(expected_status_names)\n    assert all(\n        status.name in expected_status_names for status in ticket_status_list.statuses\n    )\n\n\ndef test_daily_status_list_initialization(setup_postgresql_db):\n    \"\"\"Daily statuses are correctly created.\"\"\"\n    daily_status_list = StatusList.query.filter(\n        StatusList.name == \"Daily Statuses\"\n    ).first()\n\n    assert isinstance(daily_status_list, StatusList)\n\n    expected_status_names = [\"Open\", \"Closed\"]\n\n    assert len(daily_status_list.statuses) == len(expected_status_names)\n\n    admin = get_admin_user()\n    assert all(\n        status.name in expected_status_names for status in daily_status_list.statuses\n    )\n    # check if the created_by and updated_by attributes\n    # are set to admin\n    assert all(status.created_by == admin for status in daily_status_list.statuses)\n    assert all(status.updated_by == admin for status in daily_status_list.statuses)\n\n\ndef test_variant_status_list_initialization(setup_postgresql_db):\n    \"\"\"Variant statuses are correctly created.\"\"\"\n    variant_status_list = StatusList.query.filter(\n        StatusList.target_entity_type == \"Variant\"\n    ).first()\n    # we do not create a specific StatusList for Variant's anymore\n    assert variant_status_list is None\n\n\ndef test_register_creates_suitable_permissions(setup_postgresql_db):\n    \"\"\"stalker.db.register is able to create suitable Permissions.\"\"\"\n\n    # create a new dummy class\n    class TestClass(object):\n        pass\n\n    stalker.db.setup.register(TestClass)\n\n    # now check if the TestClass entry is created in Permission table\n    permissions_db = Permission.query.filter(Permission.class_name == \"TestClass\").all()\n\n    logger.debug(f\"{permissions_db}\")\n\n    actions = defaults.actions\n    assert all(action.action in actions for action in permissions_db)\n\n\ndef test_register_raise_type_error_for_wrong_class_name_argument(setup_postgresql_db):\n    \"\"\"TypeError is raised if the class_name argument is not an instance\n    of type or str.\"\"\"\n    with pytest.raises(TypeError):\n        stalker.db.setup.register(23425)\n\n\n@pytest.mark.parametrize(\"error_class\", [IntegrityError])\ndef test_register_handles_integrity_errors(\n    setup_postgresql_db, monkeypatch, error_class\n):\n    \"\"\"create_ticket_statuses() handles IntegrityErrors.\"\"\"\n\n    # create a new dummy class\n    class TestClass(object):\n        pass\n\n    class PatchedDBSession(object):\n        rollback_is_called = False\n\n        @classmethod\n        def patched_commit(cls, *args, **kwargs):\n            raise error_class(statement=\"\", params=[], orig=None)\n\n        @classmethod\n        def patched_rollback(cls):\n            cls.rollback_is_called = True\n\n    monkeypatch.setattr(\n        \"stalker.db.setup.DBSession.commit\",\n        PatchedDBSession.patched_commit,\n    )\n    monkeypatch.setattr(\n        \"stalker.db.setup.DBSession.rollback\",\n        PatchedDBSession.patched_rollback,\n    )\n\n    assert PatchedDBSession.rollback_is_called is False\n    # this should raise the errors now\n    stalker.db.setup.register(TestClass)\n    assert PatchedDBSession.rollback_is_called is True\n\n\ndef test_permissions_created_for_all_the_classes(setup_postgresql_db):\n    \"\"\"Permission instances are created for classes in the SOM.\"\"\"\n    permission_db = Permission.query.all()\n    assert len(permission_db) == len(CLASS_NAMES) * len(defaults.actions) * 2\n    assert all(permission.access in [\"Allow\", \"Deny\"] for permission in permission_db)\n    assert all(permission.action in defaults.actions for permission in permission_db)\n    assert all(permission.class_name in CLASS_NAMES for permission in permission_db)\n\n\ndef test_permissions_not_created_over_and_over_again(setup_postgresql_db):\n    \"\"\"Permissions are created only once and trying to call __init_db__ will\n    not raise any error.\"\"\"\n    data = setup_postgresql_db\n    DBSession.remove()\n    # DBSession.close()\n    stalker.db.setup.setup(data[\"config\"])\n    stalker.db.setup.init()\n\n    # this should not give any error\n    DBSession.remove()\n\n    stalker.db.setup.setup(data[\"config\"])\n    stalker.db.setup.init()\n\n    # and we still have correct amount of Permissions\n    permissions = Permission.query.all()\n    assert len(permissions) == 430\n\n\ndef test_ticket_statuses_are_not_created_over_and_over_again(setup_postgresql_db):\n    \"\"\"Ticket Statuses are created only once and calling init() don't raise an error.\"\"\"\n    data = setup_postgresql_db\n    # create the environment variable and point it to a temp directory\n    DBSession.remove()\n\n    stalker.db.setup.setup(data[\"config\"])\n    stalker.db.setup.init()\n\n    # this should not give any error\n    stalker.db.setup.setup(data[\"config\"])\n    stalker.db.setup.init()\n\n    # this should not give any error\n    stalker.db.setup.setup(data[\"config\"])\n    stalker.db.setup.init()\n\n    # and we still have correct amount of Statuses\n    statuses = Status.query.all()\n    assert len(statuses) == 17\n\n    status_list = StatusList.query.filter_by(target_entity_type=\"Ticket\").first()\n    assert status_list is not None\n    assert status_list.name == \"Ticket Statuses\"\n\n\ndef test_project_status_list_initialization(setup_postgresql_db):\n    \"\"\"Project statuses are correctly created.\"\"\"\n    project_status_list = (\n        StatusList.query.filter(StatusList.name == \"Project Statuses\")\n        .filter(StatusList.target_entity_type == \"Project\")\n        .first()\n    )\n    assert isinstance(project_status_list, StatusList)\n    expected_status_names = [\"Ready To Start\", \"Work In Progress\", \"Completed\"]\n    expected_status_codes = [\"RTS\", \"WIP\", \"CMPL\"]\n    assert len(project_status_list.statuses) == len(expected_status_names)\n    db_status_names = map(lambda x: x.name, project_status_list.statuses)\n    db_status_codes = map(lambda x: x.code, project_status_list.statuses)\n    assert sorted(expected_status_names) == sorted(db_status_names)\n    assert sorted(expected_status_codes) == sorted(db_status_codes)\n\n    # check if the created_by and updated_by attributes are correctly set\n    # to the admin\n    admin = get_admin_user()\n    assert all(status.created_by == admin for status in project_status_list.statuses)\n    assert all(status.updated_by == admin for status in project_status_list.statuses)\n\n\ndef test_task_status_list_initialization(setup_postgresql_db):\n    \"\"\"Task statuses are correctly created.\"\"\"\n    task_status_list = (\n        StatusList.query.filter(StatusList.name == \"Task Statuses\")\n        .filter(StatusList.target_entity_type == \"Task\")\n        .first()\n    )\n    assert isinstance(task_status_list, StatusList)\n    expected_status_names = [\n        \"Waiting For Dependency\",\n        \"Ready To Start\",\n        \"Work In Progress\",\n        \"Pending Review\",\n        \"Has Revision\",\n        \"Dependency Has Revision\",\n        \"On Hold\",\n        \"Stopped\",\n        \"Completed\",\n    ]\n    expected_status_codes = [\n        \"WFD\",\n        \"RTS\",\n        \"WIP\",\n        \"PREV\",\n        \"HREV\",\n        \"DREV\",\n        \"OH\",\n        \"STOP\",\n        \"CMPL\",\n    ]\n    assert len(task_status_list.statuses) == len(expected_status_names)\n    db_status_names = map(lambda x: x.name, task_status_list.statuses)\n    db_status_codes = map(lambda x: x.code, task_status_list.statuses)\n    assert sorted(expected_status_names) == sorted(db_status_names)\n    assert sorted(expected_status_codes) == sorted(db_status_codes)\n    # check if the created_by and updated_by attributes are correctly set\n    # to the admin\n    admin = get_admin_user()\n    assert all(status.created_by == admin for status in task_status_list.statuses)\n    assert all(status.updated_by == admin for status in task_status_list.statuses)\n\n\ndef test_asset_status_list_initialization(setup_postgresql_db):\n    \"\"\"Asset statuses are correctly created.\"\"\"\n    asset_status_list = StatusList.query.filter(\n        StatusList.target_entity_type == \"Asset\"\n    ).first()\n    # we do not generate a specific StatusList for Assets anymore\n    # as Task specific StatusLists can be used.\n    assert asset_status_list is None\n\n\ndef test_shot_status_list_initialization(setup_postgresql_db):\n    \"\"\"Shot statuses are correctly created.\"\"\"\n    shot_status_list = StatusList.query.filter(\n        StatusList.target_entity_type == \"Shot\"\n    ).first()\n    # we do not generate a specific StatusList for Shots anymore\n    # as Task specific StatusLists can be used.\n    assert shot_status_list is None\n\n\ndef test_sequence_status_list_initialization(setup_postgresql_db):\n    \"\"\"Sequence statuses are correctly created.\"\"\"\n    sequence_status_list = StatusList.query.filter(\n        StatusList.target_entity_type == \"Sequence\"\n    ).first()\n    # we do not generate a specific StatusList for Sequences anymore\n    # as Task specific StatusLists can be used.\n    assert sequence_status_list is None\n\n\ndef test_scene_status_list_initialization(setup_postgresql_db):\n    \"\"\"Scene statuses are correctly created.\"\"\"\n    scene_status_list = StatusList.query.filter(\n        StatusList.target_entity_type == \"Scene\"\n    ).first()\n    # we do not generate a specific StatusList for Scenes anymore\n    # as Task specific StatusLists can be used.\n    assert scene_status_list is None\n\n\ndef test_variant_status_list_initialization(setup_postgresql_db):\n    \"\"\"Variant statuses are correctly created.\"\"\"\n    variant_status_list = StatusList.query.filter(\n        StatusList.target_entity_type == \"Variant\"\n    ).first()\n    # we do not generate a specific StatusList for Variant anymore\n    # as Task specific StatusLists can be used.\n    assert variant_status_list is None\n\n\ndef test_review_status_list_initialization(setup_postgresql_db):\n    \"\"\"Review statuses are correctly created.\"\"\"\n    review_status_list = StatusList.query.filter(\n        StatusList.name == \"Review Statuses\"\n    ).first()\n    assert isinstance(review_status_list, StatusList)\n    expected_status_names = [\n        \"New\",\n        \"Requested Revision\",\n        \"Approved\",\n    ]\n    expected_status_codes = [\"NEW\", \"RREV\", \"APP\"]\n    assert len(review_status_list.statuses) == len(expected_status_names)\n    db_status_names = map(lambda x: x.name, review_status_list.statuses)\n    db_status_codes = map(lambda x: x.code, review_status_list.statuses)\n    assert sorted(expected_status_names) == sorted(db_status_names)\n    assert sorted(expected_status_codes) == sorted(db_status_codes)\n    # check if the created_by and updated_by attributes are correctly set\n    # to the admin\n    admin = get_admin_user()\n    for status in review_status_list.statuses:\n        assert status.created_by == admin\n        assert status.updated_by == admin\n\n\ndef test___create_entity_statuses_no_entity_type_supplied(setup_postgresql_db):\n    \"\"\"db.__create_entity_statuses() raise a ValueError if no entity_type is given.\"\"\"\n    kwargs = {\"status_names\": [\"A\", \"B\"], \"status_codes\": [\"A\", \"B\"]}\n    with pytest.raises(ValueError) as cm:\n        create_entity_statuses(**kwargs)\n\n    assert str(cm.value) == \"Please supply entity_type\"\n\n\ndef test___create_entity_statuses_no_status_names_supplied(setup_postgresql_db):\n    \"\"\"db.__create_entity_statuses() raise a ValueError if no status_names is given.\"\"\"\n    kwargs = {\"entity_type\": \"Hede Hodo\", \"status_codes\": [\"A\", \"B\"]}\n    with pytest.raises(ValueError) as cm:\n        create_entity_statuses(**kwargs)\n\n    assert str(cm.value) == \"Please supply status names\"\n\n\ndef test___create_entity_statuses_no_status_codes_supplied(setup_postgresql_db):\n    \"\"\"db.__create_entity_statuses() raise a ValueError if no status_codes is given.\"\"\"\n    kwargs = {\"entity_type\": \"Hede Hodo\", \"status_names\": [\"A\", \"B\"]}\n    with pytest.raises(ValueError) as cm:\n        create_entity_statuses(**kwargs)\n\n    assert str(cm.value) == \"Please supply status codes\"\n\n\ndef test_initialization_of_alembic_version_table(setup_postgresql_db):\n    \"\"\"db.init() will also create a table called alembic_version.\"\"\"\n    sql_query = 'select version_num from \"alembic_version\"'\n    version_num = DBSession.connection().execute(text(sql_query)).fetchone()[0]\n    assert alembic_version == version_num\n\n\ndef test_initialization_of_alembic_version_table_multiple_times(setup_postgresql_db):\n    \"\"\"db.create_alembic_table() will handle initializing the table multiple times.\"\"\"\n    sql_query = 'select version_num from \"alembic_version\"'\n    version_num = DBSession.connection().execute(text(sql_query)).fetchone()[0]\n    assert alembic_version == version_num\n\n    DBSession.remove()\n    stalker.db.setup.init()\n    stalker.db.setup.init()\n    stalker.db.setup.init()\n\n    version_nums = DBSession.connection().execute(text(sql_query)).fetchall()\n\n    # no additional version is created\n    assert len(version_nums) == 1\n\n\ndef test_alembic_version_mismatch(setup_postgresql_db):\n    \"\"\"db.init() raise ValueError if DB alembic version don't match Stalker alembic_version.\"\"\"\n    data = setup_postgresql_db\n    stalker.db.setup.init()\n\n    # now change the alembic_version\n    sql = \"update alembic_version set version_num='some_random_number'\"\n    DBSession.connection().execute(text(sql))\n    DBSession.commit()\n\n    # check if it is updated correctly\n    sql = \"select version_num from alembic_version\"\n    result = DBSession.connection().execute(text(sql)).fetchone()\n    assert result[0] == \"some_random_number\"\n\n    # close the connection\n    DBSession.connection().close()\n    DBSession.remove()\n\n    # re-setup\n    with pytest.raises(ValueError) as cm:\n        stalker.db.setup.setup(data[\"config\"])\n\n    assert str(cm.value) == (\n        f\"Please update the database to version: {stalker.db.setup.alembic_version}\"\n    )\n\n    # also it is not possible to continue with the current DBSession\n    with pytest.raises(PendingRollbackError) as cm:\n        DBSession.query(User.id).all()\n\n    assert \"Can't reconnect until invalid transaction is rolled back.\" in str(cm.value)\n\n    # rollback and reconnect to the database\n    DBSession.rollback()\n\n    # expect it happen again\n    with pytest.raises(ValueError) as cm:\n        stalker.db.setup.setup(data[\"config\"])\n\n    assert str(cm.value) == (\n        f\"Please update the database to version: {stalker.db.setup.alembic_version}\"\n    )\n\n    # rollback and insert the correct alembic version number\n    DBSession.rollback()\n\n    sql = f\"update alembic_version set version_num='{alembic_version}'\"\n    DBSession.connection().execute(text(sql))\n    DBSession.commit()\n\n    # and now expect everything to work correctly\n    stalker.db.setup.setup(data[\"config\"])\n    all_users = DBSession.query(User).all()\n    assert all_users is not None\n\n\ndef test_initialization_of_repo_environment_variables(setup_postgresql_db):\n    \"\"\"db.create_repo_env_vars() creates envvars for each repository in the system.\"\"\"\n    data = setup_postgresql_db\n    # create a couple of repositories\n    repo1 = Repository(name=\"Repo1\", code=\"R1\")\n    repo2 = Repository(name=\"Repo2\", code=\"R2\")\n    repo3 = Repository(name=\"Repo3\", code=\"R3\")\n\n    all_repos = [repo1, repo2, repo3]\n    DBSession.add_all(all_repos)\n    DBSession.commit()\n\n    # remove any auto created repo vars\n    for repo in all_repos:\n        try:\n            os.environ.pop(f\"REPO{repo.code}\")\n        except KeyError:\n            pass\n\n    # check if all removed\n    for repo in all_repos:\n        # check if environment vars are created\n        assert (f\"REPO{repo.code}\") not in os.environ\n\n    # remove db connection\n    DBSession.remove()\n\n    # reconnect\n    stalker.db.setup.setup(data[\"config\"])\n\n    all_repos = Repository.query.all()\n\n    for repo in all_repos:\n        # check if environment vars are created\n        assert (f\"REPO{repo.code}\") in os.environ\n\n\ndef test_db_init_with_studio_instance(setup_postgresql_db):\n    \"\"\"db.init() using existing Studio instance for config values.\"\"\"\n    data = setup_postgresql_db\n    # check the defaults\n    assert defaults.daily_working_hours != 8\n    assert defaults.weekly_working_days != 4\n    assert defaults.weekly_working_hours != 32\n    assert defaults.yearly_working_days != 180\n    assert defaults.timing_resolution != datetime.timedelta(minutes=5)\n\n    # check no studio\n    studios = Studio.query.all()\n    assert studios == []\n\n    wh = WorkingHours(\n        working_hours={\n            \"mon\": [[10 * 60, 18 * 60]],\n            \"tue\": [[10 * 60, 18 * 60]],\n            \"wed\": [[10 * 60, 18 * 60]],\n            \"thu\": [[10 * 60, 18 * 60]],\n            \"fri\": [],\n            \"sat\": [],\n            \"sun\": [],\n        }\n    )\n    wh.daily_working_hours = 8\n\n    test_studio = Studio(\n        name=\"Test Studio\",\n    )\n    test_studio.working_hours = wh\n    test_studio.timing_resolution = datetime.timedelta(minutes=5)\n\n    DBSession.add(test_studio)\n    DBSession.commit()\n\n    # remove everything\n    DBSession.connection().close()\n    DBSession.remove()\n\n    # re-init db\n    logger.debug('data[\"config\"]: {}'.format(data[\"config\"]))\n    logger.debug(\"defaults: {}\".format(defaults))\n    logger.debug(\"id(defaults) 1: {}\".format(id(defaults)))\n    stalker.db.setup.setup(data[\"config\"])\n    logger.debug(\"id(defaults) 2: {}\".format(id(defaults)))\n\n    # and expect the defaults to be updated with studio defaults\n    assert defaults.daily_working_hours == 8\n    assert defaults.weekly_working_days == 4\n    assert defaults.weekly_working_hours == 32\n    assert defaults.yearly_working_days == 209\n    assert defaults.timing_resolution == datetime.timedelta(minutes=5)\n\n\ndef test_get_alembic_version_is_working_as_expected_when_there_is_no_alembic_version_table(\n    setup_postgresql_db,\n):\n    \"\"\"get_alembic_version() working as expected if there is no alembic_version table.\"\"\"\n    # drop the table\n    DBSession.connection().execute(text(\"DROP TABLE IF EXISTS alembic_version\"))\n    # now get the alembic_version\n    # this should not raise an OperationalError\n    alembic_version_ = stalker.db.setup.get_alembic_version()\n    assert alembic_version_ is None\n\n\n@pytest.mark.parametrize(\"error_class\", [OperationalError, ProgrammingError, TypeError])\ndef test_get_alembic_version_handles_errors(monkeypatch, error_class):\n    \"\"\"stalker.db.setup.get_alembic_version() handles errors db related.\"\"\"\n\n    class PatchedDialect(object):\n\n        def has_table(*args, **kwargs):\n            return True\n\n    class PatchedEngine(object):\n\n        dialect = PatchedDialect()\n\n    class PatchedConnection(object):\n        engine = PatchedEngine()\n\n        def execute(*args, **kwargs):\n            if error_class in (OperationalError, ProgrammingError):\n                raise error_class(statement=\"\", params=[], orig=None)\n            else:\n                raise error_class\n\n    class PatchedDBSession(object):\n\n        rollback_called = False\n\n        @classmethod\n        def connection(cls):\n            return PatchedConnection()\n\n        @classmethod\n        def rollback(cls):\n            cls.rollback_called = True\n\n    monkeypatch.setattr(\"stalker.db.setup.DBSession\", PatchedDBSession)\n    with pytest.raises(error_class) as cm:\n        PatchedDBSession.connection().execute()\n\n    assert PatchedDBSession.rollback_called is False\n    return_value = stalker.db.setup.get_alembic_version()\n    assert PatchedDBSession.rollback_called is True\n    assert return_value is None\n\n\ndef test_create_ticket_statuses_called_multiple_times(setup_postgresql_db):\n    \"\"\"no IntegrityError is raised if create_ticket_statuses() called multiple times.\"\"\"\n    stalker.db.setup.create_ticket_statuses()\n    stalker.db.setup.create_ticket_statuses()\n    stalker.db.setup.create_ticket_statuses()\n\n\ndef test_create_ticket_statuses_handles_integrity_errors(\n    setup_postgresql_db, monkeypatch\n):\n    \"\"\"create_ticket_statuses() handles IntegrityErrors.\"\"\"\n\n    class PatchedDBSession(object):\n        rollback_is_called = False\n\n        @classmethod\n        def patched_commit(cls, *args, **kwargs):\n            raise IntegrityError(statement=\"\", params=[], orig=None)\n\n        @classmethod\n        def patched_rollback(cls):\n            cls.rollback_is_called = True\n\n    monkeypatch.setattr(\n        \"stalker.db.setup.DBSession.commit\",\n        PatchedDBSession.patched_commit,\n    )\n    monkeypatch.setattr(\n        \"stalker.db.setup.DBSession.rollback\",\n        PatchedDBSession.patched_rollback,\n    )\n\n    assert PatchedDBSession.rollback_is_called is False\n    # this should raise IntegrityError now\n    stalker.db.setup.create_ticket_statuses()\n    assert PatchedDBSession.rollback_is_called is True\n\n\ndef test_create_entity_statuses_called_multiple_times(setup_postgresql_db):\n    \"\"\"no IntegrityError is raised if create_entity_statuses() called multiple times.\"\"\"\n    # create statuses for Tickets\n    ticket_names = defaults.ticket_status_names\n    ticket_codes = defaults.ticket_status_codes\n    admin = get_admin_user()\n    stalker.db.setup.create_entity_statuses(\"Ticket\", ticket_names, ticket_codes, admin)\n    stalker.db.setup.create_entity_statuses(\"Ticket\", ticket_names, ticket_codes, admin)\n\n\n@pytest.mark.parametrize(\"error_class\", [IntegrityError, OperationalError])\ndef test_create_entity_statuses_handles_errors(\n    setup_postgresql_db, monkeypatch, error_class\n):\n    \"\"\"create_ticket_statuses() handles IntegrityErrors.\"\"\"\n    ticket_names = defaults.ticket_status_names\n    ticket_codes = defaults.ticket_status_codes\n    admin = get_admin_user()\n\n    class PatchedDBSession(object):\n        rollback_is_called = False\n\n        @classmethod\n        def patched_commit(cls, *args, **kwargs):\n            raise error_class(statement=\"\", params=[], orig=None)\n\n        @classmethod\n        def patched_rollback(cls):\n            cls.rollback_is_called = True\n\n    monkeypatch.setattr(\n        \"stalker.db.setup.DBSession.commit\",\n        PatchedDBSession.patched_commit,\n    )\n    monkeypatch.setattr(\n        \"stalker.db.setup.DBSession.rollback\",\n        PatchedDBSession.patched_rollback,\n    )\n\n    assert PatchedDBSession.rollback_is_called is False\n    # this should raise the errors now\n    stalker.db.setup.create_entity_statuses(\"Ticket\", ticket_names, ticket_codes, admin)\n    assert PatchedDBSession.rollback_is_called is True\n\n\ndef test_register_called_multiple_times(setup_postgresql_db):\n    \"\"\"calling db.register() multiple times will not raise any errors.\"\"\"\n    stalker.db.setup.register(User)\n    stalker.db.setup.register(User)\n    stalker.db.setup.register(User)\n    stalker.db.setup.register(User)\n\n\ndef test_setup_without_settings(setup_postgresql_db):\n    \"\"\"db.setup() will use the default settings if no setting is supplied.\"\"\"\n    stalker.db.setup.setup()\n    # db.init()\n    conn = DBSession.connection()\n    engine = conn.engine\n    assert str(engine.url) == \"sqlite://\"\n\n\ndef test_setup_with_settings(setup_postgresql_db):\n    \"\"\"db.setup() will use the given settings if no setting is supplied.\"\"\"\n    # the default setup is already using the\n    with pytest.raises(ArgumentError) as cm:\n        stalker.db.setup.setup({\"sqlalchemy.url\": \"random url\"})\n\n    assert \"Could not parse SQLAlchemy URL from\" in str(cm.value)\n\n\n# tests the database model with a PostgreSQL database\n#\n# NOTE TO DEVELOPERS:\n#\n# Most of the tests in this TestCase uses parts of the system which are\n# tested but probably not tested while running the individual tests.\n#\n# Incomplete isolation is against to the logic behind unit testing, every\n# test should only cover a unit of the code, and a complete isolation should\n# be created. But this cannot be done in persistence tests (AFAIK), it needs\n# to be done in this way for now. Mocks cannot be used because every created\n# object goes to the database, so they need to be real objects.\n\n\ndef test_persistence_of_asset(setup_postgresql_db):\n    \"\"\"Persistence of Asset.\"\"\"\n    test_user = User(\n        name=\"Test User\", login=\"tu\", email=\"test@user.com\", password=\"secret\"\n    )\n    asset_type = Type(\n        name=\"A new asset type A\", code=\"anata\", target_entity_type=\"Asset\"\n    )\n    test_repository_type = Type(\n        name=\"Test Repository Type A\",\n        code=\"trta\",\n        target_entity_type=\"Repository\",\n    )\n    test_repository = Repository(\n        name=\"Test Repository A\", code=\"TRA\", type=test_repository_type\n    )\n    commercial_type = Type(\n        name=\"Commercial A\", code=\"comm\", target_entity_type=\"Project\"\n    )\n    test_project = Project(\n        name=\"Test Project For Asset Creation\",\n        code=\"TPFAC\",\n        type=commercial_type,\n        repository=test_repository,\n    )\n\n    DBSession.add(test_project)\n    DBSession.commit()\n\n    kwargs = {\n        \"name\": \"Test Asset\",\n        \"code\": \"test_asset\",\n        \"description\": \"This is a test Asset object\",\n        \"type\": asset_type,\n        \"project\": test_project,\n        \"created_by\": test_user,\n        \"responsible\": [test_user],\n    }\n\n    test_asset = Asset(**kwargs)\n    # logger.debug(f'test_asset.project : {test_asset.project}')\n\n    DBSession.add(test_asset)\n    DBSession.commit()\n\n    # logger.debug(f'test_asset.project (after commit): {test_asset.project}')\n\n    test_task1 = Task(\n        name=\"test task 1\",\n        status=0,\n        parent=test_asset,\n    )\n\n    test_task2 = Task(\n        name=\"test task 2\",\n        status=0,\n        parent=test_asset,\n    )\n\n    test_task3 = Task(\n        name=\"test task 3\",\n        status=0,\n        parent=test_asset,\n    )\n\n    DBSession.add_all([test_task1, test_task2, test_task3])\n    DBSession.commit()\n\n    code = test_asset.code\n    created_by = test_asset.created_by\n    date_created = test_asset.date_created\n    date_updated = test_asset.date_updated\n    duration = test_asset.duration\n    description = test_asset.description\n    end = test_asset.end\n    name = test_asset.name\n    nice_name = test_asset.nice_name\n    notes = test_asset.notes\n    project = test_asset.project\n    references = test_asset.references\n    status = test_asset.status\n    status_list = test_asset.status_list\n    start = test_asset.start\n    tags = test_asset.tags\n    children = test_asset.children\n    type_ = test_asset.type\n    updated_by = test_asset.updated_by\n\n    del test_asset\n\n    test_asset_db = Asset.query.filter_by(name=kwargs[\"name\"]).one()\n    assert isinstance(test_asset_db, Asset)\n\n    # assert test_asset, test_asset_DB)\n    assert code == test_asset_db.code\n    assert test_asset_db.created_by is not None\n    assert created_by == test_asset_db.created_by\n    assert date_created == test_asset_db.date_created\n    assert date_updated == test_asset_db.date_updated\n    assert description == test_asset_db.description\n    assert duration == test_asset_db.duration\n    assert end == test_asset_db.end\n    assert name == test_asset_db.name\n    assert nice_name == test_asset_db.nice_name\n    assert notes == test_asset_db.notes\n    assert project == test_asset_db.project\n    assert references == test_asset_db.references\n    assert start == test_asset_db.start\n    assert status == test_asset_db.status\n    assert status_list == test_asset_db.status_list\n    assert tags == test_asset_db.tags\n    assert children == test_asset_db.children\n    assert type_ == test_asset_db.type\n    assert updated_by == test_asset_db.updated_by\n\n    # now test the deletion of the asset class\n    DBSession.delete(test_asset_db)\n    DBSession.commit()\n\n    # we should still have the user\n    assert User.query.filter(User.id == created_by.id).first() is not None\n\n    # we should still have the project\n    assert Project.query.filter(Project.id == project.id).first() is not None\n\n\ndef test_persistence_of_variant(setup_postgresql_db):\n    \"\"\"Persistence of Variant.\"\"\"\n    test_user = User(\n        name=\"Test User\", login=\"tu\", email=\"test@user.com\", password=\"secret\"\n    )\n    test_repository_type = Type(\n        name=\"Test Repository Type A\",\n        code=\"trta\",\n        target_entity_type=\"Repository\",\n    )\n    test_repository = Repository(\n        name=\"Test Repository A\", code=\"TRA\", type=test_repository_type\n    )\n    commercial_type = Type(\n        name=\"Commercial A\", code=\"comm\", target_entity_type=\"Project\"\n    )\n    test_project = Project(\n        name=\"Test Project For Asset Creation\",\n        code=\"TPFAC\",\n        type=commercial_type,\n        repository=test_repository,\n    )\n\n    DBSession.add(test_project)\n    DBSession.commit()\n\n    kwargs = {\n        \"name\": \"Base\",\n        \"description\": \"This is a test Variant\",\n        \"project\": test_project,\n        \"created_by\": test_user,\n    }\n\n    test_variant = Variant(**kwargs)\n\n    DBSession.add(test_variant)\n    DBSession.commit()\n\n    test_task1 = Task(\n        name=\"test task 1\",\n        status=0,\n        project=test_project,\n    )\n\n    test_task2 = Task(\n        name=\"test task 2\",\n        status=0,\n        parent=test_task1,\n    )\n\n    test_task3 = Task(\n        name=\"test task 3\",\n        status=0,\n        parent=test_task2,\n    )\n    test_variant.parent = test_task3\n\n    DBSession.add_all([test_task1, test_task2, test_task3])\n    DBSession.commit()\n\n    created_by = test_variant.created_by\n    date_created = test_variant.date_created\n    date_updated = test_variant.date_updated\n    duration = test_variant.duration\n    description = test_variant.description\n    end = test_variant.end\n    name = test_variant.name\n    nice_name = test_variant.nice_name\n    notes = test_variant.notes\n    project = test_variant.project\n    references = test_variant.references\n    status = test_variant.status\n    status_list = test_variant.status_list\n    start = test_variant.start\n    tags = test_variant.tags\n    children = test_variant.children\n    parent = test_variant.parent\n    type_ = test_variant.type\n    updated_by = test_variant.updated_by\n\n    del test_variant\n\n    test_asset_db = Variant.query.filter_by(name=kwargs[\"name\"]).one()\n    assert isinstance(test_asset_db, Variant)\n\n    # assert test_asset, test_asset_DB)\n    assert test_asset_db.created_by is not None\n    assert created_by == test_asset_db.created_by\n    assert date_created == test_asset_db.date_created\n    assert date_updated == test_asset_db.date_updated\n    assert description == test_asset_db.description\n    assert duration == test_asset_db.duration\n    assert end == test_asset_db.end\n    assert name == test_asset_db.name\n    assert nice_name == test_asset_db.nice_name\n    assert notes == test_asset_db.notes\n    assert project == test_asset_db.project\n    assert references == test_asset_db.references\n    assert start == test_asset_db.start\n    assert status == test_asset_db.status\n    assert status_list == test_asset_db.status_list\n    assert tags == test_asset_db.tags\n    assert children == test_asset_db.children\n    assert parent == test_asset_db.parent\n    assert type_ == test_asset_db.type\n    assert updated_by == test_asset_db.updated_by\n\n    # now test the deletion of the asset class\n    DBSession.delete(test_asset_db)\n    DBSession.commit()\n\n    # we should still have the user\n    assert User.query.filter(User.id == created_by.id).first() is not None\n\n    # we should still have the project\n    assert Project.query.filter(Project.id == project.id).first() is not None\n\n\ndef test_persistence_of_budget_and_budget_entry(setup_postgresql_db):\n    \"\"\"Persistence of Budget and BudgetEntry classes.\"\"\"\n    test_user = User(\n        name=\"Test User\", login=\"tu\", email=\"test@user.com\", password=\"secret\"\n    )\n\n    status1 = Status.query.filter_by(code=\"NEW\").first()\n    status2 = Status.query.filter_by(code=\"WIP\").first()\n    status3 = Status.query.filter_by(code=\"CMPL\").first()\n\n    test_repository_type = Type(\n        name=\"Test Repository Type A\",\n        code=\"trta\",\n        target_entity_type=\"Repository\",\n    )\n\n    test_repository = Repository(\n        name=\"Test Repository A\", code=\"TRA\", type=test_repository_type\n    )\n\n    budget_status_list = StatusList(\n        name=\"Budget Statuses\",\n        statuses=[status1, status2, status3],\n        target_entity_type=\"Budget\",\n    )\n\n    commercial_type = Type(\n        name=\"Commercial A\", code=\"comm\", target_entity_type=\"Project\"\n    )\n\n    test_project = Project(\n        name=\"Test Project For Budget Creation\",\n        code=\"TPFBC\",\n        type=commercial_type,\n        repository=test_repository,\n    )\n\n    DBSession.add(test_project)\n    DBSession.commit()\n\n    kwargs = {\n        \"name\": \"Test Budget\",\n        \"project\": test_project,\n        \"created_by\": test_user,\n        \"status_list\": budget_status_list,\n        \"status\": status1,\n    }\n\n    test_budget = Budget(**kwargs)\n\n    DBSession.add(test_budget)\n    DBSession.commit()\n\n    good = Good(name=\"Some Good\", cost=9, msrp=10, unit=\"$/hour\")\n    DBSession.add(good)\n    DBSession.commit()\n\n    # create some entries\n    entry1 = BudgetEntry(budget=test_budget, good=good, amount=5.0)\n    entry2 = BudgetEntry(budget=test_budget, good=good, amount=1.0)\n\n    DBSession.add_all([entry1, entry2])\n    DBSession.commit()\n\n    created_by = test_budget.created_by\n    date_created = test_budget.date_created\n    date_updated = test_budget.date_updated\n    name = test_budget.name\n    nice_name = test_budget.nice_name\n    project = test_budget.project\n    tags = test_budget.tags\n    updated_by = test_budget.updated_by\n    notes = test_budget.notes\n    entries = test_budget.entries\n    status = test_budget.status\n\n    del test_budget\n\n    test_budget_db = Budget.query.filter_by(name=kwargs[\"name\"]).one()\n\n    assert isinstance(test_budget_db, Budget)\n\n    assert test_budget_db.created_by is not None\n    assert created_by == test_budget_db.created_by\n    assert date_created == test_budget_db.date_created\n    assert date_updated == test_budget_db.date_updated\n    assert name == test_budget_db.name\n    assert nice_name == test_budget_db.nice_name\n    assert notes == test_budget_db.notes\n    assert project == test_budget_db.project\n    assert tags == test_budget_db.tags\n    assert updated_by == test_budget_db.updated_by\n    assert entries == test_budget_db.entries\n    assert status == status1\n\n    # and we should have our entries intact\n    assert BudgetEntry.query.all() != []\n\n    # now test the deletion of the asset class\n    DBSession.delete(test_budget_db)\n    DBSession.commit()\n\n    # we should still have the user\n    assert User.query.filter(User.id == created_by.id).first() is not None\n\n    # we should still have the project\n    assert Project.query.filter(Project.id == project.id).first() is not None\n\n    # and we should have our page deleted\n    assert Budget.query.filter(Budget.name == kwargs[\"name\"]).first() is None\n\n    # and we should have our entries deleted\n    assert BudgetEntry.query.all() == []\n\n    # we still should have the good\n    good_db = Good.query.filter(Good.name == \"Some Good\").first()\n    assert good_db is not None\n\n\ndef test_persistence_of_invoice(setup_postgresql_db):\n    \"\"\"Persistence of Invoice instances.\"\"\"\n    test_user = User(\n        name=\"Test User\", login=\"tu\", email=\"test@user.com\", password=\"secret\"\n    )\n\n    status1 = Status.query.filter_by(code=\"NEW\").first()\n    status2 = Status.query.filter_by(code=\"WIP\").first()\n    status3 = Status.query.filter_by(code=\"CMPL\").first()\n\n    test_repository_type = Type(\n        name=\"Test Repository Type A\",\n        code=\"trta\",\n        target_entity_type=\"Repository\",\n    )\n\n    test_repository = Repository(\n        name=\"Test Repository A\", code=\"TRA\", type=test_repository_type\n    )\n\n    budget_status_list = StatusList(\n        name=\"Budget Statuses\",\n        statuses=[status1, status2, status3],\n        target_entity_type=\"Budget\",\n    )\n\n    commercial_type = Type(\n        name=\"Commercial A\", code=\"comm\", target_entity_type=\"Project\"\n    )\n\n    test_client = Client(name=\"Test Client\")\n    DBSession.add(test_client)\n\n    test_project = Project(\n        name=\"Test Project For Budget Creation\",\n        code=\"TPFBC\",\n        type=commercial_type,\n        repository=test_repository,\n        clients=[test_client],\n    )\n\n    DBSession.add(test_project)\n    DBSession.commit()\n\n    test_budget = Budget(\n        name=\"Test Budget\",\n        project=test_project,\n        created_by=test_user,\n        status_list=budget_status_list,\n        status=status1,\n    )\n\n    DBSession.add(test_budget)\n    DBSession.commit()\n\n    good = Good(name=\"Some Good\", cost=9, msrp=10, unit=\"$/hour\")\n    DBSession.add(good)\n    DBSession.commit()\n\n    # create some entries\n    entry1 = BudgetEntry(budget=test_budget, good=good, amount=5.0)\n    entry2 = BudgetEntry(budget=test_budget, good=good, amount=1.0)\n\n    DBSession.add_all([entry1, entry2])\n    DBSession.commit()\n\n    # create an invoice\n    test_invoice = Invoice(\n        created_by=test_user,\n        budget=test_budget,\n        client=test_client,\n        amount=1232.4,\n        unit=\"TRY\",\n    )\n    DBSession.add(test_invoice)\n\n    created_by = test_invoice.created_by\n    date_created = test_invoice.date_created\n    date_updated = test_invoice.date_updated\n    name = test_invoice.name\n    nice_name = test_invoice.nice_name\n    tags = test_invoice.tags\n    notes = test_invoice.notes\n    updated_by = test_invoice.updated_by\n\n    budget = test_budget\n    client = test_client\n    amount = 1232.4\n    unit = \"TRY\"\n\n    del test_invoice\n\n    test_invoice_db = Invoice.query.filter(Invoice.name == name).first()\n\n    assert isinstance(test_invoice_db, Invoice)\n\n    assert test_user == test_invoice_db.created_by\n    assert created_by == test_invoice_db.created_by\n    assert date_created == test_invoice_db.date_created\n    assert date_updated == test_invoice_db.date_updated\n    assert name == test_invoice_db.name\n    assert nice_name == test_invoice_db.nice_name\n    assert notes == test_invoice_db.notes\n    assert tags == test_invoice_db.tags\n    assert updated_by == test_invoice_db.updated_by\n\n    assert budget == test_invoice_db.budget\n    assert client == test_invoice_db.client\n    assert amount == test_invoice_db.amount\n    assert unit == test_invoice_db.unit\n\n    # now test the deletion of the invoice instance\n    DBSession.delete(test_invoice_db)\n    DBSession.commit()\n\n    # we should still have the budget\n    assert Budget.query.filter(Budget.id == budget.id).first() == budget\n\n    # we should still have the client\n    assert Client.query.filter(Client.id == client.id).first() == client\n\n    # and we should have the invoice deleted\n    assert Invoice.query.filter(Invoice.name == test_invoice_db.name).first() is None\n\n\ndef test_persistence_of_payment(setup_postgresql_db):\n    \"\"\"Persistence of Payment instances.\"\"\"\n    test_user = User(\n        name=\"Test User\", login=\"tu\", email=\"test@user.com\", password=\"secret\"\n    )\n    status1 = Status.query.filter_by(code=\"NEW\").first()\n    status2 = Status.query.filter_by(code=\"WIP\").first()\n    status3 = Status.query.filter_by(code=\"CMPL\").first()\n\n    test_repository_type = Type(\n        name=\"Test Repository Type A\",\n        code=\"trta\",\n        target_entity_type=\"Repository\",\n    )\n\n    test_repository = Repository(\n        name=\"Test Repository A\", code=\"TRA\", type=test_repository_type\n    )\n\n    budget_status_list = StatusList(\n        name=\"Budget Statuses\",\n        statuses=[status1, status2, status3],\n        target_entity_type=\"Budget\",\n    )\n\n    commercial_type = Type(\n        name=\"Commercial A\", code=\"comm\", target_entity_type=\"Project\"\n    )\n\n    test_client = Client(name=\"Test Client\")\n    DBSession.add(test_client)\n\n    test_project = Project(\n        name=\"Test Project For Budget Creation\",\n        code=\"TPFBC\",\n        type=commercial_type,\n        repository=test_repository,\n        clients=[test_client],\n    )\n\n    DBSession.add(test_project)\n    DBSession.commit()\n\n    test_budget = Budget(\n        name=\"Test Budget\",\n        project=test_project,\n        created_by=test_user,\n        status_list=budget_status_list,\n        status=status1,\n    )\n\n    DBSession.add(test_budget)\n    DBSession.commit()\n\n    good = Good(name=\"Some Good\", cost=9, msrp=10, unit=\"$/hour\")\n    DBSession.add(good)\n    DBSession.commit()\n\n    # create some entries\n    entry1 = BudgetEntry(budget=test_budget, good=good, amount=5.0)\n    entry2 = BudgetEntry(budget=test_budget, good=good, amount=1.0)\n\n    DBSession.add_all([entry1, entry2])\n    DBSession.commit()\n\n    # create an invoice\n    test_invoice = Invoice(\n        created_by=test_user,\n        budget=test_budget,\n        client=test_client,\n        amount=1232.4,\n        unit=\"TRY\",\n    )\n    DBSession.save(test_invoice)\n\n    test_payment = Payment(\n        created_by=test_user, invoice=test_invoice, amount=123.4, unit=\"TRY\"\n    )\n    DBSession.save(test_payment)\n\n    created_by = test_payment.created_by\n    date_created = test_payment.date_created\n    date_updated = test_payment.date_updated\n    name = test_payment.name\n    nice_name = test_payment.nice_name\n    tags = test_payment.tags\n    notes = test_payment.notes\n    updated_by = test_payment.updated_by\n\n    invoice = test_invoice\n    amount = 123.4\n    unit = \"TRY\"\n\n    del test_payment\n\n    test_payment_db = Payment.query.filter(Payment.name == name).first()\n\n    assert isinstance(test_payment_db, Payment)\n\n    assert test_user == test_payment_db.created_by\n    assert created_by == test_payment_db.created_by\n    assert date_created == test_payment_db.date_created\n    assert date_updated == test_payment_db.date_updated\n    assert name == test_payment_db.name\n    assert nice_name == test_payment_db.nice_name\n    assert notes == test_payment_db.notes\n    assert tags == test_payment_db.tags\n    assert updated_by == test_payment_db.updated_by\n\n    assert invoice == test_payment_db.invoice\n    assert amount == test_payment_db.amount\n    assert unit == test_payment_db.unit\n\n    # now test the deletion of the invoice instance\n    DBSession.delete(test_payment_db)\n    DBSession.commit()\n\n    # we should still have the budget\n    assert Budget.query.filter(Budget.id == test_budget.id).first() == test_budget\n\n    # we should still have the Invoice\n    assert Invoice.query.filter(Invoice.id == test_invoice.id).first() == test_invoice\n\n    # and we should have the payment deleted\n    assert Payment.query.filter(Payment.name == test_payment_db.name).first() is None\n\n\ndef test_persistence_of_page(setup_postgresql_db):\n    \"\"\"Persistence of Page.\"\"\"\n    test_user = User(\n        name=\"Test User\", login=\"tu\", email=\"test@user.com\", password=\"secret\"\n    )\n\n    _status1 = Status.query.filter_by(code=\"NEW\").first()\n    _status2 = Status.query.filter_by(code=\"WIP\").first()\n    _status3 = Status.query.filter_by(code=\"CMPL\").first()\n\n    test_repository_type = Type(\n        name=\"Test Repository Type A\",\n        code=\"trta\",\n        target_entity_type=\"Repository\",\n    )\n\n    test_repository = Repository(\n        name=\"Test Repository A\", code=\"TRA\", type=test_repository_type\n    )\n\n    commercial_type = Type(\n        name=\"Commercial A\", code=\"comm\", target_entity_type=\"Project\"\n    )\n\n    test_project = Project(\n        name=\"Test Project For Asset Creation\",\n        code=\"TPFAC\",\n        type=commercial_type,\n        repository=test_repository,\n    )\n\n    DBSession.add(test_project)\n    DBSession.commit()\n\n    kwargs = {\n        \"title\": \"Test Wiki Page\",\n        \"content\": \"This is a test wiki page\",\n        \"project\": test_project,\n        \"created_by\": test_user,\n    }\n\n    test_page = Page(**kwargs)\n\n    DBSession.add(test_page)\n    DBSession.commit()\n\n    created_by = test_page.created_by\n    date_created = test_page.date_created\n    date_updated = test_page.date_updated\n    name = test_page.name\n    nice_name = test_page.nice_name\n    project = test_page.project\n    tags = test_page.tags\n    updated_by = test_page.updated_by\n    title = test_page.title\n    content = test_page.content\n    notes = test_page.notes\n\n    del test_page\n\n    test_page_db = Page.query.filter_by(title=kwargs[\"title\"]).one()\n\n    assert isinstance(test_page_db, Page)\n\n    # assert test_asset, test_asset_DB)\n    assert test_page_db.created_by is not None\n    assert created_by == test_page_db.created_by\n    assert date_created == test_page_db.date_created\n    assert date_updated == test_page_db.date_updated\n    assert content == test_page_db.content\n    assert name == test_page_db.name\n    assert nice_name == test_page_db.nice_name\n    assert notes == test_page_db.notes\n    assert project == test_page_db.project\n    assert tags == test_page_db.tags\n    assert title == test_page_db.title\n    assert updated_by == test_page_db.updated_by\n\n    # now test the deletion of the asset class\n    DBSession.delete(test_page_db)\n    DBSession.commit()\n\n    # we should still have the user\n    assert User.query.filter(User.id == created_by.id).first() is not None\n\n    # we should still have the project\n    assert Project.query.filter(Project.id == project.id).first() is not None\n\n    # and we should have our page deleted\n    assert Page.query.filter(Page.title == kwargs[\"title\"]).first() is None\n\n\ndef test_persistence_of_timelog(setup_postgresql_db):\n    \"\"\"Persistence of TimeLog.\"\"\"\n    logger.setLevel(log.logging_level)\n    description = \"this is a time log\"\n    start = datetime.datetime(2013, 1, 10, tzinfo=pytz.utc)\n    end = datetime.datetime(2013, 1, 13, tzinfo=pytz.utc)\n    user1 = User(name=\"User1\", login=\"user1\", email=\"user1@users.com\", password=\"pass\")\n    user2 = User(name=\"User2\", login=\"user2\", email=\"user2@users.com\", password=\"pass\")\n    _stat1 = Status(name=\"Work In Progress\", code=\"WIP\")\n    _stat2 = Status(name=\"Completed\", code=\"CMPL\")\n    repo = Repository(\n        name=\"Commercials Repository\",\n        code=\"CR\",\n        linux_path=\"/mnt/shows\",\n        windows_path=\"S:/\",\n        macos_path=\"/mnt/shows\",\n    )\n    projtype = Type(\n        name=\"Commercial Project\", code=\"comm\", target_entity_type=\"Project\"\n    )\n    proj1 = Project(name=\"Test Project\", code=\"tp\", type=projtype, repository=repo)\n    test_task = Task(\n        name=\"Test Task\",\n        start=start,\n        end=end,\n        resources=[user1, user2],\n        project=proj1,\n        responsible=[user1],\n    )\n    test_time_log = TimeLog(\n        task=test_task,\n        resource=user1,\n        start=datetime.datetime(2013, 1, 10, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 1, 13, tzinfo=pytz.utc),\n        description=description,\n    )\n\n    DBSession.add(test_time_log)\n    DBSession.commit()\n    tlog_id = test_time_log.id\n\n    del test_time_log\n\n    # now retrieve it back\n    test_time_log_db = TimeLog.query.filter_by(id=tlog_id).first()\n\n    assert description == test_time_log_db.description\n    assert start == test_time_log_db.start\n    assert end == test_time_log_db.end\n    assert user1 == test_time_log_db.resource\n    assert test_task == test_time_log_db.task\n\n\ndef test_persistence_of_timelog_raw_sql(setup_postgresql_db):\n    \"\"\"Persistence of TimeLog.\"\"\"\n    start = datetime.datetime(2013, 1, 10, tzinfo=pytz.utc)\n    end = datetime.datetime(2013, 1, 13, tzinfo=pytz.utc)\n    user1 = User(name=\"User1\", login=\"user1\", email=\"user1@users.com\", password=\"pass\")\n    user2 = User(name=\"User2\", login=\"user2\", email=\"user2@users.com\", password=\"pass\")\n    _stat1 = Status(name=\"Work In Progress\", code=\"WIP\")\n    _stat2 = Status(name=\"Completed\", code=\"CMPL\")\n    repo = Repository(\n        name=\"Commercials Repository\",\n        code=\"CR\",\n        linux_path=\"/mnt/shows\",\n        windows_path=\"S:/\",\n        macos_path=\"/mnt/shows\",\n    )\n    projtype = Type(\n        name=\"Commercial Project\", code=\"comm\", target_entity_type=\"Project\"\n    )\n    proj1 = Project(name=\"Test Project\", code=\"tp\", type=projtype, repository=repo)\n    test_task = Task(\n        name=\"Test Task\",\n        start=start,\n        end=end,\n        resources=[user1, user2],\n        project=proj1,\n        responsible=[user1],\n    )\n    DBSession.add(test_task)\n    DBSession.commit()\n\n    # now insert a new TimeLog in to the Timelogs table which has the same\n    # value for start and end arguments, which should automatically raise\n    # an IntegrityError by the database itself.\n\n    # try insert start = start\n    new_tl1 = TimeLog(task=test_task, resource=user1, start=start, end=end)\n    DBSession.add(new_tl1)\n    DBSession.commit()\n\n    # create a new TimeLog\n    new_tl2 = TimeLog(\n        task=test_task,\n        resource=user1,\n        start=end,\n        end=end + datetime.timedelta(hours=3),\n    )\n    DBSession.add(new_tl2)\n    DBSession.commit()\n\n    # update it to have overlapping timing values with new_tl1\n    new_tl2.start = start + datetime.timedelta(hours=2)\n\n    with pytest.raises(IntegrityError):\n        DBSession.commit()\n\n\ndef test_persistence_of_client(setup_postgresql_db):\n    \"\"\"Persistence of Client.\"\"\"\n    logger.setLevel(log.logging_level)\n\n    name = \"TestClient\"\n    description = \"this is for testing purposes\"\n    created_by = None\n    updated_by = None\n    date_created = datetime.datetime.now(pytz.utc)\n    date_updated = datetime.datetime.now(pytz.utc)\n\n    test_client = Client(\n        name=name,\n        description=description,\n        created_by=created_by,\n        updated_by=updated_by,\n        date_created=date_created,\n        date_updated=date_updated,\n    )\n\n    DBSession.add(test_client)\n    DBSession.commit()\n\n    # create three users\n\n    # user1\n    user1 = User(\n        name=\"User1 Test Persistence Department\",\n        login=\"u1tpd\",\n        initials=\"u1tpd\",\n        description=\"this is for testing purposes\",\n        created_by=None,\n        updated_by=None,\n        login_name=\"user1_tp_client\",\n        first_name=\"user1_first_name\",\n        last_name=\"user1_last_name\",\n        email=\"user1@client.com\",\n        companies=[test_client],\n        password=\"password\",\n    )\n\n    # user2\n    user2 = User(\n        name=\"User2 Test Persistence Client\",\n        login=\"u2tpd\",\n        initials=\"u2tpd\",\n        description=\"this is for testing purposes\",\n        created_by=None,\n        updated_by=None,\n        login_name=\"user2_tp_client\",\n        first_name=\"user2_first_name\",\n        last_name=\"user2_last_name\",\n        email=\"user2@client.com\",\n        companies=[test_client],\n        password=\"password\",\n    )\n\n    # user3\n    user3 = User(\n        name=\"User3 Test Persistence Client\",\n        login=\"u3tpd\",\n        initials=\"u3tpd\",\n        description=\"this is for testing purposes\",\n        created_by=None,\n        updated_by=None,\n        login_name=\"user3_tp_client\",\n        first_name=\"user3_first_name\",\n        last_name=\"user3_last_name\",\n        email=\"user3@client.com\",\n        companies=[test_client],\n        password=\"password\",\n    )\n\n    good1 = Good(name=\"Test Good 1\")\n    good2 = Good(name=\"Test Good 2\")\n    good3 = Good(name=\"Test Good 3\")\n    good4 = Good(name=\"Test Good 4\")\n    good5 = Good(name=\"Test Good 5\")\n    test_client.goods = [good1, good2, good3, good5]\n\n    DBSession.add_all([good1, good2, good3, good4, good5])\n    DBSession.add_all([user1, user2, user3, test_client])\n    DBSession.commit()\n\n    assert test_client in DBSession\n\n    created_by = test_client.created_by\n    date_created = test_client.date_created\n    date_updated = test_client.date_updated\n    description = test_client.description\n    users = [u for u in test_client.users]\n    name = test_client.name\n    nice_name = test_client.nice_name\n    notes = test_client.notes\n    tags = test_client.tags\n    updated_by = test_client.updated_by\n    goods = [good1, good2, good3]  # not included good5 on purpose\n\n    # remove the good5 from list to see if it will still exist in the db\n    test_client.goods.remove(good5)\n    DBSession.commit()\n\n    del test_client\n\n    # let's check the data\n    # first get the client from the db\n    client_db = Client.query.filter_by(name=name).first()\n\n    assert isinstance(client_db, Client)\n\n    assert created_by == client_db.created_by\n    assert date_created == client_db.date_created\n    assert date_updated == client_db.date_updated\n    assert description == client_db.description\n    assert users == client_db.users\n    assert name == client_db.name\n    assert nice_name == client_db.nice_name\n    assert notes == client_db.notes\n    assert tags == client_db.tags\n    assert updated_by == client_db.updated_by\n    assert sorted(goods, key=lambda x: x.id) == sorted(\n        client_db.goods, key=lambda x: x.id\n    )\n\n    # delete the client and expect the users are still there\n    DBSession.delete(client_db)\n    DBSession.commit()\n\n    user1_db = User.query.filter_by(login=\"u1tpd\").first()\n    user2_db = User.query.filter_by(login=\"u2tpd\").first()\n    user3_db = User.query.filter_by(login=\"u3tpd\").first()\n\n    assert user1_db is not None\n    assert user2_db is not None\n    assert user3_db is not None\n\n    # goods should be deleted with client\n    good1 = Good.query.filter_by(name=\"Test Good 1\").first()\n    good2 = Good.query.filter_by(name=\"Test Good 2\").first()\n    good3 = Good.query.filter_by(name=\"Test Good 3\").first()\n    good4 = Good.query.filter_by(name=\"Test Good 4\").first()\n    good5 = Good.query.filter_by(name=\"Test Good 5\").first()\n\n    assert good1 is None\n    assert good2 is None\n    assert good3 is None\n    assert good4 is not None\n    assert good5 is not None\n\n\ndef test_persistence_of_daily(setup_postgresql_db):\n    \"\"\"Persistence of a Daily instance.\"\"\"\n    test_user1 = User(\n        name=\"User1\", login=\"user1\", email=\"user1@user1.com\", password=\"12345\"\n    )\n\n    test_repo = Repository(name=\"Test Repository\", code=\"TR\")\n    test_project = Project(\n        name=\"Test Project\",\n        code=\"TP\",\n        repository=test_repo,\n    )\n\n    test_task1 = Task(\n        name=\"Test Task 1\", project=test_project, responsible=[test_user1]\n    )\n    test_task2 = Task(\n        name=\"Test Task 2\", project=test_project, responsible=[test_user1]\n    )\n    test_task3 = Task(\n        name=\"Test Task 3\", project=test_project, responsible=[test_user1]\n    )\n    DBSession.add_all([test_task1, test_task2, test_task3])\n    DBSession.commit()\n\n    test_version1 = Version(task=test_task1)\n    DBSession.add(test_version1)\n    DBSession.commit()\n\n    test_version2 = Version(task=test_task1)\n    DBSession.add(test_version2)\n    DBSession.commit()\n\n    test_version3 = Version(task=test_task1)\n    DBSession.add(test_version3)\n    DBSession.commit()\n\n    test_version4 = Version(task=test_task2)\n    DBSession.add(test_version4)\n    DBSession.commit()\n\n    test_file1 = File(original_filename=\"test_render1.jpg\")\n    test_file2 = File(original_filename=\"test_render2.jpg\")\n    test_file3 = File(original_filename=\"test_render3.jpg\")\n    test_file4 = File(original_filename=\"test_render4.jpg\")\n\n    test_version1.files = [test_file1, test_file2, test_file3]\n    test_version4.files = [test_file4]\n\n    DBSession.add_all(\n        [\n            test_task1,\n            test_task2,\n            test_task3,\n            test_version1,\n            test_version2,\n            test_version3,\n            test_version4,\n            test_file1,\n            test_file2,\n            test_file3,\n            test_file4,\n        ]\n    )\n    DBSession.commit()\n\n    # arguments\n    name = \"Test Daily\"\n    files = [test_file1, test_file2, test_file3]\n\n    daily = Daily(name=name, project=test_project)\n    daily.files = files\n\n    DBSession.add(daily)\n    DBSession.commit()\n\n    daily_id = daily.id\n\n    del daily\n\n    daily_db = DBSession.get(Daily, daily_id)\n\n    assert daily_db.name == name\n    assert daily_db.files == files\n    assert daily_db.project == test_project\n\n    file1_id = test_file1.id\n    file2_id = test_file2.id\n    file3_id = test_file3.id\n    file4_id = test_file4.id\n\n    # delete tests\n    DBSession.delete(daily_db)\n    DBSession.commit()\n\n    # test if files are still there\n    test_file1_db = DBSession.get(File, file1_id)\n    test_file2_db = DBSession.get(File, file2_id)\n    test_file3_db = DBSession.get(File, file3_id)\n    test_file4_db = DBSession.get(File, file4_id)\n\n    assert test_file1_db is not None\n    assert isinstance(test_file1_db, File)\n\n    assert test_file2_db is not None\n    assert isinstance(test_file2_db, File)\n\n    assert test_file3_db is not None\n    assert isinstance(test_file3_db, File)\n\n    assert test_file4_db is not None\n    assert isinstance(test_file4_db, File)\n\n    assert DailyFile.query.all() == []\n    assert File.query.count() == 4  # Versions are not files anymore\n\n\ndef test_persistence_of_department(setup_postgresql_db):\n    \"\"\"Persistence of Department.\"\"\"\n    logger.setLevel(log.logging_level)\n\n    name = \"TestDepartment_test_persistence_Department\"\n    description = \"this is for testing purposes\"\n    created_by = None\n    updated_by = None\n\n    date_created = datetime.datetime.now(pytz.utc)\n    date_updated = datetime.datetime.now(pytz.utc)\n\n    test_dep = Department(\n        name=name,\n        description=description,\n        created_by=created_by,\n        updated_by=updated_by,\n        date_created=date_created,\n        date_updated=date_updated,\n    )\n    DBSession.save(test_dep)\n\n    # create three users, one for lead and two for users\n\n    # user1\n    user1 = User(\n        name=\"User1 Test Persistence Department\",\n        login=\"u1tpd\",\n        initials=\"u1tpd\",\n        description=\"this is for testing purposes\",\n        created_by=None,\n        updated_by=None,\n        login_name=\"user1_tp_department\",\n        first_name=\"user1_first_name\",\n        last_name=\"user1_last_name\",\n        email=\"user1@department.com\",\n        departments=[test_dep],\n        password=\"password\",\n    )\n\n    # user2\n    user2 = User(\n        name=\"User2 Test Persistence Department\",\n        login=\"u2tpd\",\n        initials=\"u2tpd\",\n        description=\"this is for testing purposes\",\n        created_by=None,\n        updated_by=None,\n        login_name=\"user2_tp_department\",\n        first_name=\"user2_first_name\",\n        last_name=\"user2_last_name\",\n        email=\"user2@department.com\",\n        departments=[test_dep],\n        password=\"password\",\n    )\n\n    # user3\n    # create three users, one for lead and two for users\n    user3 = User(\n        name=\"User3 Test Persistence Department\",\n        login=\"u3tpd\",\n        initials=\"u3tpd\",\n        description=\"this is for testing purposes\",\n        created_by=None,\n        updated_by=None,\n        login_name=\"user3_tp_department\",\n        first_name=\"user3_first_name\",\n        last_name=\"user3_last_name\",\n        email=\"user3@department.com\",\n        departments=[test_dep],\n        password=\"password\",\n    )\n    DBSession.save([user1, user2, user3])\n\n    # add as the users\n    test_dep.users = [user1, user2, user3]\n\n    DBSession.save(test_dep)\n\n    assert test_dep in DBSession\n\n    created_by = test_dep.created_by\n    date_created = test_dep.date_created\n    date_updated = test_dep.date_updated\n    description = test_dep.description\n    users = [u for u in test_dep.users]\n    name = test_dep.name\n    nice_name = test_dep.nice_name\n    notes = test_dep.notes\n    tags = test_dep.tags\n    updated_by = test_dep.updated_by\n\n    del test_dep\n\n    # let's check the data\n    # first get the department from the db\n    test_dep_db = Department.query.filter_by(name=name).first()\n\n    assert isinstance(test_dep_db, Department)\n\n    assert created_by == test_dep_db.created_by\n    assert date_created == test_dep_db.date_created\n    assert date_updated == test_dep_db.date_updated\n    assert description == test_dep_db.description\n    assert users == test_dep_db.users\n    assert name == test_dep_db.name\n    assert nice_name == test_dep_db.nice_name\n    assert notes == test_dep_db.notes\n    assert tags == test_dep_db.tags\n    assert updated_by == test_dep_db.updated_by\n\n\ndef test_persistence_of_entity(setup_postgresql_db):\n    \"\"\"Persistence of Entity.\"\"\"\n    # create an Entity with a couple of tags\n    # the Tag1\n    name = \"Tag1_test_creating_an_Entity\"\n    description = \"this is for testing purposes\"\n    created_by = None\n    updated_by = None\n    date_created = date_updated = datetime.datetime.now(pytz.utc)\n\n    tag1 = Tag(\n        name=name,\n        description=description,\n        created_by=created_by,\n        updated_by=updated_by,\n        date_created=date_created,\n        date_updated=date_updated,\n    )\n\n    # the Tag2\n    name = \"Tag2_test_creating_an_Entity\"\n    description = \"this is for testing purposes\"\n    created_by = None\n    updated_by = None\n\n    date_created = date_updated = datetime.datetime.now(pytz.utc)\n\n    tag2 = Tag(\n        name=name,\n        description=description,\n        created_by=created_by,\n        updated_by=updated_by,\n        date_created=date_created,\n        date_updated=date_updated,\n    )\n\n    # the note\n    note1 = Note(content=\"content for note1\")\n    note2 = Note(content=\"content for note2\")\n\n    # the entity\n    name = \"TestEntity\"\n    description = \"this is for testing purposes\"\n    created_by = None\n    updated_by = None\n    date_created = date_updated = datetime.datetime.now(pytz.utc)\n\n    test_entity = Entity(\n        name=name,\n        description=description,\n        created_by=created_by,\n        updated_by=updated_by,\n        date_created=date_created,\n        date_updated=date_updated,\n        tags=[tag1, tag2],\n        notes=[note1, note2],\n    )\n\n    # assign the note1 also to another entity\n    test_entity2 = Entity(name=\"Test Entity 2\", notes=[note1])\n\n    # persist it to the database\n    DBSession.add_all([test_entity, test_entity2])\n    DBSession.commit()\n\n    # store attributes\n    created_by = test_entity.created_by\n    date_created = test_entity.date_created\n    date_updated = test_entity.date_updated\n    description = test_entity.description\n    name = test_entity.name\n    nice_name = test_entity.nice_name\n    notes = test_entity.notes\n    tags = test_entity.tags\n    updated_by = test_entity.updated_by\n\n    # delete the previous test_entity\n    del test_entity\n\n    # now try to retrieve it\n    test_entity_db = Entity.query.filter_by(name=name).first()\n\n    assert isinstance(test_entity_db, Entity)\n\n    assert created_by == test_entity_db.created_by\n    assert date_created == test_entity_db.date_created\n    assert date_updated == test_entity_db.date_updated\n    assert description == test_entity_db.description\n    assert name == test_entity_db.name\n    assert nice_name == test_entity_db.nice_name\n    assert sorted(notes, key=lambda x: x.name) == sorted(\n        [note1, note2], key=lambda x: x.name\n    )\n\n    assert notes == test_entity_db.notes\n    assert tags == test_entity_db.tags\n    assert updated_by == test_entity_db.updated_by\n\n    # delete tests\n\n    # Deleting an Entity should also delete the associated notes\n    DBSession.delete(test_entity_db)\n    DBSession.commit()\n\n    test_entity2_db = Entity.query.filter_by(name=\"Test Entity 2\").first()\n    assert isinstance(test_entity2_db, Entity)\n\n    assert sorted([note1, note2], key=lambda x: x.name) == sorted(\n        Note.query.all(), key=lambda x: x.name\n    )\n    assert sorted([note1], key=lambda x: x.name) == sorted(\n        test_entity2_db.notes, key=lambda x: x.name\n    )\n\n\ndef test_persistence_of_entity_group(setup_postgresql_db):\n    \"\"\"Persistence of EntityGroup.\"\"\"\n    # create a couple of task\n    user1 = User(\n        name=\"User1\",\n        login=\"user1\",\n        email=\"user1@user.com\",\n        password=\"1234\",\n    )\n    user2 = User(\n        name=\"User2\",\n        login=\"user2\",\n        email=\"user2@user.com\",\n        password=\"1234\",\n    )\n    user3 = User(\n        name=\"User3\",\n        login=\"user3\",\n        email=\"user3@user.com\",\n        password=\"1234\",\n    )\n    repo = Repository(\n        name=\"Test Repo\",\n        code=\"TR\",\n        linux_path=\"/mnt/M/JOBs\",\n        windows_path=\"M:/JOBs\",\n        macos_path=\"/Users/Shared/Servers/M\",\n    )\n    project1 = Project(\n        name=\"Tests Project\",\n        code=\"tp\",\n        repository=repo,\n    )\n    char_asset_type = Type(\n        name=\"Character Asset\", code=\"char\", target_entity_type=\"Asset\"\n    )\n    asset1 = Asset(\n        name=\"Char1\",\n        code=\"char1\",\n        type=char_asset_type,\n        project=project1,\n        responsible=[user1],\n    )\n    task1 = Task(\n        name=\"Test Task\",\n        watchers=[user3],\n        parent=asset1,\n    )\n    child_task1 = Task(\n        name=\"Child Task 1\",\n        resources=[user1, user2],\n        parent=task1,\n    )\n    child_task2 = Task(\n        name=\"Child Task 2\",\n        resources=[user1, user2],\n        parent=task1,\n    )\n    task2 = Task(\n        name=\"Another Task\",\n        project=project1,\n        resources=[user1],\n        responsible=[user2],\n    )\n\n    entity_group1 = EntityGroup(name=\"My Tasks\")\n    entity_group1.entities = [task1, child_task2, task2]\n\n    DBSession.add_all(\n        [task1, child_task1, child_task2, task2, user1, user2, entity_group1]\n    )\n    DBSession.commit()\n\n    created_by = entity_group1.created_by\n    date_created = entity_group1.date_created\n    date_updated = entity_group1.date_updated\n    name = entity_group1.name\n    entities = entity_group1.entities\n    tags = entity_group1.tags\n    type_ = entity_group1.type\n    updated_by = entity_group1.updated_by\n\n    del entity_group1\n\n    # now query it back\n    entity_group1_db = EntityGroup.query.filter_by(name=name).first()\n\n    assert isinstance(entity_group1_db, EntityGroup)\n\n    assert created_by == entity_group1_db.created_by\n    assert date_created == entity_group1_db.date_created\n    assert date_updated == entity_group1_db.date_updated\n    assert name == entity_group1_db.name\n    assert tags == entity_group1_db.tags\n    assert sorted(entities, key=lambda x: x.name) == sorted(\n        entity_group1_db.entities, key=lambda x: x.name\n    )\n    assert sorted(entities, key=lambda x: x.id) == sorted(\n        [task1, child_task2, task2], key=lambda x: x.id\n    )\n    assert type_ == entity_group1_db.type\n    assert updated_by == entity_group1_db.updated_by\n\n    # delete tests\n    # deleting entity group will not delete the contained entities\n    DBSession.delete(entity_group1_db)\n    DBSession.commit()\n\n    assert sorted(\n        [task1, asset1, child_task1, child_task2, task2], key=lambda x: x.name\n    ) == sorted(Task.query.all(), key=lambda x: x.name)\n\n    # We still should have the users intact\n    admin = User.query.filter_by(name=\"admin\").first()\n    assert sorted([user1, user2, user3, admin], key=lambda x: x.name) == sorted(\n        User.query.all(), key=lambda x: x.name\n    )\n\n    assert sorted(\n        [asset1, task1, child_task1, child_task2, task2], key=lambda x: x.name\n    ) == sorted(Task.query.all(), key=lambda x: x.name)\n\n\ndef test_persistence_of_filename_template(setup_postgresql_db):\n    \"\"\"Persistence of FilenameTemplate.\"\"\"\n    ref_type = Type.query.filter_by(name=\"Reference\").first()\n\n    # create a FilenameTemplate object for movie files\n    kwargs = {\n        \"name\": \"Movie Files Template\",\n        \"target_entity_type\": \"File\",\n        \"type\": ref_type,\n        \"description\": \"This is a template to be used for movie files.\",\n        \"path\": \"REFS/{{file.type.name}}\",\n        \"filename\": \"{{file.file_name}}\",\n        \"output_path\": \"OUTPUT\",\n        \"output_file_code\": \"{{file.file_name}}\",\n    }\n\n    new_type_template = FilenameTemplate(**kwargs)\n\n    # persist it\n    DBSession.add(new_type_template)\n    DBSession.commit()\n\n    created_by = new_type_template.created_by\n    date_created = new_type_template.date_created\n    date_updated = new_type_template.date_updated\n    description = new_type_template.description\n    filename = new_type_template.filename\n    name = new_type_template.name\n    nice_name = new_type_template.nice_name\n    notes = new_type_template.notes\n    path = new_type_template.path\n    tags = new_type_template.tags\n    target_entity_type = new_type_template.target_entity_type\n    updated_by = new_type_template.updated_by\n    type_ = new_type_template.type\n\n    del new_type_template\n\n    # get it back\n    new_type_template_db = FilenameTemplate.query.filter_by(name=kwargs[\"name\"]).first()\n\n    assert isinstance(new_type_template_db, FilenameTemplate)\n\n    assert new_type_template_db.created_by == created_by\n    assert new_type_template_db.date_created == date_created\n    assert new_type_template_db.date_updated == date_updated\n    assert new_type_template_db.description == description\n    assert new_type_template_db.filename == filename\n    assert new_type_template_db.name == name\n    assert new_type_template_db.nice_name == nice_name\n    assert new_type_template_db.notes == notes\n    assert new_type_template_db.path == path\n    assert new_type_template_db.tags == tags\n    assert new_type_template_db.target_entity_type == target_entity_type\n    assert new_type_template_db.updated_by == updated_by\n    assert new_type_template_db.type == type_\n\n\ndef test_persistence_of_image_format(setup_postgresql_db):\n    \"\"\"Persistence of ImageFormat.\"\"\"\n    # create a new ImageFormat object and try to read it back\n    kwargs = {\n        \"name\": \"HD\",\n        \"description\": \"test image format\",\n        \"width\": 1920,\n        \"height\": 1080,\n        \"pixel_aspect\": 1.0,\n        \"print_resolution\": 300.0,\n    }\n\n    # create the ImageFormat object\n    im_format = ImageFormat(**kwargs)\n\n    # persist it\n    DBSession.add(im_format)\n    DBSession.commit()\n\n    # store attributes\n    created_by = im_format.created_by\n    date_created = im_format.date_created\n    date_updated = im_format.date_updated\n    description = im_format.description\n    device_aspect = im_format.device_aspect\n    height = im_format.height\n    name = im_format.name\n    nice_name = im_format.nice_name\n    notes = im_format.notes\n    pixel_aspect = im_format.pixel_aspect\n    print_resolution = im_format.print_resolution\n    tags = im_format.tags\n    updated_by = im_format.updated_by\n    width = im_format.width\n\n    # delete the previous im_format\n    del im_format\n\n    # get it back\n    im_format_db = ImageFormat.query.filter_by(name=kwargs[\"name\"]).first()\n\n    assert isinstance(im_format_db, ImageFormat)\n\n    # just test the repository part of the attributes\n    assert im_format_db.created_by == created_by\n    assert im_format_db.date_created == date_created\n    assert im_format_db.date_updated == date_updated\n    assert im_format_db.description == description\n    assert im_format_db.device_aspect == device_aspect\n    assert im_format_db.height == height\n    assert im_format_db.name == name\n    assert im_format_db.nice_name == nice_name\n    assert im_format_db.notes == notes\n    assert im_format_db.pixel_aspect == pixel_aspect\n    assert im_format_db.print_resolution == print_resolution\n    assert im_format_db.tags == tags\n    assert im_format_db.updated_by == updated_by\n    assert im_format_db.width == width\n\n\ndef test_persistence_of_file(setup_postgresql_db):\n    \"\"\"Persistence of File.\"\"\"\n    # user\n    user1 = User(\n        name=\"Test User 1\", login=\"tu1\", email=\"test@users.com\", password=\"secret\"\n    )\n    DBSession.add(user1)\n    DBSession.commit()\n    # create a file Type\n    sound_file_type = Type(name=\"Sound\", code=\"sound\", target_entity_type=\"File\")\n    image_seq_type = Type(\n        name=\"JPEG Sequence\", code=\"JPEGSeq\", target_entity_type=\"File\"\n    )\n    video_type = Type(name=\"Video\", code=\"Video\", target_entity_type=\"File\")\n\n    # create some reference Files\n    ref1 = File(\n        name=\"My Image Sequence #1\",\n        full_path=\"M:/PROJECTS/my_image_sequence.#.jpg\",\n        type=image_seq_type,\n        created_by=user1,\n        created_with=\"Maya\",\n    )\n    ref2 = File(\n        name=\"My Movie #1\",\n        full_path=\"M:/PROJECTS/my_movie.mp4\",\n        type=video_type,\n        created_by=user1,\n        created_with=\"Blender\",\n    )\n    DBSession.save([ref1, ref2])\n\n    # create the main File\n    kwargs = {\n        \"name\": \"My Sound\",\n        \"full_path\": \"M:/PROJECTS/my_movie_sound.wav\",\n        \"references\": [ref1, ref2],\n        \"type\": sound_file_type,\n        \"created_by\": user1,\n        \"created_with\": \"Houdini\",\n    }\n    file1 = File(**kwargs)\n\n    # persist it\n    DBSession.add_all([sound_file_type, file1])\n    DBSession.commit()\n\n    # use it as a task reference\n    repo1 = Repository(name=\"test repo\", code=\"TR\")\n\n    project1 = Project(name=\"Test Project 1\", code=\"TP1\", repository=repo1)\n\n    task1 = Task(name=\"Test Task\", project=project1, responsible=[user1])\n    task1.references.append(file1)\n\n    DBSession.add(task1)\n    DBSession.commit()\n\n    # store attributes\n    created_by = file1.created_by\n    created_with = file1.created_with\n    date_created = file1.date_created\n    date_updated = file1.date_updated\n    description = file1.description\n    full_path = file1.full_path\n    name = file1.name\n    nice_name = file1.nice_name\n    notes = file1.notes\n    references = file1.references\n    assert isinstance(references, list)\n    assert len(references) > 0\n    tags = file1.tags\n    type_ = file1.type\n    updated_by = file1.updated_by\n\n    # delete the File\n    del file1\n\n    # retrieve it back\n    file1_db = File.query.filter_by(name=kwargs[\"name\"]).first()\n\n    assert isinstance(file1_db, File)\n\n    assert file1_db.created_by == created_by\n    assert file1_db.created_with == created_with\n    assert file1_db.date_created == date_created\n    assert file1_db.date_updated == date_updated\n    assert file1_db.description == description\n    assert file1_db.full_path == full_path\n    assert file1_db.name == name\n    assert file1_db.nice_name == nice_name\n    assert file1_db.notes == notes\n    assert file1_db.references == references\n    assert file1_db.tags == tags\n    assert file1_db.type == type_\n    assert file1_db.updated_by == updated_by\n    assert file1_db == task1.references[0]\n\n    # delete tests\n    task1.references.remove(file1_db)\n\n    # Deleting a File should not delete anything else\n    DBSession.delete(file1_db)\n    DBSession.commit()\n\n    # We still should have the user and the type intact\n    assert DBSession.get(User, user1.id) is not None\n    assert user1 == DBSession.get(User, user1.id)\n\n    assert DBSession.get(Type, type_.id) is not None\n    assert DBSession.get(Type, type_.id) == type_\n\n    # The task should stay\n    assert DBSession.get(Task, task1.id) is not None\n    assert DBSession.get(Task, task1.id) == task1\n\n\ndef test_persistence_of_note(setup_postgresql_db):\n    \"\"\"Persistence of Note.\"\"\"\n    # create a Note and attach it to an entity\n\n    # create a Note object\n    note_kwargs = {\n        \"name\": \"Note1\",\n        \"description\": \"This Note is created for the purpose of testing \\\n        the Note object\",\n        \"content\": \"Please be carefull about this asset, I will fix the \\\n        rig later on\",\n    }\n\n    test_note = Note(**note_kwargs)\n\n    # create an entity\n    entity_kwargs = {\n        \"name\": \"Entity with Note\",\n        \"description\": \"This Entity is created for testing purposes\",\n        \"notes\": [test_note],\n    }\n\n    test_entity = Entity(**entity_kwargs)\n\n    DBSession.add_all([test_entity, test_note])\n    DBSession.commit()\n\n    # store the attributes\n    content = test_note.content\n    created_by = test_note.created_by\n    date_created = test_note.date_created\n    date_updated = test_note.date_updated\n    description = test_note.description\n    name = test_note.name\n    nice_name = test_note.nice_name\n    updated_by = test_note.updated_by\n\n    # delete the note\n    del test_note\n\n    # try to get the note directly\n    test_note_db = Note.query.filter(Note.name == note_kwargs[\"name\"]).first()\n\n    assert isinstance(test_note_db, Note)\n\n    assert test_note_db.content == content\n    assert test_note_db.created_by == created_by\n    assert test_note_db.date_created == date_created\n    assert test_note_db.date_updated == date_updated\n    assert test_note_db.description == description\n    assert test_note_db.name == name\n    assert test_note_db.nice_name == nice_name\n    assert test_note_db.updated_by == updated_by\n\n\ndef test_persistence_of_good(setup_postgresql_db):\n    \"\"\"hte persistence of Good.\"\"\"\n    g1 = Good(name=\"Test Good 1\", cost=10, msrp=100, unit=\"TRY\")\n\n    DBSession.add(g1)\n    DBSession.commit()\n\n    name = g1.name\n    cost = g1.cost\n    msrp = g1.msrp\n    unit = g1.unit\n\n    del g1\n\n    g1_db = Good.query.first()\n\n    assert g1_db.name == name\n    assert g1_db.cost == cost\n    assert g1_db.msrp == msrp\n    assert g1_db.unit == unit\n\n    # attach a client\n    client = Client(name=\"Test Client\")\n    DBSession.add(client)\n\n    g1_db.client = client\n    DBSession.commit()\n    del g1_db\n\n    g1_db2 = Good.query.first()\n    assert g1_db2.client == client\n\n    # Delete the good\n    DBSession.delete(g1_db2)\n    DBSession.commit()\n\n    # except the client still exist\n    client_db = Client.query.filter(Client.name == \"Test Client\").first()\n\n    assert client_db is not None\n\n\ndef test_persistence_of_group(setup_postgresql_db):\n    \"\"\"Persistence of Group.\"\"\"\n    group1 = Group(name=\"Test Group\")\n\n    user1 = User(name=\"User1\", login=\"user1\", email=\"user1@test.com\", password=\"12\")\n    user2 = User(name=\"User2\", login=\"user2\", email=\"user2@test.com\", password=\"34\")\n\n    group1.users = [user1, user2]\n\n    DBSession.add(group1)\n    DBSession.commit()\n\n    name = group1.name\n    users = group1.users\n\n    del group1\n    group_db = Group.query.filter_by(name=name).first()\n\n    assert group_db.name == name\n    assert group_db.users == users\n\n\ndef test_persistence_of_price_list(setup_postgresql_db):\n    \"\"\"Persistence of PriceList.\"\"\"\n    g1 = Good(name=\"Test Good 1\")\n    g2 = Good(name=\"Test Good 2\")\n    g3 = Good(name=\"Test Good 3\")\n\n    p = PriceList(name=\"Test Price List\", goods=[g1, g2, g3])\n\n    DBSession.add_all([p, g1, g2, g3])\n    DBSession.commit()\n\n    del p\n\n    p_db = PriceList.query.first()\n\n    assert p_db.name == \"Test Price List\"\n    assert sorted(p_db.goods, key=lambda x: x.id) == sorted(\n        [g1, g2, g3], key=lambda x: x.id\n    )\n\n    DBSession.delete(p_db)\n    DBSession.commit()\n\n    # we should still have goods\n    assert g1 is not None\n    assert g2 is not None\n    assert g3 is not None\n\n    g1_db = Good.query.filter_by(name=\"Test Good 1\").first()\n    assert g1_db is not None\n    assert g1_db.name == \"Test Good 1\"\n\n    g2_db = Good.query.filter_by(name=\"Test Good 2\").first()\n    assert g2_db is not None\n    assert g2_db.name == \"Test Good 2\"\n\n    g3_db = Good.query.filter_by(name=\"Test Good 3\").first()\n    assert g3_db is not None\n    assert g3_db.name == \"Test Good 3\"\n\n\ndef test_persistence_of_project(setup_postgresql_db):\n    \"\"\"Persistence of Project.\"\"\"\n    # create mock objects\n    start = datetime.datetime(2016, 11, 17, tzinfo=pytz.utc) + datetime.timedelta(10)\n    end = start + datetime.timedelta(days=20)\n    lead = User(name=\"lead\", login=\"lead\", email=\"lead@lead.com\", password=\"password\")\n    user1 = User(\n        name=\"user1\", login=\"user1\", email=\"user1@user1.com\", password=\"password\"\n    )\n    user2 = User(\n        name=\"user2\", login=\"user2\", email=\"user1@user2.com\", password=\"password\"\n    )\n    user3 = User(\n        name=\"user3\", login=\"user3\", email=\"user3@user3.com\", password=\"password\"\n    )\n    image_format = ImageFormat(name=\"HD\", width=1920, height=1080)\n    project_type = Type(\n        name=\"Commercial Project\", code=\"commproj\", target_entity_type=\"Project\"\n    )\n    structure_type = Type(\n        name=\"Commercial Structure\", code=\"commstr\", target_entity_type=\"Project\"\n    )\n    project_structure = Structure(\n        name=\"Commercial Structure\",\n        custom_templates=\"{{project.code}}\\n\"\n        \"{{project.code}}/ASSETS\\n\"\n        \"{{project.code}}/SEQUENCES\\n\",\n        type=structure_type,\n    )\n\n    repo = Repository(\n        name=\"Commercials Repository\",\n        code=\"CR\",\n        linux_path=\"/mnt/M/Projects\",\n        windows_path=\"M:/Projects\",\n        macos_path=\"/mnt/M/Projects\",\n    )\n\n    # create data for mixins\n    # Reference Mixin\n    file_type = Type(name=\"Image\", code=\"image\", target_entity_type=\"File\")\n    ref1 = File(\n        name=\"Ref1\",\n        full_path=\"/mnt/M/JOBs/TEST_PROJECT\",\n        filename=\"1.jpg\",\n        type=file_type,\n    )\n    ref2 = File(\n        name=\"Ref2\",\n        full_path=\"/mnt/M/JOBs/TEST_PROJECT\",\n        filename=\"1.jpg\",\n        type=file_type,\n    )\n    DBSession.save([lead, ref1, ref2])\n    working_hours = WorkingHours(\n        working_hours={\n            \"mon\": [[570, 720], [780, 1170]],\n            \"tue\": [[570, 720], [780, 1170]],\n            \"wed\": [[570, 720], [780, 1170]],\n            \"thu\": [[570, 720], [780, 1170]],\n            \"fri\": [[570, 720], [780, 1170]],\n            \"sat\": [[570, 720], [780, 1170]],\n            \"sun\": [],\n        }\n    )\n\n    # create a project object\n    kwargs = {\n        \"name\": \"Test Project\",\n        \"code\": \"TP\",\n        \"description\": \"This is a project object for testing purposes\",\n        \"image_format\": image_format,\n        \"fps\": 25,\n        \"type\": project_type,\n        \"structure\": project_structure,\n        \"repositories\": [repo],\n        \"is_stereoscopic\": False,\n        \"display_width\": 1.0,\n        \"start\": start,\n        \"end\": end,\n        \"status\": 0,\n        \"references\": [ref1, ref2],\n        \"working_hours\": working_hours,\n    }\n\n    new_project = Project(**kwargs)\n\n    # persist it in the database\n    DBSession.add(new_project)\n    DBSession.commit()\n    task1 = Task(\n        name=\"task1\",\n        status=0,\n        project=new_project,\n        resources=[user1, user2],\n        responsible=[user1],\n    )\n    task2 = Task(\n        name=\"task2\",\n        status=0,\n        project=new_project,\n        resources=[user3],\n        responsible=[user1],\n    )\n    dt = datetime.datetime\n    td = datetime.timedelta\n    new_project._computed_start = dt.now(pytz.utc)\n    new_project._computed_end = dt.now(pytz.utc) + td(10)\n\n    DBSession.add_all([task1, task2])\n    DBSession.commit()\n\n    # add tickets\n    ticket1 = Ticket(project=new_project)\n    DBSession.add(ticket1)\n    DBSession.commit()\n\n    # create dailies\n    d1 = Daily(name=\"Daily1\", project=new_project)\n    d2 = Daily(name=\"Daily2\", project=new_project)\n    d3 = Daily(name=\"Daily3\", project=new_project)\n    DBSession.add_all([d1, d2, d3])\n    DBSession.commit()\n\n    # store the attributes\n    assets = new_project.assets\n    code = new_project.code\n    created_by = new_project.created_by\n    date_created = new_project.date_created\n    date_updated = new_project.date_updated\n    description = new_project.description\n    end = new_project.end\n    duration = new_project.duration\n    fps = new_project.fps\n    image_format = new_project.image_format\n    is_stereoscopic = new_project.is_stereoscopic\n    name = new_project.name\n    nice_name = new_project.nice_name\n    notes = new_project.notes\n    references = new_project.references\n    repositories = [repo]\n    sequences = new_project.sequences\n    start = new_project.start\n    status = new_project.status\n    status_list = new_project.status_list\n    structure = new_project.structure\n    tags = new_project.tags\n    tasks = new_project.tasks\n    type_ = new_project.type\n    updated_by = new_project.updated_by\n    users = [user for user in new_project.users]\n    computed_start = new_project.computed_start\n    computed_end = new_project.computed_end\n\n    # delete the project\n    del new_project\n\n    # now get it\n    new_project_db = DBSession.query(Project).filter_by(name=kwargs[\"name\"]).first()\n\n    assert isinstance(new_project_db, Project)\n\n    assert new_project_db.assets == assets\n    assert new_project_db.code == code\n    assert new_project_db.computed_start == computed_start\n    assert new_project_db.computed_end == computed_end\n    assert new_project_db.created_by == created_by\n    assert new_project_db.date_created == date_created\n    assert new_project_db.date_updated == date_updated\n    assert new_project_db.description == description\n    assert new_project_db.end == end\n    assert new_project_db.duration == duration\n    assert new_project_db.fps == fps\n    assert new_project_db.image_format == image_format\n    assert new_project_db.is_stereoscopic == is_stereoscopic\n    assert new_project_db.name == name\n    assert new_project_db.nice_name == nice_name\n    assert new_project_db.notes == notes\n    assert new_project_db.references == references\n    assert new_project_db.repositories == repositories\n    assert new_project_db.sequences == sequences\n    assert new_project_db.start == start\n    assert new_project_db.status == status\n    assert new_project_db.status_list == status_list\n    assert new_project_db.structure == structure\n    assert new_project_db.tags == tags\n    assert new_project_db.tasks == tasks\n    assert new_project_db.type == type_\n    assert new_project_db.updated_by == updated_by\n    assert new_project_db.users == users\n\n    # delete tests\n    # now delete the project and expect the following also to be deleted\n    #\n    # Tasks\n    # Tickets\n    DBSession.delete(new_project_db)\n    DBSession.commit()\n\n    # Tasks\n    assert Task.query.all() == []\n\n    # Tickets\n    assert Ticket.query.all() == []\n\n    # Dailies\n    assert Daily.query.all() == []\n\n\ndef test_persistence_of_repository(setup_postgresql_db):\n    \"\"\"Persistence of Repository.\"\"\"\n    # create a new Repository object and try to read it back\n    kwargs = {\n        \"name\": \"Movie-Repo\",\n        \"code\": \"MR\",\n        \"description\": \"test repository\",\n        \"linux_path\": \"/mnt/M\",\n        \"macos_path\": \"/Volumes/M\",\n        \"windows_path\": \"M:/\",\n    }\n\n    # create the repository object\n    repo = Repository(**kwargs)\n\n    # save it to database\n    DBSession.add(repo)\n    DBSession.commit()\n\n    # store attributes\n    created_by = repo.created_by\n    code = repo.code\n    date_created = repo.date_created\n    date_updated = repo.date_updated\n    description = repo.description\n    linux_path = repo.linux_path\n    name = repo.name\n    nice_name = repo.nice_name\n    notes = repo.notes\n    macos_path = repo.macos_path\n    path = repo.path\n    tags = repo.tags\n    updated_by = repo.updated_by\n    windows_path = repo.windows_path\n\n    # delete the repo\n    del repo\n\n    # get it back\n    repo_db = Repository.query.filter_by(name=kwargs[\"name\"]).first()\n\n    assert isinstance(repo_db, Repository)\n\n    assert repo_db.created_by == created_by\n    assert repo_db.code == code\n    assert repo_db.date_created == date_created\n    assert repo_db.date_updated == date_updated\n    assert repo_db.description == description\n    assert repo_db.linux_path == linux_path\n    assert repo_db.name == name\n    assert repo_db.nice_name == nice_name\n    assert repo_db.notes == notes\n    assert repo_db.macos_path == macos_path\n    assert repo_db.path == path\n    assert repo_db.tags == tags\n    assert repo_db.updated_by == updated_by\n    assert repo_db.windows_path == windows_path\n\n\ndef test_persistence_of_scene(setup_postgresql_db):\n    \"\"\"Persistence of Scene.\"\"\"\n    repo1 = Repository(\n        name=\"Commercial Repository\",\n        code=\"CR\",\n    )\n    user1 = User(\n        name=\"User1\",\n        login=\"user1\",\n        email=\"user1@user.com\",\n        password=\"1234\",\n    )\n    commercial_project_type = Type(\n        name=\"Commercial Project\", code=\"commproj\", target_entity_type=\"Project\"\n    )\n    test_project1 = Project(\n        name=\"Test Project\",\n        code=\"TP\",\n        type=commercial_project_type,\n        repository=repo1,\n    )\n    DBSession.add(test_project1)\n    DBSession.commit()\n\n    kwargs = {\n        \"name\": \"Test Scene\",\n        \"code\": \"TSce\",\n        \"description\": \"this is a test scene\",\n        \"project\": test_project1,\n    }\n\n    test_scene = Scene(**kwargs)\n\n    # now add the shots\n    shot1 = Shot(\n        code=\"SH001\",\n        project=test_project1,\n        scene=test_scene,\n        responsible=[user1],\n    )\n    shot2 = Shot(\n        code=\"SH002\",\n        project=test_project1,\n        scene=test_scene,\n        responsible=[user1],\n    )\n    shot3 = Shot(\n        code=\"SH003\",\n        project=test_project1,\n        scene=test_scene,\n        responsible=[user1],\n    )\n    DBSession.add_all([shot1, shot2, shot3])\n    DBSession.add(test_scene)\n    DBSession.commit()\n\n    # store the attributes\n    code = test_scene.code\n    created_by = test_scene.created_by\n    date_created = test_scene.date_created\n    date_updated = test_scene.date_updated\n    description = test_scene.description\n    name = test_scene.name\n    nice_name = test_scene.nice_name\n    notes = test_scene.notes\n    project = test_scene.project\n    shots = test_scene.shots\n    tags = test_scene.tags\n    updated_by = test_scene.updated_by\n\n    # delete the test_sequence\n    del test_scene\n\n    test_scene_db = Scene.query.filter_by(name=kwargs[\"name\"]).first()\n\n    assert test_scene_db.code == code\n    assert test_scene_db.created_by == created_by\n    assert test_scene_db.date_created == date_created\n    assert test_scene_db.date_updated == date_updated\n    assert test_scene_db.description == description\n    assert test_scene_db.name == name\n    assert test_scene_db.nice_name == nice_name\n    assert test_scene_db.notes == notes\n    assert test_scene_db.project == project\n    assert test_scene_db.shots == shots\n    assert test_scene_db.tags == tags\n    assert test_scene_db.updated_by == updated_by\n\n\ndef test_persistence_of_sequence(setup_postgresql_db):\n    \"\"\"Persistence of Sequence.\"\"\"\n    repo1 = Repository(name=\"Commercial Repository\", code=\"CR\")\n    commercial_project_type = Type(\n        name=\"Commercial Project\", code=\"commproj\", target_entity_type=\"Project\"\n    )\n    lead = User(name=\"lead\", login=\"lead\", email=\"lead@lead.com\", password=\"password\")\n    test_project1 = Project(\n        name=\"Test Project\",\n        code=\"TP\",\n        type=commercial_project_type,\n        repository=repo1,\n    )\n    DBSession.add(test_project1)\n    DBSession.commit()\n    kwargs = {\n        \"name\": \"Test Sequence\",\n        \"code\": \"TS\",\n        \"description\": \"this is a test sequence\",\n        \"project\": test_project1,\n        \"schedule_model\": ScheduleModel.Effort,\n        \"schedule_timing\": 50,\n        \"schedule_unit\": TimeUnit.Day,\n        \"responsible\": [lead],\n    }\n    test_sequence = Sequence(**kwargs)\n\n    # now add the shots\n    shot1 = Shot(\n        code=\"SH001\",\n        project=test_project1,\n        sequence=test_sequence,\n        responsible=[lead],\n    )\n    shot2 = Shot(\n        code=\"SH002\",\n        project=test_project1,\n        sequence=test_sequence,\n        responsible=[lead],\n    )\n    shot3 = Shot(\n        code=\"SH003\",\n        project=test_project1,\n        sequence=test_sequence,\n        responsible=[lead],\n    )\n\n    DBSession.add_all([shot1, shot2, shot3])\n    DBSession.add(test_sequence)\n    DBSession.commit()\n\n    # store the attributes\n    code = test_sequence.code\n    created_by = test_sequence.created_by\n    date_created = test_sequence.date_created\n    date_updated = test_sequence.date_updated\n    description = test_sequence.description\n    end = test_sequence.end\n    name = test_sequence.name\n    nice_name = test_sequence.nice_name\n    notes = test_sequence.notes\n    project = test_sequence.project\n    references = test_sequence.references\n    shots = test_sequence.shots\n    start = test_sequence.start\n    status = test_sequence.status\n    status_list = test_sequence.status_list\n    tags = test_sequence.tags\n    children = test_sequence.children\n    tasks = test_sequence.tasks\n    updated_by = test_sequence.updated_by\n    schedule_model = test_sequence.schedule_model\n    schedule_timing = test_sequence.schedule_timing\n    schedule_unit = test_sequence.schedule_unit\n\n    # delete the test_sequence\n    del test_sequence\n\n    test_sequence_db = Sequence.query.filter_by(name=kwargs[\"name\"]).first()\n\n    assert test_sequence_db.code == code\n    assert test_sequence_db.created_by == created_by\n    assert test_sequence_db.date_created == date_created\n    assert test_sequence_db.date_updated == date_updated\n    assert test_sequence_db.description == description\n    assert test_sequence_db.end == end\n    assert test_sequence_db.name == name\n    assert test_sequence_db.nice_name == nice_name\n    assert test_sequence_db.notes == notes\n    assert test_sequence_db.project == project\n    assert test_sequence_db.references == references\n    assert test_sequence_db.shots == shots\n    assert test_sequence_db.start == start\n    assert test_sequence_db.status == status\n    assert test_sequence_db.status_list == status_list\n    assert test_sequence_db.tags == tags\n    assert test_sequence_db.children == children\n    assert test_sequence_db.tasks == tasks\n    assert test_sequence_db.updated_by == updated_by\n    assert test_sequence_db.schedule_model == schedule_model\n    assert test_sequence_db.schedule_timing == schedule_timing\n    assert test_sequence_db.schedule_unit == schedule_unit\n\n\ndef test_persistence_of_shot(setup_postgresql_db):\n    \"\"\"Persistence of Shot.\"\"\"\n    commercial_project_type = Type(\n        name=\"Commercial Project\",\n        code=\"commproj\",\n        target_entity_type=\"Project\",\n    )\n\n    repo1 = Repository(name=\"Commercial Repository\", code=\"CR\")\n\n    lead = User(name=\"lead\", login=\"lead\", email=\"lead@lead.com\", password=\"password\")\n\n    test_project1 = Project(\n        name=\"Test project\",\n        code=\"tp\",\n        type=commercial_project_type,\n        repository=repo1,\n    )\n    DBSession.add(test_project1)\n    DBSession.commit()\n\n    kwargs = {\n        \"name\": \"Test Sequence 1\",\n        \"code\": \"tseq1\",\n        \"description\": \"this is a test sequence\",\n        \"project\": test_project1,\n        \"responsible\": [lead],\n    }\n    test_seq1 = Sequence(**kwargs)\n    kwargs[\"name\"] = \"Test Sequence 2\"\n    kwargs[\"code\"] = \"tseq2\"\n    test_seq2 = Sequence(**kwargs)\n\n    test_sce1 = Scene(name=\"Test Scene 1\", code=\"tsce1\", project=test_project1)\n\n    test_sce2 = Scene(name=\"Test Scene 2\", code=\"tsce2\", project=test_project1)\n\n    # now add the shots\n    shot_kwargs = {\n        \"code\": \"SH001\",\n        \"project\": test_project1,\n        \"sequence\": test_seq1,\n        \"scene\": test_sce1,\n        \"status\": 0,\n        \"responsible\": [lead],\n    }\n\n    test_shot = Shot(**shot_kwargs)\n\n    DBSession.save([test_shot, test_seq1])\n\n    # store the attributes\n    code = test_shot.code\n    children = test_shot.children\n    cut_duration = test_shot.cut_duration\n    cut_in = test_shot.cut_in\n    cut_out = test_shot.cut_out\n    date_created = test_shot.date_created\n    date_updated = test_shot.date_updated\n    description = test_shot.description\n    name = test_shot.name\n    nice_name = test_shot.nice_name\n    notes = test_shot.notes\n    references = test_shot.references\n    sequence = test_shot.sequence\n    scene = test_shot.scene\n    status = test_shot.status\n    status_list = test_shot.status_list\n    tags = test_shot.tags\n    tasks = test_shot.tasks\n    updated_by = test_shot.updated_by\n    fps = test_shot.fps\n\n    # delete the shot\n    del test_shot\n\n    test_shot_db = Shot.query.filter_by(code=shot_kwargs[\"code\"]).first()\n\n    assert test_shot_db.code == code\n    assert test_shot_db.children == children\n    assert test_shot_db.cut_duration == cut_duration\n    assert test_shot_db.cut_in == cut_in\n    assert test_shot_db.cut_out == cut_out\n    assert test_shot_db.date_created == date_created\n    assert test_shot_db.date_updated == date_updated\n    assert test_shot_db.description == description\n    assert test_shot_db.name == name\n    assert test_shot_db.nice_name == nice_name\n    assert test_shot_db.notes == notes\n    assert test_shot_db.references == references\n    assert test_shot_db.scene == scene\n    assert test_shot_db.sequence == sequence\n    assert test_shot_db.status == status\n    assert test_shot_db.status_list == status_list\n    assert test_shot_db.tags == tags\n    assert test_shot_db.tasks == tasks\n    assert test_shot_db.updated_by == updated_by\n    assert test_shot_db.fps == fps\n\n\ndef test_persistence_of_simple_entity(setup_postgresql_db):\n    \"\"\"Persistence of SimpleEntity.\"\"\"\n    thumbnail = File()\n    DBSession.add(thumbnail)\n    kwargs = {\n        \"name\": \"Simple Entity 1\",\n        \"description\": \"this is for testing purposes\",\n        \"thumbnail\": thumbnail,\n        \"html_style\": \"width: 100px; color: purple\",\n        \"html_class\": \"purple\",\n        \"generic_text\": json.dumps({\"some_string\": \"hello world\"}, sort_keys=True),\n    }\n    test_simple_entity = SimpleEntity(**kwargs)\n    # persist it to the database\n    DBSession.add(test_simple_entity)\n    DBSession.commit()\n\n    created_by = test_simple_entity.created_by\n    date_created = test_simple_entity.date_created\n    date_updated = test_simple_entity.date_updated\n    description = test_simple_entity.description\n    name = test_simple_entity.name\n    nice_name = test_simple_entity.nice_name\n    updated_by = test_simple_entity.updated_by\n    html_style = test_simple_entity.html_style\n    html_class = test_simple_entity.html_class\n    generic_text = test_simple_entity.generic_text\n    stalker_version = test_simple_entity.stalker_version\n\n    del test_simple_entity\n\n    # now try to retrieve it\n    test_simple_entity_db = SimpleEntity.query.filter(\n        SimpleEntity.name == kwargs[\"name\"]\n    ).first()\n\n    assert isinstance(test_simple_entity_db, SimpleEntity)\n\n    assert test_simple_entity_db.created_by == created_by\n    assert test_simple_entity_db.date_created == date_created\n    assert test_simple_entity_db.date_updated == date_updated\n    assert test_simple_entity_db.description == description\n    assert test_simple_entity_db.name == name\n    assert test_simple_entity_db.nice_name == nice_name\n    assert test_simple_entity_db.updated_by == updated_by\n    assert test_simple_entity_db.html_style == html_style\n    assert test_simple_entity_db.html_class == html_class\n    print(test_simple_entity_db.stalker_version)\n    assert test_simple_entity_db.stalker_version == stalker_version\n    assert thumbnail is not None\n    assert test_simple_entity_db.thumbnail == thumbnail\n    assert generic_text is not None\n    assert test_simple_entity_db.generic_text == generic_text\n\n\ndef test_persistence_of_status(setup_postgresql_db):\n    \"\"\"Persistence of Status.\"\"\"\n    # the status\n    kwargs = {\n        \"name\": \"TestStatus_test_creating_Status\",\n        \"description\": \"this is for testing purposes\",\n        \"code\": \"TSTST\",\n    }\n    test_status = Status(**kwargs)\n\n    # persist it to the database\n    DBSession.add(test_status)\n    DBSession.commit()\n\n    # store the attributes\n    code = test_status.code\n    created_by = test_status.created_by\n    date_created = test_status.date_created\n    date_updated = test_status.date_updated\n    description = test_status.description\n    name = test_status.name\n    nice_name = test_status.nice_name\n    notes = test_status.notes\n    tags = test_status.tags\n    updated_by = test_status.updated_by\n\n    # delete the test_status\n    del test_status\n\n    # now try to retrieve it\n    test_status_db = Status.query.filter(Status.name == kwargs[\"name\"]).first()\n\n    assert isinstance(test_status_db, Status)\n\n    # just test the status part of the object\n    assert test_status_db.code == code\n    assert test_status_db.created_by == created_by\n    assert test_status_db.date_created == date_created\n    assert test_status_db.date_updated == date_updated\n    assert test_status_db.description == description\n    assert test_status_db.name == name\n    assert test_status_db.nice_name == nice_name\n    assert test_status_db.notes == notes\n    assert test_status_db.tags == tags\n    assert test_status_db.updated_by == updated_by\n\n\ndef test_persistence_of_status_list(setup_postgresql_db):\n    \"\"\"Persistence of StatusList.\"\"\"\n    # create a couple of statuses\n    statuses = [\n        Status(name=\"Waiting To Start\", code=\"WTS\"),\n        Status(name=\"On Hold A\", code=\"OHA\"),\n        Status(name=\"Work In Progress A\", code=\"WIPA\"),\n        Status(name=\"Complete A\", code=\"CMPLA\"),\n    ]\n\n    kwargs = dict(\n        name=\"Hede Hodo Status List\",\n        statuses=statuses,\n        target_entity_type=\"Hede Hodo\",\n    )\n\n    sequence_status_list = StatusList(**kwargs)\n    DBSession.add(sequence_status_list)\n    DBSession.commit()\n\n    # store the attributes\n    created_by = sequence_status_list.created_by\n    date_created = sequence_status_list.date_created\n    date_updated = sequence_status_list.date_updated\n    description = sequence_status_list.description\n    name = sequence_status_list.name\n    nice_name = sequence_status_list.nice_name\n    notes = sequence_status_list.notes\n    statuses = sequence_status_list.statuses\n    tags = sequence_status_list.tags\n    target_entity_type = sequence_status_list.target_entity_type\n    updated_by = sequence_status_list.updated_by\n\n    # delete the sequence_status_list\n    del sequence_status_list\n\n    # now get it back\n    sequence_status_list_db = StatusList.query.filter_by(name=kwargs[\"name\"]).first()\n\n    assert isinstance(sequence_status_list_db, StatusList)\n\n    assert sequence_status_list_db.created_by == created_by\n    assert sequence_status_list_db.date_created == date_created\n    assert sequence_status_list_db.date_updated == date_updated\n    assert sequence_status_list_db.description == description\n    assert sequence_status_list_db.name == name\n    assert sequence_status_list_db.nice_name == nice_name\n    assert sequence_status_list_db.notes == notes\n    assert sequence_status_list_db.statuses == statuses\n    assert sequence_status_list_db.tags == tags\n    assert sequence_status_list_db.target_entity_type == target_entity_type\n    assert sequence_status_list_db.updated_by == updated_by\n\n    # try to create another StatusList for the same target_entity_type\n    # and do not expect an IntegrityError unless it is committed.\n    kwargs[\"name\"] = \"new Sequence Status List\"\n    new_sequence_list = StatusList(**kwargs)\n\n    DBSession.add(new_sequence_list)\n    assert new_sequence_list in DBSession\n    with pytest.raises(IntegrityError) as cm:\n        DBSession.commit()\n\n    assert (\n        \"(psycopg2.errors.UniqueViolation) duplicate key value \"\n        \"violates unique constraint \"\n        '\"StatusLists_target_entity_type_key\"' in str(cm.value)\n    )\n\n    # roll it back\n    DBSession.rollback()\n\n\ndef test_persistence_of_structure(setup_postgresql_db):\n    \"\"\"Persistence of Structure.\"\"\"\n    # create pipeline steps for character\n    modeling_task_type = Type(\n        name=\"Modeling\",\n        code=\"model\",\n        description=\"This is the step where all the modeling job is done\",\n        target_entity_type=\"Task\",\n    )\n\n    animation_task_type = Type(\n        name=\"Animation\",\n        description=\"This is the step where all the animation job is \"\n        \"done it is not limited with characters, other \"\n        \"things can also be animated\",\n        code=\"Anim\",\n        target_entity_type=\"Task\",\n    )\n\n    # create a new asset Type\n    char_asset_type = Type(\n        name=\"Character\",\n        code=\"char\",\n        description=\"This is the asset type which covers animated \" \"characters\",\n        target_entity_type=\"Asset\",\n    )\n\n    # get the Version Type for FilenameTemplates\n    v_type = (\n        Type.query.filter_by(target_entity_type=\"FilenameTemplate\")\n        .filter_by(name=\"Version\")\n        .first()\n    )\n\n    # create a new type template for character assets\n    asset_template = FilenameTemplate(\n        name=\"Character Asset Template\",\n        description=\"This is the template for character assets\",\n        path=\"Assets/{{asset_type.name}}/{{pipeline_step.code}}\",\n        filename=\"{{asset.name}}_{{asset_type.name}}\"\n        \"_r{{version.revision_number}}\"\n        \"_v{{version.version_number}}\",\n        target_entity_type=\"Asset\",\n        type=v_type,\n    )\n\n    # create a new file type\n    image_file_type = Type(\n        name=\"Image\",\n        code=\"image\",\n        description=\"It is used for image files.\",\n        target_entity_type=\"File\",\n    )\n\n    # get reference Type of FilenameTemplates\n    r_type = (\n        Type.query.filter_by(target_entity_type=\"FilenameTemplate\")\n        .filter_by(name=\"Reference\")\n        .first()\n    )\n\n    # create a new template for references\n    image_reference_template = FilenameTemplate(\n        name=\"Image Reference Template\",\n        description=\"this is the template for image references, it \"\n        \"shows where to place the image files\",\n        path=\"REFS/{{reference.type.name}}\",\n        filename=\"{{reference.file_name}}\",\n        target_entity_type=\"File\",\n        type=r_type,\n    )\n\n    commercial_structure_type = Type(\n        name=\"Commercial\", code=\"commercial\", target_entity_type=\"Structure\"\n    )\n\n    # create a new structure\n    kwargs = {\n        \"name\": \"Commercial Structure\",\n        \"description\": \"The structure for commercials\",\n        \"custom_template\": \"\"\"\n            Assets\n            Sequences\n            Sequences/{% for sequence in project.sequences %}\n            {{sequence.code}}\"\"\",\n        \"templates\": [asset_template, image_reference_template],\n        \"type\": commercial_structure_type,\n    }\n    new_structure = Structure(**kwargs)\n    DBSession.add_all(\n        [\n            new_structure,\n            modeling_task_type,\n            animation_task_type,\n            char_asset_type,\n            image_file_type,\n        ]\n    )\n    DBSession.commit()\n\n    # store the attributes\n    templates = new_structure.templates\n    created_by = new_structure.created_by\n    date_created = new_structure.date_created\n    date_updated = new_structure.date_updated\n    description = new_structure.description\n    name = new_structure.name\n    nice_name = new_structure.nice_name\n    notes = new_structure.notes\n    custom_template = new_structure.custom_template\n    tags = new_structure.tags\n    updated_by = new_structure.updated_by\n\n    # delete the new_structure\n    del new_structure\n\n    new_structure_db = Structure.query.filter_by(name=kwargs[\"name\"]).first()\n\n    assert isinstance(new_structure_db, Structure)\n\n    assert new_structure_db.templates == templates\n    assert new_structure_db.created_by == created_by\n    assert new_structure_db.date_created == date_created\n    assert new_structure_db.date_updated == date_updated\n    assert new_structure_db.description == description\n    assert new_structure_db.name == name\n    assert new_structure_db.nice_name == nice_name\n    assert new_structure_db.notes == notes\n    assert new_structure_db.custom_template == custom_template\n    assert new_structure_db.tags == tags\n    assert new_structure_db.updated_by == updated_by\n\n\ndef test_persistence_of_studio(setup_postgresql_db):\n    \"\"\"Persistence of Studio.\"\"\"\n    test_studio = Studio(name=\"Test Studio\")\n    DBSession.add(test_studio)\n    DBSession.commit()\n\n    # customize attributes\n    test_studio.daily_working_hours = 11\n    test_studio.working_hours = WorkingHours(\n        working_hours={\"mon\": [], \"sat\": [[100, 1300]]}\n    )\n    test_studio.timing_resolution = datetime.timedelta(hours=1, minutes=30)\n\n    name = test_studio.name\n    daily_working_hours = test_studio.daily_working_hours\n    timing_resolution = test_studio._timing_resolution\n    working_hours = test_studio.working_hours\n    # now = test_studio.now\n\n    del test_studio\n\n    # get it back\n    test_studio_db = Studio.query.first()\n\n    assert test_studio_db.name == name\n    assert test_studio_db.daily_working_hours == daily_working_hours\n    assert test_studio_db.timing_resolution == timing_resolution\n    assert test_studio_db.working_hours == working_hours\n\n\ndef test_persistence_of_tag(setup_postgresql_db):\n    \"\"\"Persistence of Tag.\"\"\"\n    name = \"Tag_test_creating_a_Tag\"\n    description = \"this is for testing purposes\"\n    created_by = None\n    updated_by = None\n    date_created = date_updated = datetime.datetime.now(pytz.utc)\n    tag = Tag(\n        name=name,\n        description=description,\n        created_by=created_by,\n        updated_by=updated_by,\n        date_created=date_created,\n        date_updated=date_updated,\n    )\n\n    # persist it to the database\n    DBSession.add(tag)\n    DBSession.commit()\n\n    # store the attributes\n    description = tag.description\n    created_by = tag.created_by\n    updated_by = tag.updated_by\n    date_created = tag.date_created\n    date_updated = tag.date_updated\n\n    # delete the aTag\n    del tag\n\n    # now try to retrieve it\n    tag_db = DBSession.query(Tag).filter_by(name=name).first()\n\n    assert isinstance(tag_db, Tag)\n\n    assert tag_db.name == name\n    assert tag_db.description == description\n    assert tag_db.created_by == created_by\n    assert tag_db.updated_by == updated_by\n    assert tag_db.date_created == date_created\n    assert tag_db.date_updated == date_updated\n\n\ndef test_persistence_of_task(setup_postgresql_db):\n    \"\"\"Persistence of Task.\"\"\"\n    # create a task\n    user1 = User(\n        name=\"User1\",\n        login=\"user1\",\n        email=\"user1@user.com\",\n        password=\"1234\",\n    )\n    user2 = User(\n        name=\"User2\",\n        login=\"user2\",\n        email=\"user2@user.com\",\n        password=\"1234\",\n    )\n    user3 = User(\n        name=\"User3\",\n        login=\"user3\",\n        email=\"user3@user.com\",\n        password=\"1234\",\n    )\n    repo = Repository(\n        name=\"Test Repo\",\n        code=\"TR\",\n        linux_path=\"/mnt/M/JOBs\",\n        windows_path=\"M:/JOBs\",\n        macos_path=\"/Users/Shared/Servers/M\",\n    )\n    project1 = Project(\n        name=\"Tests Project\",\n        code=\"tp\",\n        repository=repo,\n    )\n    DBSession.add(project1)\n    DBSession.commit()\n    char_asset_type = Type(\n        name=\"Character Asset\", code=\"char\", target_entity_type=\"Asset\"\n    )\n    asset1 = Asset(\n        name=\"Char1\",\n        code=\"char1\",\n        type=char_asset_type,\n        project=project1,\n        responsible=[user1],\n    )\n    task1 = Task(\n        name=\"Test Task\",\n        watchers=[user3],\n        parent=asset1,\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Hour,\n        schedule_model=ScheduleModel.Effort,\n        schedule_constraint=ScheduleConstraint.Start,\n    )\n    child_task1 = Task(\n        name=\"Child Task 1\",\n        resources=[user1, user2],\n        parent=task1,\n    )\n    child_task2 = Task(\n        name=\"Child Task 2\",\n        resources=[user1, user2],\n        parent=task1,\n    )\n    task2 = Task(\n        name=\"Another Task\",\n        project=project1,\n        resources=[user1],\n        responsible=[user2],\n    )\n    DBSession.add_all([asset1, task1, child_task1, child_task2, task2])\n    DBSession.commit()\n\n    # time logs\n    time_log1 = TimeLog(\n        task=child_task1,\n        resource=user1,\n        start=datetime.datetime.now(pytz.utc),\n        end=datetime.datetime.now(pytz.utc) + datetime.timedelta(1),\n    )\n    task1.computed_start = datetime.datetime.now(pytz.utc)\n    task1.computed_end = datetime.datetime.now(pytz.utc) + datetime.timedelta(10)\n    time_log2 = TimeLog(\n        task=child_task2,\n        resource=user1,\n        start=datetime.datetime.now(pytz.utc) + datetime.timedelta(1),\n        end=datetime.datetime.now(pytz.utc) + datetime.timedelta(2),\n    )\n    # time log for another task\n    time_log3 = TimeLog(\n        task=task2,\n        resource=user1,\n        start=datetime.datetime.now(pytz.utc) + datetime.timedelta(2),\n        end=datetime.datetime.now(pytz.utc) + datetime.timedelta(3),\n    )\n    # Versions\n    repr_type = Type(name=\"Representation\", code=\"Repr\", target_entity_type=\"File\")\n    DBSession.save(repr_type)\n\n    file1 = File(name=\"Version1 Base Repr\", type=repr_type)\n    file2 = File(name=\"Version2 Base Repr\", type=repr_type)\n    file3 = File(name=\"Version3 Base Repr\", type=repr_type)\n    file4 = File(name=\"Version4 Base Repr\", type=repr_type)\n    DBSession.save([file1, file2, file3, file4])\n    file3.references = [file2]\n    file2.references = [file1, file4]\n    DBSession.commit()\n\n    version1 = Version(task=task1)\n    DBSession.add(version1)\n    DBSession.commit()\n\n    version2 = Version(task=task1)\n    DBSession.add(version2)\n    DBSession.commit()\n\n    version3 = Version(task=task2)\n    DBSession.add(version3)\n    DBSession.commit()\n\n    version4 = Version(task=task2)\n    DBSession.add(version4)\n    DBSession.commit()\n\n    DBSession.add(version1)\n    DBSession.commit()\n\n    # references\n    ref1 = File(full_path=\"some_path\", original_filename=\"original_filename\")\n    ref2 = File(full_path=\"some_path\", original_filename=\"original_filename\")\n    task1.references.append(ref1)\n    task1.references.append(ref2)\n\n    DBSession.add_all(\n        [\n            task1,\n            child_task1,\n            child_task2,\n            task2,\n            time_log1,\n            time_log2,\n            time_log3,\n            user1,\n            user2,\n            version1,\n            version2,\n            version3,\n            version4,\n            ref1,\n            ref2,\n        ]\n    )\n    DBSession.commit()\n\n    computed_start = task1.computed_start\n    computed_end = task1.computed_end\n    created_by = task1.created_by\n    date_created = task1.date_created\n    date_updated = task1.date_updated\n    duration = task1.duration\n    end = task1.end\n    is_milestone = task1.is_milestone\n    name = task1.name\n    parent = task1.parent\n    priority = task1.priority\n    resources = task1.resources\n    schedule_unit = task1.schedule_unit\n    schedule_constraint = task1.schedule_constraint\n    schedule_model = task1.schedule_model\n    schedule_timing = task1.schedule_timing\n    schedule_unit = task1.schedule_unit\n    start = task1.start\n    status = task1.status\n    status_list = task1.status_list\n    tasks = task1.tasks\n    tags = task1.tags\n    time_logs = task1.time_logs\n    type_ = task1.type\n    updated_by = task1.updated_by\n    versions = [version1, version2]\n    watchers = task1.watchers\n\n    del task1\n\n    # now query it back\n    task1_db = Task.query.filter_by(name=name).first()\n\n    assert isinstance(task1_db, Task)\n\n    assert task1_db.time_logs == time_logs\n    assert task1_db.created_by == created_by\n    assert task1_db.computed_start == computed_start\n    assert task1_db.computed_end == computed_end\n    assert task1_db.date_created == date_created\n    assert task1_db.date_updated == date_updated\n    assert task1_db.duration == duration\n    assert task1_db.end == end\n    assert task1_db.is_milestone == is_milestone\n    assert task1_db.name == name\n    assert task1_db.parent == parent\n    assert task1_db.priority == priority\n    assert resources == []  # it is a parent task, no child\n    assert task1_db.resources == resources\n    assert task1_db.start == start\n    assert task1_db.status == status\n    assert task1_db.status_list == status_list\n    assert task1_db.tags == tags\n    assert sorted(tasks, key=lambda x: x.name) == sorted(\n        task1_db.tasks, key=lambda x: x.name\n    )\n    assert len([child_task1, child_task2]) == len(tasks)\n    assert sorted([child_task1, child_task2], key=lambda x: x.name) == sorted(\n        tasks, key=lambda x: x.name\n    )\n    assert task1_db.type == type_\n    assert task1_db.updated_by == updated_by\n    assert task1_db.versions == versions\n    assert task1_db.watchers == watchers\n    assert task1_db.schedule_unit == schedule_unit\n    assert isinstance(task1_db.schedule_unit, TimeUnit)\n    assert task1_db.schedule_constraint == schedule_constraint\n    assert isinstance(task1_db.schedule_constraint, ScheduleConstraint)\n    assert task1_db.schedule_model == schedule_model\n    assert isinstance(task1_db.schedule_model, ScheduleModel)\n    assert task1_db.schedule_timing == schedule_timing\n    assert task1_db.schedule_unit == schedule_unit\n\n    DBSession.delete(task1_db)\n    DBSession.commit()\n\n    # we still should have the versions that are in the inputs (version3\n    # and version4) of the original versions (version1, version2)\n    assert DBSession.get(Version, version3.id) is not None\n    assert DBSession.get(Version, version4.id) is not None\n\n    # Expect to have all child tasks also to be deleted\n    assert sorted([asset1, task2], key=lambda x: x.name) == sorted(\n        Task.query.all(), key=lambda x: x.name\n    )\n\n    # Expect to have time logs related to this task are deleted\n    assert TimeLog.query.all() == [time_log3]\n\n    # We still should have the users intact\n    admin = User.query.filter_by(name=\"admin\").first()\n    assert sorted([user1, user2, user3, admin], key=lambda x: x.name) == sorted(\n        User.query.all(), key=lambda x: x.name\n    )\n\n    # When updating the test to include deletion, the test task became a\n    # parent task, so all the resources are removed, thus the resource\n    # attribute should be tested separately.\n    resources = task2.resources\n    id_ = task2.id\n    del task2\n\n    another_task_db = DBSession.get(Task, id_)\n    assert resources == [user1]\n    assert another_task_db.resources == resources\n\n\ndef test_persistence_of_review(setup_postgresql_db):\n    \"\"\"Persistence of Review.\"\"\"\n    # create a task\n    repo = Repository(\n        name=\"Test Repo\",\n        code=\"TR\",\n        linux_path=\"/some/random/path\",\n        windows_path=\"/some/random/path\",\n        macos_path=\"/some/random/path\",\n    )\n    user1 = User(\n        name=\"User1\",\n        login=\"user1\",\n        email=\"user1@user.com\",\n        password=\"1234\",\n    )\n    user2 = User(\n        name=\"User2\",\n        login=\"user2\",\n        email=\"user2@user.com\",\n        password=\"1234\",\n    )\n    user3 = User(\n        name=\"User3\",\n        login=\"user3\",\n        email=\"user3@user.com\",\n        password=\"1234\",\n    )\n    project1 = Project(\n        name=\"Tests Project\",\n        code=\"tp\",\n        repository=repo,\n    )\n    char_asset_type = Type(\n        name=\"Character Asset\", code=\"char\", target_entity_type=\"Asset\"\n    )\n    asset1 = Asset(\n        name=\"Char1\",\n        code=\"char1\",\n        type=char_asset_type,\n        project=project1,\n        responsible=[user1],\n    )\n    task1 = Task(\n        name=\"Test Task\",\n        watchers=[user3],\n        parent=asset1,\n        schedule_timing=5,\n        schedule_unit=TimeUnit.Hour,\n    )\n    child_task1 = Task(\n        name=\"Child Task 1\",\n        resources=[user1, user2],\n        parent=task1,\n    )\n    child_task2 = Task(\n        name=\"Child Task 2\",\n        resources=[user1, user2],\n        parent=task1,\n    )\n    task2 = Task(\n        name=\"Another Task\",\n        project=project1,\n        resources=[user1],\n        responsible=[user1],\n    )\n    # time logs\n    time_log1 = TimeLog(\n        task=child_task1,\n        resource=user1,\n        start=datetime.datetime.now(pytz.utc),\n        end=datetime.datetime.now(pytz.utc) + datetime.timedelta(1),\n    )\n    task1.computed_start = datetime.datetime.now(pytz.utc)\n    task1.computed_end = datetime.datetime.now(pytz.utc) + datetime.timedelta(10)\n\n    time_log2 = TimeLog(\n        task=child_task2,\n        resource=user1,\n        start=datetime.datetime.now(pytz.utc) + datetime.timedelta(1),\n        end=datetime.datetime.now(pytz.utc) + datetime.timedelta(2),\n    )\n    # time log for another task\n    time_log3 = TimeLog(\n        task=task2,\n        resource=user1,\n        start=datetime.datetime.now(pytz.utc) + datetime.timedelta(2),\n        end=datetime.datetime.now(pytz.utc) + datetime.timedelta(3),\n    )\n    DBSession.save(\n        [\n            task1,\n            child_task1,\n            child_task2,\n            task2,\n            time_log1,\n            time_log2,\n            time_log3,\n            user1,\n            user2,\n        ]\n    )\n\n    version1 = Version(task=task2)\n    DBSession.save(version1)\n\n    rev1 = Review(\n        task=task2,\n        reviewer=user1,\n        version=version1,\n        schedule_timing=1,\n        schedule_unit=TimeUnit.Hour,\n    )\n    DBSession.save(rev1)\n\n    created_by = rev1.created_by\n    date_created = rev1.date_created\n    date_updated = rev1.date_updated\n    name = rev1.name\n    schedule_timing = rev1.schedule_timing\n    schedule_unit = rev1.schedule_unit\n    task = rev1.task\n    updated_by = rev1.updated_by\n    version = rev1.version\n\n    del rev1\n\n    # now query it back\n    rev1_db = Review.query.filter_by(name=name).first()\n\n    assert isinstance(rev1_db, Review)\n\n    assert rev1_db.created_by == created_by\n    assert rev1_db.date_created == date_created\n    assert rev1_db.date_updated == date_updated\n    assert rev1_db.name == name\n    assert rev1_db.task == task\n    assert rev1_db.updated_by == updated_by\n    assert rev1_db.schedule_timing == schedule_timing\n    assert rev1_db.schedule_unit == schedule_unit\n    assert rev1_db.version == version\n\n    # delete tests\n\n    # deleting a Review should be fairly simple:\n    DBSession.delete(rev1_db)\n    DBSession.commit()\n\n    # Expect to have no task is deleted\n    assert sorted(\n        [asset1, task1, task2, child_task1, child_task2], key=lambda x: x.name\n    ) == sorted(Task.query.all(), key=lambda x: x.name)\n\n\ndef test_persistence_of_ticket(setup_postgresql_db):\n    \"\"\"Persistence of Ticket.\"\"\"\n    repo = Repository(name=\"Test Repository\", code=\"TR\")\n    proj_structure = Structure(name=\"Commercials Structure\")\n    proj1 = Project(\n        name=\"Test Project 1\",\n        code=\"TP1\",\n        repository=repo,\n        structure=proj_structure,\n    )\n    simple_entity = SimpleEntity(name=\"Test Simple Entity\")\n    entity = Entity(name=\"Test Entity\")\n    user1 = User(name=\"user 1\", login=\"user1\", email=\"user1@users.com\", password=\"pass\")\n    user2 = User(name=\"user 2\", login=\"user2\", email=\"user2@users.com\", password=\"pass\")\n    note1 = Note(content=\"This is the content of the note 1\")\n    note2 = Note(content=\"This is the content of the note 2\")\n\n    related_ticket1 = Ticket(project=proj1)\n    DBSession.add(related_ticket1)\n    DBSession.commit()\n\n    related_ticket2 = Ticket(project=proj1)\n    DBSession.add(related_ticket2)\n    DBSession.commit()\n\n    # create Tickets\n    test_ticket = Ticket(\n        project=proj1,\n        links=[simple_entity, entity],\n        notes=[note1, note2],\n        reported_by=user1,\n        related_tickets=[related_ticket1, related_ticket2],\n    )\n\n    test_ticket.reassign(user1, user2)\n    test_ticket.priority = \"MAJOR\"\n\n    DBSession.add(test_ticket)\n    DBSession.commit()\n\n    comments = test_ticket.comments\n    created_by = test_ticket.created_by\n    date_created = test_ticket.date_created\n    date_updated = test_ticket.date_updated\n    description = test_ticket.description\n    logs = test_ticket.logs\n    links = test_ticket.links\n    name = test_ticket.name\n    notes = test_ticket.notes\n    number = test_ticket.number\n    owner = test_ticket.owner\n    priority = test_ticket.priority\n    project = test_ticket.project\n    related_tickets = test_ticket.related_tickets\n    reported_by = test_ticket.reported_by\n    resolution = test_ticket.resolution\n    status = test_ticket.status\n    type_ = test_ticket.type\n    updated_by = test_ticket.updated_by\n\n    del test_ticket\n\n    # now query it back\n    test_ticket_db = Ticket.query.filter_by(name=name).first()\n\n    assert comments == test_ticket_db.comments\n    assert created_by == test_ticket_db.created_by\n    assert date_created == test_ticket_db.date_created\n    assert date_updated == test_ticket_db.date_updated\n    assert description == test_ticket_db.description\n    assert logs != []\n    assert logs == test_ticket_db.logs\n    assert links == test_ticket_db.links\n    assert name == test_ticket_db.name\n    assert notes == test_ticket_db.notes\n    assert number == test_ticket_db.number\n    assert owner == test_ticket_db.owner\n    assert priority == test_ticket_db.priority\n    assert project == test_ticket_db.project\n    assert related_tickets == test_ticket_db.related_tickets\n    assert reported_by == test_ticket_db.reported_by\n    assert resolution == test_ticket_db.resolution\n    assert status == test_ticket_db.status\n    assert type_ == test_ticket_db.type\n    assert updated_by == test_ticket_db.updated_by\n\n    # delete tests\n    # Deleting a Ticket should also delete all the logs related to the\n    # ticket\n    assert sorted(test_ticket_db.logs, key=lambda x: x.name) == sorted(\n        logs, key=lambda x: x.name\n    )\n\n    DBSession.delete(test_ticket_db)\n    DBSession.commit()\n    assert TicketLog.query.all() == []\n\n\ndef test_persistence_of_user(setup_postgresql_db):\n    \"\"\"Persistence of User.\"\"\"\n    # create a new user save and retrieve it back\n\n    # create a Department for the user\n    dep_kwargs = {\n        \"name\": \"Test Department\",\n        \"description\": \"This department has been created for testing \\\n        purposes\",\n    }\n    new_department = Department(**dep_kwargs)\n\n    # create the user\n    user_kwargs = {\n        \"name\": \"Test\",\n        \"login\": \"testuser\",\n        \"email\": \"testuser@test.com\",\n        \"password\": \"12345\",\n        \"description\": \"This user has been created for testing purposes\",\n        \"departments\": [new_department],\n        \"efficiency\": 2.5,\n    }\n    user1 = User(**user_kwargs)\n    DBSession.add_all([user1, new_department])\n    DBSession.commit()\n\n    vacation1 = Vacation(\n        user=user1,\n        start=datetime.datetime.now(pytz.utc),\n        end=datetime.datetime.now(pytz.utc) + datetime.timedelta(1),\n    )\n    vacation2 = Vacation(\n        user=user1,\n        start=datetime.datetime.now(pytz.utc) + datetime.timedelta(2),\n        end=datetime.datetime.now(pytz.utc) + datetime.timedelta(3),\n    )\n\n    user1.vacations.append(vacation1)\n    user1.vacations.append(vacation2)\n    DBSession.add(user1)\n    DBSession.commit()\n\n    # create a test project\n    repo1 = Repository(name=\"Test Repo\", code=\"TR\")\n    project1 = Project(\n        name=\"Test Project\",\n        code=\"TP\",\n        repository=repo1,\n    )\n    task1 = Task(\n        name=\"Test Task 1\", project=project1, resources=[user1], responsible=[user1]\n    )\n    dt = datetime.datetime\n    td = datetime.timedelta\n    time_log1 = TimeLog(\n        task=task1,\n        resource=user1,\n        start=dt.now(pytz.utc),\n        end=dt.now(pytz.utc) + td(1),\n    )\n    DBSession.add(time_log1)\n    DBSession.add(task1)\n    DBSession.commit()\n\n    # store attributes\n    created_by = user1.created_by\n    date_created = user1.date_created\n    date_updated = user1.date_updated\n    departments = [dep for dep in user1.departments]\n    description = user1.description\n    efficiency = user1.efficiency\n    email = user1.email\n    authentication_logs = user1.authentication_logs\n    login = user1.login\n    name = user1.name\n    nice_name = user1.nice_name\n    notes = user1.notes\n    password = user1.password\n    groups = user1.groups\n    projects = [project for project in user1.projects]\n    tags = user1.tags\n    tasks = user1.tasks\n    watching = user1.watching\n    updated_by = user1.updated_by\n    vacations = [vacation1, vacation2]\n\n    # delete new_user\n    del user1\n\n    user1_db = User.query.filter(User.name == user_kwargs[\"name\"]).first()\n\n    assert isinstance(user1_db, User)\n\n    # the user itself\n    # assert new_user in new_user_DB\n    assert user1_db.created_by == created_by\n    assert user1_db.date_created == date_created\n    assert user1_db.date_updated == date_updated\n    assert user1_db.departments == departments\n    assert user1_db.description == description\n    assert user1_db.efficiency == efficiency\n    assert user1_db.email == email\n    assert user1_db.authentication_logs == authentication_logs\n    assert user1_db.login == login\n    assert user1_db.name == name\n    assert user1_db.nice_name == nice_name\n    assert user1_db.notes == notes\n    assert user1_db.password == password\n    assert user1_db.groups == groups\n    assert user1_db.projects == projects\n    assert user1_db.tags == tags\n    assert user1_db.tasks == tasks\n    assert sorted(vacations, key=lambda x: x.name) == sorted(\n        user1_db.vacations, key=lambda x: x.name\n    )\n    assert user1_db.watching == watching\n    assert user1_db.updated_by == updated_by\n\n    # as the member of a department\n    department_db = Department.query.filter(\n        Department.name == dep_kwargs[\"name\"]\n    ).first()\n\n    assert user1_db == department_db.users[0]\n\n    # delete tests\n    assert sorted([vacation1, vacation2], key=lambda x: x.name) == sorted(\n        Vacation.query.all(), key=lambda x: x.name\n    )\n\n    # deleting a user should also delete its vacations\n    DBSession.delete(user1_db)\n    DBSession.commit()\n\n    assert Vacation.query.all() == []\n\n    # deleting a user should also delete the time logs\n    assert TimeLog.query.all() == []\n\n\ndef test_persistence_of_authentication_log(setup_postgresql_db):\n    \"\"\"Persistence of AuthenticationLog.\"\"\"\n    user1 = User(\n        name=\"Test User 1\",\n        login=\"tuser1\",\n        email=\"tuser1@users.com\",\n        password=\"sosecret\",\n    )\n    DBSession.add(user1)\n    DBSession.commit()\n\n    al1 = AuthenticationLog(\n        user=user1, action=LOGIN, date=datetime.datetime.now(pytz.utc)\n    )\n    al2 = AuthenticationLog(\n        user=user1,\n        action=LOGOUT,\n        date=datetime.datetime.now(pytz.utc) + datetime.timedelta(minutes=10),\n    )\n    DBSession.add_all([al1, al2])\n    DBSession.commit()\n\n    al1_id = al1.id\n    action = al1.action\n    date = al1.date\n\n    del al1\n\n    al1_from_db = DBSession.get(AuthenticationLog, al1_id)\n\n    assert al1_from_db.user == user1\n    assert al1_from_db.date == date\n    assert al1_from_db.action == action\n\n    # check if users data is also updated\n    assert sorted(user1.authentication_logs) == sorted([al1_from_db, al2])\n\n    # delete tests\n    DBSession.delete(al1_from_db)\n    DBSession.commit()\n\n    # check the user still exists\n    user1_from_db = DBSession.get(User, user1.id)\n    assert user1_from_db is not None\n\n    # check if the other log is still there\n    al2_from_db = DBSession.get(AuthenticationLog, al2.id)\n    assert al2_from_db is not None\n\n    # delete the other AuthenticationLog\n    DBSession.delete(al2_from_db)\n    DBSession.commit()\n\n    # check if the user is still there\n    user1_from_db = DBSession.get(User, user1.id)\n    assert user1_from_db is not None\n\n\ndef test_persistence_of_vacation(setup_postgresql_db):\n    \"\"\"Persistence of Vacation instances.\"\"\"\n    # create a User\n    new_user = User(\n        name=\"Test User\", login=\"testuser\", email=\"test@user.com\", password=\"secret\"\n    )\n\n    # personal vacation type\n    personal_vacation = Type(\n        name=\"Personal\", code=\"PERS\", target_entity_type=\"Vacation\"\n    )\n\n    start = datetime.datetime(2013, 6, 7, 15, 0, tzinfo=pytz.utc)\n    end = datetime.datetime(2013, 6, 21, 0, 0, tzinfo=pytz.utc)\n    vacation = Vacation(user=new_user, type=personal_vacation, start=start, end=end)\n\n    DBSession.add(vacation)\n    DBSession.commit()\n    name = vacation.name\n\n    del vacation\n\n    # get it back\n    vacation_db = Vacation.query.filter_by(name=name).first()\n\n    assert isinstance(vacation_db, Vacation)\n    assert vacation_db.user == new_user\n    assert vacation_db.start == start\n    assert vacation_db.end == end\n    assert vacation_db.type == personal_vacation\n\n\ndef test_persistence_of_version(setup_postgresql_db):\n    \"\"\"Persistence of Version instances.\"\"\"\n    # create a FilenameTemplate for Tasks\n    test_filename_template = FilenameTemplate(\n        name=\"Task Filename Template\",\n        target_entity_type=\"Task\",\n        path=\"{{project.code}}/{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/{%- endfor -%}\",\n        filename=\"{{version.nice_name}}\"\n        '_r{{\"%02d\"|format(version.revision_number)}}'\n        '_v{{\"%03d\"|format(version.version_number)}}',\n    )\n    DBSession.add(test_filename_template)\n    DBSession.commit()\n\n    # create a Structure\n    test_structure = Structure(\n        name=\"Project Structure\",\n        templates=[test_filename_template],\n    )\n    DBSession.add(test_structure)\n    DBSession.commit()\n\n    # create a project\n    test_project = Project(\n        name=\"Test Project\",\n        code=\"tp\",\n        repository=Repository(\n            name=\"Film Projects\",\n            code=\"FP\",\n            windows_path=\"M:/\",\n            linux_path=\"/mnt/M/\",\n            macos_path=\"/Users/Volumes/M/\",\n        ),\n        structure=test_structure,\n    )\n    DBSession.add(test_project)\n    DBSession.commit()\n\n    # create a task\n    test_task = Task(\n        name=\"Modeling\",\n        project=test_project,\n        responsible=[User(name=\"user1\", login=\"user1\", email=\"u@u\", password=\"12\")],\n    )\n    DBSession.add(test_task)\n    DBSession.commit()\n\n    # create a new version\n    test_version = Version(\n        name=\"version for task modeling\",\n        task=test_task,\n        revision_number=12,\n        full_path=\"M:/Shows/Proj1/Seq1/Shots/SH001/Lighting\"\n        \"/Proj1_Seq1_Sh001_MAIN_Lighting_v001.ma\",\n        outputs=[\n            File(\n                name=\"Renders\",\n                full_path=\"M:/Shows/Proj1/Seq1/Shots/SH001/Lighting/\"\n                \"Output/test1.###.jpg\",\n            ),\n        ],\n    )\n\n    # now save it to the database\n    DBSession.add(test_version)\n    DBSession.commit()\n    assert test_version.revision_number == 12\n    assert test_version.version_number == 1\n\n    # create a new version\n    test_version_2 = Version(\n        name=\"version for task modeling\",\n        task=test_task,\n        revision_number=12,\n        full_path=\"M:/Shows/Proj1/Seq1/Shots/SH001/Lighting\"\n        \"/Proj1_Seq1_Sh001_MAIN_Lighting_v002.ma\",\n    )\n    DBSession.add(test_version_2)\n    DBSession.commit()\n    assert test_version_2.revision_number == 12\n    assert test_version_2.version_number == 2\n\n    created_by = test_version.created_by\n    date_created = test_version.date_created\n    date_updated = test_version.date_updated\n    name = test_version.name\n    nice_name = test_version.nice_name\n    notes = test_version.notes\n    files = test_version.files\n    is_published = test_version.is_published\n    full_path = test_version.generate_path()\n    tags = test_version.tags\n    type_ = test_version.type\n    updated_by = test_version.updated_by\n    revision_number = test_version.revision_number\n    assert revision_number == 12\n    version_number = test_version.version_number\n    task = test_version.task\n\n    del test_version\n\n    # get it back from the db\n    test_version_db = Version.query.filter_by(version_number=1).first()\n\n    assert isinstance(test_version_db, Version)\n\n    assert test_version_db.created_by == created_by\n    assert test_version_db.date_created == date_created\n    assert test_version_db.date_updated == date_updated\n    assert test_version_db.name == name\n    assert test_version_db.nice_name == nice_name\n    assert test_version_db.notes == notes\n    assert test_version_db.files == files\n    assert test_version_db.is_published == is_published\n    assert test_version_db.generate_path() == full_path\n    assert test_version_db.tags == tags\n    assert test_version_db.type == type_\n    assert test_version_db.updated_by == updated_by\n    assert test_version_db.version_number == version_number\n    assert test_version_db.task == task\n    assert test_version_db.revision_number == revision_number\n\n    # try to delete version and expect the task, user and other versions\n    # to be intact\n    DBSession.delete(test_version_db)\n    DBSession.commit()\n\n    # version_2\n    test_version_3 = Version(\n        name=\"version for task modeling\",\n        task=test_task,\n        full_path=\"M:/Shows/Proj1/Seq1/Shots/SH001/Lighting\"\n        \"/Proj1_Seq1_Sh001_MAIN_Lighting_v003.ma\",\n    )\n    DBSession.add(test_version_3)\n    DBSession.commit()\n\n    # now delete test_version_2\n    DBSession.delete(test_version_2)\n    DBSession.commit()\n\n    # and check if test_version_3 is still present in the database\n    test_version_3_db = (\n        Version.query.filter(Version.name == test_version_3.name)\n        .filter(Version.task == test_version_3.task)\n        .filter(Version.version_number == test_version_3.version_number)\n        .first()\n    )\n\n    assert test_version_3_db is not None\n    assert test_version_3_db.task == test_version_3.task\n    assert test_version_3_db.version_number == test_version_3.version_number\n\n    # create a new version append it to version_3.children and then delete\n    # version_3\n    test_version_4 = Version(name=\"version for task modeling\", task=test_task)\n    test_version_3.children.append(test_version_4)\n    DBSession.save(test_version_4)\n    assert test_version_3.children == [test_version_4]\n    assert test_version_4.parent == test_version_3\n\n    # and check if test_version_4 is still present in the database\n    test_version_4_db = (\n        Version.query.filter(Version.name == test_version_4.name)\n        .filter(Version.task == test_version_4.task)\n        .filter(Version.version_number == test_version_4.version_number)\n        .first()\n    )\n\n    assert test_version_4_db is not None\n    assert test_version_4_db.task == test_version_4.task\n    assert test_version_4_db.version_number == test_version_4.version_number\n    assert test_version_4_db.parent == test_version_3\n\n    # now delete test_version_3\n    DBSession.delete(test_version_3)\n    DBSession.commit()\n\n    # and check if test_version_4 is still present in the database\n    test_version_4_db = (\n        Version.query.filter(Version.name == test_version_4.name)\n        .filter(Version.task == test_version_4.task)\n        .filter(Version.version_number == test_version_4.version_number)\n        .first()\n    )\n\n    assert test_version_4_db is not None\n    assert test_version_4_db.task == test_version_4.task\n    assert test_version_4_db.version_number == test_version_4.version_number\n    assert test_version_4_db.parent is None\n\n    # create a new version and assign it as a child of version_5\n    test_version_5 = Version(task=test_task)\n    DBSession.save(test_version_5)\n    test_version_4.children = [test_version_5]\n    DBSession.commit()\n\n    # now delete test_version_5\n    test_version_5_id = test_version_5.id\n    DBSession.delete(test_version_5)\n    DBSession.commit()\n\n    # query it from db\n    assert DBSession.get(Version, test_version_5_id) is None\n    assert test_version_4.children == []\n\n\ndef test_persistence_of_working_hours(setup_postgresql_db):\n    \"\"\"Persistence of WorkingHours instances.\"\"\"\n    wh = WorkingHours(\n        name=\"Default Working Hours\",\n        working_hours={\n            \"mon\": [[9, 12], [13, 18]],\n            \"tue\": [[9, 12], [13, 18]],\n            \"wed\": [[9, 12], [13, 18]],\n            \"thu\": [[9, 12], [13, 18]],\n            \"fri\": [[9, 12], [13, 18]],\n            \"sat\": [],\n            \"sun\": [],\n        },\n        daily_working_hours=8,\n    )\n\n    DBSession.add(wh)\n    DBSession.commit()\n\n    name = wh.name\n    hours = wh.working_hours\n    daily_working_hours = 8\n\n    del wh\n\n    wh_db = WorkingHours.query.filter_by(name=name).first()\n\n    assert wh_db.name == name\n    assert wh_db.working_hours == hours\n    assert wh_db.daily_working_hours == daily_working_hours\n\n\ndef test_timezones_with_sqlite3(setup_sqlite3):\n    \"\"\"Timezones is correctly handled in SQLite3.\"\"\"\n    stalker.db.setup.setup()\n    stalker.db.setup.init()\n\n    # check if we're really using SQLite3\n    assert str(DBSession.connection().engine.url) == \"sqlite://\"\n\n    # create a simple entity\n    test_se_1 = SimpleEntity(name=\"Test Entry 1\")\n\n    # check if it has UTC as timezone\n    assert test_se_1.date_created.tzinfo == pytz.utc\n\n    # commit to database\n    DBSession.save(test_se_1)\n\n    # now delete the local copy and retrieve it back\n    del test_se_1\n\n    test_se_1_db = SimpleEntity.query.filter_by(name=\"Test Entry 1\").first()\n\n    # now check if the test_se_1_db has the local time zone in its\n    # date_created field\n    local_tz = tzlocal.get_localzone()\n    now = datetime.datetime.now(local_tz)\n    assert test_se_1_db.date_created.tzinfo == now.tzinfo\n"
  },
  {
    "path": "tests/db/test_dbsession.py",
    "content": "from stalker import User\nfrom stalker.db.session import DBSession, ExtendedScopedSession\n\n\ndef test_dbsession_save_method_is_correctly_created(setup_postgresql_db):\n    \"\"\"DBSession is correctly created from ExtendedScopedSession class.\"\"\"\n    assert isinstance(DBSession, ExtendedScopedSession)\n\n\ndef test_dbsession_save_method_is_working_as_expected_for_single_entity(\n    setup_postgresql_db,\n):\n    \"\"\"DBSession.save() method is working as expected for single entity.\"\"\"\n    test_user = User(\n        name=\"Test User\", login=\"tuser\", email=\"tuser@gmail.com\", password=\"12345\"\n    )\n    DBSession.save(test_user)\n\n    del test_user\n    test_user_db = User.query.filter(User.name == \"Test User\").first()\n    assert test_user_db is not None\n\n\ndef test_dbsession_save_method_is_working_as_expected_for_multiple_entity(\n    setup_postgresql_db,\n):\n    \"\"\"DBSession.save() method is working as expected for single entity.\"\"\"\n    test_user1 = User(\n        name=\"Test User 1\",\n        login=\"tuser1\",\n        email=\"tuser1@gmail.com\",\n        password=\"12345\",\n    )\n    test_user2 = User(\n        name=\"Test User 2\",\n        login=\"tuser2\",\n        email=\"tuser2@gmail.com\",\n        password=\"12345\",\n    )\n\n    DBSession.save([test_user1, test_user2])\n\n    del test_user1\n    del test_user2\n    test_user1_db = User.query.filter(User.name == \"Test User 1\").first()\n    test_user2_db = User.query.filter(User.name == \"Test User 2\").first()\n    assert test_user1_db is not None\n    assert test_user2_db is not None\n\n\ndef test_dbsession_save_method_is_working_as_expected_for_no_entry(setup_postgresql_db):\n    \"\"\"DBSession.save() method is working as expected with no parameters.\"\"\"\n    test_user = User(\n        name=\"Test User\", login=\"tuser\", email=\"tuser@gmail.com\", password=\"12345\"\n    )\n    DBSession.add(test_user)\n    DBSession.save()\n\n    del test_user\n    test_user_db = User.query.filter(User.name == \"Test User\").first()\n    assert test_user_db is not None\n"
  },
  {
    "path": "tests/db/test_types.py",
    "content": "# -*- coding: utf-8 -*-\n\nimport pytest\n\nfrom sqlalchemy import Column, ForeignKey, Integer\n\nfrom stalker.db.setup import init, setup\nfrom stalker.db.session import DBSession\nfrom stalker.db.types import GenericJSON\nfrom stalker.models.entity import Entity\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_db(setup_sqlite3):\n    \"\"\"setup test db.\"\"\"\n\n    class MyEntityClass(Entity):\n        __tablename__ = \"MyEntityClasses\"\n        __table_args__ = {\n            \"extend_existing\": True,\n        }\n        __mapper_args__ = {\n            \"polymorphic_identity\": \"MyEntityClass\",\n        }\n        my_entity_id = Column(\n            \"id\", Integer, ForeignKey(\"Entities.id\"), primary_key=True\n        )\n        data = Column(GenericJSON)\n\n    # setup and initialize db\n    setup()\n    init()\n\n    yield MyEntityClass\n\n\ndef test_json_encoded_dict_with_generic_data_stored(setup_db):\n    \"\"\"JSONEncodedDict with generic data.\"\"\"\n    MyEntityClass = setup_db\n\n    my_entity = MyEntityClass()\n    my_entity.data = {\n        \"some key\": \"and this is the value\",\n    }\n    DBSession.add(my_entity)\n    DBSession.commit()\n\n\ndef test_json_encoded_dict_with_generic_data_none_data_stored(setup_db):\n    \"\"\"JSONEncodedDict with generic data.\"\"\"\n    MyEntityClass = setup_db\n\n    my_entity = MyEntityClass()\n    my_entity.data = None\n    DBSession.add(my_entity)\n    DBSession.commit()\n\n\ndef test_json_encoded_dict_with_generic_data_retrieved(setup_db):\n    \"\"\"JSONEncodedDict with generic data.\"\"\"\n    MyEntityClass = setup_db\n\n    test_data = {\n        \"some key\": \"and this is the value\",\n    }\n\n    my_entity = MyEntityClass()\n    my_entity.data = test_data\n    DBSession.add(my_entity)\n    DBSession.commit()\n\n    del my_entity\n\n    retrieved_data = MyEntityClass.query.first()\n    assert retrieved_data.data == test_data\n"
  },
  {
    "path": "tests/mixins/__init__.py",
    "content": ""
  },
  {
    "path": "tests/mixins/test_acl_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"ACLMixin related tests.\"\"\"\n\nimport pytest\n\nfrom sqlalchemy import Column, Integer\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import ACLMixin, Permission\nfrom stalker.db.declarative import Base\n\n\nclass TestClassForACL(Base, ACLMixin):\n    \"\"\"A class for testing ACLMixing.\"\"\"\n\n    __tablename__ = \"TestClassForACLs\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n\n    def __init__(self):\n        super(TestClassForACL, self).__init__()\n        self.name = None\n\n\n@pytest.fixture(scope=\"function\")\ndef acl_mixin_test_setup():\n    \"\"\"stalker.models.mixins.ACLMixin class.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = dict()\n    # create permissions\n    data[\"test_perm1\"] = Permission(\n        access=\"Allow\", action=\"Create\", class_name=\"Something\"\n    )\n    data[\"test_instance\"] = TestClassForACL()\n    data[\"test_instance\"].name = \"Test\"\n    data[\"test_instance\"].permissions.append(data[\"test_perm1\"])\n    return data\n\n\ndef test_permission_attribute_accept_permission_instances_only(acl_mixin_test_setup):\n    \"\"\"permissions attribute accepts only Permission instances.\"\"\"\n    data = acl_mixin_test_setup\n    with pytest.raises(TypeError) as cm:\n        data[\"test_instance\"].permissions = [234]\n\n    assert str(cm.value) == (\n        \"TestClassForACL.permissions should be all instances of \"\n        \"stalker.models.auth.Permission, not int: '234'\"\n    )\n\n\ndef test_permission_attribute_is_working_as_expected(acl_mixin_test_setup):\n    \"\"\"permissions attribute is working as expected.\"\"\"\n    data = acl_mixin_test_setup\n    assert data[\"test_instance\"].permissions == [data[\"test_perm1\"]]\n\n\ndef test_acl_property_returns_a_list(acl_mixin_test_setup):\n    \"\"\"__acl__ property returns a list.\"\"\"\n    data = acl_mixin_test_setup\n    assert isinstance(data[\"test_instance\"].__acl__, list)\n\n\ndef test_acl_property_returns_a_proper_ACL_list(acl_mixin_test_setup):\n    \"\"\"__acl__ property is a list of ACLs according to the given permissions.\"\"\"\n    data = acl_mixin_test_setup\n    assert data[\"test_instance\"].__acl__ == [\n        (\"Allow\", \"TestClassForACL:Test\", \"Create_Something\")\n    ]\n"
  },
  {
    "path": "tests/mixins/test_amount_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"AmountMixin related tests.\"\"\"\n\nimport pytest\n\nfrom sqlalchemy import ForeignKey, Integer\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import AmountMixin, SimpleEntity\n\n\nclass AmountMixinFooMixedInClass(SimpleEntity, AmountMixin):\n    \"\"\"A class which derives from another which has and __init__ already.\"\"\"\n\n    __tablename__ = \"AmountMixinFooMixedInClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"AmountMixinFooMixedInClass\"}\n    amountMixinFooMixedInClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n    __id_column__ = \"amountMixinFooMixedInClass_id\"\n\n    def __init__(self, **kwargs):\n        super(AmountMixinFooMixedInClass, self).__init__(**kwargs)\n        AmountMixin.__init__(self, **kwargs)\n\n\ndef test_mixed_in_class_initialization():\n    \"\"\"init() is working as expected.\"\"\"\n    a = AmountMixinFooMixedInClass(amount=1500)\n    assert isinstance(a, AmountMixinFooMixedInClass)\n    assert a.amount == 1500\n\n\ndef test_amount_argument_is_skipped():\n    \"\"\"amount attribute will be 0 if the amount argument is skipped.\"\"\"\n    entry = AmountMixinFooMixedInClass()\n    assert entry.amount == 0.0\n\n\ndef test_amount_argument_is_set_to_none():\n    \"\"\"amount attribute will be 0 if the amount argument is None.\"\"\"\n    entry = AmountMixinFooMixedInClass(amount=None)\n    assert entry.amount == 0.0\n\n\ndef test_amount_attribute_is_set_to_none():\n    \"\"\"amount attribute will be set to 0 if it is set to None.\"\"\"\n    entry = AmountMixinFooMixedInClass(amount=10.0)\n    assert entry.amount == 10.0\n    entry.amount = None\n    assert entry.amount == 0.0\n\n\ndef test_amount_argument_is_not_a_number():\n    \"\"\"TypeError will be raised if the amount argument is not a number.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        AmountMixinFooMixedInClass(amount=\"some string\")\n\n    assert str(cm.value) == (\n        \"AmountMixinFooMixedInClass.amount should be a number, not str: 'some string'\"\n    )\n\n\ndef test_amount_attribute_is_not_a_number():\n    \"\"\"TypeError will be raised if amount attribute is not a number.\"\"\"\n    entry = AmountMixinFooMixedInClass(amount=10)\n    with pytest.raises(TypeError) as cm:\n        entry.amount = \"some string\"\n\n    assert str(cm.value) == (\n        \"AmountMixinFooMixedInClass.amount should be a number, not str: 'some string'\"\n    )\n\n\ndef test_amount_argument_is_working_as_expected():\n    \"\"\"amount argument value is correctly passed to the amount attribute.\"\"\"\n    entry = AmountMixinFooMixedInClass(amount=10)\n    assert entry.amount == 10.0\n\n\ndef test_amount_attribute_is_working_as_expected():\n    \"\"\"amount attribute is working as expected.\"\"\"\n    entry = AmountMixinFooMixedInClass(amount=10)\n    test_value = 5.0\n    assert entry.amount != test_value\n    entry.amount = test_value\n    assert entry.amount == test_value\n"
  },
  {
    "path": "tests/mixins/test_code_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"CodeMixin related tests.\"\"\"\nimport pytest\n\nfrom sqlalchemy import ForeignKey, Integer\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import CodeMixin, SimpleEntity\n\n\nclass CodeMixFooMixedInClass(SimpleEntity, CodeMixin):\n    \"\"\"A class which derives from another which has and __init__ already.\"\"\"\n\n    __tablename__ = \"CodeMixFooMixedInClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"CodeMixFooMixedInClass\"}\n    codeMixFooMixedInClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(CodeMixFooMixedInClass, self).__init__(**kwargs)\n        CodeMixin.__init__(self, **kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef code_mixin_tester_setup():\n    \"\"\"Set up the test.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = {\n        \"kwargs\": {\n            \"name\": \"Test Code Mixin\",\n            \"code\": \"this_is_a_test_code\",\n            \"description\": \"This is a simple entity object for testing \"\n            \"DateRangeMixin\",\n        },\n    }\n    data[\"test_foo_obj\"] = CodeMixFooMixedInClass(**data[\"kwargs\"])\n    return data\n\n\ndef test_code_argument_is_skipped(code_mixin_tester_setup):\n    \"\"\"TypeError is raised if the code argument is skipped.\"\"\"\n    data = code_mixin_tester_setup\n    data[\"kwargs\"].pop(\"code\")\n    with pytest.raises(TypeError) as cm:\n        CodeMixFooMixedInClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"CodeMixFooMixedInClass.code cannot be None\"\n\n\ndef test_code_argument_is_none(code_mixin_tester_setup):\n    \"\"\"TypeError is raised if the code argument is None.\"\"\"\n    data = code_mixin_tester_setup\n    data[\"kwargs\"][\"code\"] = None\n    with pytest.raises(TypeError) as cm:\n        CodeMixFooMixedInClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"CodeMixFooMixedInClass.code cannot be None\"\n\n\ndef test_code_attribute_is_none(code_mixin_tester_setup):\n    \"\"\"TypeError is raised if teh code attribute is set to None.\"\"\"\n    data = code_mixin_tester_setup\n    with pytest.raises(TypeError) as cm:\n        data[\"test_foo_obj\"].code = None\n\n    assert str(cm.value) == \"CodeMixFooMixedInClass.code cannot be None\"\n\n\ndef test_code_argument_is_not_a_string(code_mixin_tester_setup):\n    \"\"\"TypeError is raised if the code argument is not a string.\"\"\"\n    data = code_mixin_tester_setup\n    data[\"kwargs\"][\"code\"] = 123\n    with pytest.raises(TypeError) as cm:\n        CodeMixFooMixedInClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"CodeMixFooMixedInClass.code should be a string, not int: '123'\"\n    )\n\n\ndef test_code_attribute_is_not_a_string(code_mixin_tester_setup):\n    \"\"\"TypeError is raised if the code attribute is set to None.\"\"\"\n    data = code_mixin_tester_setup\n    with pytest.raises(TypeError) as cm:\n        data[\"test_foo_obj\"].code = 2342\n\n    assert str(cm.value) == (\n        \"CodeMixFooMixedInClass.code should be a string, not int: '2342'\"\n    )\n\n\ndef test_code_argument_is_an_empty_string(code_mixin_tester_setup):\n    \"\"\"ValueError is raised if the code attribute is an empty string.\"\"\"\n    data = code_mixin_tester_setup\n    data[\"kwargs\"][\"code\"] = \"\"\n    with pytest.raises(ValueError) as cm:\n        CodeMixFooMixedInClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"CodeMixFooMixedInClass.code cannot be an empty string\"\n\n\ndef test_code_attribute_is_set_to_an_empty_string(code_mixin_tester_setup):\n    \"\"\"ValueError is raised if the code attribute is set to an empty string.\"\"\"\n    data = code_mixin_tester_setup\n    with pytest.raises(ValueError) as cm:\n        data[\"test_foo_obj\"].code = \"\"\n\n    assert str(cm.value) == \"CodeMixFooMixedInClass.code cannot be an empty string\"\n\n\ndef test_code_argument_is_working_as_expected(code_mixin_tester_setup):\n    \"\"\"code argument value is passed to the code attribute.\"\"\"\n    data = code_mixin_tester_setup\n    assert data[\"test_foo_obj\"].code == data[\"kwargs\"][\"code\"]\n\n\ndef test_code_attribute_is_working_as_expected(code_mixin_tester_setup):\n    \"\"\"code attribute is working as expected.\"\"\"\n    data = code_mixin_tester_setup\n    test_value = \"new code\"\n    data[\"test_foo_obj\"].code = test_value\n    assert data[\"test_foo_obj\"].code == test_value\n"
  },
  {
    "path": "tests/mixins/test_create_secondary_table.py",
    "content": "# -*- coding: utf-8 -*-\n\nimport pytest\n\nfrom sqlalchemy import Column, ForeignKey, Integer, Table\n\nfrom stalker import SimpleEntity\nfrom stalker.db.declarative import Base\nfrom stalker.models.mixins import create_secondary_table\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_test_class():\n    \"\"\"Create a test class.\"\"\"\n\n    class TestEntity(SimpleEntity):\n        \"\"\"Test class.\"\"\"\n\n        __tablename__ = \"TestEntities\"\n        __table_args__ = {\"extend_existing\": True}\n        __mapper_args__ = {\"polymorphic_identity\": \"TestEntity\"}\n        test_entity_id = Column(\n            \"id\", Integer, ForeignKey(\"SimpleEntities.id\"), primary_key=True\n        )\n\n    yield TestEntity\n\n\ndef test_primary_cls_name_is_none(setup_test_class):\n    \"\"\"primary_cls_name is None raises TypeError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(TypeError) as cm:\n        create_secondary_table(\n            None,  # \"TestEntity\",\n            \"File\",\n            \"TestEntities\",\n            \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"primary_cls_name should be a str containing the primary class name, \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test_primary_cls_name_is_not_a_string(setup_test_class):\n    \"\"\"primary_cls_name is not str raises TypeError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(TypeError) as cm:\n        create_secondary_table(\n            1234,  # \"TestEntity\",\n            \"File\",\n            \"TestEntities\",\n            \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"primary_cls_name should be a str containing the primary class name, \"\n        \"not int: '1234'\"\n    )\n\n\ndef test_primary_cls_name_is_empty_string(setup_test_class):\n    \"\"\"primary_cls_name is an empty str raises ValueError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(ValueError) as cm:\n        create_secondary_table(\n            \"\",  # \"TestEntity\",\n            \"File\",\n            \"TestEntities\",\n            \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"primary_cls_name should be a str containing the primary class name, not: ''\"\n    )\n\n\ndef test_secondary_cls_name_is_none(setup_test_class):\n    \"\"\"secondary_cls_name is None raises TypeError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(TypeError) as cm:\n        create_secondary_table(\n            \"TestEntity\",\n            None,  # \"File\",\n            \"TestEntities\",\n            \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"secondary_cls_name should be a str containing the secondary class name, \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test_secondary_cls_name_is_not_a_string(setup_test_class):\n    \"\"\"secondary_cls_name is not str raises TypeError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(TypeError) as cm:\n        create_secondary_table(\n            \"TestEntity\",\n            1234,  # \"File\",\n            \"TestEntities\",\n            \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"secondary_cls_name should be a str containing the secondary class name, \"\n        \"not int: '1234'\"\n    )\n\n\ndef test_secondary_cls_name_is_empty_string(setup_test_class):\n    \"\"\"secondary_cls_name is an empty str raises ValueError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(ValueError) as cm:\n        create_secondary_table(\n            \"TestEntity\",\n            \"\",  # \"File\",\n            \"TestEntities\",\n            \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"secondary_cls_name should be a str containing the secondary class name, \"\n        \"not: ''\"\n    )\n\n\ndef test_secondary_cls_name_is_converted_to_plural(setup_test_class):\n    \"\"\"secondary_cls_name is converted to plural.\"\"\"\n    return_value = create_secondary_table(\n        \"TestEntity\",\n        \"File\",\n        \"TestEntities\",\n        \"Files\",\n        None,  # \"TestEntity_References\"\n    )\n    assert return_value.name == \"TestEntity_Files\"\n\n\ndef test_primary_cls_table_name_is_none(setup_test_class):\n    \"\"\"primary_cls_table_name is None raises TypeError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(TypeError) as cm:\n        create_secondary_table(\n            \"TestEntity\",\n            \"File\",\n            None,  # \"TestEntities\",\n            \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"primary_cls_table_name should be a str containing the primary class table \"\n        \"name, not NoneType: 'None'\"\n    )\n\n\ndef test_primary_cls_table_name_is_not_a_string(setup_test_class):\n    \"\"\"primary_cls_table_name is not str raises TypeError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(TypeError) as cm:\n        create_secondary_table(\n            \"TestEntity\",\n            \"File\",\n            1234,  # \"TestEntities\",\n            \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"primary_cls_table_name should be a str containing the primary class table \"\n        \"name, not int: '1234'\"\n    )\n\n\ndef test_primary_cls_table_name_is_empty_string(setup_test_class):\n    \"\"\"primary_cls_table_name is an empty str raises ValueError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(ValueError) as cm:\n        create_secondary_table(\n            \"TestEntity\",\n            \"File\",\n            \"\",  # \"TestEntities\",\n            \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"primary_cls_table_name should be a str containing the primary class table \"\n        \"name, not: ''\"\n    )\n\n\ndef test_secondary_cls_table_name_is_none(setup_test_class):\n    \"\"\"secondary_cls_table_name is None raises TypeError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(TypeError) as cm:\n        create_secondary_table(\n            \"TestEntity\",\n            \"File\",\n            \"TestEntities\",\n            None,  # \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"secondary_cls_table_name should be a str containing the secondary class table \"\n        \"name, not NoneType: 'None'\"\n    )\n\n\ndef test_secondary_cls_table_name_is_not_a_string(setup_test_class):\n    \"\"\"secondary_cls_table_name is not str raises TypeError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(TypeError) as cm:\n        create_secondary_table(\n            \"TestEntity\",\n            \"File\",\n            \"TestEntities\",\n            1234,  # \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"secondary_cls_table_name should be a str containing the secondary class table \"\n        \"name, not int: '1234'\"\n    )\n\n\ndef test_secondary_cls_table_name_is_empty_string(setup_test_class):\n    \"\"\"secondary_cls_table_name is an empty str raises ValueError.\"\"\"\n    _ = setup_test_class\n    with pytest.raises(ValueError) as cm:\n        create_secondary_table(\n            \"TestEntity\",\n            \"File\",\n            \"TestEntities\",\n            \"\",  # \"Files\",\n            \"TestEntity_References\",\n        )\n\n    assert str(cm.value) == (\n        \"secondary_cls_table_name should be a str containing the secondary class table \"\n        \"name, not: ''\"\n    )\n\n\ndef test_secondary_table_name_can_be_none(setup_test_class):\n    \"\"\"secondary_table_name can be None.\"\"\"\n    return_value = create_secondary_table(\n        \"TestEntity\",\n        \"File\",\n        \"TestEntities\",\n        \"Files\",\n        None,  # \"TestEntity_References\"\n    )\n    assert return_value.name == \"TestEntity_Files\"\n\n\ndef test_secondary_table_name_is_not_a_str(setup_test_class):\n    \"\"\"secondary_table_name is not str raises TypeError.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = create_secondary_table(\n            \"TestEntity\",\n            \"File\",\n            \"TestEntities\",\n            \"Files\",\n            1234,  # \"TestEntity_References\"\n        )\n    assert str(cm.value) == (\n        \"secondary_table_name should be a str containing the secondary table name, \"\n        \"or it can be None or an empty string to let Stalker to auto generate one, \"\n        \"not int: '1234'\"\n    )\n\n\ndef test_secondary_table_name_is_an_empty_str(setup_test_class):\n    \"\"\"secondary_table_name is an empty string generates new name from class names.\"\"\"\n    return_value = create_secondary_table(\n        \"TestEntity\",\n        \"File\",\n        \"TestEntities\",\n        \"Files\",\n        \"\",  # \"TestEntity_References\"\n    )\n    assert return_value.name == \"TestEntity_Files\"\n\n\ndef test_secondary_table_name_already_exists_in_base_metadata(setup_test_class):\n    \"\"\"secondary_table_name already exists will use that table.\"\"\"\n    assert \"TestEntity_References\" not in Base.metadata\n    return_value_1 = create_secondary_table(\n        \"TestEntity\", \"File\", \"TestEntities\", \"Files\", \"TestEntity_References\"\n    )\n    assert \"TestEntity_References\" in Base.metadata\n    # should not generate any errors\n    return_value_2 = create_secondary_table(\n        \"TestEntity\", \"File\", \"TestEntities\", \"Files\", \"TestEntity_References\"\n    )\n    # and return the same table\n    assert return_value_2.name == \"TestEntity_References\"\n    assert return_value_1 == return_value_2\n\n\ndef test_returns_a_table(setup_test_class):\n    \"\"\"create_secondary_table returns a table.\"\"\"\n    return_value = create_secondary_table(\n        \"TestEntity\", \"File\", \"TestEntities\", \"Files\", \"TestEntity_References\"\n    )\n    assert isinstance(return_value, Table)\n"
  },
  {
    "path": "tests/mixins/test_dag_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"DAGMixin related tests.\"\"\"\n\nimport copy\nimport sys\n\nimport pytest\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import log\nfrom stalker.db.session import DBSession\nfrom stalker.exceptions import CircularDependencyError\nfrom stalker.models.entity import SimpleEntity\nfrom stalker.models.mixins import DAGMixin\n\nlog.get_logger(\"stalker.models.studio\")\n\n\nclass DAGMixinFooMixedInClass(SimpleEntity, DAGMixin):\n    \"\"\"A class which derives from another which has and __init__ already.\"\"\"\n\n    __tablename__ = \"DAGMixinFooMixedInClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"DAGMixinFooMixedInClass\"}\n    dagMixinFooMixedInClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n    __id_column__ = \"dagMixinFooMixedInClass_id\"\n\n    def __init__(self, **kwargs):\n        super(DAGMixinFooMixedInClass, self).__init__(**kwargs)\n        DAGMixin.__init__(self, **kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef dag_mixin_test_case():\n    \"\"\"Set up the DAGMixin class tests.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = {\"kwargs\": {\"name\": \"Test DAG Mixin\"}}\n    return data\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_dag_db(setup_postgresql_db):\n    \"\"\"Set up the test for DAGMixin.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = setup_postgresql_db\n    data[\"kwargs\"] = {\"name\": \"Test DAG Mixin\"}\n    return data\n\n\ndef test_parent_argument_is_skipped(dag_mixin_test_case):\n    \"\"\"parent attribute is None if the parent argument is skipped.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d = DAGMixinFooMixedInClass(**kwargs)\n    assert d.parent is None\n\n\ndef test_parent_argument_is_none(dag_mixin_test_case):\n    \"\"\"parent attribute is None if the parent argument is None.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = None\n    d = DAGMixinFooMixedInClass(**kwargs)\n    assert d.parent is None\n\n\ndef test_parent_argument_is_not_a_correct_class_instance(dag_mixin_test_case):\n    \"\"\"TypeError is raised if the parent argument is not correct type.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = \"not a correct type\"\n    with pytest.raises(TypeError) as cm:\n        _ = DAGMixinFooMixedInClass(**kwargs)\n\n    assert str(cm.value) == (\n        \"DAGMixinFooMixedInClass.parent should be an instance of \"\n        \"DAGMixinFooMixedInClass class or derivative, not str: 'not a correct type'\"\n    )\n\n\ndef test_parent_attribute_is_not_a_correct_class_instance(dag_mixin_test_case):\n    \"\"\"TypeError is raised if the parent attribute is set to a wrong class instance.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d = DAGMixinFooMixedInClass(**kwargs)\n    with pytest.raises(TypeError) as cm:\n        d.parent = \"not a correct type\"\n\n    assert str(cm.value) == (\n        \"DAGMixinFooMixedInClass.parent should be an instance of \"\n        \"DAGMixinFooMixedInClass class or derivative, not str: 'not a correct type'\"\n    )\n\n\ndef test_parent_attribute_creates_a_cycle(dag_mixin_test_case):\n    \"\"\"CircularDependency is raised if a child is tried to be set as the parent.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = d1\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n\n    with pytest.raises(CircularDependencyError) as cm:\n        d1.parent = d2\n\n    assert (\n        str(cm.value) == \"<Test DAG Mixin (DAGMixinFooMixedInClass)> \"\n        \"(DAGMixinFooMixedInClass) and \"\n        \"<Test DAG Mixin (DAGMixinFooMixedInClass)> \"\n        \"(DAGMixinFooMixedInClass) are in a circular dependency in \"\n        'their \"children\" attribute'\n    )\n\n\ndef test_parent_argument_is_working_as_expected(dag_mixin_test_case):\n    \"\"\"parent argument is working as expected.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = d1\n\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n    assert d1 == d2.parent\n\n\ndef test_parent_attribute_is_working_as_expected(dag_mixin_test_case):\n    \"\"\"parent attribute is working as expected.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n    assert d2.parent != d1\n    d2.parent = d1\n    assert d2.parent == d1\n\n\ndef test_children_attribute_is_an_empty_list_by_default(dag_mixin_test_case):\n    \"\"\"children attribute is an empty list by default.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d = DAGMixinFooMixedInClass(**kwargs)\n    assert d.children == []\n\n\ndef test_children_attribute_is_set_to_none(dag_mixin_test_case):\n    \"\"\"TypeError is raised if the children attribute is set to None.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d = DAGMixinFooMixedInClass(**kwargs)\n    with pytest.raises(TypeError) as cm:\n        d.children = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_children_attribute_accepts_correct_class_instances_only(dag_mixin_test_case):\n    \"\"\"children attribute accepts only correct class instances.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d = DAGMixinFooMixedInClass(**kwargs)\n    with pytest.raises(TypeError) as cm:\n        d.children = [\"not\", 1, \"\", \"of\", \"correct\", \"instances\"]\n\n    assert str(cm.value) == (\n        \"DAGMixinFooMixedInClass.children should only contain instances of \"\n        \"DAGMixinFooMixedInClass (or derivative), not str: 'not'\"\n    )\n\n\ndef test_children_attribute_is_working_as_expected(dag_mixin_test_case):\n    \"\"\"children attribute is working as expected.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"name\"] = \"Test DAG Mixin 1\"\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    kwargs[\"name\"] = \"Test DAG Mixin 2\"\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n    kwargs[\"name\"] = \"Test DAG Mixin 3\"\n    d3 = DAGMixinFooMixedInClass(**kwargs)\n\n    assert d1.children == []\n    d1.children.append(d2)\n    assert d1.children == [d2]\n    d1.children = [d3]\n    assert d1.children == [d3]\n\n\ndef test_is_leaf_attribute_is_read_only(dag_mixin_test_case):\n    \"\"\"is_leaf attribute is a read only attribute.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    with pytest.raises(AttributeError) as cm:\n        d1.is_leaf = \"this will not work\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'is_leaf'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'is_leaf' of 'DAGMixinFooMixedInClass' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_is_leaf_attribute_is_working_as_expected(dag_mixin_test_case):\n    \"\"\"is_leaf attribute is True for an instance without a child and False\n    for another one with at least one child.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n    d3 = DAGMixinFooMixedInClass(**kwargs)\n    d1.children = [d2, d3]\n    assert d1.is_leaf is False\n    assert d2.is_leaf is True\n    assert d3.is_leaf is True\n\n\ndef test_is_root_attribute_is_read_only(dag_mixin_test_case):\n    \"\"\"is_root attribute is a read only attribute.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    with pytest.raises(AttributeError) as cm:\n        d1.is_root = \"this will not work\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'is_root'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'is_root' of 'DAGMixinFooMixedInClass' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_is_root_attribute_is_working_as_expected(dag_mixin_test_case):\n    \"\"\"is_root is True for an instance without a parent and False otherwise.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n    d3 = DAGMixinFooMixedInClass(**kwargs)\n    d1.children = [d2, d3]\n    assert d1.is_root is True\n    assert d2.is_root is False\n    assert d3.is_root is False\n\n\ndef test_is_container_attribute_is_read_only(dag_mixin_test_case):\n    \"\"\"is_container attribute is a read only attribute.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    with pytest.raises(AttributeError) as cm:\n        d1.is_container = \"this will not work\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'is_container'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'is_container' of 'DAGMixinFooMixedInClass' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_is_container_attribute_working_as_expected(dag_mixin_test_case):\n    \"\"\"is_container is True if at least one child exist and False otherwise.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n    d3 = DAGMixinFooMixedInClass(**kwargs)\n    d4 = DAGMixinFooMixedInClass(**kwargs)\n\n    d1.children = [d2, d3]\n    d2.children = [d4]\n    assert d1.is_container is True\n    assert d2.is_container is True\n    assert d3.is_container is False\n    assert d4.is_container is False\n\n\ndef test_parents_property_is_read_only(dag_mixin_test_case):\n    \"\"\"parents property is read-only.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    with pytest.raises(AttributeError) as cm:\n        d1.parents = \"this will not work\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'parents'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'parents' of 'DAGMixinFooMixedInClass' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_parents_property_is_working_as_expected(dag_mixin_test_case):\n    \"\"\"parents property is read-only.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n    d3 = DAGMixinFooMixedInClass(**kwargs)\n    d4 = DAGMixinFooMixedInClass(**kwargs)\n\n    d1.children = [d2, d3]\n    d2.children = [d4]\n\n    assert d1.parents == []\n    assert d2.parents == [d1]\n    assert d3.parents == [d1]\n    assert d4.parents == [d1, d2]\n\n\ndef test_walk_hierarchy_is_working_as_expected(dag_mixin_test_case):\n    \"\"\"walk_hierarchy method is working as expected.\"\"\"\n    data = dag_mixin_test_case\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n    d3 = DAGMixinFooMixedInClass(**kwargs)\n    d4 = DAGMixinFooMixedInClass(**kwargs)\n\n    d1.children = [d2, d3]\n    d2.children = [d4]\n\n    entities_walked = []\n    for e in d1.walk_hierarchy():\n        entities_walked.append(e)\n    assert entities_walked == [d1, d2, d4, d3]\n\n    entities_walked = []\n    for e in d1.walk_hierarchy(method=1):\n        entities_walked.append(e)\n    assert entities_walked == [d1, d2, d3, d4]\n\n    entities_walked = []\n    for e in d2.walk_hierarchy():\n        entities_walked.append(e)\n    assert entities_walked == [d2, d4]\n\n    entities_walked = []\n    for e in d3.walk_hierarchy():\n        entities_walked.append(e)\n    assert entities_walked == [d3]\n\n    entities_walked = []\n    for e in d4.walk_hierarchy():\n        entities_walked.append(e)\n    assert entities_walked == [d4]\n\n\ndef test_committing_data(setup_dag_db):\n    \"\"\"Committing and retrieving data back.\"\"\"\n    data = setup_dag_db\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n    d3 = DAGMixinFooMixedInClass(**kwargs)\n    d4 = DAGMixinFooMixedInClass(**kwargs)\n\n    d1.children = [d2, d3]\n    d2.children = [d4]\n\n    DBSession.add_all([d1, d2, d3, d4])\n    DBSession.commit()\n\n    del d1, d2, d3, d4\n\n    all_data = DAGMixinFooMixedInClass.query.all()\n\n    assert len(all_data) == 4\n    assert isinstance(all_data[0], DAGMixinFooMixedInClass)\n    assert isinstance(all_data[1], DAGMixinFooMixedInClass)\n    assert isinstance(all_data[2], DAGMixinFooMixedInClass)\n    assert isinstance(all_data[3], DAGMixinFooMixedInClass)\n\n\ndef test_deleting_data(setup_dag_db):\n    \"\"\"Deleting data.\"\"\"\n    data = setup_dag_db\n    kwargs = copy.copy(data[\"kwargs\"])\n    d1 = DAGMixinFooMixedInClass(**kwargs)\n    d2 = DAGMixinFooMixedInClass(**kwargs)\n    d3 = DAGMixinFooMixedInClass(**kwargs)\n    d4 = DAGMixinFooMixedInClass(**kwargs)\n\n    d1.children = [d2, d3]\n    d2.children = [d4]\n\n    DBSession.add_all([d1, d2, d3, d4])\n    DBSession.commit()\n\n    DBSession.delete(d1)\n    DBSession.commit()\n\n    all_data = DAGMixinFooMixedInClass.query.all()\n    assert len(all_data) == 0\n"
  },
  {
    "path": "tests/mixins/test_date_range_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"DateRangeMixin class related tests.\"\"\"\nimport datetime\nimport logging\nimport sys\n\nimport pytest\n\nimport pytz\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nimport stalker\nfrom stalker import DateRangeMixin, SimpleEntity, defaults, log\nfrom stalker.db.session import DBSession\nfrom stalker.models.studio import Studio\n\nlogger = log.get_logger(__name__)\n\n\nclass DateRangeMixFooMixedInClass(SimpleEntity, DateRangeMixin):\n    \"\"\"A class which derives from another which has and __init__ already.\"\"\"\n\n    __tablename__ = \"DateRangeMixFooMixedInClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"DateRangeMixFooMixedInClass\"}\n    schedMixFooMixedInClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(DateRangeMixFooMixedInClass, self).__init__(**kwargs)\n        DateRangeMixin.__init__(self, **kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef date_range_mixin_tester():\n    \"\"\"Fixture for the DateRangeMixin tests.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    # create mock objects\n    stalker.defaults.config_values = stalker.defaults.default_config_values.copy()\n    stalker.defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n    data = dict()\n    data[\"start\"] = datetime.datetime(2013, 3, 22, 15, 15, tzinfo=pytz.utc)\n    data[\"end\"] = data[\"start\"] + datetime.timedelta(days=20)\n    data[\"duration\"] = datetime.timedelta(days=10)\n\n    data[\"kwargs\"] = {\n        \"name\": \"Test Daterange Mixin\",\n        \"description\": \"This is a simple entity object for testing \" \"DateRangeMixin\",\n        \"start\": data[\"start\"],\n        \"end\": data[\"end\"],\n        \"duration\": data[\"duration\"],\n    }\n    data[\"test_foo_obj\"] = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    yield data\n    stalker.defaults.config_values = stalker.defaults.default_config_values.copy()\n    stalker.defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n\n\n@pytest.mark.parametrize(\"test_value\", [1, 1.2, \"str\", [\"a\", \"date\"]])\ndef test_start_argument_is_not_a_date_object(test_value, date_range_mixin_tester):\n    \"\"\"Default values are used if the start attribute is not a datetime object.\"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"][\"start\"] = test_value\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_obj.start == new_foo_obj.end - new_foo_obj.duration\n\n\n@pytest.mark.parametrize(\"test_value\", [1, 1.2, \"str\", [\"a\", \"date\"]])\ndef test_start_attribute_is_not_a_date_object(test_value, date_range_mixin_tester):\n    \"\"\"Default values are used if start attribute is set not datetime object.\"\"\"\n    data = date_range_mixin_tester\n    end = data[\"test_foo_obj\"].end\n    duration = data[\"test_foo_obj\"].duration\n    data[\"test_foo_obj\"].start = test_value\n    assert (\n        data[\"test_foo_obj\"].start\n        == data[\"test_foo_obj\"].end - data[\"test_foo_obj\"].duration\n    )\n    # check if we still have the same end\n    assert data[\"test_foo_obj\"].end == end\n    # check if we still have the same duration\n    assert data[\"test_foo_obj\"].duration == duration\n\n\ndef test_start_attribute_is_set_to_none_use_the_default_value(date_range_mixin_tester):\n    \"\"\"Setting the start attribute to None will update the start to today.\"\"\"\n    data = date_range_mixin_tester\n    data[\"test_foo_obj\"].start = None\n    assert data[\"test_foo_obj\"].start == datetime.datetime(\n        2013, 3, 22, 15, 00, tzinfo=pytz.utc\n    )\n    assert isinstance(data[\"test_foo_obj\"].start, datetime.datetime)\n\n\ndef test_start_attribute_works_as_expected(date_range_mixin_tester):\n    \"\"\"start attribute is working as expected.\"\"\"\n    data = date_range_mixin_tester\n    test_value = datetime.datetime(2011, 1, 1, tzinfo=pytz.utc)\n    data[\"test_foo_obj\"].start = test_value\n    assert data[\"test_foo_obj\"].start == test_value\n\n\n@pytest.mark.parametrize(\n    \"test_value\", [1, 1.2, \"str\", [\"a\", \"date\"], datetime.timedelta(days=100)]\n)\ndef test_end_argument_is_not_a_date_object(test_value, date_range_mixin_tester):\n    \"\"\"Defaults used for the end attribute if due date not a datetime instance.\"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"][\"end\"] = test_value\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_obj.end == new_foo_obj.start + new_foo_obj.duration\n\n\n@pytest.mark.parametrize(\n    \"test_value\", [1, 1.2, \"str\", [\"a\", \"date\"], datetime.timedelta(days=100)]\n)\ndef test_end_attribute_is_not_a_date_object(test_value, date_range_mixin_tester):\n    \"\"\"Defaults used for the end attribute if it is not a datetime object.\"\"\"\n    data = date_range_mixin_tester\n    data[\"test_foo_obj\"].end = test_value\n    assert (\n        data[\"test_foo_obj\"].end\n        == data[\"test_foo_obj\"].start + data[\"test_foo_obj\"].duration\n    )\n\n\ndef test_end_argument_is_tried_to_set_to_a_time_before_start(date_range_mixin_tester):\n    \"\"\"end attribute is updated to start+duration if end arg is a date before start.\"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"][\"end\"] = data[\"kwargs\"][\"start\"] - datetime.timedelta(days=10)\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_obj.end == new_foo_obj.start + new_foo_obj.duration\n\n\ndef test_end_attribute_is_tried_to_set_to_a_time_before_start(date_range_mixin_tester):\n    \"\"\"end attribute is updated to start+duration if end is a date before start.\"\"\"\n    data = date_range_mixin_tester\n    new_end = data[\"test_foo_obj\"].start - datetime.timedelta(days=10)\n    data[\"test_foo_obj\"].end = new_end\n    assert (\n        data[\"test_foo_obj\"].end\n        == data[\"test_foo_obj\"].start + data[\"test_foo_obj\"].duration\n    )\n\n\ndef test_end_attribute_is_shifted_if_start_passes_it(date_range_mixin_tester):\n    \"\"\"end attribute is shifted if the start attribute passes it.\"\"\"\n    data = date_range_mixin_tester\n    time_delta = data[\"test_foo_obj\"].end - data[\"test_foo_obj\"].start\n    data[\"test_foo_obj\"].start += 2 * time_delta\n    assert data[\"test_foo_obj\"].end - data[\"test_foo_obj\"].start == time_delta\n\n\n@pytest.mark.parametrize(\"test_value\", [None, 1, 1.2, \"10\", \"10 days\"])\ndef test_duration_argument_is_not_an_instance_of_timedelta_no_problem_if_start_and_end_is_present(\n    test_value, date_range_mixin_tester\n):\n    \"\"\"No error raised if duration arg is not a datetime instance if start and end args are present.\"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"][\"duration\"] = test_value\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_obj.duration == new_foo_obj.end - new_foo_obj.start\n\n\n@pytest.mark.parametrize(\"test_value\", [1, 1.2, \"10\", \"10 days\"])\ndef test_duration_argument_is_not_an_instance_of_date_if_start_argument_is_missing(\n    test_value, date_range_mixin_tester\n):\n    \"\"\"defaults.timing_resolution is used if duration arg is not a datetime if start arg is also missing.\"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"].pop(\"start\")\n    data[\"kwargs\"][\"duration\"] = test_value\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_obj.duration == defaults.timing_resolution\n\n\n@pytest.mark.parametrize(\"test_value\", [1, 1.2, \"10\", \"10 days\"])\ndef test_duration_argument_is_not_an_instance_of_date_if_end_argument_is_missing(\n    test_value, date_range_mixin_tester\n):\n    \"\"\"defaults.timing_resolution is used if the duration arg is not a datetime and if end arg is also missing.\"\"\"\n    data = date_range_mixin_tester\n    defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n    # some wrong values for the duration\n    data[\"kwargs\"].pop(\"end\")\n    data[\"kwargs\"][\"duration\"] = test_value\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_obj.duration == defaults.timing_resolution\n\n\ndef test_duration_argument_is_smaller_than_timing_resolution(date_range_mixin_tester):\n    \"\"\"defaults.timing_resolution is used for duration if duration arg is smaller than defaults.timing_resolution\"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"].pop(\"end\")\n    data[\"kwargs\"][\"duration\"] = datetime.timedelta(minutes=1)\n    obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert obj.start == datetime.datetime(2013, 3, 22, 15, 0, tzinfo=pytz.utc)\n    assert obj.end == datetime.datetime(2013, 3, 22, 16, 0, tzinfo=pytz.utc)\n\n\ndef test_duration_attribute_is_calculated_correctly(date_range_mixin_tester):\n    \"\"\"duration attribute is calculated correctly.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_entity = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    new_foo_entity.start = datetime.datetime(2013, 3, 22, 15, 0, tzinfo=pytz.utc)\n    new_foo_entity.end = new_foo_entity.start + datetime.timedelta(201)\n    assert new_foo_entity.duration == datetime.timedelta(201)\n\n\n@pytest.mark.parametrize(\"test_value\", [None, 1, 1.2, \"10\", \"10 days\"])\ndef test_duration_attribute_is_set_to_not_an_instance_of_timedelta(\n    test_value,\n    date_range_mixin_tester,\n):\n    \"\"\"duration attribute reset to a calculated value if it is not a timedelta.\"\"\"\n    data = date_range_mixin_tester\n    # no problem if there are start and end arguments\n    data[\"test_foo_obj\"].duration = test_value\n    # check the value\n    assert (\n        data[\"test_foo_obj\"].duration\n        == data[\"test_foo_obj\"].end - data[\"test_foo_obj\"].start\n    )\n\n\ndef test_duration_attribute_expands_then_end_shifts(date_range_mixin_tester):\n    \"\"\"duration attribute is expanded then the end attribute is shifted.\"\"\"\n    data = date_range_mixin_tester\n    _ = data[\"test_foo_obj\"].end\n    start = data[\"test_foo_obj\"].start\n    duration = data[\"test_foo_obj\"].duration\n\n    # change the duration\n    new_duration = duration * 10\n    data[\"test_foo_obj\"].duration = new_duration\n\n    # duration expanded\n    assert data[\"test_foo_obj\"].duration == new_duration\n\n    # start is not changed\n    assert data[\"test_foo_obj\"].start == start\n\n    # end is postponed\n    assert data[\"test_foo_obj\"].end == start + new_duration\n\n\ndef test_duration_attribute_contracts_then_end_shifts_back(date_range_mixin_tester):\n    \"\"\"duration attribute is contracted then the end attribute is shifted back.\"\"\"\n    data = date_range_mixin_tester\n    _ = data[\"test_foo_obj\"].end\n    start = data[\"test_foo_obj\"].start\n    duration = data[\"test_foo_obj\"].duration\n\n    # change the duration\n    new_duration = duration / 2\n    data[\"test_foo_obj\"].duration = new_duration\n\n    # duration expanded\n    assert data[\"test_foo_obj\"].duration == new_duration\n\n    # start is not changed\n    assert data[\"test_foo_obj\"].start == start\n\n    # end is postponed\n    assert data[\"test_foo_obj\"].end == start + new_duration\n\n\ndef test_duration_attribute_is_smaller_then_timing_resolution(date_range_mixin_tester):\n    \"\"\"defaults.timing_resolution is used for the duration if it is smaller than it.\"\"\"\n    data = date_range_mixin_tester\n    data[\"test_foo_obj\"].duration = datetime.timedelta(minutes=10)\n    assert data[\"test_foo_obj\"].duration == defaults.timing_resolution\n\n\ndef test_duration_is_a_negative_timedelta(date_range_mixin_tester):\n    \"\"\"duration is a negative timedelta will set the duration to 1 days.\"\"\"\n    data = date_range_mixin_tester\n    start = data[\"test_foo_obj\"].start\n    data[\"test_foo_obj\"].duration = datetime.timedelta(-10)\n    assert data[\"test_foo_obj\"].duration == datetime.timedelta(1)\n    assert data[\"test_foo_obj\"].start == start\n\n\ndef test_init_all_parameters_skipped(date_range_mixin_tester):\n    \"\"\"Attributes are initialized to the following values.\n\n    start = datetime.datetime.now(pytz.utc)\n    duration = stalker.config.Config.timing_resolution\n    end = start + duration\n    \"\"\"\n    data = date_range_mixin_tester\n    # self.fail(\"test is not implemented yet\")\n    data[\"kwargs\"].pop(\"start\")\n    data[\"kwargs\"].pop(\"end\")\n    data[\"kwargs\"].pop(\"duration\")\n\n    new_foo_entity = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n\n    assert isinstance(new_foo_entity.start, datetime.datetime)\n    # cannot check for start, just don't want to struggle with the round\n    # thing\n    # assert \\\n    #     new_foo_entity.start == \\\n    #     datetime.datetime(2013, 3, 22, 15, 30, tzinfo=pytz.utc)\n    assert new_foo_entity.duration == defaults.timing_resolution\n    assert new_foo_entity.end == new_foo_entity.start + new_foo_entity.duration\n\n\ndef test_init_only_start_argument_is_given(date_range_mixin_tester):\n    \"\"\"Attributes are initialized to the following values.\n\n    duration = defaults.timing_resolution\n    end = start + duration\n    \"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"].pop(\"end\")\n    data[\"kwargs\"].pop(\"duration\")\n    new_foo_entity = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_entity.duration == defaults.timing_resolution\n    assert new_foo_entity.end == new_foo_entity.start + new_foo_entity.duration\n\n\ndef test_init_start_and_end_argument_is_given(date_range_mixin_tester):\n    \"\"\"Attributes are initialized to the following values.\n\n    duration = end - start\n    \"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"].pop(\"duration\")\n    new_foo_entity = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_entity.duration == new_foo_entity.end - new_foo_entity.start\n\n\ndef test_init_start_and_end_argument_is_given_but_duration_is_smaller_than_timing_resolution(\n    date_range_mixin_tester,\n):\n    \"\"\"Start is anchored and the end is updated if duration is smaller than timing_resolution.\n\n    duration = end - start\n    \"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"].pop(\"duration\")\n    data[\"kwargs\"][\"start\"] = datetime.datetime(2013, 12, 22, 23, 8, tzinfo=pytz.utc)\n    data[\"kwargs\"][\"end\"] = datetime.datetime(2013, 12, 22, 23, 15, tzinfo=pytz.utc)\n    obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert obj.start == datetime.datetime(2013, 12, 22, 23, 0, tzinfo=pytz.utc)\n    assert obj.end == datetime.datetime(2013, 12, 23, 0, 0, tzinfo=pytz.utc)\n\n\ndef test_init_start_and_duration_argument_is_given(date_range_mixin_tester):\n    \"\"\"Attributes are initialized to:\n\n    end = start + duration\n    \"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"].pop(\"end\")\n    new_foo_entity = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_entity.end == new_foo_entity.start + new_foo_entity.duration\n\n\ndef test_init_all_arguments_are_given(date_range_mixin_tester):\n    \"\"\"Attributes are initialized to:\n\n    duration = end - start\n    \"\"\"\n    data = date_range_mixin_tester\n    new_foo_entity = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_entity.duration == new_foo_entity.end - new_foo_entity.start\n\n\ndef test_init_end_and_duration_argument_is_given(date_range_mixin_tester):\n    \"\"\"Attributes are initialized to:\n\n    start = end - duration\n    \"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"].pop(\"start\")\n    new_foo_entity = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_entity.start == new_foo_entity.end - new_foo_entity.duration\n\n\ndef test_init_only_end_argument_is_given(date_range_mixin_tester):\n    \"\"\"Attributes are initialized to:\n\n    duration = defaults.timing_resolution\n    start = end - duration\n    \"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"].pop(\"duration\")\n    data[\"kwargs\"].pop(\"start\")\n    new_foo_entity = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_entity.duration == defaults.timing_resolution\n    assert new_foo_entity.start == new_foo_entity.end - new_foo_entity.duration\n\n\ndef test_init_only_duration_argument_is_given(date_range_mixin_tester):\n    \"\"\"Attributes are initialized to:\n\n    start = datetime.datetime.now(pytz.utc)\n    end = start + duration\n    \"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"].pop(\"end\")\n    data[\"kwargs\"].pop(\"start\")\n\n    new_foo_entity = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n\n    # just check if it is an instance of datetime.datetime\n    assert isinstance(new_foo_entity.start, datetime.datetime)\n    # cannot check for start\n    # assert new_foo_entity.start == \\\n    #        datetime.datetime(2013, 3, 22, 15, 30, tzinfo=pytz.utc\n    assert new_foo_entity.end == new_foo_entity.start + new_foo_entity.duration\n\n\ndef test_start_end_and_duration_values_are_rounded_to_the_default_timing_resolution(\n    date_range_mixin_tester,\n):\n    \"\"\"start and end dates are rounded to the default timing_resolution (no Studio).\"\"\"\n    data = date_range_mixin_tester\n    data[\"kwargs\"][\"start\"] = datetime.datetime(\n        2013, 3, 22, 2, 38, 55, 531, tzinfo=pytz.utc\n    )\n    data[\"kwargs\"][\"end\"] = datetime.datetime(\n        2013, 3, 24, 16, 46, 32, 102, tzinfo=pytz.utc\n    )\n    defaults[\"timing_resolution\"] = datetime.timedelta(minutes=5)\n\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    # check the start\n    expected_start = datetime.datetime(2013, 3, 22, 2, 40, tzinfo=pytz.utc)\n    assert new_foo_obj.start == expected_start\n    # check the end\n    expected_end = datetime.datetime(2013, 3, 24, 16, 45, tzinfo=pytz.utc)\n    assert new_foo_obj.end == expected_end\n    # check the duration\n    assert new_foo_obj.duration == expected_end - expected_start\n\n\ndef test_computed_start_is_none_for_a_non_scheduled_class(date_range_mixin_tester):\n    \"\"\"computed_start attribute is None for a non-scheduled class.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_obj.computed_start is None\n\n\ndef test_computed_end_is_none_for_a_non_scheduled_class(date_range_mixin_tester):\n    \"\"\"computed_end attribute is None for a non-scheduled class.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    assert new_foo_obj.computed_end is None\n\n\ndef test_computed_duration_attribute_is_none_if_there_is_no_computed_start_and_no_computed_end(\n    date_range_mixin_tester,\n):\n    \"\"\"computed_start attr is None if there is no computed_start and no computed_end.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    new_foo_obj.computed_start = None\n    new_foo_obj.computed_end = None\n    assert new_foo_obj.computed_duration is None\n\n\ndef test_computed_duration_attribute_is_none_if_there_is_computed_start_but_no_computed_end(\n    date_range_mixin_tester,\n):\n    \"\"\"computed_start attr is None if there is computed_start but no computed_end.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    new_foo_obj.computed_start = datetime.datetime.now(pytz.utc)\n    new_foo_obj.computed_end = None\n    assert new_foo_obj.computed_duration is None\n\n\ndef test_computed_duration_attribute_is_none_if_there_is_no_computed_start_but_computed_end(\n    date_range_mixin_tester,\n):\n    \"\"\"computed_start attr is None if there is no computed_start but computed_end.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    new_foo_obj.computed_start = None\n    new_foo_obj.computed_end = datetime.datetime.now(pytz.utc)\n    assert new_foo_obj.computed_duration is None\n\n\ndef test_computed_duration_attribute_is_calculated_correctly_if_there_are_both_computed_start_and_computed_end(\n    date_range_mixin_tester,\n):\n    \"\"\"computed_duration is calculated correctly if both computed_start and computed_end given.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    new_foo_obj.computed_start = datetime.datetime.now(pytz.utc)\n    new_foo_obj.computed_end = new_foo_obj.computed_start + datetime.timedelta(12)\n    assert new_foo_obj.computed_duration == datetime.timedelta(12)\n\n\ndef test_computed_duration_is_read_only(date_range_mixin_tester):\n    \"\"\"computed_duration attribute is read-only.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    with pytest.raises(AttributeError) as cm:\n        new_foo_obj.computed_duration = datetime.timedelta(10)\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'computed_duration'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'computed_duration' of 'DateRangeMixFooMixedInClass' \"\n        \"object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_total_seconds_attribute_is_read_only(date_range_mixin_tester):\n    \"\"\"total_seconds is read only.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    with pytest.raises(AttributeError) as cm:\n        new_foo_obj.total_seconds = 234234\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'total_seconds'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'total_seconds' of 'DateRangeMixFooMixedInClass' \"\n        \"object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_total_seconds_attribute_is_working_as_expected(date_range_mixin_tester):\n    \"\"\"total_seconds is read only.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    new_foo_obj.start = datetime.datetime(2013, 5, 31, 10, 0, tzinfo=pytz.utc)\n    new_foo_obj.end = datetime.datetime(2013, 5, 31, 18, 0, tzinfo=pytz.utc)\n    assert new_foo_obj.total_seconds == 8 * 60 * 60\n\n\ndef test_computed_total_seconds_attribute_is_read_only(date_range_mixin_tester):\n    \"\"\"computed_total_seconds is read only.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    with pytest.raises(AttributeError) as cm:\n        new_foo_obj.computed_total_seconds = 234234\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'computed_total_seconds'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'computed_total_seconds' of 'DateRangeMixFooMixedInClass' \"\n        \"object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_computed_total_seconds_attribute_is_working_as_expected(\n    date_range_mixin_tester,\n):\n    \"\"\"computed_total_seconds is read only.\"\"\"\n    data = date_range_mixin_tester\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    new_foo_obj.computed_start = datetime.datetime(2013, 5, 31, 10, 0, tzinfo=pytz.utc)\n    new_foo_obj.computed_end = datetime.datetime(2013, 5, 31, 18, 0, tzinfo=pytz.utc)\n    assert new_foo_obj.computed_total_seconds == 8 * 60 * 60\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_date_range_mixin_db(setup_postgresql_db):\n    \"\"\"Set up the tests that needs a database.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = setup_postgresql_db\n\n    # create mock objects\n    data[\"start\"] = datetime.datetime(2013, 3, 22, 15, 15, tzinfo=pytz.utc)\n    data[\"end\"] = data[\"start\"] + datetime.timedelta(days=20)\n    data[\"duration\"] = datetime.timedelta(days=10)\n\n    data[\"kwargs\"] = {\n        \"name\": \"Test Daterange Mixin\",\n        \"description\": \"This is a simple entity object for testing \" \"DateRangeMixin\",\n        \"start\": data[\"start\"],\n        \"end\": data[\"end\"],\n        \"duration\": data[\"duration\"],\n    }\n    data[\"test_foo_obj\"] = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    return data\n\n\ndef test_start_end_and_duration_values_are_rounded_to_the_studio_timing_resolution(\n    setup_date_range_mixin_db,\n):\n    \"\"\"start and end dates are rounded to the Studio timing_resolution.\"\"\"\n    data = setup_date_range_mixin_db\n    log.set_level(logging.DEBUG)\n    studio = Studio.query.first()\n    if not studio:\n        logger.debug(\"No studio found! Creating one!\")\n        studio = Studio(\n            name=\"Test Studio\", timing_resolution=datetime.timedelta(minutes=5)\n        )\n        DBSession.add(studio)\n        DBSession.commit()\n    else:\n        logger.debug(\"A studio found! Updating timing resolution!\")\n        studio.timing_resolution = datetime.timedelta(minutes=5)\n        studio.update_defaults()\n\n    data[\"kwargs\"][\"start\"] = datetime.datetime(\n        2013, 3, 22, 2, 38, 55, 531, tzinfo=pytz.utc\n    )\n    data[\"kwargs\"][\"end\"] = datetime.datetime(\n        2013, 3, 24, 16, 46, 32, 102, tzinfo=pytz.utc\n    )\n\n    new_foo_obj = DateRangeMixFooMixedInClass(**data[\"kwargs\"])\n    # check the start\n    expected_start = datetime.datetime(2013, 3, 22, 2, 40, tzinfo=pytz.utc)\n    assert new_foo_obj.start == expected_start\n    # check the end\n    expected_end = datetime.datetime(2013, 3, 24, 16, 45, tzinfo=pytz.utc)\n    assert new_foo_obj.end == expected_end\n    # check the duration\n    assert new_foo_obj.duration == expected_end - expected_start\n"
  },
  {
    "path": "tests/mixins/test_declarative_project_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"ProjectMixin related tests.\"\"\"\nimport pytest\n\nfrom sqlalchemy import Column, ForeignKey, Integer\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import (\n    Project,\n    ProjectMixin,\n    Repository,\n    SimpleEntity,\n    Status,\n    StatusList,\n    Type,\n)\n\n\nclass DeclProjMixA(SimpleEntity, ProjectMixin):\n    \"\"\"A class for testing ProjectMixin.\"\"\"\n\n    __tablename__ = \"DeclProjMixAs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"DeclProjMixA\"}\n    a_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(DeclProjMixA, self).__init__(**kwargs)\n        ProjectMixin.__init__(self, **kwargs)\n\n\nclass DeclProjMixB(SimpleEntity, ProjectMixin):\n    \"\"\"A class for testing ProjectMixin.\"\"\"\n\n    __tablename__ = \"DeclProjMixBs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"DeclProjMixB\"}\n    b_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(DeclProjMixB, self).__init__(**kwargs)\n        ProjectMixin.__init__(self, **kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_project_mixin_tester():\n    \"\"\"Set up the tests for ProjectMixin.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = dict()\n    data[\"test_stat1\"] = Status(name=\"On Hold\", code=\"OH\")\n    data[\"test_stat2\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"test_stat3\"] = Status(name=\"Approved\", code=\"APP\")\n    data[\"test_status_list_1\"] = StatusList(\n        name=\"A Statuses\",\n        statuses=[data[\"test_stat1\"], data[\"test_stat3\"]],\n        target_entity_type=DeclProjMixA,\n    )\n\n    data[\"test_status_list_2\"] = StatusList(\n        name=\"B Statuses\",\n        statuses=[data[\"test_stat2\"], data[\"test_stat3\"]],\n        target_entity_type=DeclProjMixB,\n    )\n\n    data[\"test_project_statuses\"] = StatusList(\n        name=\"Project Statuses\",\n        statuses=[data[\"test_stat2\"], data[\"test_stat3\"]],\n        target_entity_type=\"Project\",\n    )\n\n    data[\"test_project_type\"] = Type(\n        name=\"Test Project Type\",\n        code=\"testproj\",\n        target_entity_type=\"Project\",\n    )\n\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repo\",\n        code=\"TR\",\n    )\n\n    data[\"test_project\"] = Project(\n        name=\"Test Project\",\n        code=\"tp\",\n        type=data[\"test_project_type\"],\n        status_list=data[\"test_project_statuses\"],\n        repository=data[\"test_repository\"],\n    )\n\n    data[\"kwargs\"] = {\n        \"name\": \"ozgur\",\n        \"status_list\": data[\"test_status_list_1\"],\n        \"project\": data[\"test_project\"],\n    }\n\n    data[\"test_a_obj\"] = DeclProjMixA(**data[\"kwargs\"])\n    return data\n\n\ndef test_project_attribute_is_working_as_expected(setup_project_mixin_tester):\n    \"\"\"project attribute is working as expected.\"\"\"\n    data = setup_project_mixin_tester\n    assert data[\"test_a_obj\"].project == data[\"test_project\"]\n"
  },
  {
    "path": "tests/mixins/test_declarative_reference_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"ReferenceMixin related tests.\"\"\"\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import File, SimpleEntity\nfrom stalker.models.mixins import ReferenceMixin\n\n\nclass DeclRefMixA(SimpleEntity, ReferenceMixin):\n    \"\"\"A test class for testing ReferenceMixin.\"\"\"\n\n    __tablename__ = \"DeclRefMixAs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"DeclRefMixA\"}\n    a_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(DeclRefMixA, self).__init__(**kwargs)\n        ReferenceMixin.__init__(self, **kwargs)\n\n\nclass DeclRefMixB(SimpleEntity, ReferenceMixin):\n    \"\"\"A test class for testing ReferenceMixin.\"\"\"\n\n    __tablename__ = \"RefMixBs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"DeclRefMixB\"}\n    b_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(DeclRefMixB, self).__init__(**kwargs)\n        ReferenceMixin.__init__(self, **kwargs)\n\n\ndef test_reference_mixin_setup():\n    \"\"\"ReferenceMixin setup.\"\"\"\n    a_ins = DeclRefMixA(name=\"ozgur\")\n    b_ins = DeclRefMixB(name=\"bozgur\")\n\n    new_file1 = File(name=\"test file 1\", full_path=\"none\")\n    new_file2 = File(name=\"test file 2\", full_path=\"no path\")\n\n    a_ins.references.append(new_file1)\n    b_ins.references.append(new_file2)\n\n    assert new_file1 in a_ins.references\n    assert new_file2 in b_ins.references\n"
  },
  {
    "path": "tests/mixins/test_declarative_schedule_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"DateRangeMixin related tests.\"\"\"\n\nimport datetime\nimport logging\n\nimport pytest\n\nimport pytz\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import log\nfrom stalker.models.entity import SimpleEntity\nfrom stalker.models.mixins import DateRangeMixin\n\n\nlogger = log.get_logger(__name__)\nlog.set_level(logging.DEBUG)\n\n\nclass DeclSchedMixA(SimpleEntity, DateRangeMixin):\n    \"\"\"A class for testing DateRangeMixin.\"\"\"\n\n    __tablename__ = \"DeclSchedMixAs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"DeclSchedMixA\"}\n    a_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(DeclSchedMixA, self).__init__(**kwargs)\n        DateRangeMixin.__init__(self, **kwargs)\n\n\nclass DeclSchedMixB(SimpleEntity, DateRangeMixin):\n    \"\"\"A class for testing DateRangeMixin.\"\"\"\n\n    __tablename__ = \"DeclSchedMixBs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"DeclSchedMixB\"}\n    b_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(DeclSchedMixB, self).__init__(**kwargs)\n        DateRangeMixin.__init__(self, **kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_schedule_mixin_tester():\n    \"\"\"Set up the tests for DateRangeMixin setup.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = dict()\n    data[\"kwargs\"] = {\n        \"name\": \"ozgur\",\n        \"start\": datetime.datetime(2013, 3, 20, 4, 0, tzinfo=pytz.utc),\n        \"end\": datetime.datetime(2013, 3, 30, 4, 0, tzinfo=pytz.utc),\n        \"duration\": datetime.timedelta(10),\n    }\n    return data\n\n\ndef test_mixin_setup_is_working_as_expected(setup_schedule_mixin_tester):\n    \"\"\"Mixin setup is working as expected.\"\"\"\n    data = setup_schedule_mixin_tester\n    new_a = DeclSchedMixA(**data[\"kwargs\"])  # should not create any problem\n    assert new_a.start == data[\"kwargs\"][\"start\"]\n    assert new_a.end == data[\"kwargs\"][\"end\"]\n    assert new_a.duration == data[\"kwargs\"][\"duration\"]\n\n    logger.debug(\"----------------------------\")\n    logger.debug(new_a.start)\n    logger.debug(new_a.end)\n    logger.debug(new_a.duration)\n\n    # try to change the start and check if the duration is also updated\n    new_a.start = datetime.datetime(2013, 3, 30, 10, 0, tzinfo=pytz.utc)\n\n    assert new_a.start == datetime.datetime(2013, 3, 30, 10, 0, tzinfo=pytz.utc)\n\n    assert new_a.end == datetime.datetime(2013, 4, 9, 10, 0, tzinfo=pytz.utc)\n\n    assert new_a.duration == datetime.timedelta(10)\n\n    a_start = new_a.start\n    a_end = new_a.end\n    a_duration = new_a.duration\n\n    # now check the start, end and duration\n    logger.debug(\"----------------------------\")\n    logger.debug(new_a.start)\n    logger.debug(new_a.end)\n    logger.debug(new_a.duration)\n\n    # create a new class\n    new_b = DeclSchedMixB(**data[\"kwargs\"])\n    # now check the start, end and duration\n    assert new_b.start == data[\"kwargs\"][\"start\"]\n    assert new_b.end == data[\"kwargs\"][\"end\"]\n    assert new_b.duration == data[\"kwargs\"][\"duration\"]\n\n    logger.debug(\"----------------------------\")\n    logger.debug(new_b.start)\n    logger.debug(new_b.end)\n    logger.debug(new_b.duration)\n\n    # now check the start, end and duration of A\n    logger.debug(\"----------------------------\")\n    logger.debug(new_a.start)\n    logger.debug(new_a.end)\n    logger.debug(new_a.duration)\n    assert new_a.start == a_start\n    assert new_a.end == a_end\n    assert new_a.duration == a_duration\n"
  },
  {
    "path": "tests/mixins/test_declarative_status_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"StatusMixin class related tests.\"\"\"\nimport pytest\n\nfrom sqlalchemy import Column, ForeignKey, Integer\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import SimpleEntity, Status, StatusList, StatusMixin\n\n\nclass DeclStatMixA(SimpleEntity, StatusMixin):\n    \"\"\"A class for testing StatusMixin.\"\"\"\n\n    __tablename__ = \"DeclStatMixAs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"DeclStatMixA\"}\n    declStatMixAs_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(DeclStatMixA, self).__init__(**kwargs)\n        StatusMixin.__init__(self, **kwargs)\n\n\nclass DeclStatMixB(SimpleEntity, StatusMixin):\n    \"\"\"A class for testing StatusMixin.\"\"\"\n\n    __tablename__ = \"DeclStatMixBs\"\n    __mapper_args__ = {\"polymorphic_identity\": \"DeclStatMixB\"}\n    b_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(DeclStatMixB, self).__init__(**kwargs)\n        StatusMixin.__init__(self, **kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_status_mixin_tester():\n    \"\"\"Set up the tests for StatusMixin.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = dict()\n    data[\"test_stat1\"] = Status(name=\"On Hold\", code=\"OH\")\n    data[\"test_stat2\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"test_stat3\"] = Status(name=\"Approved\", code=\"APP\")\n    data[\"test_a_statusList\"] = StatusList(\n        name=\"A Statuses\",\n        statuses=[data[\"test_stat1\"], data[\"test_stat3\"]],\n        target_entity_type=\"DeclStatMixA\",\n    )\n    data[\"test_b_statusList\"] = StatusList(\n        name=\"B Statuses\",\n        statuses=[data[\"test_stat2\"], data[\"test_stat3\"]],\n        target_entity_type=\"DeclStatMixB\",\n    )\n    data[\"kwargs\"] = {\"name\": \"ozgur\", \"status_list\": data[\"test_a_statusList\"]}\n    return data\n\n\ndef test_status_list_argument_not_set(setup_status_mixin_tester):\n    \"\"\"TypeError will be raised if the status_list argument is not set.\"\"\"\n    data = setup_status_mixin_tester\n    data[\"kwargs\"].pop(\"status_list\")\n    with pytest.raises(TypeError) as cm:\n        DeclStatMixA(**data[\"kwargs\"])\n    assert (\n        str(cm.value) == \"DeclStatMixA instances cannot be initialized without a \"\n        \"stalker.models.status.StatusList instance, please pass a suitable StatusList \"\n        \"(StatusList.target_entity_type=DeclStatMixA) with the 'status_list' argument\"\n    )\n\n\ndef test_status_list_argument_is_not_correct(setup_status_mixin_tester):\n    \"\"\"TypeError is raised if status_list argument is not a StatusList.\"\"\"\n    data = setup_status_mixin_tester\n    data[\"kwargs\"][\"status_list\"] = data[\"test_b_statusList\"]\n    with pytest.raises(TypeError) as cm:\n        DeclStatMixA(**data[\"kwargs\"])\n    assert (\n        str(cm.value) == \"The given StatusLists' target_entity_type is DeclStatMixB, \"\n        \"whereas the entity_type of this object is DeclStatMixA\"\n    )\n\n\ndef test_status_list_working_as_expected(setup_status_mixin_tester):\n    \"\"\"status_list attribute is working as expected.\"\"\"\n    data = setup_status_mixin_tester\n    new_a_ins = DeclStatMixA(name=\"Ozgur\", status_list=data[\"test_a_statusList\"])\n    assert data[\"test_stat1\"] in new_a_ins.status_list\n    assert data[\"test_stat2\"] not in new_a_ins.status_list\n    assert data[\"test_stat3\"] in new_a_ins.status_list\n"
  },
  {
    "path": "tests/mixins/test_project_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"ProjectMixin related tests.\"\"\"\nimport pytest\n\nfrom sqlalchemy import Column, ForeignKey, Integer\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import Project, ProjectMixin, Repository, SimpleEntity, Type\n\n\nclass ProjMixClass(SimpleEntity, ProjectMixin):\n    \"\"\"A class for testing ProjectMixin.\"\"\"\n\n    __tablename__ = \"ProjMixClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"ProjMixClass\"}\n    projMixClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(ProjMixClass, self).__init__(**kwargs)\n        ProjectMixin.__init__(self, **kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_project_mixin_tester():\n    \"\"\"Set up the tests for the ProjectMixin.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    # create a repository\n    data = dict()\n    data[\"repository_type\"] = Type(\n        name=\"Test Repository Type\",\n        code=\"testproj\",\n        target_entity_type=\"Repository\",\n    )\n\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"repository_type\"],\n    )\n\n    # statuses\n    from stalker import Status\n\n    data[\"status1\"] = Status(name=\"Status1\", code=\"STS1\")\n    data[\"status2\"] = Status(name=\"Status2\", code=\"STS2\")\n    data[\"status3\"] = Status(name=\"Status3\", code=\"STS3\")\n\n    # project status list\n    from stalker import StatusList\n\n    data[\"project_status_list\"] = StatusList(\n        name=\"Project Status List\",\n        statuses=[\n            data[\"status1\"],\n            data[\"status2\"],\n            data[\"status3\"],\n        ],\n        target_entity_type=\"Project\",\n    )\n\n    # project type\n    data[\"test_project_type\"] = Type(\n        name=\"Test Project Type\",\n        code=\"testproj\",\n        target_entity_type=\"Project\",\n    )\n\n    # create projects\n    data[\"test_project1\"] = Project(\n        name=\"Test Project 1\",\n        code=\"tp1\",\n        type=data[\"test_project_type\"],\n        status_list=data[\"project_status_list\"],\n        repository=data[\"test_repository\"],\n    )\n    data[\"test_project2\"] = Project(\n        name=\"Test Project 2\",\n        code=\"tp2\",\n        type=data[\"test_project_type\"],\n        status_list=data[\"project_status_list\"],\n        repository=data[\"test_repository\"],\n    )\n    data[\"kwargs\"] = {\n        \"name\": \"Test Class\",\n        \"project\": data[\"test_project1\"],\n    }\n    data[\"test_foo_obj\"] = ProjMixClass(**data[\"kwargs\"])\n    return data\n\n\ndef test_project_argument_is_skipped(setup_project_mixin_tester):\n    \"\"\"TypeError will be raised if the project argument is skipped.\"\"\"\n    data = setup_project_mixin_tester\n    data[\"kwargs\"].pop(\"project\")\n    with pytest.raises(TypeError) as cm:\n        ProjMixClass(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value)\n        == \"ProjMixClass.project cannot be None it must be an instance of \"\n        \"stalker.models.project.Project\"\n    )\n\n\ndef test_project_argument_is_none(setup_project_mixin_tester):\n    \"\"\"TypeError will be raised if the project argument is None.\"\"\"\n    data = setup_project_mixin_tester\n    data[\"kwargs\"][\"project\"] = None\n    with pytest.raises(TypeError) as cm:\n        ProjMixClass(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value)\n        == \"ProjMixClass.project cannot be None it must be an instance of \"\n        \"stalker.models.project.Project\"\n    )\n\n\ndef test_project_attribute_is_none(setup_project_mixin_tester):\n    \"\"\"TypeError is raised if the project attribute is set to None.\"\"\"\n    data = setup_project_mixin_tester\n    with pytest.raises(TypeError) as cm:\n        data[\"test_foo_obj\"].project = None\n\n    assert (\n        str(cm.value)\n        == \"ProjMixClass.project cannot be None it must be an instance of \"\n        \"stalker.models.project.Project\"\n    )\n\n\ndef test_project_argument_is_not_a_project_instance(setup_project_mixin_tester):\n    \"\"\"TypeError is raised if the project argument is not a Project.\"\"\"\n    data = setup_project_mixin_tester\n    data[\"kwargs\"][\"project\"] = \"a project\"\n    with pytest.raises(TypeError) as cm:\n        ProjMixClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"ProjMixClass.project should be an instance of stalker.models.project.Project \"\n        \"instance, not str: 'a project'\"\n    )\n\n\ndef test_project_attribute_is_not_a_project_instance(setup_project_mixin_tester):\n    \"\"\"TypeError is raised if the project attribute is not a Project.\"\"\"\n    data = setup_project_mixin_tester\n    with pytest.raises(TypeError) as cm:\n        data[\"test_foo_obj\"].project = \"a project\"\n\n    assert str(cm.value) == (\n        \"ProjMixClass.project should be an instance of stalker.models.project.Project \"\n        \"instance, not str: 'a project'\"\n    )\n\n\ndef test_project_attribute_is_working_as_expected(setup_project_mixin_tester):\n    \"\"\"project attribute is working as expected.\"\"\"\n    data = setup_project_mixin_tester\n    data[\"test_foo_obj\"].project = data[\"test_project2\"]\n    assert data[\"test_foo_obj\"].project == data[\"test_project2\"]\n"
  },
  {
    "path": "tests/mixins/test_reference_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"ReferenceMixin related tests.\"\"\"\n\nimport pytest\n\nfrom sqlalchemy import Column, ForeignKey, Integer\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import Entity, File, ReferenceMixin, SimpleEntity, Type\n\n\nclass RefMixFooClass(SimpleEntity, ReferenceMixin):\n    \"\"\"class for ReferenceMixin tests.\"\"\"\n\n    __tablename__ = \"RefMixFooClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"RefMixFooClass\"}\n    refMixFooClass_id: Mapped[int] = mapped_column(\n        \"id\", Integer, ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(RefMixFooClass, self).__init__(**kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_reference_mixin_tester():\n    \"\"\"Set up the tests for the ReferenceMixin.\n\n    Returns:\n        dict: test data.\n    \"\"\"\n    # file type\n    data = dict()\n    data[\"test_file_type\"] = Type(\n        name=\"Test File Type\",\n        code=\"testfile\",\n        target_entity_type=File,\n    )\n\n    # create a couple of File objects\n    data[\"test_file1\"] = File(\n        name=\"Test File 1\",\n        type=data[\"test_file_type\"],\n        full_path=\"test_path\",\n        filename=\"test_filename\",\n    )\n\n    data[\"test_file2\"] = File(\n        name=\"Test File 2\",\n        type=data[\"test_file_type\"],\n        full_path=\"test_path\",\n        filename=\"test_filename\",\n    )\n\n    data[\"test_file3\"] = File(\n        name=\"Test File 3\",\n        type=data[\"test_file_type\"],\n        full_path=\"test_path\",\n        filename=\"test_filename\",\n    )\n\n    data[\"test_file4\"] = File(\n        name=\"Test File 4\",\n        type=data[\"test_file_type\"],\n        full_path=\"test_path\",\n        filename=\"test_filename\",\n    )\n\n    data[\"test_entity1\"] = Entity(\n        name=\"Test Entity 1\",\n    )\n\n    data[\"test_entity2\"] = Entity(\n        name=\"Test Entity 2\",\n    )\n\n    data[\"test_files\"] = [\n        data[\"test_file1\"],\n        data[\"test_file2\"],\n        data[\"test_file3\"],\n        data[\"test_file4\"],\n    ]\n\n    data[\"test_foo_obj\"] = RefMixFooClass(name=\"Ref Mixin Test\")\n    return data\n\n\ndef test_references_attribute_accepting_empty_list(setup_reference_mixin_tester):\n    \"\"\"references attribute accepting empty lists.\"\"\"\n    data = setup_reference_mixin_tester\n    data[\"test_foo_obj\"].references = []\n\n\ndef test_references_attribute_only_accepts_list_like_objects(\n    setup_reference_mixin_tester,\n):\n    \"\"\"references attribute accepts only list-like objects.\"\"\"\n    data = setup_reference_mixin_tester\n    with pytest.raises(TypeError) as cm:\n        data[\"test_foo_obj\"].references = \"a string\"\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_references_attribute_accepting_only_lists_of_file_instances(\n    setup_reference_mixin_tester,\n):\n    \"\"\"references attribute accepting only lists of Files.\"\"\"\n    data = setup_reference_mixin_tester\n    test_value = [1, 2.2, \"some references\"]\n\n    with pytest.raises(TypeError) as cm:\n        data[\"test_foo_obj\"].references = test_value\n\n    assert str(cm.value) == (\n        \"RefMixFooClass.references should only contain instances of \"\n        \"stalker.models.file.File, not int: '1'\"\n    )\n\n\ndef test_references_attribute_elements_accepts_files_only(setup_reference_mixin_tester):\n    \"\"\"TypeError is raised if non File assigned to references attribute.\"\"\"\n    data = setup_reference_mixin_tester\n    with pytest.raises(TypeError) as cm:\n        data[\"test_foo_obj\"].references = [data[\"test_entity1\"], data[\"test_entity2\"]]\n\n    assert str(cm.value) == (\n        \"RefMixFooClass.references should only contain instances of \"\n        \"stalker.models.file.File, not Entity: '<Test Entity 1 (Entity)>'\"\n    )\n\n\ndef test_references_attribute_is_working_as_expected(setup_reference_mixin_tester):\n    \"\"\"references attribute working as expected.\"\"\"\n    data = setup_reference_mixin_tester\n    data[\"test_foo_obj\"].references = data[\"test_files\"]\n    assert data[\"test_foo_obj\"].references == data[\"test_files\"]\n\n    test_value = [data[\"test_file1\"], data[\"test_file2\"]]\n    data[\"test_foo_obj\"].references = test_value\n    assert sorted(data[\"test_foo_obj\"].references, key=lambda x: x.name) == sorted(\n        test_value, key=lambda x: x.name\n    )\n\n\ndef test_references_application_test(setup_reference_mixin_tester):\n    \"\"\"example of ReferenceMixin usage.\"\"\"\n\n    class GreatEntity(SimpleEntity, ReferenceMixin):\n        __tablename__ = \"GreatEntities\"\n        __mapper_args__ = {\"polymorphic_identity\": \"GreatEntity\"}\n        ge_id: Mapped[int] = mapped_column(\n            \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n        )\n\n    my_ge = GreatEntity(name=\"Test\")\n    # we should have a references attribute right now\n    _ = my_ge.references\n    image_file_type = Type(name=\"Image\", code=\"image\", target_entity_type=\"File\")\n    new_file = File(\n        name=\"NewTestFile\",\n        full_path=\"nopath\",\n        filename=\"nofilename\",\n        type=image_file_type,\n    )\n    test_value = [new_file]\n    my_ge.references = test_value\n    assert my_ge.references == test_value\n"
  },
  {
    "path": "tests/mixins/test_schedule_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"ScheduleMixin related tests.\"\"\"\nimport datetime\n\nimport pytest\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nimport stalker\nfrom stalker import ScheduleMixin, SimpleEntity, defaults\nfrom stalker.models.enum import TimeUnit\nfrom stalker.models.enum import ScheduleModel\n\n\nclass MixedInClass(SimpleEntity, ScheduleMixin):\n    \"\"\"class derived from SimpleEntity and mixed in with ScheduleMixin for testing.\"\"\"\n\n    __tablename__ = \"ScheduleMixFooMixedInClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"ScheduleMixFooMixedInClass\"}\n    schedMixFooMixedInClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        SimpleEntity.__init__(self, **kwargs)\n        ScheduleMixin.__init__(self, **kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_schedule_mixin_tests():\n    \"\"\"Set up the tests for the ScheduleMixin.\n\n    Yields:\n        dict: Test data.\n    \"\"\"\n    stalker.defaults.config_values = stalker.defaults.default_config_values.copy()\n    stalker.defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n    data = dict()\n    data[\"kwargs\"] = {\n        \"name\": \"Test Object\",\n        \"schedule_timing\": 1,\n        \"schedule_unit\": TimeUnit.Hour,\n        \"schedule_model\": ScheduleModel.Effort,\n        \"schedule_constraint\": 0,\n    }\n    data[\"test_obj\"] = MixedInClass(**data[\"kwargs\"])\n    yield data\n    stalker.defaults.config_values = stalker.defaults.default_config_values.copy()\n    stalker.defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n\n\ndef test_schedule_model_attribute_is_effort_by_default(setup_schedule_mixin_tests):\n    \"\"\"schedule_model is effort by default.\"\"\"\n    data = setup_schedule_mixin_tests\n    assert data[\"test_obj\"].schedule_model == ScheduleModel.Effort\n\n\ndef test_schedule_model_argument_is_none(setup_schedule_mixin_tests):\n    \"\"\"schedule model is 'effort' if the schedule_model argument is set to None.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"][\"schedule_model\"] = None\n    new_task = MixedInClass(**data[\"kwargs\"])\n    assert new_task.schedule_model == ScheduleModel.Effort\n\n\ndef test_schedule_model_attribute_is_set_to_none(setup_schedule_mixin_tests):\n    \"\"\"schedule_model will be 'effort' if it is set to None.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"test_obj\"].schedule_model = None\n    assert data[\"test_obj\"].schedule_model == ScheduleModel.Effort\n\n\ndef test_schedule_model_argument_is_not_a_string(setup_schedule_mixin_tests):\n    \"\"\"TypeError is raised if the schedule_model argument is not a string.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"][\"schedule_model\"] = 234\n    with pytest.raises(TypeError) as cm:\n        MixedInClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], not int: '234'\"\n    )\n\n\ndef test_schedule_model_attribute_is_not_a_string(setup_schedule_mixin_tests):\n    \"\"\"TypeError is raised if the schedule_model attribute is not a string.\"\"\"\n    data = setup_schedule_mixin_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_obj\"].schedule_model = 2343\n\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], \"\n        \"not int: '2343'\"\n    )\n\n\ndef test_schedule_model_argument_is_not_in_correct_value(setup_schedule_mixin_tests):\n    \"\"\"ValueError is raised if the schedule_model argument is not valid.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"][\"schedule_model\"] = \"not in the list\"\n    with pytest.raises(ValueError) as cm:\n        MixedInClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], not \"\n        \"'not in the list'\"\n    )\n\n\ndef test_schedule_model_attribute_is_not_in_correct_value(setup_schedule_mixin_tests):\n    \"\"\"ValueError is raised if the schedule_model attribute is not valid.\"\"\"\n    data = setup_schedule_mixin_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_obj\"].schedule_model = \"not in the list\"\n\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], \"\n        \"not 'not in the list'\"\n    )\n\n\n@pytest.mark.parametrize(\"schedule_model\", [\"duration\", ScheduleModel.Duration])\ndef test_schedule_model_argument_is_working_as_expected(\n    setup_schedule_mixin_tests, schedule_model\n):\n    \"\"\"schedule_model arg is passed to the schedule_model attribute.\"\"\"\n    data = setup_schedule_mixin_tests\n    test_value = schedule_model\n    data[\"kwargs\"][\"schedule_model\"] = test_value\n    new_task = MixedInClass(**data[\"kwargs\"])\n    assert new_task.schedule_model == ScheduleModel.to_model(test_value)\n\n\n@pytest.mark.parametrize(\"schedule_model\", [\"duration\", ScheduleModel.Duration])\ndef test_schedule_model_attribute_is_working_as_expected(\n    setup_schedule_mixin_tests, schedule_model\n):\n    \"\"\"schedule_model attribute is working as expected.\"\"\"\n    data = setup_schedule_mixin_tests\n    test_value = schedule_model\n    assert data[\"test_obj\"].schedule_model != ScheduleModel.to_model(test_value)\n    data[\"test_obj\"].schedule_model = test_value\n    assert data[\"test_obj\"].schedule_model == ScheduleModel.to_model(test_value)\n\n\ndef test_schedule_constraint_is_0_by_default(setup_schedule_mixin_tests):\n    \"\"\"schedule_constraint attribute is None by default.\"\"\"\n    data = setup_schedule_mixin_tests\n    assert data[\"test_obj\"].schedule_constraint == 0\n\n\ndef test_schedule_constraint_argument_is_skipped(setup_schedule_mixin_tests):\n    \"\"\"schedule_constraint attribute is 0 if schedule_constraint is skipped.\"\"\"\n    data = setup_schedule_mixin_tests\n    try:\n        data[\"kwargs\"].pop(\"schedule_constraint\")\n    except KeyError:\n        pass\n    new_task = MixedInClass(**data[\"kwargs\"])\n    assert new_task.schedule_constraint == 0\n\n\ndef test_schedule_constraint_argument_is_none(setup_schedule_mixin_tests):\n    \"\"\"schedule_constraint attribute will be 0 if schedule_constraint is None.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"][\"schedule_constraint\"] = None\n    new_task = MixedInClass(**data[\"kwargs\"])\n    assert new_task.schedule_constraint == 0\n\n\ndef test_schedule_constraint_attribute_is_set_to_none(setup_schedule_mixin_tests):\n    \"\"\"schedule_constraint attribute will be 0 if it is set to None.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"test_obj\"].schedule_constraint = None\n    assert data[\"test_obj\"].schedule_constraint == 0\n\n\ndef test_schedule_constraint_argument_is_not_an_integer(setup_schedule_mixin_tests):\n    \"\"\"TypeError is raised if the schedule_constraint argument is not an int.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"][\"schedule_constraint\"] = \"not an int\"\n    with pytest.raises(ValueError) as cm:\n        MixedInClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"constraint should be a ScheduleConstraint enum value or one of \"\n        \"['None', 'Start', 'End', 'Both'], not 'not an int'\"\n    )\n\n\ndef test_schedule_constraint_attribute_is_not_an_integer(setup_schedule_mixin_tests):\n    \"\"\"TypeError is raised if the schedule_constraint attribute is not an int.\"\"\"\n    data = setup_schedule_mixin_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_obj\"].schedule_constraint = \"not an int\"\n\n    assert str(cm.value) == (\n        \"constraint should be a ScheduleConstraint enum value or one of \"\n        \"['None', 'Start', 'End', 'Both'], not 'not an int'\"\n    )\n\n\ndef test_schedule_constraint_argument_is_working_as_expected(\n    setup_schedule_mixin_tests,\n):\n    \"\"\"schedule_constraint arg value is passed to schedule_constraint attribute.\"\"\"\n    data = setup_schedule_mixin_tests\n    test_value = 2\n    data[\"kwargs\"][\"schedule_constraint\"] = test_value\n    new_task = MixedInClass(**data[\"kwargs\"])\n    assert new_task.schedule_constraint == test_value\n\n\ndef test_schedule_constraint_attribute_is_working_as_expected(\n    setup_schedule_mixin_tests,\n):\n    \"\"\"schedule_constraint attribute value is correctly changed.\"\"\"\n    data = setup_schedule_mixin_tests\n    test_value = 3\n    data[\"test_obj\"].schedule_constraint = test_value\n    assert data[\"test_obj\"].schedule_constraint == test_value\n\n\n@pytest.mark.parametrize(\"test_value\", [-1, 4])\ndef test_schedule_constraint_argument_value_is_out_of_range(\n    setup_schedule_mixin_tests,\n    test_value,\n):\n    \"\"\"schedule_constraint is clamped to the [0-3] range if it is out of range.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"][\"schedule_constraint\"] = test_value\n    with pytest.raises(ValueError) as cm:\n        _ = MixedInClass(**data[\"kwargs\"])\n    assert str(cm.value) == (f\"{test_value} is not a valid ScheduleConstraint\")\n\n\n@pytest.mark.parametrize(\"test_value\", [-1, 4])\ndef test_schedule_constraint_attribute_value_is_out_of_range(\n    setup_schedule_mixin_tests,\n    test_value,\n):\n    \"\"\"schedule_constraint is clamped to the [0-3] range if it is out of range.\"\"\"\n    data = setup_schedule_mixin_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_obj\"].schedule_constraint = test_value\n\n    assert str(cm.value) == (f\"{test_value} is not a valid ScheduleConstraint\")\n\n\ndef test_schedule_timing_argument_skipped(setup_schedule_mixin_tests):\n    \"\"\"schedule_timing is equal to 1h if the schedule_timing arg is skipped.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"].pop(\"schedule_timing\")\n    new_task = MixedInClass(**data[\"kwargs\"])\n\n    assert new_task.schedule_timing == MixedInClass.__default_schedule_timing__\n\n\ndef test_schedule_timing_argument_is_none(setup_schedule_mixin_tests):\n    \"\"\"schedule_timing==Config.timing_resolution.seconds/60 if the schedule_timing arg is None.\"\"\"\n    data = setup_schedule_mixin_tests\n    defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n    data[\"kwargs\"][\"schedule_timing\"] = None\n    new_task = MixedInClass(**data[\"kwargs\"])\n    assert new_task.schedule_timing == defaults.timing_resolution.seconds / 60.0\n\n\ndef test_schedule_timing_attribute_is_set_to_none(setup_schedule_mixin_tests):\n    \"\"\"schedule_timing==Config.timing_resolution.seconds/60 if it is set to None.\"\"\"\n    data = setup_schedule_mixin_tests\n    defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n    data[\"test_obj\"].schedule_timing = None\n    assert data[\"test_obj\"].schedule_timing == defaults.timing_resolution.seconds / 60.0\n\n\ndef test_schedule_timing_argument_is_not_an_integer_or_float(\n    setup_schedule_mixin_tests,\n):\n    \"\"\"TypeError is raised if the schedule_timing is not an int or float.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"][\"schedule_timing\"] = \"10d\"\n    with pytest.raises(TypeError) as cm:\n        MixedInClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"MixedInClass.schedule_timing should be an integer or float number showing the \"\n        \"value of the schedule timing of this MixedInClass, not str: '10d'\"\n    )\n\n\ndef test_schedule_timing_attribute_is_not_an_int_or_float(\n    setup_schedule_mixin_tests,\n):\n    \"\"\"TypeError is raised if the schedule_timing attribute is not int or float.\"\"\"\n    data = setup_schedule_mixin_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_obj\"].schedule_timing = \"10d\"\n\n    assert str(cm.value) == (\n        \"MixedInClass.schedule_timing should be an integer or float number showing the \"\n        \"value of the schedule timing of this MixedInClass, not str: '10d'\"\n    )\n\n\ndef test_schedule_timing_attribute_is_working_as_expected(setup_schedule_mixin_tests):\n    \"\"\"schedule_timing attribute is working as expected.\"\"\"\n    data = setup_schedule_mixin_tests\n    test_value = 18\n    data[\"test_obj\"].schedule_timing = test_value\n    assert data[\"test_obj\"].schedule_timing == test_value\n\n\ndef test_schedule_unit_argument_skipped(setup_schedule_mixin_tests):\n    \"\"\"schedule_unit attribute defaults if schedule_unit argument is skipped.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"].pop(\"schedule_unit\")\n    new_task = MixedInClass(**data[\"kwargs\"])\n    assert new_task.schedule_unit == MixedInClass.__default_schedule_unit__\n\n\ndef test_schedule_unit_argument_is_none(setup_schedule_mixin_tests):\n    \"\"\"schedule_unit attribute defaults if the schedule_unit argument is None.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"][\"schedule_unit\"] = None\n    new_task = MixedInClass(**data[\"kwargs\"])\n    assert new_task.schedule_unit == MixedInClass.__default_schedule_unit__\n\n\ndef test_schedule_unit_attribute_is_set_to_none(setup_schedule_mixin_tests):\n    \"\"\"schedule_unit attribute will use the default value if it is set to None.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"test_obj\"].schedule_unit = None\n    assert data[\"test_obj\"].schedule_unit == MixedInClass.__default_schedule_unit__\n\n\ndef test_schedule_unit_argument_is_not_a_string(setup_schedule_mixin_tests):\n    \"\"\"TypeError will be raised if the schedule_unit is not an int.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"][\"schedule_unit\"] = 10\n    with pytest.raises(TypeError) as cm:\n        MixedInClass(**data[\"kwargs\"])\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not int: '10'\"\n    )\n\n\ndef test_schedule_unit_attribute_is_not_a_string(setup_schedule_mixin_tests):\n    \"\"\"TypeError is raised if schedule_unit attribute is not set to a string.\"\"\"\n    data = setup_schedule_mixin_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_obj\"].schedule_unit = 23\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not int: '23'\"\n    )\n\n\ndef test_schedule_unit_attribute_is_working_as_expected(setup_schedule_mixin_tests):\n    \"\"\"schedule_unit attribute is working as expected.\"\"\"\n    data = setup_schedule_mixin_tests\n    test_value = TimeUnit.Week\n    data[\"test_obj\"].schedule_unit = test_value\n    assert data[\"test_obj\"].schedule_unit == test_value\n\n\ndef test_schedule_unit_argument_value_is_not_in_defaults_datetime_units(\n    setup_schedule_mixin_tests,\n):\n    \"\"\"ValueError is raised if the schedule_unit is not in datetime_units list.\"\"\"\n    data = setup_schedule_mixin_tests\n    data[\"kwargs\"][\"schedule_unit\"] = \"os\"\n    with pytest.raises(ValueError) as cm:\n        MixedInClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not 'os'\"\n    )\n\n\ndef test_schedule_unit_attribute_value_is_not_in_defaults_datetime_units(\n    setup_schedule_mixin_tests,\n):\n    \"\"\"ValueError is raised if schedule_unit not in datetime_units.\"\"\"\n    data = setup_schedule_mixin_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_obj\"].schedule_unit = \"so\"\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not 'so'\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"input_value,expected_result\",\n    [\n        [[60, True], (1, TimeUnit.Minute)],\n        [[125, True], (2, TimeUnit.Minute)],\n        [[1800, True], (30, TimeUnit.Minute)],\n        [[3600, True], (1, TimeUnit.Hour)],\n        [[5400, True], (90, TimeUnit.Minute)],\n        [[6000, True], (100, TimeUnit.Minute)],\n        [[7200, True], (2, TimeUnit.Hour)],\n        [[9600, True], (160, TimeUnit.Minute)],\n        [[10000, True], (166, TimeUnit.Minute)],\n        [[12000, True], (200, TimeUnit.Minute)],\n        [[14400, True], (4, TimeUnit.Hour)],\n        [[15000, True], (250, TimeUnit.Minute)],\n        [[18000, True], (5, TimeUnit.Hour)],\n        [[32400, True], (1, TimeUnit.Day)],\n        [[32400, False], (9, TimeUnit.Hour)],\n        [[64800, True], (2, TimeUnit.Day)],\n        [[64800, False], (18, TimeUnit.Hour)],\n        [[86400, True], (24, TimeUnit.Hour)],\n        [[86400, False], (1, TimeUnit.Day)],\n        [[162000, True], (1, TimeUnit.Week)],\n        [[162000, False], (45, TimeUnit.Hour)],\n        [[604800, False], (1, TimeUnit.Week)],\n        [[648000, True], (1, TimeUnit.Month)],\n        [[648000, False], (180, TimeUnit.Hour)],\n        [[8424000, True], (1, TimeUnit.Year)],\n        [[8424000, False], (2340, TimeUnit.Hour)],\n        [[2419200, False], (1, TimeUnit.Month)],\n        [[31536000, False], (1, TimeUnit.Year)],\n    ],\n)\ndef test_least_meaningful_time_unit_is_working_as_expected(\n    input_value, expected_result, setup_schedule_mixin_tests\n):\n    \"\"\"least_meaningful_time_unit is working as expected.\"\"\"\n    data = setup_schedule_mixin_tests\n\n    defaults[\"daily_working_hours\"] = 9\n    defaults[\"weekly_working_days\"] = 5\n    defaults[\"weekly_working_hours\"] = 45\n    defaults[\"yearly_working_days\"] = 52.1428 * 5\n\n    assert expected_result == data[\"test_obj\"].least_meaningful_time_unit(*input_value)\n\n\n@pytest.mark.parametrize(\n    \"schedule_model,schedule_timing,schedule_unit,expected_value\",\n    [\n        # effort values\n        [\"effort\", 1, \"min\", 60],\n        [\"effort\", 1, \"h\", 3600],\n        [\"effort\", 1, \"d\", 32400],\n        [\"effort\", 1, \"w\", 162000],\n        [\"effort\", 1, \"m\", 648000],\n        [\"effort\", 1, \"y\", 8424000],\n        [\"effort\", 1, TimeUnit.Minute, 60],\n        [\"effort\", 1, TimeUnit.Hour, 3600],\n        [\"effort\", 1, TimeUnit.Day, 32400],\n        [\"effort\", 1, TimeUnit.Week, 162000],\n        [\"effort\", 1, TimeUnit.Month, 648000],\n        [\"effort\", 1, TimeUnit.Year, 8424000],\n        [ScheduleModel.Effort, 1, \"min\", 60],\n        [ScheduleModel.Effort, 1, \"h\", 3600],\n        [ScheduleModel.Effort, 1, \"d\", 32400],\n        [ScheduleModel.Effort, 1, \"w\", 162000],\n        [ScheduleModel.Effort, 1, \"m\", 648000],\n        [ScheduleModel.Effort, 1, \"y\", 8424000],\n        [ScheduleModel.Effort, 1, TimeUnit.Minute, 60],\n        [ScheduleModel.Effort, 1, TimeUnit.Hour, 3600],\n        [ScheduleModel.Effort, 1, TimeUnit.Day, 32400],\n        [ScheduleModel.Effort, 1, TimeUnit.Week, 162000],\n        [ScheduleModel.Effort, 1, TimeUnit.Month, 648000],\n        [ScheduleModel.Effort, 1, TimeUnit.Year, 8424000],\n        # length values\n        [\"length\", 1, \"min\", 60],\n        [\"length\", 1, \"h\", 3600],\n        [\"length\", 1, \"d\", 32400],\n        [\"length\", 1, \"w\", 162000],\n        [\"length\", 1, \"m\", 648000],\n        [\"length\", 1, \"y\", 8424000],\n        [\"length\", 1, TimeUnit.Minute, 60],\n        [\"length\", 1, TimeUnit.Hour, 3600],\n        [\"length\", 1, TimeUnit.Day, 32400],\n        [\"length\", 1, TimeUnit.Week, 162000],\n        [\"length\", 1, TimeUnit.Month, 648000],\n        [\"length\", 1, TimeUnit.Year, 8424000],\n        [ScheduleModel.Length, 1, \"min\", 60],\n        [ScheduleModel.Length, 1, \"h\", 3600],\n        [ScheduleModel.Length, 1, \"d\", 32400],\n        [ScheduleModel.Length, 1, \"w\", 162000],\n        [ScheduleModel.Length, 1, \"m\", 648000],\n        [ScheduleModel.Length, 1, \"y\", 8424000],\n        [ScheduleModel.Length, 1, TimeUnit.Minute, 60],\n        [ScheduleModel.Length, 1, TimeUnit.Hour, 3600],\n        [ScheduleModel.Length, 1, TimeUnit.Day, 32400],\n        [ScheduleModel.Length, 1, TimeUnit.Week, 162000],\n        [ScheduleModel.Length, 1, TimeUnit.Month, 648000],\n        [ScheduleModel.Length, 1, TimeUnit.Year, 8424000],\n        # duration values\n        [\"duration\", 1, \"min\", 60],\n        [\"duration\", 1, \"h\", 3600],\n        [\"duration\", 1, \"d\", 86400],\n        [\"duration\", 1, \"w\", 604800],\n        [\"duration\", 1, \"m\", 2419200],\n        [\"duration\", 1, \"y\", 31536000],\n        [\"duration\", 1, TimeUnit.Minute, 60],\n        [\"duration\", 1, TimeUnit.Hour, 3600],\n        [\"duration\", 1, TimeUnit.Day, 86400],\n        [\"duration\", 1, TimeUnit.Week, 604800],\n        [\"duration\", 1, TimeUnit.Month, 2419200],\n        [\"duration\", 1, TimeUnit.Year, 31536000],\n        [ScheduleModel.Duration, 1, \"min\", 60],\n        [ScheduleModel.Duration, 1, \"h\", 3600],\n        [ScheduleModel.Duration, 1, \"d\", 86400],\n        [ScheduleModel.Duration, 1, \"w\", 604800],\n        [ScheduleModel.Duration, 1, \"m\", 2419200],\n        [ScheduleModel.Duration, 1, \"y\", 31536000],\n        [ScheduleModel.Duration, 1, TimeUnit.Minute, 60],\n        [ScheduleModel.Duration, 1, TimeUnit.Hour, 3600],\n        [ScheduleModel.Duration, 1, TimeUnit.Day, 86400],\n        [ScheduleModel.Duration, 1, TimeUnit.Week, 604800],\n        [ScheduleModel.Duration, 1, TimeUnit.Month, 2419200],\n        [ScheduleModel.Duration, 1, TimeUnit.Year, 31536000],\n    ],\n)\ndef test_to_seconds_is_working_as_expected(\n    schedule_model,\n    schedule_timing,\n    schedule_unit,\n    expected_value,\n    setup_schedule_mixin_tests,\n):\n    \"\"\"to_seconds method is working as expected.\"\"\"\n    data = setup_schedule_mixin_tests\n\n    defaults[\"daily_working_hours\"] = 9\n    defaults[\"weekly_working_days\"] = 5\n    defaults[\"weekly_working_hours\"] = 45\n    defaults[\"yearly_working_days\"] = 52.1428 * 5\n\n    data[\"test_obj\"].schedule_model = schedule_model\n    data[\"test_obj\"].schedule_timing = schedule_timing\n    data[\"test_obj\"].schedule_unit = schedule_unit\n    assert expected_value == data[\"test_obj\"].to_seconds(\n        data[\"test_obj\"].schedule_timing,\n        data[\"test_obj\"].schedule_unit,\n        data[\"test_obj\"].schedule_model,\n    )\n\n\ndef test_to_unit_unit_is_none(setup_schedule_mixin_tests):\n    \"\"\"to_unit method is working as expected.\"\"\"\n    data = setup_schedule_mixin_tests\n\n    defaults[\"daily_working_hours\"] = 9\n    defaults[\"weekly_working_days\"] = 5\n    defaults[\"weekly_working_hours\"] = 45\n    defaults[\"yearly_working_days\"] = 52.1428 * 5\n\n    assert data[\"test_obj\"].to_unit(10, None, \"effort\") is None\n\n\n@pytest.mark.parametrize(\n    \"schedule_model,schedule_timing,schedule_unit,seconds\",\n    [\n        # effort values\n        [\"effort\", 1, \"min\", 60],\n        [\"effort\", 10, \"min\", 600],\n        [\"effort\", 20, \"min\", 1200],\n        [\"effort\", 1, \"h\", 3600],\n        [\"effort\", 1.01, \"h\", 3636],\n        [\"effort\", 2, \"h\", 7200],\n        [\"effort\", 1, \"d\", 32400],\n        [\"effort\", 1, \"w\", 162000],\n        [\"effort\", 1, \"m\", 648000],\n        [\"effort\", 1, \"y\", 8424000],\n        # length values\n        [\"length\", 1, \"min\", 60],\n        [\"length\", 540, \"min\", 32400],\n        [\"length\", 1, \"h\", 3600],\n        [\"length\", 1, \"d\", 32400],\n        [\"length\", 1, \"w\", 162000],\n        [\"length\", 1, \"m\", 648000],\n        [\"length\", 1, \"y\", 8424000],\n        # duration values\n        [\"duration\", 1, \"min\", 60],\n        [\"duration\", 60, \"min\", 3600],\n        [\"duration\", 1440, \"min\", 86400],\n        [\"duration\", 1, \"h\", 3600],\n        [\"duration\", 1.5, \"h\", 5400],\n        [\"duration\", 2, \"h\", 7200],\n        [\"duration\", 1, \"d\", 86400],\n        [\"duration\", 1, \"w\", 604800],\n        [\"duration\", 1, \"m\", 2419200],\n        [\"duration\", 1, \"y\", 31536000],\n    ],\n)\ndef test_to_unit_is_working_as_expected(\n    schedule_model, schedule_timing, schedule_unit, seconds, setup_schedule_mixin_tests\n):\n    \"\"\"to_unit method is working as expected.\"\"\"\n    data = setup_schedule_mixin_tests\n\n    defaults[\"daily_working_hours\"] = 9\n    defaults[\"weekly_working_days\"] = 5\n    defaults[\"weekly_working_hours\"] = 45\n    defaults[\"yearly_working_days\"] = 52.1428 * 5\n\n    assert schedule_timing == data[\"test_obj\"].to_unit(\n        seconds, schedule_unit, schedule_model\n    )\n\n\n@pytest.mark.parametrize(\n    \"schedule_model,schedule_timing,schedule_unit,expected_value\",\n    [\n        # effort values\n        [\"effort\", 1, \"min\", 60],\n        [\"effort\", 1, \"h\", 3600],\n        [\"effort\", 1, \"d\", 32400],\n        [\"effort\", 1, \"w\", 162000],\n        [\"effort\", 1, \"m\", 648000],\n        [\"effort\", 1, \"y\", 8424000],\n        # length values\n        [\"length\", 1, \"min\", 60],\n        [\"length\", 1, \"h\", 3600],\n        [\"length\", 1, \"d\", 32400],\n        [\"length\", 1, \"w\", 162000],\n        [\"length\", 1, \"m\", 648000],\n        [\"length\", 1, \"y\", 8424000],\n        # duration values\n        [\"duration\", 1, \"min\", 60],\n        [\"duration\", 1, \"h\", 3600],\n        [\"duration\", 1, \"d\", 86400],\n        [\"duration\", 1, \"w\", 604800],\n        [\"duration\", 1, \"m\", 2419200],\n        [\"duration\", 1, \"y\", 31536000],\n    ],\n)\ndef test_schedule_seconds_is_working_as_expected(\n    schedule_model,\n    schedule_timing,\n    schedule_unit,\n    expected_value,\n    setup_schedule_mixin_tests,\n):\n    \"\"\"schedule_seconds property is working as expected.\"\"\"\n    data = setup_schedule_mixin_tests\n\n    defaults[\"daily_working_hours\"] = 9\n    defaults[\"weekly_working_days\"] = 5\n    defaults[\"weekly_working_hours\"] = 45\n    defaults[\"yearly_working_days\"] = 52.1428 * 5\n\n    data[\"test_obj\"].schedule_model = schedule_model\n    data[\"test_obj\"].schedule_timing = schedule_timing\n    data[\"test_obj\"].schedule_unit = schedule_unit\n    assert expected_value == data[\"test_obj\"].schedule_seconds\n"
  },
  {
    "path": "tests/mixins/test_status_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"StatusMixin class related tests.\"\"\"\n\nimport pytest\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import SimpleEntity, Status, StatusList, StatusMixin\nfrom stalker.db.session import DBSession\n\n\nclass StatMixClass(SimpleEntity, StatusMixin):\n    \"\"\"A class for testing StatusMixin.\"\"\"\n\n    __tablename__ = \"StatMixClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"StatMixClass\"}\n    StatMixClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(StatMixClass, self).__init__(**kwargs)\n        StatusMixin.__init__(self, **kwargs)\n\n\nclass StatMixDerivedClass(StatMixClass):\n    \"\"\"A class deriving from StatMixClass.\n\n    With the new approach it should be possible to use the StatusLists created\n    for the StatMixClass.\n    \"\"\"\n\n    __tablename__ = \"StatMixDerivedClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"StatMixDerivedClass\"}\n    StatMixDerivedClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"StatMixClasses.id\"), primary_key=True\n    )\n\n\n@pytest.fixture(scope=\"function\")\ndef status_mixin_tests():\n    \"\"\"Set up the tests for the StatusMixin class.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = dict()\n    data[\"test_status1\"] = Status(name=\"Status1\", code=\"STS1\")\n    data[\"test_status2\"] = Status(name=\"Status2\", code=\"STS2\")\n    data[\"test_status3\"] = Status(name=\"Status3\", code=\"STS3\")\n    data[\"test_status4\"] = Status(name=\"Status4\", code=\"STS4\")\n    data[\"test_status5\"] = Status(name=\"Status5\", code=\"STS5\")\n\n    # statuses which are not going to be used\n    data[\"test_status6\"] = Status(name=\"Status6\", code=\"STS6\")\n    data[\"test_status7\"] = Status(name=\"Status7\", code=\"STS7\")\n    data[\"test_status8\"] = Status(name=\"Status8\", code=\"STS8\")\n\n    # a test StatusList object\n    data[\"test_status_list1\"] = StatusList(\n        name=\"Test Status List 1\",\n        statuses=[\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ],\n        target_entity_type=\"StatMixClass\",\n    )\n\n    # another test StatusList object\n    data[\"test_status_list2\"] = StatusList(\n        name=\"Test Status List 2\",\n        statuses=[\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ],\n        target_entity_type=\"StatMixClass\",\n    )\n\n    data[\"kwargs\"] = {\n        \"name\": \"Test Class\",\n        \"status_list\": data[\"test_status_list1\"],\n        \"status\": data[\"test_status_list1\"].statuses[0],\n    }\n\n    data[\"test_mixed_obj\"] = StatMixClass(**data[\"kwargs\"])\n    data[\"test_mixed_obj\"].status_list = data[\"test_status_list1\"]\n\n    # create another one without status_list set to something\n    data[\"test_mixed_obj2\"] = StatMixClass(**data[\"kwargs\"])\n    return data\n\n\ndef test_status_list_arg_is_not_a_status_list_instance(status_mixin_tests):\n    \"\"\"TypeError is raised if status_list arg is not a StatusList.\"\"\"\n    data = status_mixin_tests\n    data[\"kwargs\"][\"status_list\"] = 100\n    with pytest.raises(TypeError) as cm:\n        StatMixClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"StatMixClass.status_list should be an instance of \"\n        \"stalker.models.status.StatusList, not int: '100'\"\n    )\n\n\ndef test_status_list_attr_is_not_a_status_list(status_mixin_tests):\n    \"\"\"TypeError is raised if status_list is not a StatusList.\"\"\"\n    data = status_mixin_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_mixed_obj\"].status_list = \"a string\"\n\n    assert str(cm.value) == (\n        \"StatMixClass.status_list should be an instance of \"\n        \"stalker.models.status.StatusList, not str: 'a string'\"\n    )\n\n\ndef test_status_list_arg_is_not_suitable_for_the_current_class(status_mixin_tests):\n    \"\"\"TypeError is raised if the Status.target_entity_type is not compatible.\"\"\"\n    data = status_mixin_tests\n    # create a new status list suitable for another class with different\n    # entity_type\n\n    new_status_list = StatusList(\n        name=\"Sequence Statuses\",\n        statuses=[\n            Status(name=\"On Hold\", code=\"OH\"),\n            Status(name=\"Complete\", code=\"CMPLT\"),\n        ],\n        target_entity_type=\"Sequence\",\n    )\n\n    data[\"kwargs\"][\"status_list\"] = new_status_list\n    data[\"kwargs\"].pop(\"status\")\n    with pytest.raises(TypeError) as cm:\n        _ = StatMixClass(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value)\n        == \"The given StatusLists' target_entity_type is Sequence, whereas \"\n        \"the entity_type of this object is StatMixClass\"\n    )\n\n\ndef test_status_list_attr_is_not_suitable_for_the_current_class(status_mixin_tests):\n    \"\"\"TypeError is raised if the Status.target_entity_type is not compatible.\"\"\"\n    data = status_mixin_tests\n    # create a new status list suitable for another class with different\n    # entity_type\n\n    new_status_list = StatusList(\n        name=\"Sequence Statuses\",\n        statuses=[\n            Status(name=\"On Hold\", code=\"OH\"),\n            Status(name=\"Complete\", code=\"CMPLT\"),\n        ],\n        target_entity_type=\"Sequence\",\n    )\n\n    with pytest.raises(TypeError) as cm:\n        data[\"test_mixed_obj\"].status_list = new_status_list\n\n    assert (\n        str(cm.value)\n        == \"The given StatusLists' target_entity_type is Sequence, whereas \"\n        \"the entity_type of this object is StatMixClass\"\n    )\n\n\ndef test_status_list_arg_is_suitable_for_the_super(status_mixin_tests):\n    \"\"\"It is possible to use a StatusList that is suitable for a super.\"\"\"\n    data = status_mixin_tests\n    # use the status list suitable for the super class\n    # this should not raise a TypeError\n    assert data[\"kwargs\"][\"status_list\"].target_entity_type != \"StatMixDerivedClass\"\n    obj = StatMixDerivedClass(**data[\"kwargs\"])\n    assert obj.status_list == data[\"kwargs\"][\"status_list\"]\n\n\ndef test_status_list_attr_is_working_as_expected(status_mixin_tests):\n    \"\"\"status_list attribute is working as expected.\"\"\"\n    data = status_mixin_tests\n    new_suitable_list = StatusList(\n        name=\"Suitable Statuses\",\n        statuses=[\n            Status(name=\"On Hold\", code=\"OH\"),\n            Status(name=\"Complete\", code=\"CMPLT\"),\n        ],\n        target_entity_type=\"StatMixClass\",\n    )\n\n    # this shouldn't raise any errors\n    data[\"test_mixed_obj\"].status_list = new_suitable_list\n    assert data[\"test_mixed_obj\"].status_list == new_suitable_list\n\n\ndef test_status_arg_set_to_none(status_mixin_tests):\n    \"\"\"first in the status_list attribute is used if the status arg is None.\"\"\"\n    data = status_mixin_tests\n    data[\"kwargs\"][\"status\"] = None\n    new_obj = StatMixClass(**data[\"kwargs\"])\n    assert new_obj.status == new_obj.status_list[0]\n\n\ndef test_status_attr_set_to_none(status_mixin_tests):\n    \"\"\"first in the status_list is used if status attribute is set to None.\"\"\"\n    data = status_mixin_tests\n    data[\"test_mixed_obj\"].status = None\n    assert data[\"test_mixed_obj\"].status == data[\"test_mixed_obj\"].status_list[0]\n\n\ndef test_status_arg_is_not_a_status_instance_or_integer(status_mixin_tests):\n    \"\"\"TypeError is raised if status arg is not a Status or int.\"\"\"\n    data = status_mixin_tests\n    data[\"kwargs\"][\"status\"] = \"0\"\n    with pytest.raises(TypeError) as cm:\n        StatMixClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"StatMixClass.status must be an instance of stalker.models.status.Status or \"\n        \"an integer showing the index of the Status object in the \"\n        \"StatMixClass.status_list, not str: '0'\"\n    )\n\n\ndef test_status_attr_is_not_a_status_or_integer(\n    status_mixin_tests,\n):\n    \"\"\"TypeError is raised if status attribute is set to not Status nor int.\"\"\"\n    data = status_mixin_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_mixed_obj\"].status = \"a string\"\n\n    assert str(cm.value) == (\n        \"StatMixClass.status must be an instance of stalker.models.status.Status \"\n        \"or an integer showing the index of the Status object in the \"\n        \"StatMixClass.status_list, not str: 'a string'\"\n    )\n\n\ndef test_status_attr_is_set_to_a_status_which_is_not_in_the_status_list(\n    status_mixin_tests,\n):\n    \"\"\"ValueError is raised if Status is not in the related StatusList.\"\"\"\n    data = status_mixin_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_mixed_obj\"].status = data[\"test_status8\"]\n\n    assert (\n        str(cm.value) == \"The given Status instance for StatMixClass.status is not in \"\n        \"the StatMixClass.status_list, please supply a status from \"\n        \"that list.\"\n    )\n\n\ndef test_status_arg_is_working_as_expected_with_status_instances(\n    status_mixin_tests,\n):\n    \"\"\"status attribute value is set correctly with Status arg value.\"\"\"\n    data = status_mixin_tests\n    test_value = data[\"kwargs\"][\"status_list\"][1]\n    data[\"kwargs\"][\"status\"] = test_value\n    new_obj = StatMixClass(**data[\"kwargs\"])\n    assert new_obj.status == test_value\n\n\ndef test_status_attr_is_working_as_expected_with_status_instances(\n    status_mixin_tests,\n):\n    \"\"\"status attribute is working as expected with Status instances.\"\"\"\n    data = status_mixin_tests\n    test_value = data[\"test_mixed_obj\"].status_list[1]\n    data[\"test_mixed_obj\"].status = test_value\n    assert data[\"test_mixed_obj\"].status == test_value\n\n\ndef test_status_arg_is_working_as_expected_with_integers(status_mixin_tests):\n    \"\"\"status attribute value is set correctly with int arg value.\"\"\"\n    data = status_mixin_tests\n    data[\"kwargs\"][\"status\"] = 1\n    test_value = data[\"kwargs\"][\"status_list\"][1]\n    new_obj = StatMixClass(**data[\"kwargs\"])\n    assert new_obj.status == test_value\n\n\ndef test_status_attr_is_working_as_expected_with_integers(status_mixin_tests):\n    \"\"\"status attribute is working as expected with integers.\"\"\"\n    data = status_mixin_tests\n    test_value = 1\n    data[\"test_mixed_obj\"].status = test_value\n    assert (\n        data[\"test_mixed_obj\"].status == data[\"test_mixed_obj\"].status_list[test_value]\n    )\n\n\ndef test_status_arg_is_an_integer_but_out_of_range(status_mixin_tests):\n    \"\"\"ValueError is raised if the status argument is out of range.\"\"\"\n    data = status_mixin_tests\n    data[\"kwargs\"][\"status\"] = 10\n    with pytest.raises(ValueError) as cm:\n        StatMixClass(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"StatMixClass.status cannot be bigger than the length of the \"\n        \"status_list\"\n    )\n\n\ndef test_status_attr_set_to_an_integer_but_out_of_range(status_mixin_tests):\n    \"\"\"ValueError is raised if the status attribute is set to out of range int.\"\"\"\n    data = status_mixin_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_mixed_obj\"].status = 10\n\n    assert (\n        str(cm.value) == \"StatMixClass.status cannot be bigger than the length of the \"\n        \"status_list\"\n    )\n\n\ndef test_status_arg_is_a_negative_integer(status_mixin_tests):\n    \"\"\"ValueError will be raised if the status argument is a negative int.\"\"\"\n    data = status_mixin_tests\n    data[\"kwargs\"][\"status\"] = -10\n    with pytest.raises(ValueError) as cm:\n        StatMixClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"StatMixClass.status must be a non-negative integer\"\n\n\ndef test_status_attr_set_to_an_negative_integer(status_mixin_tests):\n    \"\"\"ValueError is raised if the status attribute is set to a negative int.\"\"\"\n    data = status_mixin_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_mixed_obj\"].status = -10\n\n    assert str(cm.value) == \"StatMixClass.status must be a non-negative integer\"\n\n\nclass StatusListAutoAddClass(SimpleEntity, StatusMixin):\n    \"\"\"A class derived from stalker.core.models.SimpleEntity for testing purposes.\"\"\"\n\n    __tablename__ = \"StatusListAutoAddClass\"\n    __mapper_args__ = {\"polymorphic_identity\": \"StatusListAutoAddClass\"}\n    statusListAutoAddClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(SimpleEntity, self).__init__(**kwargs)\n        StatusMixin.__init__(self, **kwargs)\n\n\nclass StatusListAutoAddDerivedClass(StatusListAutoAddClass):\n    \"\"\"A class derived from StatusListAutoAddClass for testing purposes.\"\"\"\n\n    __tablename__ = \"StatusListAutoAddDerivedClass\"\n    __mapper_args__ = {\"polymorphic_identity\": \"StatusListAutoAddDerivedClass\"}\n    statusListAutoAddClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"StatusListAutoAddClass.id\"), primary_key=True\n    )\n\n\nclass StatusListNoAutoAddClass(SimpleEntity, StatusMixin):\n    \"\"\"A class derived from stalker.core.models.SimpleEntity for testing purposes.\"\"\"\n\n    __tablename__ = \"StatusListNoAutoAddClass\"\n    __mapper_args__ = {\"polymorphic_identity\": \"StatusListNoAutoAddClass\"}\n    statusListNoAutoAddClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(SimpleEntity, self).__init__(**kwargs)\n        StatusMixin.__init__(self, **kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_status_mixin_db_tests(setup_postgresql_db):\n    \"\"\"Set up tests for the StatusMixin with a DB.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = setup_postgresql_db\n    data[\"test_status1\"] = Status(name=\"Status1\", code=\"STS1\")\n    data[\"test_status2\"] = Status(name=\"Status2\", code=\"STS2\")\n    data[\"test_status3\"] = Status(name=\"Status3\", code=\"STS3\")\n    data[\"test_status4\"] = Status(name=\"Status4\", code=\"STS4\")\n    data[\"test_status5\"] = Status(name=\"Status5\", code=\"STS5\")\n\n    # statuses which are not going to be used\n    data[\"test_status6\"] = Status(name=\"Status6\", code=\"STS6\")\n    data[\"test_status7\"] = Status(name=\"Status7\", code=\"STS7\")\n    data[\"test_status8\"] = Status(name=\"Status8\", code=\"STS8\")\n\n    # a test StatusList object\n    data[\"test_status_list1\"] = StatusList(\n        name=\"Test Status List 1\",\n        statuses=[\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ],\n        target_entity_type=\"StatMixClass\",\n    )\n\n    # another test StatusList object\n    data[\"test_status_list2\"] = StatusList(\n        name=\"Test Status List 2\",\n        statuses=[\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ],\n        target_entity_type=\"StatMixClass\",\n    )\n\n    data[\"kwargs\"] = {\n        \"name\": \"Test Class\",\n        \"status_list\": data[\"test_status_list1\"],\n        \"status\": data[\"test_status_list1\"].statuses[0],\n    }\n\n    data[\"test_mixed_obj\"] = StatMixClass(**data[\"kwargs\"])\n    data[\"test_mixed_obj\"].status_list = data[\"test_status_list1\"]\n\n    # create another one without status_list set to something\n    data[\"test_mixed_obj2\"] = StatMixClass(**data[\"kwargs\"])\n    return data\n\n\ndef test_status_list_arg_is_skipped_and_there_is_a_db_setup(\n    setup_status_mixin_db_tests,\n):\n    \"\"\"no error raised, status_list is filled with StatusList instance, with db.\"\"\"\n    # create a StatusList for StatusListAutoAddClass\n    test_status_list = StatusList(\n        name=\"StatusListAutoAddClass Statuses\",\n        statuses=[\n            Status(name=\"Status1\", code=\"Sts1\"),\n            Status(name=\"Status2\", code=\"Sts2\"),\n            Status(name=\"Status3\", code=\"Sts3\"),\n        ],\n        target_entity_type=StatusListAutoAddClass,\n    )\n\n    # add it to the db\n    DBSession.add(test_status_list)\n    DBSession.commit()\n\n    # now try to create a StatusListAutoAddClass without a status_list\n    # argument\n    test_status_list_auto_add_class = StatusListAutoAddClass(\n        name=\"Test StatusListAutoAddClass\",\n    )\n\n    # now check if the status_list is equal to test_status_list\n    assert test_status_list_auto_add_class.status_list == test_status_list\n\n\ndef test_status_list_arg_is_skipped_and_there_is_a_db_setup_but_no_suitable_status_list(\n    setup_status_mixin_db_tests,\n):\n    \"\"\"TypeError is raised no suitable StatusList in the database.\"\"\"\n    # create a StatusList for StatusListAutoAddClass\n    test_status_list = StatusList(\n        name=\"StatusListAutoAddClass Statuses\",\n        statuses=[\n            Status(name=\"Status1\", code=\"Sts1\"),\n            Status(name=\"Status2\", code=\"Sts2\"),\n            Status(name=\"Status3\", code=\"Sts3\"),\n        ],\n        target_entity_type=StatusListAutoAddClass,\n    )\n\n    # add it to the db\n    DBSession.add(test_status_list)\n    DBSession.commit()\n\n    # now try to create a StatusListAutoAddClass without a status_list\n    # argument\n\n    with pytest.raises(TypeError) as cm:\n        StatusListNoAutoAddClass(name=\"Test StatusListNoAutoAddClass\")\n\n    assert (\n        str(cm.value) == \"StatusListNoAutoAddClass instances cannot be initialized \"\n        \"without a stalker.models.status.StatusList instance, please \"\n        \"pass a suitable StatusList \"\n        \"(StatusList.target_entity_type=StatusListNoAutoAddClass) with \"\n        \"the 'status_list' argument\"\n    )\n\n\ndef test_status_list_arg_is_none(setup_status_mixin_db_tests):\n    \"\"\"TypeError is raised if trying to initialize status_list with None.\"\"\"\n    data = setup_status_mixin_db_tests\n    data[\"kwargs\"][\"status_list\"] = None\n    with pytest.raises(TypeError) as cm:\n        StatMixClass(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"StatMixClass instances cannot be initialized without a \"\n        \"stalker.models.status.StatusList instance, please pass a \"\n        \"suitable StatusList \"\n        \"(StatusList.target_entity_type=StatMixClass) with the \"\n        \"'status_list' argument\"\n    )\n\n\ndef test_status_list_arg_skipped(setup_status_mixin_db_tests):\n    \"\"\"TypeError is raised if status_list argument is skipped.\"\"\"\n    data = setup_status_mixin_db_tests\n    data[\"kwargs\"].pop(\"status_list\")\n    with pytest.raises(TypeError) as cm:\n        StatMixClass(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"StatMixClass instances cannot be initialized without a \"\n        \"stalker.models.status.StatusList instance, please pass a \"\n        \"suitable StatusList \"\n        \"(StatusList.target_entity_type=StatMixClass) with the \"\n        \"'status_list' argument\"\n    )\n\n\ndef test_status_list_attr_set_to_none(setup_status_mixin_db_tests):\n    \"\"\"TypeError is raised if trying to set the status_list to None.\"\"\"\n    data = setup_status_mixin_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_mixed_obj\"].status_list = None\n\n    assert (\n        str(cm.value) == \"StatMixClass instances cannot be initialized without a \"\n        \"stalker.models.status.StatusList instance, please pass a \"\n        \"suitable StatusList \"\n        \"(StatusList.target_entity_type=StatMixClass) with the \"\n        \"'status_list' argument\"\n    )\n\n\ndef test_status_list_is_found_automatically_for_derived_class(\n    setup_status_mixin_db_tests,\n):\n    \"\"\"StatusList can be found automatically for StatusListAutoAddDerivedClass.\"\"\"\n    data = setup_status_mixin_db_tests\n    status_list = StatusList(\n        name=\"Test Status List\",\n        target_entity_type=\"StatusListAutoAddClass\",\n        statuses=[\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ],\n    )\n    DBSession.save(status_list)\n    assert status_list.target_entity_type == \"StatusListAutoAddClass\"\n\n    assert StatusListAutoAddClass in StatusListAutoAddDerivedClass.__mro__\n\n    test_obj = StatusListAutoAddDerivedClass()\n    assert test_obj.status_list == status_list\n"
  },
  {
    "path": "tests/mixins/test_target_entity_type_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"TargetEntityTypeMixin related tests.\"\"\"\n\nimport sys\nimport pytest\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import Project, SimpleEntity\nfrom stalker.models.mixins import TargetEntityTypeMixin\n\n\nclass TestClass(object):\n    \"\"\"A simple class for testing purposes.\"\"\"\n\n    pass\n\n\nclass TargetEntityTypeMixedClass(SimpleEntity, TargetEntityTypeMixin):\n    \"\"\"A simple class for TargetEntityTypeMixin tests.\"\"\"\n\n    __tablename__ = \"TarEntMixClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"TarEntMixClass\"}\n    tarMixClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n\n    def __init__(self, **kwargs):\n        super(TargetEntityTypeMixedClass, self).__init__(**kwargs)\n        TargetEntityTypeMixin.__init__(self, **kwargs)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_target_entity_mixin_tests():\n    \"\"\"Set up tests for the TargetEntityMixin.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = dict()\n    data[\"kwargs\"] = {\"name\": \"Test object\", \"target_entity_type\": Project}\n    data[\"test_object\"] = TargetEntityTypeMixedClass(**data[\"kwargs\"])\n    return data\n\n\ndef test_target_entity_type_argument_is_skipped(setup_target_entity_mixin_tests):\n    \"\"\"TypeError is raised if target_entity_type argument is skipped.\"\"\"\n    data = setup_target_entity_mixin_tests\n    data[\"kwargs\"].pop(\"target_entity_type\")\n    with pytest.raises(TypeError) as cm:\n        TargetEntityTypeMixedClass(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"TargetEntityTypeMixedClass.target_entity_type cannot be None\"\n    )\n\n\ndef test_target_entity_type_argument_being_empty_string(\n    setup_target_entity_mixin_tests,\n):\n    \"\"\"ValueError is raised if the target_entity_type argument is given as None.\"\"\"\n    data = setup_target_entity_mixin_tests\n    data[\"kwargs\"][\"target_entity_type\"] = \"\"\n    with pytest.raises(ValueError) as cm:\n        TargetEntityTypeMixedClass(**data[\"kwargs\"])\n    assert (\n        str(cm.value) == \"TargetEntityTypeMixedClass.target_entity_type cannot be empty\"\n    )\n\n\ndef test_target_entity_type_argument_being_none(setup_target_entity_mixin_tests):\n    \"\"\"TypeError is raised if target_entity_type argument is given as None.\"\"\"\n    data = setup_target_entity_mixin_tests\n    data[\"kwargs\"][\"target_entity_type\"] = None\n    with pytest.raises(TypeError) as cm:\n        TargetEntityTypeMixedClass(**data[\"kwargs\"])\n    assert (\n        str(cm.value) == \"TargetEntityTypeMixedClass.target_entity_type cannot be None\"\n    )\n\n\ndef test_target_entity_type_attribute_is_read_only(setup_target_entity_mixin_tests):\n    \"\"\"target_entity_type argument is read-only.\"\"\"\n    data = setup_target_entity_mixin_tests\n    # try to set the target_entity_type attribute and expect AttributeError\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_object\"].target_entity_type = \"Project\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'TargetEntityTypeMixedClass' object has no setter\",\n        12: \"property of 'TargetEntityTypeMixedClass' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_target_entity_type_getter' of 'TargetEntityTypeMixedClass' \"\n        \"object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_target_entity_type_argument_accepts_classes(setup_target_entity_mixin_tests):\n    \"\"\"target_entity_type argument accepts classes.\"\"\"\n    data = setup_target_entity_mixin_tests\n    data[\"kwargs\"][\"target_entity_type\"] = TestClass\n    new_object = TargetEntityTypeMixedClass(**data[\"kwargs\"])\n    assert new_object.target_entity_type == \"TestClass\"\n"
  },
  {
    "path": "tests/mixins/test_unit_mixin.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"UnitMixin class related tests.\"\"\"\n\nimport pytest\n\nfrom sqlalchemy import ForeignKey\nfrom sqlalchemy.orm import Mapped, mapped_column\n\nfrom stalker import SimpleEntity, UnitMixin\n\n\nclass UnitMixinFooMixedInClass(SimpleEntity, UnitMixin):\n    \"\"\"A class which derives from another which has and __init__ already.\"\"\"\n\n    __tablename__ = \"UnitMixinFooMixedInClasses\"\n    __mapper_args__ = {\"polymorphic_identity\": \"UnitMixinFooMixedInClass\"}\n    unitMixinFooMixedInClass_id: Mapped[int] = mapped_column(\n        \"id\", ForeignKey(\"SimpleEntities.id\"), primary_key=True\n    )\n    __id_column__ = \"unitMixinFooMixedInClass_id\"\n\n    def __init__(self, **kwargs):\n        super(UnitMixinFooMixedInClass, self).__init__(**kwargs)\n        UnitMixin.__init__(self, **kwargs)\n\n\ndef test_mixed_in_class_initialization():\n    \"\"\"init is working as expected.\"\"\"\n    a = UnitMixinFooMixedInClass(unit=\"TRY\")\n    assert isinstance(a, UnitMixinFooMixedInClass)\n    assert a.unit == \"TRY\"\n\n\ndef test_unit_argument_is_skipped():\n    \"\"\"unit attribute is an empty string if the unit argument is skipped.\"\"\"\n    g = UnitMixinFooMixedInClass()\n    assert g.unit == \"\"\n\n\ndef test_unit_argument_is_none():\n    \"\"\"unit attribute will be an empty string if the unit argument is None.\"\"\"\n    g = UnitMixinFooMixedInClass(unit=None)\n    assert g.unit == \"\"\n\n\ndef test_unit_attribute_is_set_to_none():\n    \"\"\"unit attribute will be an empty string if it is set to None.\"\"\"\n    g = UnitMixinFooMixedInClass(unit=\"TRY\")\n    assert g.unit != \"\"\n    g.unit = None\n    assert g.unit == \"\"\n\n\ndef test_unit_argument_is_not_a_string():\n    \"\"\"TypeError is raised if the unit argument is not a str.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        UnitMixinFooMixedInClass(unit=1234)\n\n    assert str(cm.value) == (\n        \"UnitMixinFooMixedInClass.unit should be a string, not int: '1234'\"\n    )\n\n\ndef test_unit_attribute_is_not_a_string():\n    \"\"\"TypeError is raised if the unit attribute is set to non-str.\"\"\"\n    g = UnitMixinFooMixedInClass(unit=\"TRY\")\n    with pytest.raises(TypeError) as cm:\n        g.unit = 2342\n\n    assert str(cm.value) == (\n        \"UnitMixinFooMixedInClass.unit should be a string, not int: '2342'\"\n    )\n\n\ndef test_unit_argument_is_working_as_expected():\n    \"\"\"unit arg value is passed to the unit attribute.\"\"\"\n    test_value = \"this is my unit\"\n    g = UnitMixinFooMixedInClass(unit=test_value)\n    assert g.unit == test_value\n\n\ndef test_unit_attribute_is_working_as_expected():\n    \"\"\"unit attribute value can be changed.\"\"\"\n    test_value = \"this is my unit\"\n    g = UnitMixinFooMixedInClass(unit=\"TRY\")\n    assert g.unit != test_value\n    g.unit = test_value\n    assert g.unit == test_value\n"
  },
  {
    "path": "tests/models/__init__.py",
    "content": ""
  },
  {
    "path": "tests/models/test_asset.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Asset class related tests.\"\"\"\n\nimport pytest\n\nfrom stalker import (\n    Asset,\n    Entity,\n    File,\n    Project,\n    Repository,\n    Sequence,\n    Shot,\n    Status,\n    StatusList,\n    Task,\n    Type,\n    User,\n)\nfrom stalker.db.session import DBSession\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_asset_tests(setup_postgresql_db):\n    \"\"\"Set up tests for the Asset class.\n\n    Args:\n        setup_postgresql_db: pytest.fixture.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = dict()\n    # users\n    data[\"test_user1\"] = User(\n        name=\"User1\", login=\"user1\", password=\"12345\", email=\"user1@user1.com\"\n    )\n    DBSession.add(data[\"test_user1\"])\n    data[\"test_user2\"] = User(\n        name=\"User2\", login=\"user2\", password=\"12345\", email=\"user2@user2.com\"\n    )\n    DBSession.add(data[\"test_user2\"])\n    DBSession.commit()\n    # statuses\n    data[\"status_wip\"] = Status.query.filter_by(code=\"WIP\").first()\n    data[\"status_cmpl\"] = Status.query.filter_by(code=\"CMPL\").first()\n    # types\n    data[\"commercial_project_type\"] = Type(\n        name=\"Commercial Project\",\n        code=\"commproj\",\n        target_entity_type=\"Project\",\n    )\n    DBSession.add(data[\"commercial_project_type\"])\n    data[\"asset_type1\"] = Type(\n        name=\"Character\", code=\"char\", target_entity_type=\"Asset\"\n    )\n    DBSession.add(data[\"asset_type1\"])\n    data[\"asset_type2\"] = Type(\n        name=\"Environment\", code=\"env\", target_entity_type=\"Asset\"\n    )\n    DBSession.add(data[\"asset_type2\"])\n    data[\"repository_type\"] = Type(\n        name=\"Test Repository Type\",\n        code=\"testrepo\",\n        target_entity_type=\"Repository\",\n    )\n    DBSession.add(data[\"repository_type\"])\n    # repository\n    data[\"repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"repository_type\"],\n    )\n    DBSession.add(data[\"repository\"])\n    # project\n    data[\"project1\"] = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=data[\"commercial_project_type\"],\n        repositories=[data[\"repository\"]],\n    )\n    DBSession.add(data[\"project1\"])\n    DBSession.commit()\n    # sequence\n    data[\"seq1\"] = Sequence(\n        name=\"Test Sequence\",\n        code=\"tseq\",\n        project=data[\"project1\"],\n        responsible=[data[\"test_user1\"]],\n    )\n    DBSession.add(data[\"seq1\"])\n    # shots\n    data[\"shot1\"] = Shot(\n        code=\"TestSH001\",\n        project=data[\"project1\"],\n        sequence=data[\"seq1\"],\n        responsible=[data[\"test_user1\"]],\n    )\n    DBSession.add(data[\"shot1\"])\n    data[\"shot2\"] = Shot(\n        code=\"TestSH002\",\n        project=data[\"project1\"],\n        sequence=data[\"seq1\"],\n        responsible=[data[\"test_user1\"]],\n    )\n    DBSession.add(data[\"shot2\"])\n    data[\"shot3\"] = Shot(\n        code=\"TestSH003\",\n        project=data[\"project1\"],\n        sequence=data[\"seq1\"],\n        responsible=[data[\"test_user1\"]],\n    )\n    DBSession.add(data[\"shot3\"])\n    data[\"shot4\"] = Shot(\n        code=\"TestSH004\",\n        project=data[\"project1\"],\n        sequence=data[\"seq1\"],\n        responsible=[data[\"test_user1\"]],\n    )\n    DBSession.add(data[\"shot4\"])\n    data[\"kwargs\"] = {\n        \"name\": \"Test Asset\",\n        \"code\": \"ta\",\n        \"description\": \"This is a test Asset object\",\n        \"project\": data[\"project1\"],\n        \"type\": data[\"asset_type1\"],\n        \"responsible\": [data[\"test_user1\"]],\n    }\n    data[\"asset1\"] = Asset(**data[\"kwargs\"])\n    DBSession.add(data[\"asset1\"])\n    # tasks\n    data[\"task1\"] = Task(\n        name=\"Task1\",\n        parent=data[\"asset1\"],\n    )\n    DBSession.add(data[\"task1\"])\n    data[\"task2\"] = Task(\n        name=\"Task2\",\n        parent=data[\"asset1\"],\n    )\n    DBSession.add(data[\"task2\"])\n    data[\"task3\"] = Task(\n        name=\"Task3\",\n        parent=data[\"asset1\"],\n    )\n    DBSession.add(data[\"task3\"])\n    DBSession.commit()\n    return data\n\n\ndef test_auto_name_class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Asset class.\"\"\"\n    assert Asset.__auto_name__ is False\n\n\ndef test_name_cannot_be_set_to_none(setup_asset_tests):\n    \"\"\"name arg cannot be set to None.\"\"\"\n    data = setup_asset_tests\n    data[\"kwargs\"][\"name\"] = None\n    with pytest.raises(TypeError) as cm:\n        _ = Asset(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Asset.name cannot be None\"\n\n\ndef test_name_cannot_be_set_to_empty_string(setup_asset_tests):\n    \"\"\"name arg cannot be set to None.\"\"\"\n    data = setup_asset_tests\n    data[\"kwargs\"][\"name\"] = \"\"\n    with pytest.raises(ValueError) as cm:\n        _ = Asset(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Asset.name cannot be an empty string\"\n\n\ndef test_equality(setup_asset_tests):\n    \"\"\"Equality of two Asset objects.\"\"\"\n    data = setup_asset_tests\n    new_asset1 = Asset(**data[\"kwargs\"])\n    new_asset2 = Asset(**data[\"kwargs\"])\n    new_entity1 = Entity(**data[\"kwargs\"])\n    data[\"kwargs\"][\"type\"] = data[\"asset_type2\"]\n    new_asset3 = Asset(**data[\"kwargs\"])\n    data[\"kwargs\"][\"name\"] = \"another name\"\n    new_asset4 = Asset(**data[\"kwargs\"])\n    assert new_asset1 == new_asset2\n    assert not new_asset1 == new_asset3\n    assert not new_asset1 == new_asset4\n    assert not new_asset3 == new_asset4\n    assert not new_asset1 == new_entity1\n\n\ndef test_inequality(setup_asset_tests):\n    \"\"\"Inequality of two Asset objects.\"\"\"\n    data = setup_asset_tests\n    new_asset1 = Asset(**data[\"kwargs\"])\n    new_asset2 = Asset(**data[\"kwargs\"])\n    new_entity1 = Entity(**data[\"kwargs\"])\n    data[\"kwargs\"][\"type\"] = data[\"asset_type2\"]\n    new_asset3 = Asset(**data[\"kwargs\"])\n    data[\"kwargs\"][\"name\"] = \"another name\"\n    new_asset4 = Asset(**data[\"kwargs\"])\n    assert not new_asset1 != new_asset2\n    assert new_asset1 != new_asset3\n    assert new_asset1 != new_asset4\n    assert new_asset3 != new_asset4\n    assert new_asset1 != new_entity1\n\n\ndef test_reference_mixin_initialization(setup_asset_tests):\n    \"\"\"ReferenceMixin part is initialized correctly.\"\"\"\n    data = setup_asset_tests\n    file_type_1 = Type(name=\"Image\", code=\"image\", target_entity_type=\"File\")\n    file1 = File(\n        name=\"Artwork 1\",\n        full_path=\"/mnt/M/JOBs/TEST_PROJECT\",\n        filename=\"a.jpg\",\n        type=file_type_1,\n    )\n    file2 = File(\n        name=\"Artwork 2\",\n        full_path=\"/mnt/M/JOBs/TEST_PROJECT\",\n        filename=\"b.jbg\",\n        type=file_type_1,\n    )\n    references = [file1, file2]\n    data[\"kwargs\"][\"code\"] = \"SH12314\"\n    data[\"kwargs\"][\"references\"] = references\n    new_asset = Asset(**data[\"kwargs\"])\n    assert new_asset.references == references\n\n\ndef test_status_mixin_initialization(setup_asset_tests):\n    \"\"\"StatusMixin part is initialized correctly.\"\"\"\n    data = setup_asset_tests\n    status_list = StatusList.query.filter_by(target_entity_type=\"Task\").first()\n    data[\"kwargs\"][\"code\"] = \"SH12314\"\n    data[\"kwargs\"][\"status\"] = 0\n    data[\"kwargs\"][\"status_list\"] = status_list\n    new_asset = Asset(**data[\"kwargs\"])\n    assert new_asset.status_list == status_list\n\n\ndef test_task_mixin_initialization(setup_asset_tests):\n    \"\"\"TaskMixin part is initialized correctly.\"\"\"\n    data = setup_asset_tests\n    commercial_project_type = Type(\n        name=\"Commercial\",\n        code=\"comm\",\n        target_entity_type=\"Project\",\n    )\n    new_project = Project(\n        name=\"Commercial\",\n        code=\"COM\",\n        type=commercial_project_type,\n        repository=data[\"repository\"],\n    )\n    character_asset_type = Type(\n        name=\"Character\", code=\"char\", target_entity_type=\"Asset\"\n    )\n    new_asset = Asset(\n        name=\"test asset\",\n        type=character_asset_type,\n        code=\"tstasset\",\n        project=new_project,\n        responsible=[data[\"test_user1\"]],\n    )\n    task1 = Task(name=\"Modeling\", parent=new_asset)\n    task2 = Task(name=\"Lighting\", parent=new_asset)\n    tasks = [task1, task2]\n\n    assert sorted(new_asset.tasks, key=lambda x: x.name) == sorted(\n        tasks, key=lambda x: x.name\n    )\n\n\ndef test_plural_class_name(setup_asset_tests):\n    \"\"\"Default plural name of the Asset class.\"\"\"\n    data = setup_asset_tests\n    assert data[\"asset1\"].plural_class_name == \"Assets\"\n\n\ndef test_strictly_typed_is_true():\n    \"\"\"__strictly_typed__ class attribute is True.\"\"\"\n    assert Asset.__strictly_typed__ is True\n\n\ndef test_hash_value(setup_asset_tests):\n    \"\"\"__hash__ returns the hash of the Asset instance.\"\"\"\n    data = setup_asset_tests\n    result = hash(data[\"asset1\"])\n    assert isinstance(result, int)\n\n\ndef test_template_variables_for_asset_related_task(setup_asset_tests):\n    \"\"\"_template_variables() for an asset related task returns correct data.\"\"\"\n    data = setup_asset_tests\n    assert data[\"task2\"]._template_variables() == {\n        \"asset\": data[\"asset1\"],\n        \"parent_tasks\": [data[\"asset1\"], data[\"task2\"]],\n        \"project\": data[\"project1\"],\n        \"scene\": None,\n        \"sequence\": None,\n        \"shot\": None,\n        \"task\": data[\"task2\"],\n        \"type\": None,\n    }\n\n\ndef test_template_variables_for_asset_itself(setup_asset_tests):\n    \"\"\"_template_variables() for an asset itself returns correct data.\"\"\"\n    data = setup_asset_tests\n    assert data[\"asset1\"]._template_variables() == {\n        \"asset\": data[\"asset1\"],\n        \"parent_tasks\": [data[\"asset1\"]],\n        \"project\": data[\"project1\"],\n        \"scene\": None,\n        \"sequence\": None,\n        \"shot\": None,\n        \"task\": data[\"asset1\"],\n        \"type\": data[\"asset_type1\"],\n    }\n\n\ndef test_assets_can_use_task_status_list():\n    \"\"\"It is possible to use TaskStatus lists with Assets.\"\"\"\n    # users\n    test_user1 = User(\n        name=\"User1\", login=\"user1\", password=\"12345\", email=\"user1@user1.com\"\n    )\n    # statuses\n    status_wip = Status(code=\"WIP\", name=\"Work In Progress\")\n    status_cmpl = Status(code=\"CMPL\", name=\"Complete\")\n\n    # Just create a StatusList for Tasks\n    task_status_list = StatusList(\n        statuses=[status_wip, status_cmpl], target_entity_type=\"Task\"\n    )\n    project_status_list = StatusList(\n        statuses=[status_wip, status_cmpl], target_entity_type=\"Project\"\n    )\n\n    # types\n    commercial_project_type = Type(\n        name=\"Commercial Project\",\n        code=\"commproj\",\n        target_entity_type=\"Project\",\n    )\n    asset_type1 = Type(name=\"Character\", code=\"char\", target_entity_type=\"Asset\")\n    # project\n    project1 = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=commercial_project_type,\n        status_list=project_status_list,\n    )\n    # this should now be possible\n    test_asset = Asset(\n        name=\"Test Asset\",\n        code=\"ta\",\n        description=\"This is a test Asset object\",\n        project=project1,\n        type=asset_type1,\n        status_list=task_status_list,\n        status=status_wip,\n        responsible=[test_user1],\n    )\n    assert test_asset.status_list == task_status_list\n"
  },
  {
    "path": "tests/models/test_authentication_log.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"AuthenticationLog class related tests.\"\"\"\nimport datetime\n\nimport pytest\n\nimport pytz\n\nfrom stalker import AuthenticationLog, User\nfrom stalker.models.auth import LOGIN, LOGOUT\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_authentication_log_tests():\n    \"\"\"Set up tests for the AuthenticationLog class.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = dict()\n    data[\"test_user1\"] = User(\n        name=\"Test User 1\",\n        login=\"tuser1\",\n        email=\"tuser1@users.com\",\n        password=\"secret\",\n    )\n    data[\"test_user2\"] = User(\n        name=\"Test User 2\",\n        login=\"tuser2\",\n        email=\"tuser2@users.com\",\n        password=\"secret\",\n    )\n    return data\n\n\ndef test_user_argument_is_skipped(setup_authentication_log_tests):\n    \"\"\"TypeError is raised if the user arg is skipped.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        AuthenticationLog(action=LOGIN, date=datetime.datetime.now(pytz.utc))\n    assert str(cm.value) == (\n        \"AuthenticationLog.user should be a User instance, not NoneType: 'None'\"\n    )\n\n\ndef test_user_argument_is_none(setup_authentication_log_tests):\n    \"\"\"TypeError is raised if the user arg is None.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        AuthenticationLog(user=None, action=LOGIN, date=datetime.datetime.now(pytz.utc))\n    assert str(cm.value) == (\n        \"AuthenticationLog.user should be a User instance, not NoneType: 'None'\"\n    )\n\n\ndef test_user_argument_is_not_a_user_instance(setup_authentication_log_tests):\n    \"\"\"TypeError is raised if user arg is not User.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        AuthenticationLog(\n            user=\"not a user instance\",\n            action=LOGIN,\n            date=datetime.datetime.now(pytz.utc),\n        )\n    assert str(cm.value) == (\n        \"AuthenticationLog.user should be a User instance, \"\n        \"not str: 'not a user instance'\"\n    )\n\n\ndef test_user_attribute_is_not_a_user_instance(setup_authentication_log_tests):\n    \"\"\"TypeError is raised if user attr is not User.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"], action=LOGIN, date=datetime.datetime.now(pytz.utc)\n    )\n    with pytest.raises(TypeError) as cm:\n        uli.user = \"not a user instance\"\n\n    assert str(cm.value) == (\n        \"AuthenticationLog.user should be a User instance, \"\n        \"not str: 'not a user instance'\"\n    )\n\n\ndef test_user_argument_is_working_as_expected(setup_authentication_log_tests):\n    \"\"\"user arg value is correctly passed to the user attribute.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"], action=LOGOUT, date=datetime.datetime.now(pytz.utc)\n    )\n    assert uli.user == data[\"test_user1\"]\n\n\ndef test_user_attribute_is_working_as_expected(setup_authentication_log_tests):\n    \"\"\"user attr is working as expected.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"], action=LOGOUT, date=datetime.datetime.now(pytz.utc)\n    )\n    assert uli.user != data[\"test_user2\"]\n    uli.user = data[\"test_user2\"]\n    assert uli.user == data[\"test_user2\"]\n\n\ndef test_action_argument_is_skipped(setup_authentication_log_tests):\n    \"\"\"action attr is \"login\" if the action argument is skipped.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"], date=datetime.datetime.now(pytz.utc)\n    )\n    assert uli.action == LOGIN\n\n\ndef test_action_argument_is_none(setup_authentication_log_tests):\n    \"\"\"action attr is \"login\" when action arg is None.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"], action=None, date=datetime.datetime.now(pytz.utc)\n    )\n    assert uli.action == LOGIN\n\n\ndef test_action_argument_value_is_not_login_or_logout(setup_authentication_log_tests):\n    \"\"\"ValueError is raised if the action attr is not one of \"login\" or \"login\".\"\"\"\n    data = setup_authentication_log_tests\n    with pytest.raises(ValueError) as cm:\n        AuthenticationLog(\n            user=data[\"test_user1\"],\n            action=\"not login\",\n            date=datetime.datetime.now(pytz.utc),\n        )\n    assert (\n        str(cm.value)\n        == 'AuthenticationLog.action should be one of \"login\" or \"logout\", '\n        'not \"not login\"'\n    )\n\n\ndef test_action_attribute_value_is_not_login_or_logout(setup_authentication_log_tests):\n    \"\"\"ValueError is raised if the action attr is not LOGIN/LOGOUT.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"], action=LOGIN, date=datetime.datetime.now(pytz.utc)\n    )\n    with pytest.raises(ValueError) as cm:\n        uli.action = \"not login\"\n    assert (\n        str(cm.value)\n        == 'AuthenticationLog.action should be one of \"login\" or \"logout\", '\n        'not \"not login\"'\n    )\n\n\ndef test_action_argument_is_working_as_expected(setup_authentication_log_tests):\n    \"\"\"action arg value is passed to the action attr.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"], action=LOGIN, date=datetime.datetime.now(pytz.utc)\n    )\n    assert uli.action == LOGIN\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"], action=LOGOUT, date=datetime.datetime.now(pytz.utc)\n    )\n    assert uli.action == LOGOUT\n\n\ndef test_action_attribute_is_working_as_expected(setup_authentication_log_tests):\n    \"\"\"action attr is working as expected.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"], action=LOGIN, date=datetime.datetime.now(pytz.utc)\n    )\n    assert uli.action != LOGOUT\n    uli.action = LOGOUT\n    assert uli.action == LOGOUT\n\n\ndef test_date_argument_is_skipped(setup_authentication_log_tests):\n    \"\"\"date attr datetime.datetime.now(pytz.utc) if date arg is skipped.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(user=data[\"test_user1\"], action=LOGIN)\n    diff = datetime.datetime.now(pytz.utc) - uli.date\n    assert diff.microseconds < 5000\n\n\ndef test_date_argument_is_none(setup_authentication_log_tests):\n    \"\"\"date attr datetime.datetime.now(pytz.utc) if date argument is None.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(user=data[\"test_user1\"], action=LOGIN, date=None)\n    diff = datetime.datetime.now(pytz.utc) - uli.date\n    assert diff < datetime.timedelta(seconds=1)\n\n\ndef test_date_attribute_is_none(setup_authentication_log_tests):\n    \"\"\"date attr is set to datetime.datetime.now(pytz.utc) if is set to None.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"],\n        action=LOGIN,\n        date=datetime.datetime.now(pytz.utc) - datetime.timedelta(days=10),\n    )\n    diff = datetime.datetime.now(pytz.utc) - uli.date\n    one_second = datetime.timedelta(seconds=1)\n    assert diff > one_second\n    uli.date = None\n    diff = datetime.datetime.now(pytz.utc) - uli.date\n    assert diff < one_second\n\n\ndef test_date_argument_is_not_a_datetime_instance(setup_authentication_log_tests):\n    \"\"\"TypeError is raised if date argument is not a datetime.datetime instance.\"\"\"\n    data = setup_authentication_log_tests\n    with pytest.raises(TypeError) as cm:\n        AuthenticationLog(\n            user=data[\"test_user1\"], action=LOGIN, date=\"not a datetime instance\"\n        )\n    assert str(cm.value) == (\n        \"AuthenticationLog.date should be a datetime.datetime instance, \"\n        \"not str: 'not a datetime instance'\"\n    )\n\n\ndef test_date_attribute_is_not_a_datetime_instance(setup_authentication_log_tests):\n    \"\"\"TypeError is raised if date attr is not datetime.datetime instance.\"\"\"\n    data = setup_authentication_log_tests\n    uli = AuthenticationLog(\n        user=data[\"test_user1\"], action=LOGIN, date=datetime.datetime.now(pytz.utc)\n    )\n    with pytest.raises(TypeError) as cm:\n        uli.date = \"not a datetime instance\"\n\n    assert str(cm.value) == (\n        \"AuthenticationLog.date should be a datetime.datetime instance, \"\n        \"not str: 'not a datetime instance'\"\n    )\n\n\ndef test_date_argument_is_working_as_expected(setup_authentication_log_tests):\n    \"\"\"date argument value is passed to the date attribute.\"\"\"\n    data = setup_authentication_log_tests\n    date = datetime.datetime(2016, 11, 14, 16, 30, tzinfo=pytz.utc)\n    uli = AuthenticationLog(user=data[\"test_user1\"], action=LOGIN, date=date)\n\n    assert uli.date == date\n\n\ndef test_date_attribute_is_working_as_expected(setup_authentication_log_tests):\n    \"\"\"date attribute value can be changed.\"\"\"\n    data = setup_authentication_log_tests\n    date1 = datetime.datetime(2016, 11, 4, 6, 30, tzinfo=pytz.utc)\n    date2 = datetime.datetime(2016, 11, 14, 16, 30, tzinfo=pytz.utc)\n    uli = AuthenticationLog(user=data[\"test_user1\"], action=LOGIN, date=date1)\n    assert uli.date != date2\n    uli.date = date2\n    assert uli.date == date2\n\n\ndef test_date_argument_is_working_as_expected2(setup_authentication_log_tests):\n    \"\"\"date argument value is passed to the date attribute.\"\"\"\n    data = setup_authentication_log_tests\n    date1 = datetime.datetime(2016, 11, 4, 6, 30, tzinfo=pytz.utc)\n    uli = AuthenticationLog(user=data[\"test_user1\"], action=LOGIN, date=date1)\n    assert uli.date == date1\n\n\ndef test_authentication_log_is_orderable_for_some_reason(\n    setup_authentication_log_tests,\n):\n    \"\"\"AuthenticationLog instances are orderable.\"\"\"\n    data = setup_authentication_log_tests\n    date1 = datetime.datetime(2024, 12, 10, 10, 0)\n    date2 = datetime.datetime(2024, 12, 10, 17, 0)\n    auth_log1 = AuthenticationLog(user=data[\"test_user1\"], action=LOGIN, date=date1)\n    auth_log2 = AuthenticationLog(user=data[\"test_user1\"], action=LOGOUT, date=date2)\n    assert (auth_log1 < auth_log2) is True\n    assert (auth_log1 > auth_log2) is False\n"
  },
  {
    "path": "tests/models/test_budget.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Budget class tests.\"\"\"\n\nimport pytest\n\nfrom stalker import (\n    Budget,\n    BudgetEntry,\n    Good,\n    Project,\n    Repository,\n    Status,\n    StatusList,\n    Type,\n    User,\n)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_budget_test_base():\n    \"\"\"Set up the tests for the Budget class.\n\n    Returns:\n        dict: Test data.\n    \"\"\"\n    data = dict()\n    data[\"status_wfd\"] = Status(name=\"Waiting For Dependency\", code=\"WFD\")\n    data[\"status_rts\"] = Status(name=\"Ready To Start\", code=\"RTS\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_prev\"] = Status(name=\"Pending Review\", code=\"PREV\")\n    data[\"status_hrev\"] = Status(name=\"Has Revision\", code=\"HREV\")\n    data[\"status_drev\"] = Status(name=\"Dependency Has Revision\", code=\"DREV\")\n    data[\"status_oh\"] = Status(name=\"On Hold\", code=\"OH\")\n    data[\"status_stop\"] = Status(name=\"Stopped\", code=\"STOP\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n    data[\"status_new\"] = Status(name=\"New\", code=\"NEW\")\n    data[\"status_app\"] = Status(name=\"Approved\", code=\"APP\")\n    data[\"budget_status_list\"] = StatusList(\n        name=\"Budget Statuses\",\n        target_entity_type=\"Budget\",\n        statuses=[data[\"status_new\"], data[\"status_prev\"], data[\"status_app\"]],\n    )\n    data[\"task_status_list\"] = StatusList(\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n    data[\"test_project_status_list\"] = StatusList(\n        name=\"Project Statuses\",\n        statuses=[data[\"status_wip\"], data[\"status_prev\"], data[\"status_cmpl\"]],\n        target_entity_type=\"Project\",\n    )\n    data[\"test_movie_project_type\"] = Type(\n        name=\"Movie Project\",\n        code=\"movie\",\n        target_entity_type=\"Project\",\n    )\n    data[\"test_repository_type\"] = Type(\n        name=\"Test Repository Type\",\n        code=\"test\",\n        target_entity_type=\"Repository\",\n    )\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"test_repository_type\"],\n        linux_path=\"/mnt/T/\",\n        windows_path=\"T:/\",\n        macos_path=\"/Volumes/T/\",\n    )\n    data[\"test_user1\"] = User(\n        name=\"User1\", login=\"user1\", email=\"user1@user1.com\", password=\"1234\"\n    )\n    data[\"test_user2\"] = User(\n        name=\"User2\", login=\"user2\", email=\"user2@user2.com\", password=\"1234\"\n    )\n    data[\"test_user3\"] = User(\n        name=\"User3\", login=\"user3\", email=\"user3@user3.com\", password=\"1234\"\n    )\n    data[\"test_user4\"] = User(\n        name=\"User4\", login=\"user4\", email=\"user4@user4.com\", password=\"1234\"\n    )\n    data[\"test_user5\"] = User(\n        name=\"User5\", login=\"user5\", email=\"user5@user5.com\", password=\"1234\"\n    )\n    data[\"test_project\"] = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=data[\"test_movie_project_type\"],\n        status_list=data[\"test_project_status_list\"],\n        repository=data[\"test_repository\"],\n    )\n    data[\"kwargs\"] = {\n        \"project\": data[\"test_project\"],\n        \"name\": \"Test Budget 1\",\n        \"status_list\": data[\"budget_status_list\"],\n    }\n    data[\"test_budget\"] = Budget(**data[\"kwargs\"])\n    data[\"test_good\"] = Good(name=\"Some Good\", cost=100, msrp=120, unit=\"$\")\n    return data\n\n\ndef test_entries_attribute_is_set_to_a_list_of_other_instances_than_a_budget_entry(\n    setup_budget_test_base,\n):\n    \"\"\"TypeError is raised if the entries attribute is not a list of BudgetEntries.\"\"\"\n    data = setup_budget_test_base\n    with pytest.raises(TypeError) as cm:\n        data[\"test_budget\"].entries = [\"some\", \"string\", 1, 2]\n\n    assert str(cm.value) == (\n        \"Budget.entries should only contain instances of BudgetEntry, not str: 'some'\"\n    )\n\n\ndef test_entries_attribute_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Entries attribute is working as expected.\"\"\"\n    data = setup_budget_test_base\n    some_other_budget = Budget(\n        name=\"Test Budget\",\n        project=data[\"test_project\"],\n        status_list=data[\"budget_status_list\"],\n    )\n    entry1 = BudgetEntry(\n        budget=some_other_budget,\n        good=data[\"test_good\"],\n    )\n    entry2 = BudgetEntry(\n        budget=some_other_budget,\n        good=data[\"test_good\"],\n    )\n    data[\"test_budget\"].entries = [entry1, entry2]\n    assert data[\"test_budget\"].entries == [entry1, entry2]\n\n\ndef test_statuses_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Budget accepts statuses.\"\"\"\n    data = setup_budget_test_base\n    data[\"test_budget\"].status = data[\"status_new\"]\n    assert data[\"test_budget\"].status == data[\"status_new\"]\n    data[\"test_budget\"].status = data[\"status_prev\"]\n    assert data[\"test_budget\"].status == data[\"status_prev\"]\n    data[\"test_budget\"].status = data[\"status_app\"]\n    assert data[\"test_budget\"].status == data[\"status_app\"]\n\n\ndef test_budget_argument_is_skipped(setup_budget_test_base):\n    \"\"\"TypeError is raised if the budget argument is skipped.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        BudgetEntry(amount=10.0)\n    assert str(cm.value) == (\n        \"BudgetEntry.budget should be a Budget instance, not NoneType: 'None'\"\n    )\n\n\ndef test_budget_argument_is_none(setup_budget_test_base):\n    \"\"\"TypeError is raised if the budget argument is None.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        BudgetEntry(budget=None, amount=10.0)\n    assert str(cm.value) == (\n        \"BudgetEntry.budget should be a Budget instance, not NoneType: 'None'\"\n    )\n\n\ndef test_budget_attribute_is_set_to_none(setup_budget_test_base):\n    \"\"\"TypeError is raised if the budget attribute is set to None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=data[\"test_good\"],\n    )\n    with pytest.raises(TypeError) as cm:\n        entry.budget = None\n    assert str(cm.value) == (\n        \"BudgetEntry.budget should be a Budget instance, not NoneType: 'None'\"\n    )\n\n\ndef test_budget_argument_is_not_a_budget_instance(setup_budget_test_base):\n    \"\"\"TypeError is raised if the budget argument is not a Budget instance.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        BudgetEntry(budget=\"not a budget\", amount=10.0)\n    assert str(cm.value) == (\n        \"BudgetEntry.budget should be a Budget instance, not str: 'not a budget'\"\n    )\n\n\ndef test_budget_attribute_is_not_a_budget_instance(setup_budget_test_base):\n    \"\"\"TypeError is raised if the budget attribute is not a Budget instance.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], amount=10.0)\n    with pytest.raises(TypeError) as cm:\n        entry.budget = \"not a budget instance\"\n    assert str(cm.value) == (\n        \"BudgetEntry.budget should be a Budget instance, \"\n        \"not str: 'not a budget instance'\"\n    )\n\n\ndef test_budget_argument_is_working_as_expected(setup_budget_test_base):\n    \"\"\"If the budget argument value is correctly passed to the budget attribute.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], amount=10.0)\n    assert entry.budget == data[\"test_budget\"]\n\n\ndef test_budget_attribute_is_working_as_expected(setup_budget_test_base):\n    \"\"\"If the budget attribute value can correctly be changed.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], amount=10.0)\n    new_budget = Budget(\n        name=\"Test Budget\",\n        project=data[\"test_project\"],\n        status_list=data[\"budget_status_list\"],\n    )\n    assert entry.budget != new_budget\n    entry.budget = new_budget\n    assert entry.budget == new_budget\n\n\ndef test_cost_attribute_value_will_be_copied_from_the_supplied_good_argument(\n    setup_budget_test_base,\n):\n    \"\"\"Cost attribute is copied from the good argument.\"\"\"\n    data = setup_budget_test_base\n    good = Good(name=\"Some Good\", cost=10, msrp=20, unit=\"$/hour\")\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=good)\n    assert entry.cost == good.cost\n\n\ndef test_cost_attribute_is_set_to_none(setup_budget_test_base):\n    \"\"\"If the cost attribute is set to 0 if it is set to None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=data[\"test_good\"],\n    )\n    assert entry.cost == data[\"test_good\"].cost\n    entry.cost = None\n    assert entry.cost == 0.0\n\n\ndef test_cost_attribute_is_not_a_number(setup_budget_test_base):\n    \"\"\"TypeError is raised if cost attribute is not a number.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=data[\"test_good\"],\n    )\n    with pytest.raises(TypeError) as cm:\n        entry.cost = \"some string\"\n    assert str(cm.value) == (\n        \"BudgetEntry.cost should be a number, not str: 'some string'\"\n    )\n\n\ndef test_cost_attribute_is_working_as_expected(setup_budget_test_base):\n    \"\"\"If the cost attribute is working as expected.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=data[\"test_good\"],\n    )\n    test_value = 5.0\n    assert entry.cost != test_value\n    entry.cost = test_value\n    assert entry.cost == test_value\n\n\ndef test_msrp_attribute_is_set_to_none(setup_budget_test_base):\n    \"\"\"Msrp attribute is 0 if it is set to None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=data[\"test_good\"],\n    )\n    assert entry.msrp == data[\"test_good\"].msrp\n    entry.msrp = None\n    assert entry.msrp == 0.0\n\n\ndef test_msrp_attribute_is_not_a_number(setup_budget_test_base):\n    \"\"\"TypeError is raised if msrp attribute is not a number.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=data[\"test_good\"],\n    )\n    with pytest.raises(TypeError) as cm:\n        entry.msrp = \"some string\"\n    assert str(cm.value) == (\n        \"BudgetEntry.msrp should be a number, not str: 'some string'\"\n    )\n\n\ndef test_msrp_attribute_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Msrp attribute is working as expected.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=data[\"test_good\"],\n    )\n    test_value = 5.0\n    assert entry.msrp != test_value\n    entry.msrp = test_value\n    assert entry.msrp == test_value\n\n\ndef test_msrp_attribute_value_will_be_copied_from_the_supplied_good_argument(\n    setup_budget_test_base,\n):\n    \"\"\"Msrp attribute value is copied from the supplied good argument value.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"])\n    assert entry.msrp == data[\"test_good\"].msrp\n\n\ndef test_price_argument_is_skipped(setup_budget_test_base):\n    \"\"\"Price attribute is 0 if the price argument is skipped.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=data[\"test_good\"],\n    )\n    assert entry.price == 0.0\n\n\ndef test_price_argument_is_set_to_none(setup_budget_test_base):\n    \"\"\"Price attribute is set to 0 if the price argument is set to None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], price=None)\n    assert entry.price == 0.0\n\n\ndef test_price_attribute_is_set_to_none(setup_budget_test_base):\n    \"\"\"Price attribute is set to 0 if price attribute is set to None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], price=10.0)\n    assert entry.price == 10.0\n    entry.price = None\n    assert entry.price == 0.0\n\n\ndef test_price_argument_is_not_a_number(setup_budget_test_base):\n    \"\"\"TypeError is raised if the price arg is not a number.\"\"\"\n    data = setup_budget_test_base\n    with pytest.raises(TypeError) as cm:\n        BudgetEntry(\n            budget=data[\"test_budget\"], good=data[\"test_good\"], price=\"some string\"\n        )\n    assert str(cm.value) == (\n        \"BudgetEntry.price should be a number, not str: 'some string'\"\n    )\n\n\ndef test_price_attribute_is_not_a_number(setup_budget_test_base):\n    \"\"\"TypeError is raised if price attribute is not a number.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], price=10)\n    with pytest.raises(TypeError) as cm:\n        entry.price = \"some string\"\n    assert str(cm.value) == (\n        \"BudgetEntry.price should be a number, not str: 'some string'\"\n    )\n\n\ndef test_price_argument_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Price arg value is passed to the price attribute.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], price=10)\n    assert entry.price == 10.0\n\n\ndef test_price_attribute_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Price attribute is working as expected.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], price=10)\n    test_value = 5.0\n    assert entry.price != test_value\n    entry.price = test_value\n    assert entry.price == test_value\n\n\ndef test_realized_total_argument_is_skipped(setup_budget_test_base):\n    \"\"\"Realized_total attribute is 0 if the realized_total arg is skipped.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"])\n    assert entry.realized_total == 0.0\n\n\ndef test_realized_total_argument_is_set_to_none(setup_budget_test_base):\n    \"\"\"Realized_total attribute is set to 0 if realized_total arg is None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"], good=data[\"test_good\"], realized_total=None\n    )\n    assert entry.realized_total == 0.0\n\n\ndef test_realized_total_attribute_is_set_to_none(setup_budget_test_base):\n    \"\"\"Realized_total attribute is set to 0 if it is set to None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"], good=data[\"test_good\"], realized_total=10.0\n    )\n    assert entry.realized_total == 10.0\n    entry.realized_total = None\n    assert entry.realized_total == 0.0\n\n\ndef test_realized_total_argument_is_not_a_number(setup_budget_test_base):\n    \"\"\"TypeError is raised if the realized_total arg not a number.\"\"\"\n    data = setup_budget_test_base\n    with pytest.raises(TypeError) as cm:\n        BudgetEntry(\n            budget=data[\"test_budget\"],\n            good=data[\"test_good\"],\n            realized_total=\"some string\",\n        )\n\n    assert str(cm.value) == (\n        \"BudgetEntry.realized_total should be a number, not str: 'some string'\"\n    )\n\n\ndef test_realized_total_attribute_is_not_a_number(setup_budget_test_base):\n    \"\"\"TypeError is raised if realized_total attribute is not a number.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"], good=data[\"test_good\"], realized_total=10\n    )\n    with pytest.raises(TypeError) as cm:\n        entry.realized_total = \"some string\"\n    assert str(cm.value) == (\n        \"BudgetEntry.realized_total should be a number, not str: 'some string'\"\n    )\n\n\ndef test_realized_total_argument_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Realized_total arg value is passed to the realized_total attribute.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"], good=data[\"test_good\"], realized_total=10\n    )\n    assert entry.realized_total == 10.0\n\n\ndef test_realized_total_attribute_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Realized_total attribute is working as expected.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"], good=data[\"test_good\"], realized_total=10\n    )\n    test_value = 5.0\n    assert entry.realized_total != test_value\n    entry.realized_total = test_value\n    assert entry.realized_total == test_value\n\n\ndef test_unit_attribute_is_set_to_none(setup_budget_test_base):\n    \"\"\"Unit attribute is set to an empty value if it is set to None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"])\n    assert entry.unit == data[\"test_good\"].unit\n    entry.unit = None\n    assert entry.unit == \"\"\n\n\ndef test_unit_attribute_is_not_a_string(setup_budget_test_base):\n    \"\"\"TypeError is raised if the unit attribute is not a str.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"])\n    with pytest.raises(TypeError) as cm:\n        entry.unit = 100.212\n    assert str(cm.value) == (\n        \"BudgetEntry.unit should be a string, not float: '100.212'\"\n    )\n\n\ndef test_unit_attribute_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Unit attribute is working as expected.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"])\n    test_value = \"TL/hour\"\n    assert entry.unit != test_value\n    entry.unit = test_value\n    assert entry.unit == test_value\n\n\ndef test_unit_attribute_value_will_be_copied_from_the_supplied_good(\n    setup_budget_test_base,\n):\n    \"\"\"Unit attribute value is copied from the good argument value.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=data[\"test_good\"],\n    )\n    assert entry.unit == data[\"test_good\"].unit\n\n\ndef test_amount_argument_is_skipped(setup_budget_test_base):\n    \"\"\"Amount attribute is 0 if the amount argument is skipped.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"])\n    assert entry.amount == 0.0\n\n\ndef test_amount_argument_is_set_to_none(setup_budget_test_base):\n    \"\"\"Amount attribute is 0 if the amount argument is set to None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], amount=None)\n    assert entry.amount == 0.0\n\n\ndef test_amount_attribute_is_set_to_none(setup_budget_test_base):\n    \"\"\"Amount attribute is set to 0 if it is set to None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], amount=10.0)\n    assert entry.amount == 10.0\n    entry.amount = None\n    assert entry.amount == 0.0\n\n\ndef test_amount_argument_is_not_a_number(setup_budget_test_base):\n    \"\"\"TypeError is raised if the amount arg not a number.\"\"\"\n    data = setup_budget_test_base\n    with pytest.raises(TypeError) as cm:\n        BudgetEntry(\n            budget=data[\"test_budget\"], good=data[\"test_good\"], amount=\"some string\"\n        )\n    assert str(cm.value) == (\n        \"BudgetEntry.amount should be a number, not str: 'some string'\"\n    )\n\n\ndef test_amount_attribute_is_not_a_number(setup_budget_test_base):\n    \"\"\"TypeError is raised if amount attribute is not a number.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], amount=10)\n    with pytest.raises(TypeError) as cm:\n        entry.amount = \"some string\"\n    assert str(cm.value) == (\n        \"BudgetEntry.amount should be a number, not str: 'some string'\"\n    )\n\n\ndef test_amount_argument_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Amount argument value is correctly passed to the amount attribute.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], amount=10)\n    assert entry.amount == 10.0\n\n\ndef test_amount_attribute_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Amount attribute is working as expected.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], amount=10)\n    test_value = 5.0\n    assert entry.amount != test_value\n    entry.amount = test_value\n    assert entry.amount == test_value\n\n\ndef test_good_argument_is_skipped(setup_budget_test_base):\n    \"\"\"TypeError is raised when the good argument is skipped.\"\"\"\n    data = setup_budget_test_base\n    with pytest.raises(TypeError) as cm:\n        BudgetEntry(budget=data[\"test_budget\"])\n    assert str(cm.value) == (\n        \"BudgetEntry.good should be a stalker.models.budget.Good instance, \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test_good_argument_is_none(setup_budget_test_base):\n    \"\"\"TypeError is raised when the good argument is None.\"\"\"\n    data = setup_budget_test_base\n    with pytest.raises(TypeError) as cm:\n        BudgetEntry(\n            budget=data[\"test_budget\"],\n            good=None,\n            amount=53,\n        )\n    assert str(cm.value) == (\n        \"BudgetEntry.good should be a stalker.models.budget.Good instance, \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test_good_attribute_is_set_to_none(setup_budget_test_base):\n    \"\"\"TypeError is raised if the good attribute is set to None.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"], good=Good(name=\"Some Good\"), amount=53\n    )\n    with pytest.raises(TypeError) as cm:\n        entry.good = None\n    assert str(cm.value) == (\n        \"BudgetEntry.good should be a stalker.models.budget.Good instance, \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test_good_argument_is_not_a_good_instance(setup_budget_test_base):\n    \"\"\"TypeError is raised when the good argument is not a Good instance.\"\"\"\n    data = setup_budget_test_base\n    with pytest.raises(TypeError) as cm:\n        _ = BudgetEntry(\n            budget=data[\"test_budget\"],\n            good=\"this is not a Good instance\",\n            amount=53,\n        )\n    assert str(cm.value) == (\n        \"BudgetEntry.good should be a stalker.models.budget.Good instance, \"\n        \"not str: 'this is not a Good instance'\"\n    )\n\n\ndef test_good_attribute_is_not_a_good_instance(setup_budget_test_base):\n    \"\"\"TypeError is raised if the good attribute is not a Good instance.\"\"\"\n    data = setup_budget_test_base\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=data[\"test_good\"],\n        amount=53,\n    )\n    with pytest.raises(TypeError) as cm:\n        entry.good = \"this is not a Good instance\"\n    assert (\n        str(cm.value) == \"BudgetEntry.good should be a stalker.models.budget.Good \"\n        \"instance, not str: 'this is not a Good instance'\"\n    )\n\n\ndef test_good_argument_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Good argument value is correctly passed to the good attribute.\"\"\"\n    data = setup_budget_test_base\n    test_value = Good(name=\"Some Good\")\n    entry = BudgetEntry(\n        budget=data[\"test_budget\"],\n        good=test_value,\n        amount=53,\n    )\n    assert entry.good == test_value\n\n\ndef test_good_attribute_is_working_as_expected(setup_budget_test_base):\n    \"\"\"Good attribute can be correctly set.\"\"\"\n    data = setup_budget_test_base\n    test_value = Good(name=\"Some Other Good\")\n    entry = BudgetEntry(budget=data[\"test_budget\"], good=data[\"test_good\"], amount=53)\n    assert entry.good != test_value\n    entry.good = test_value\n    assert entry.good == test_value\n\n\ndef test_parent_child_relation(setup_budget_test_base):\n    \"\"\"Parent/child relation of Budgets.\"\"\"\n    data = setup_budget_test_base\n    b1 = Budget(**data[\"kwargs\"])\n    b2 = Budget(**data[\"kwargs\"])\n    b2.parent = b1\n    assert b1.children == [b2]\n"
  },
  {
    "path": "tests/models/test_client.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests Client class.\"\"\"\nimport datetime\n\nimport pytest\n\nimport pytz\n\nfrom stalker import Client, Entity, Good, Project, Repository, Status, StatusList, User\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_client_tests():\n    \"\"\"Set up the tests for the Client class.\"\"\"\n    data = dict()\n\n    # create a couple of test users\n    data[\"test_user1\"] = User(\n        name=\"User1\",\n        login=\"user1\",\n        email=\"user1@test.com\",\n        password=\"123456\",\n    )\n\n    data[\"test_user2\"] = User(\n        name=\"User2\",\n        login=\"user2\",\n        email=\"user2@test.com\",\n        password=\"123456\",\n    )\n\n    data[\"test_user3\"] = User(\n        name=\"User3\",\n        login=\"user3\",\n        email=\"user3@test.com\",\n        password=\"123456\",\n    )\n\n    data[\"test_user4\"] = User(\n        name=\"User4\",\n        login=\"user4\",\n        email=\"user4@test.com\",\n        password=\"123456\",\n    )\n\n    data[\"users_list\"] = [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n        data[\"test_user3\"],\n        data[\"test_user4\"],\n    ]\n\n    data[\"test_admin\"] = User(\n        name=\"admin\", login=\"admin\", email=\"admin@admins.com\", password=\"1234\"\n    )\n\n    data[\"status_new\"] = Status(name=\"New\", code=\"NEW\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n\n    data[\"project_statuses\"] = StatusList(\n        name=\"Project Status List\",\n        statuses=[data[\"status_new\"], data[\"status_wip\"], data[\"status_cmpl\"]],\n        target_entity_type=\"Project\",\n    )\n\n    data[\"test_repo\"] = Repository(name=\"Test Repository\", code=\"TR\")\n\n    data[\"test_project1\"] = Project(\n        name=\"Test Project 1\",\n        code=\"proj1\",\n        status_list=data[\"project_statuses\"],\n        repository=data[\"test_repo\"],\n    )\n\n    data[\"test_project2\"] = Project(\n        name=\"Test Project 1\",\n        code=\"proj2\",\n        status_list=data[\"project_statuses\"],\n        repository=data[\"test_repo\"],\n    )\n    data[\"test_project3\"] = Project(\n        name=\"Test Project 1\",\n        code=\"proj3\",\n        status_list=data[\"project_statuses\"],\n        repository=data[\"test_repo\"],\n    )\n    data[\"projects_list\"] = [\n        data[\"test_project1\"],\n        data[\"test_project2\"],\n        data[\"test_project3\"],\n    ]\n    data[\"date_created\"] = data[\"date_updated\"] = datetime.datetime.now(pytz.utc)\n    data[\"kwargs\"] = {\n        \"name\": \"Test Client\",\n        \"description\": \"This is a client for testing purposes\",\n        \"created_by\": data[\"test_admin\"],\n        \"updated_by\": data[\"test_admin\"],\n        \"date_created\": data[\"date_created\"],\n        \"date_updated\": data[\"date_updated\"],\n        \"users\": data[\"users_list\"],\n        \"projects\": data[\"projects_list\"],\n    }\n    # create a default client object\n    data[\"test_client\"] = Client(**data[\"kwargs\"])\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Department class.\"\"\"\n    assert Client.__auto_name__ is False\n\n\ndef test_users_argument_accepts_an_empty_list(setup_client_tests):\n    \"\"\"users argument accepts an empty list.\"\"\"\n    data = setup_client_tests\n    # this should work without raising any error\n    data[\"kwargs\"][\"users\"] = []\n    new_dep = Client(**data[\"kwargs\"])\n    assert isinstance(new_dep, Client)\n\n\ndef test_users_attribute_accepts_an_empty_list(setup_client_tests):\n    \"\"\"users attribute accepts an empty list.\"\"\"\n    data = setup_client_tests\n    # this should work without raising any error\n    data[\"test_client\"].users = []\n\n\ndef test_users_argument_accepts_only_a_list_of_user_objects(setup_client_tests):\n    \"\"\"users argument accepts only a list of user objects.\"\"\"\n    data = setup_client_tests\n    test_value = [1, 2.3, [], {}]\n    data[\"kwargs\"][\"users\"] = test_value\n    # this should raise a TypeError\n    with pytest.raises(TypeError) as cm:\n        Client(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"ClientUser.user should be an instance of stalker.models.auth.User, \"\n        \"not int: '1'\"\n    )\n\n\ndef test_users_attribute_accepts_only_a_list_of_user_objects(setup_client_tests):\n    \"\"\"users attribute accepts only a list of user objects.\"\"\"\n    data = setup_client_tests\n    test_value = [1, 2.3, [], {}]\n    # this should raise a TypeError\n    with pytest.raises(TypeError) as cm:\n        data[\"test_client\"].users = test_value\n\n    assert str(cm.value) == (\n        \"ClientUser.user should be an instance of stalker.models.auth.User, \"\n        \"not int: '1'\"\n    )\n\n\ndef test_users_attribute_elements_accepts_user_only_append(setup_client_tests):\n    \"\"\"TypeError is raised if users list assigned a value other than a User instance.\"\"\"\n    data = setup_client_tests\n    # append\n    with pytest.raises(TypeError) as cm:\n        data[\"test_client\"].users.append(0)\n\n    assert str(cm.value) == (\n        \"ClientUser.user should be an instance of stalker.models.auth.User, \"\n        \"not int: '0'\"\n    )\n\n\ndef test_users_attribute_elements_accepts_user_only_setitem(setup_client_tests):\n    \"\"\"TypeError is raised if users list assigned a value other than a User instance.\"\"\"\n    data = setup_client_tests\n    # __setitem__\n    with pytest.raises(TypeError) as cm:\n        data[\"test_client\"].users[0] = 0\n\n    assert str(cm.value) == (\n        \"ClientUser.user should be an instance of stalker.models.auth.User, \"\n        \"not int: '0'\"\n    )\n\n\ndef test_users_argument_is_not_iterable(setup_client_tests):\n    \"\"\"TypeError is raised if the given users argument is not a list.\"\"\"\n    data = setup_client_tests\n    data[\"kwargs\"][\"users\"] = \"a user\"\n    with pytest.raises(TypeError) as cm:\n        Client(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"ClientUser.user should be an instance of stalker.models.auth.User, \"\n        \"not str: 'a'\"\n    )\n\n\ndef test_users_attribute_is_not_iterable(setup_client_tests):\n    \"\"\"TypeError is raised if the users attribute is not iterable.\"\"\"\n    data = setup_client_tests\n    test_value = \"a user\"\n    with pytest.raises(TypeError) as cm:\n        data[\"test_client\"].users = test_value\n\n    assert str(cm.value) == (\n        \"ClientUser.user should be an instance of stalker.models.auth.User, \"\n        \"not str: 'a'\"\n    )\n\n\ndef test_users_attribute_defaults_to_empty_list(setup_client_tests):\n    \"\"\"users attribute defaults to an empty list if the users argument is skipped.\"\"\"\n    data = setup_client_tests\n    data[\"kwargs\"].pop(\"users\")\n    new_client = Client(**data[\"kwargs\"])\n    assert new_client.users == []\n\n\ndef test_users_attribute_set_to_none(setup_client_tests):\n    \"\"\"TypeError will be raised if the users attribute is set to None.\"\"\"\n    data = setup_client_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_client\"].users = None\n    assert str(cm.value) == \"'NoneType' object is not iterable\"\n\n\ndef test_projects_argument_accepts_an_empty_list(setup_client_tests):\n    \"\"\"projects argument accepts an empty list.\"\"\"\n    data = setup_client_tests\n    # this should work without raising any error\n    data[\"kwargs\"][\"projects\"] = []\n    new_dep = Client(**data[\"kwargs\"])\n    assert isinstance(new_dep, Client)\n\n\ndef test_projects_attribute_accepts_an_empty_list(setup_client_tests):\n    \"\"\"projects attribute accepts an empty list.\"\"\"\n    data = setup_client_tests\n    # this should work without raising any error\n    data[\"test_client\"].projects = []\n\n\ndef test_projects_argument_accepts_only_a_list_of_project_objects(setup_client_tests):\n    \"\"\"projects argument accepts only a list of project objects.\"\"\"\n    data = setup_client_tests\n    test_value = [1, 2.3, [], {}]\n    data[\"kwargs\"][\"projects\"] = test_value\n    # this should raise a TypeError\n    with pytest.raises(TypeError) as cm:\n        Client(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"ProjectClient.project should be a stalker.models.project.Project instance, \"\n        \"not int: '1'\"\n    )\n\n\ndef test_projects_attribute_accepts_only_a_list_of_project_objects(setup_client_tests):\n    \"\"\"users attribute accepts only a list of project objects.\"\"\"\n    data = setup_client_tests\n    test_value = [1, 2.3, \"a project\"]\n    # this should raise a TypeError\n    with pytest.raises(TypeError) as cm:\n        data[\"test_client\"].projects = test_value\n\n    assert str(cm.value) == (\n        \"ProjectClient.project should be a stalker.models.project.Project instance, \"\n        \"not int: '1'\"\n    )\n\n\ndef test_projects_attribute_elements_accepts_project_only_append(setup_client_tests):\n    \"\"\"TypeError is raised if assigned a non Project instance to the project attr.\"\"\"\n    data = setup_client_tests\n    # append\n    with pytest.raises(TypeError) as cm:\n        data[\"test_client\"].projects.append(0)\n\n    assert str(cm.value) == (\n        \"ProjectClient.project should be a stalker.models.project.Project instance, \"\n        \"not int: '0'\"\n    )\n\n\ndef test_projects_attribute_elements_accepts_project_only_setitem(setup_client_tests):\n    \"\"\"TypeError is raised if assigned a non Project instance to the projects attr.\"\"\"\n    data = setup_client_tests\n    # __setitem__\n    with pytest.raises(TypeError) as cm:\n        data[\"test_client\"].projects[0] = 0\n\n    assert str(cm.value) == (\n        \"ProjectClient.project should be a stalker.models.project.Project instance, \"\n        \"not int: '0'\"\n    )\n\n\ndef test_projects_argument_is_not_iterable(setup_client_tests):\n    \"\"\"TypeError is raised if the given projects argument is not a list.\"\"\"\n    data = setup_client_tests\n    data[\"kwargs\"][\"projects\"] = \"a project\"\n    with pytest.raises(TypeError) as cm:\n        Client(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"ProjectClient.project should be a stalker.models.project.Project instance, \"\n        \"not str: 'a'\"\n    )\n\n\ndef test_projects_attribute_is_not_iterable(setup_client_tests):\n    \"\"\"TypeError is raised if the projects attr is set to a non-iterable value.\"\"\"\n    data = setup_client_tests\n    test_value = \"a project\"\n    with pytest.raises(TypeError) as cm:\n        data[\"test_client\"].projects = test_value\n\n    assert str(cm.value) == (\n        \"ProjectClient.project should be a stalker.models.project.Project instance, \"\n        \"not str: 'a'\"\n    )\n\n\ndef test_projects_attribute_defaults_to_empty_list(setup_client_tests):\n    \"\"\"projects attr defaults to an empty list if the projects argument is skipped.\"\"\"\n    data = setup_client_tests\n    data[\"kwargs\"].pop(\"projects\")\n    new_client = Client(**data[\"kwargs\"])\n    assert new_client.projects == []\n\n\ndef test_projects_attribute_set_to_none(setup_client_tests):\n    \"\"\"TypeError is raised if the projects attribute is set to None.\"\"\"\n    data = setup_client_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_client\"].projects = None\n\n    assert str(cm.value) == \"'NoneType' object is not iterable\"\n\n\ndef test_user_remove_also_removes_client_from_user(setup_client_tests):\n    \"\"\"Removing user from the users removes the client from the users companies.\"\"\"\n    data = setup_client_tests\n    # check if the user is in the company\n    assert data[\"test_client\"] in data[\"test_user1\"].companies\n\n    # now remove the user from the company\n    data[\"test_client\"].users.remove(data[\"test_user1\"])\n\n    # now check if company is not in users companies anymore\n    assert data[\"test_client\"] not in data[\"test_user1\"].companies\n\n    # assign the user back\n    data[\"test_user1\"].companies.append(data[\"test_client\"])\n\n    # check if the user is in the companies users list\n    assert data[\"test_user1\"] in data[\"test_client\"].users\n\n\n# def test_project_remove_also_removes_project_from_client(setup_client_tests):\n#     \"\"\"removing user from the users removes the client from the users companies.\"\"\"\n#     data = setup_client_tests\n#     # check if the project is registered with the client\n#     assert data[\"test_client\"] in data[\"test_project1\"].clients\n#\n#     # now remove the project from the client\n#     # data[\"test_client\"].projects.remove(data[\"test_project1\"])\n#     data[\"test_client\"].project_role.remove(data[\"test_client\"].project_role[0])\n#\n#     # now check if project no longer belongs to client\n#     assert data[\"test_project1\"] not in data[\"test_client\"].projects\n#\n#     # assign the project back\n#     data[\"test_client\"].projects.append(data[\"test_project1\"])\n#\n#     # check if the project is in the companies projects list\n#     assert data[\"test_project1\"] in data[\"test_client\"].projects\n\n\ndef test_equality(setup_client_tests):\n    \"\"\"equality of two Client objects.\"\"\"\n    data = setup_client_tests\n    client1 = Client(**data[\"kwargs\"])\n    client2 = Client(**data[\"kwargs\"])\n\n    entity_kwargs = data[\"kwargs\"].copy()\n    entity_kwargs.pop(\"users\")\n    entity_kwargs.pop(\"projects\")\n    entity1 = Entity(**entity_kwargs)\n\n    data[\"kwargs\"][\"name\"] = \"Company X\"\n    client3 = Client(**data[\"kwargs\"])\n\n    assert client1 == client2\n    assert not client1 == client3\n    assert not client1 == entity1\n\n\ndef test_inequality(setup_client_tests):\n    \"\"\"inequality of two Client objects.\"\"\"\n    data = setup_client_tests\n    client1 = Client(**data[\"kwargs\"])\n    client2 = Client(**data[\"kwargs\"])\n\n    entity_kwargs = data[\"kwargs\"].copy()\n    entity_kwargs.pop(\"users\")\n    entity_kwargs.pop(\"projects\")\n    entity1 = Entity(**entity_kwargs)\n\n    data[\"kwargs\"][\"name\"] = \"Company X\"\n    client3 = Client(**data[\"kwargs\"])\n\n    assert not client1 != client2\n    assert client1 != client3\n    assert client1 != entity1\n\n\ndef test_to_tjp_method_is_working_as_expected(setup_client_tests):\n    \"\"\"to_tjp method is working as expected.\"\"\"\n    data = setup_client_tests\n    client1 = Client(**data[\"kwargs\"])\n    assert client1.to_tjp == \"\"\n\n\ndef test_hash_is_correctly_calculated(setup_client_tests):\n    \"\"\"hash value is correctly calculated.\"\"\"\n    data = setup_client_tests\n    client1 = Client(**data[\"kwargs\"])\n    assert client1.__hash__() == hash(\n        \"{}:{}:{}\".format(client1.id, client1.name, client1.entity_type)\n    )\n\n\ndef test_goods_attribute_is_set_to_none(setup_client_tests):\n    \"\"\"TypeError is raised if good is set to None.\"\"\"\n    data = setup_client_tests\n    client1 = Client(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        client1.goods = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_goods_attribute_is_set_to_a_list_of_non_good_instances(setup_client_tests):\n    \"\"\"TypeError is raised if the goods attr is set to a list of non Good instances.\"\"\"\n    data = setup_client_tests\n    client1 = Client(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        client1.goods = [\"not\", 1, \"list\", \"of\", \"goods\"]\n\n    assert str(cm.value) == (\n        \"Client.goods should only contain instances of \"\n        \"stalker.models.budget.Good, not str: 'not'\"\n    )\n\n\ndef test_goods_attribute_is_working_as_expected(setup_client_tests):\n    \"\"\"goods attribute is working as expected.\"\"\"\n    data = setup_client_tests\n    client1 = Client(**data[\"kwargs\"])\n    good1 = Good(name=\"Test Good 1\")\n    good2 = Good(name=\"Test Good 2\")\n    good3 = Good(name=\"Test Good 3\")\n    client1.goods = [good1, good2, good3]\n\n    assert client1.goods == [good1, good2, good3]\n"
  },
  {
    "path": "tests/models/test_client_user.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the ClientUser class.\"\"\"\n\nimport pytest\n\nfrom stalker import Client, ClientUser, User\n\n\ndef test_role_argument_is_not_a_role_instance():\n    \"\"\"TypeError will be raised when the role argument is not a Role instance.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        ClientUser(\n            client=Client(name=\"Test Client\"),\n            user=User(\n                name=\"Test User\", login=\"tuser\", email=\"u@u.com\", password=\"secret\"\n            ),\n            role=\"not a role instance\",\n        )\n\n    assert str(cm.value) == (\n        \"ClientUser.role should be a stalker.models.auth.Role instance, \"\n        \"not str: 'not a role instance'\"\n    )\n"
  },
  {
    "path": "tests/models/test_daily.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the stalker.models.review.Daily class.\"\"\"\n\nimport pytest\n\nfrom stalker import (\n    Daily,\n    DailyFile,\n    File,\n    Project,\n    Repository,\n    Status,\n    StatusList,\n    Task,\n    Version,\n)\nfrom stalker.db.session import DBSession\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_daily_tests():\n    \"\"\"Set up Daily test data.\"\"\"\n    data = dict()\n    data[\"status_new\"] = Status(name=\"Mew\", code=\"NEW\")\n    data[\"status_wfd\"] = Status(name=\"Waiting For Dependency\", code=\"WFD\")\n    data[\"status_rts\"] = Status(name=\"Ready To Start\", code=\"RTS\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_prev\"] = Status(name=\"Pending Review\", code=\"PREV\")\n    data[\"status_hrev\"] = Status(name=\"Has Revision\", code=\"HREV\")\n    data[\"status_drev\"] = Status(name=\"Dependency Has Revision\", code=\"DREV\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n    data[\"status_open\"] = Status(name=\"Open\", code=\"OPEN\")\n    data[\"status_cls\"] = Status(name=\"Closed\", code=\"CLS\")\n\n    data[\"daily_status_list\"] = StatusList(\n        name=\"Daily Statuses\",\n        statuses=[data[\"status_open\"], data[\"status_cls\"]],\n        target_entity_type=\"Daily\",\n    )\n\n    data[\"task_status_list\"] = StatusList(\n        name=\"Task Statuses\",\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n\n    data[\"test_project_status_list\"] = StatusList(\n        name=\"Project Statuses\",\n        target_entity_type=\"Project\",\n        statuses=[data[\"status_new\"], data[\"status_wip\"], data[\"status_cmpl\"]],\n    )\n\n    data[\"test_repo\"] = Repository(name=\"Test Repository\", code=\"TR\")\n\n    data[\"test_project\"] = Project(\n        name=\"Test Project\",\n        code=\"TP\",\n        repository=data[\"test_repo\"],\n        status_list=data[\"test_project_status_list\"],\n    )\n\n    data[\"test_task1\"] = Task(\n        name=\"Test Task 1\",\n        project=data[\"test_project\"],\n        status_list=data[\"task_status_list\"],\n    )\n    data[\"test_task2\"] = Task(\n        name=\"Test Task 2\",\n        project=data[\"test_project\"],\n        status_list=data[\"task_status_list\"],\n    )\n    data[\"test_task3\"] = Task(\n        name=\"Test Task 3\",\n        project=data[\"test_project\"],\n        status_list=data[\"task_status_list\"],\n    )\n\n    data[\"test_version1\"] = Version(task=data[\"test_task1\"])\n    data[\"test_version2\"] = Version(task=data[\"test_task1\"])\n    data[\"test_version3\"] = Version(task=data[\"test_task1\"])\n    data[\"test_version4\"] = Version(task=data[\"test_task2\"])\n\n    data[\"test_file1\"] = File(original_filename=\"test_render1.jpg\")\n    data[\"test_file2\"] = File(original_filename=\"test_render2.jpg\")\n    data[\"test_file3\"] = File(original_filename=\"test_render3.jpg\")\n    data[\"test_file4\"] = File(original_filename=\"test_render4.jpg\")\n\n    data[\"test_version1\"].files = [\n        data[\"test_file1\"],\n        data[\"test_file2\"],\n        data[\"test_file3\"],\n    ]\n    data[\"test_version4\"].files = [data[\"test_file4\"]]\n    return data\n\n\ndef test_daily_instance_creation(setup_daily_tests):\n    \"\"\"It is possible to create a Daily without a problem.\"\"\"\n    data = setup_daily_tests\n    daily = Daily(\n        name=\"Test Daily\",\n        project=data[\"test_project\"],\n        status_list=data[\"daily_status_list\"],\n    )\n    assert isinstance(daily, Daily)\n\n\ndef test_files_argument_is_skipped(setup_daily_tests):\n    \"\"\"files attribute is an empty list if the files argument is skipped.\"\"\"\n    data = setup_daily_tests\n    daily = Daily(\n        name=\"Test Daily\",\n        project=data[\"test_project\"],\n        status_list=data[\"daily_status_list\"],\n    )\n    assert daily.files == []\n\n\ndef test_files_argument_is_none(setup_daily_tests):\n    \"\"\"files attribute is an empty list if the files argument is None.\"\"\"\n    data = setup_daily_tests\n    daily = Daily(\n        name=\"Test Daily\",\n        files=None,\n        project=data[\"test_project\"],\n        status_list=data[\"daily_status_list\"],\n    )\n    assert daily.files == []\n\n\ndef test_files_attribute_is_set_to_none(setup_daily_tests):\n    \"\"\"TypeError is raised if the files attribute is set to None.\"\"\"\n    data = setup_daily_tests\n    daily = Daily(\n        name=\"Test Daily\",\n        project=data[\"test_project\"],\n        status_list=data[\"daily_status_list\"],\n    )\n    with pytest.raises(TypeError):\n        daily.files = None\n\n\ndef test_files_argument_is_not_a_list_instance(setup_daily_tests):\n    \"\"\"TypeError is raised if the files argument is not a list.\"\"\"\n    data = setup_daily_tests\n    with pytest.raises(TypeError) as cm:\n        Daily(\n            name=\"Test Daily\",\n            files=\"not a list of Daily instances\",\n            project=data[\"test_project\"],\n            status_list=data[\"daily_status_list\"],\n        )\n\n    assert (\n        str(cm.value) == \"DailyFile.file should be an instance of \"\n        \"stalker.models.file.File instance, not str: 'n'\"\n    )\n\n\ndef test_files_argument_is_not_a_list_of_file_instances(setup_daily_tests):\n    \"\"\"TypeError is raised if the files argument is not a list of File instances.\"\"\"\n    data = setup_daily_tests\n    with pytest.raises(TypeError) as cm:\n        Daily(\n            name=\"Test Daily\",\n            files=[\"not\", 1, \"list\", \"of\", File, \"instances\"],\n            project=data[\"test_project\"],\n            status_list=data[\"daily_status_list\"],\n        )\n\n    assert str(cm.value) == (\n        \"DailyFile.file should be an instance of stalker.models.file.File instance, \"\n        \"not str: 'not'\"\n    )\n\n\ndef test_files_argument_is_working_as_expected(setup_daily_tests):\n    \"\"\"files argument value is correctly passed to the files attribute.\"\"\"\n    data = setup_daily_tests\n    test_value = [data[\"test_file1\"], data[\"test_file2\"]]\n    daily = Daily(\n        name=\"Test Daily\",\n        files=test_value,\n        project=data[\"test_project\"],\n        status_list=data[\"daily_status_list\"],\n    )\n    assert daily.files == test_value\n\n\ndef test_files_attribute_is_working_as_expected(setup_daily_tests):\n    \"\"\"files attribute is working as expected.\"\"\"\n    data = setup_daily_tests\n    daily = Daily(\n        name=\"Test Daily\",\n        project=data[\"test_project\"],\n        status_list=data[\"daily_status_list\"],\n    )\n    daily.files.append(data[\"test_file1\"])\n\n    assert daily.files == [data[\"test_file1\"]]\n\n\ndef test_versions_attribute_is_read_only(setup_daily_tests):\n    \"\"\"versions attribute is a read only attribute.\"\"\"\n    data = setup_daily_tests\n    daily = Daily(\n        name=\"Test Daily\",\n        project=data[\"test_project\"],\n        status_list=data[\"daily_status_list\"],\n    )\n    with pytest.raises(AttributeError):\n        setattr(daily, \"versions\", 10)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_daily_db_tests(setup_postgresql_db):\n    \"\"\"Set up Daily test with a Postgres DB.\"\"\"\n    data = dict()\n\n    data[\"status_new\"] = Status.query.filter_by(code=\"NEW\").first()\n    data[\"status_wfd\"] = Status.query.filter_by(code=\"WFD\").first()\n    data[\"status_rts\"] = Status.query.filter_by(code=\"RTS\").first()\n    data[\"status_wip\"] = Status.query.filter_by(code=\"WIP\").first()\n    data[\"status_prev\"] = Status.query.filter_by(code=\"PREV\").first()\n    data[\"status_hrev\"] = Status.query.filter_by(code=\"HREV\").first()\n    data[\"status_drev\"] = Status.query.filter_by(code=\"DREV\").first()\n    data[\"status_cmpl\"] = Status.query.filter_by(code=\"CMPL\").first()\n\n    data[\"status_open\"] = Status.query.filter_by(code=\"OPEN\").first()\n    data[\"status_cls\"] = Status.query.filter_by(code=\"CLS\").first()\n\n    data[\"daily_status_list\"] = StatusList.query.filter_by(\n        target_entity_type=\"Daily\"\n    ).first()\n\n    data[\"task_status_list\"] = StatusList.query.filter_by(\n        target_entity_type=\"Task\"\n    ).first()\n\n    data[\"test_repo\"] = Repository(name=\"Test Repository\", code=\"TR\")\n    DBSession.add(data[\"test_repo\"])\n\n    data[\"test_project\"] = Project(\n        name=\"Test Project\",\n        code=\"TP\",\n        repository=data[\"test_repo\"],\n    )\n    DBSession.add(data[\"test_project\"])\n\n    data[\"test_task1\"] = Task(\n        name=\"Test Task 1\",\n        project=data[\"test_project\"],\n        status_list=data[\"task_status_list\"],\n    )\n    DBSession.add(data[\"test_task1\"])\n    data[\"test_task2\"] = Task(\n        name=\"Test Task 2\",\n        project=data[\"test_project\"],\n        status_list=data[\"task_status_list\"],\n    )\n    DBSession.add(data[\"test_task2\"])\n    data[\"test_task3\"] = Task(\n        name=\"Test Task 3\",\n        project=data[\"test_project\"],\n        status_list=data[\"task_status_list\"],\n    )\n    DBSession.add(data[\"test_task3\"])\n    DBSession.commit()\n\n    data[\"test_version1\"] = Version(task=data[\"test_task1\"])\n    DBSession.add(data[\"test_version1\"])\n    DBSession.commit()\n    data[\"test_version2\"] = Version(task=data[\"test_task1\"])\n    DBSession.add(data[\"test_version2\"])\n    DBSession.commit()\n    data[\"test_version3\"] = Version(task=data[\"test_task1\"])\n    DBSession.add(data[\"test_version3\"])\n    DBSession.commit()\n    data[\"test_version4\"] = Version(task=data[\"test_task2\"])\n    DBSession.add(data[\"test_version4\"])\n    DBSession.commit()\n\n    data[\"test_file1\"] = File(original_filename=\"test_render1.jpg\")\n    data[\"test_file2\"] = File(original_filename=\"test_render2.jpg\")\n    data[\"test_file3\"] = File(original_filename=\"test_render3.jpg\")\n    data[\"test_file4\"] = File(original_filename=\"test_render4.jpg\")\n    DBSession.add_all(\n        [\n            data[\"test_file1\"],\n            data[\"test_file2\"],\n            data[\"test_file3\"],\n            data[\"test_file4\"],\n        ]\n    )\n\n    data[\"test_version1\"].files = [\n        data[\"test_file1\"],\n        data[\"test_file2\"],\n        data[\"test_file3\"],\n    ]\n    data[\"test_version4\"].files = [data[\"test_file4\"]]\n    DBSession.commit()\n    yield data\n\n\ndef test_tasks_attribute_will_return_a_list_of_tasks(setup_daily_db_tests):\n    \"\"\"tasks attribute is a list of Task instances related to the given files.\"\"\"\n    data = setup_daily_db_tests\n    daily = Daily(\n        name=\"Test Daily\",\n        project=data[\"test_project\"],\n        status_list=data[\"daily_status_list\"],\n    )\n    daily.files = [data[\"test_file1\"], data[\"test_file2\"]]\n    DBSession.add(daily)\n    DBSession.commit()\n    assert daily.tasks == [data[\"test_task1\"]]\n\n\ndef test_versions_attribute_will_return_a_list_of_versions(setup_daily_db_tests):\n    \"\"\"versions attribute is a list of Version instances related to the given files.\"\"\"\n    data = setup_daily_db_tests\n    daily = Daily(\n        name=\"Test Daily\",\n        project=data[\"test_project\"],\n        status_list=data[\"daily_status_list\"],\n    )\n    daily.files = [data[\"test_file1\"], data[\"test_file2\"]]\n    DBSession.add(daily)\n    DBSession.commit()\n    assert daily.versions == [data[\"test_version1\"]]\n\n\ndef test_rank_argument_is_skipped():\n    \"\"\"rank attribute will use the default value is if skipped.\"\"\"\n    dl = DailyFile()\n    assert dl.rank == 0\n\n\ndef test_daily_argument_is_not_a_daily_instance(setup_daily_tests):\n    \"\"\"TypeError is raised if the daily argument is not a Daily and not None.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        DailyFile(daily=\"not a daily\")\n\n    assert str(cm.value) == (\n        \"DailyFile.daily should be an instance of stalker.models.review.Daily \"\n        \"instance, not str: 'not a daily'\"\n    )\n"
  },
  {
    "path": "tests/models/test_department.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Department class.\"\"\"\n\nimport datetime\n\nimport pytest\n\nimport pytz\n\nfrom stalker import Department, DepartmentUser, Entity, User\nfrom stalker.db.session import DBSession\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_department_tests():\n    \"\"\"Set up the tests foe the Department class.\"\"\"\n    data = dict()\n    data[\"test_admin\"] = User(\n        name=\"admin\",\n        login=\"admin\",\n        email=\"admin@admins.com\",\n        password=\"12345\",\n    )\n\n    # create a couple of test users\n    data[\"test_user1\"] = User(\n        name=\"User1\",\n        login=\"user1\",\n        email=\"user1@test.com\",\n        password=\"123456\",\n    )\n\n    data[\"test_user2\"] = User(\n        name=\"User2\",\n        login=\"user2\",\n        email=\"user2@test.com\",\n        password=\"123456\",\n    )\n\n    data[\"test_user3\"] = User(\n        name=\"User3\",\n        login=\"user3\",\n        email=\"user3@test.com\",\n        password=\"123456\",\n    )\n\n    data[\"test_user4\"] = User(\n        name=\"User4\",\n        login=\"user4\",\n        email=\"user4@test.com\",\n        password=\"123456\",\n    )\n\n    data[\"test_user5\"] = User(\n        name=\"User5\",\n        login=\"user5\",\n        email=\"user5@test.com\",\n        password=\"123456\",\n    )\n\n    data[\"users_list\"] = [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n        data[\"test_user3\"],\n        data[\"test_user4\"],\n    ]\n\n    data[\"date_created\"] = data[\"date_updated\"] = datetime.datetime.now(pytz.utc)\n\n    data[\"kwargs\"] = {\n        \"name\": \"Test Department\",\n        \"description\": \"This is a department for testing purposes\",\n        \"created_by\": data[\"test_admin\"],\n        \"updated_by\": data[\"test_admin\"],\n        \"date_created\": data[\"date_created\"],\n        \"date_updated\": data[\"date_updated\"],\n        \"users\": data[\"users_list\"],\n    }\n\n    # create a default department object\n    data[\"test_department\"] = Department(**data[\"kwargs\"])\n\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Department class.\"\"\"\n    assert Department.__auto_name__ is False\n\n\ndef test___hash___value_is_correctly_calculated(setup_department_tests):\n    \"\"\"__hash__ value is correctly calculated.\"\"\"\n    data = setup_department_tests\n    assert data[\"test_department\"].__hash__() == hash(\n        \"{}:{}:{}\".format(\n            data[\"test_department\"].id,\n            data[\"test_department\"].name,\n            data[\"test_department\"].entity_type,\n        )\n    )\n\n\ndef test_users_argument_accepts_an_empty_list(setup_department_tests):\n    \"\"\"users argument accepts an empty list.\"\"\"\n    data = setup_department_tests\n    # this should work without raising any error\n    data[\"kwargs\"][\"users\"] = []\n    new_dep = Department(**data[\"kwargs\"])\n    assert isinstance(new_dep, Department)\n\n\ndef test_users_attribute_accepts_an_empty_list(setup_department_tests):\n    \"\"\"users attribute accepts an empty list.\"\"\"\n    data = setup_department_tests\n    # this should work without raising any error\n    data[\"test_department\"].users = []\n\n\ndef test_users_argument_accepts_only_a_list_of_user_objects(setup_department_tests):\n    \"\"\"users argument accepts only a list of user objects.\"\"\"\n    data = setup_department_tests\n    test_value = [1, 2.3, [], {}]\n    data[\"kwargs\"][\"users\"] = test_value\n    # this should raise a TypeError\n    with pytest.raises(TypeError) as cm:\n        Department(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"DepartmentUser.user should be a stalker.models.auth.User instance, \"\n        \"not int: '1'\"\n    )\n\n\ndef test_users_attribute_accepts_only_a_list_of_user_objects(setup_department_tests):\n    \"\"\"users attribute accepts only a list of user objects.\"\"\"\n    data = setup_department_tests\n    test_value = [1, 2.3, [], {}]\n    # this should raise a TypeError\n    with pytest.raises(TypeError) as cm:\n        data[\"test_department\"].users = test_value\n\n    assert str(cm.value) == (\n        \"DepartmentUser.user should be a stalker.models.auth.User instance, \"\n        \"not int: '1'\"\n    )\n\n\ndef test_users_attribute_elements_accepts_user_only_1(setup_department_tests):\n    \"\"\"TypeError is raised if append non-User to the users attr.\"\"\"\n    data = setup_department_tests\n    # append\n    with pytest.raises(TypeError) as cm:\n        data[\"test_department\"].users.append(0)\n\n    assert str(cm.value) == (\n        \"DepartmentUser.user should be a stalker.models.auth.User instance, \"\n        \"not int: '0'\"\n    )\n\n\ndef test_users_attribute_elements_accepts_user_only_2(setup_department_tests):\n    \"\"\"TypeError is raised if non list assigned to the users attr.\"\"\"\n    data = setup_department_tests\n    # __setitem__\n    with pytest.raises(TypeError) as cm:\n        data[\"test_department\"].users[0] = 0\n\n    assert str(cm.value) == (\n        \"DepartmentUser.user should be a stalker.models.auth.User instance, \"\n        \"not int: '0'\"\n    )\n\n\ndef test_users_argument_is_not_iterable(setup_department_tests):\n    \"\"\"TypeError is raised if the given users argument is not an instance of list.\"\"\"\n    data = setup_department_tests\n    data[\"kwargs\"][\"users\"] = \"a user\"\n    with pytest.raises(TypeError) as cm:\n        Department(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"DepartmentUser.user should be a stalker.models.auth.User instance, \"\n        \"not str: 'a'\"\n    )\n\n\ndef test_users_attribute_is_not_iterable(setup_department_tests):\n    \"\"\"TypeError is raised if the users attr is not iterable value.\"\"\"\n    data = setup_department_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_department\"].users = \"a user\"\n\n    assert str(cm.value) == (\n        \"DepartmentUser.user should be a stalker.models.auth.User instance, \"\n        \"not str: 'a'\"\n    )\n\n\ndef test_users_attribute_defaults_to_empty_list(setup_department_tests):\n    \"\"\"users attribute defaults to an empty list if the users argument is skipped.\"\"\"\n    data = setup_department_tests\n    data[\"kwargs\"].pop(\"users\")\n    new_department = Department(**data[\"kwargs\"])\n    assert new_department.users == []\n\n\ndef test_users_attribute_set_to_none(setup_department_tests):\n    \"\"\"TypeError is raised if the users attribute is set to None.\"\"\"\n    data = setup_department_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_department\"].users = None\n\n    assert str(cm.value) == \"'NoneType' object is not iterable\"\n\n\ndef test_equality(setup_department_tests):\n    \"\"\"equality of two Department objects.\"\"\"\n    data = setup_department_tests\n    dep1 = Department(**data[\"kwargs\"])\n    dep2 = Department(**data[\"kwargs\"])\n\n    entity_kwargs = data[\"kwargs\"].copy()\n    entity_kwargs.pop(\"users\")\n    entity1 = Entity(**entity_kwargs)\n\n    data[\"kwargs\"][\"name\"] = \"Animation\"\n    dep3 = Department(**data[\"kwargs\"])\n\n    assert dep1 == dep2\n    assert not dep1 == dep3\n    assert not dep1 == entity1\n\n\ndef test_inequality(setup_department_tests):\n    \"\"\"inequality of two Department objects.\"\"\"\n    data = setup_department_tests\n    dep1 = Department(**data[\"kwargs\"])\n    dep2 = Department(**data[\"kwargs\"])\n\n    entity_kwargs = data[\"kwargs\"].copy()\n    entity_kwargs.pop(\"users\")\n    entity1 = Entity(**entity_kwargs)\n\n    data[\"kwargs\"][\"name\"] = \"Animation\"\n    dep3 = Department(**data[\"kwargs\"])\n\n    assert not dep1 != dep2\n    assert dep1 != dep3\n    assert dep1 != entity1\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_department_db_tests(setup_postgresql_db):\n    \"\"\"set up Database tests for Department class.\"\"\"\n    data = dict()\n    data[\"test_admin\"] = User.query.filter_by(login=\"admin\").first()\n\n    # create a couple of test users\n    data[\"test_user1\"] = User(\n        name=\"User1\",\n        login=\"user1\",\n        email=\"user1@test.com\",\n        password=\"123456\",\n    )\n    DBSession.add(data[\"test_user1\"])\n\n    data[\"test_user2\"] = User(\n        name=\"User2\",\n        login=\"user2\",\n        email=\"user2@test.com\",\n        password=\"123456\",\n    )\n    DBSession.add(data[\"test_user2\"])\n\n    data[\"test_user3\"] = User(\n        name=\"User3\",\n        login=\"user3\",\n        email=\"user3@test.com\",\n        password=\"123456\",\n    )\n    DBSession.add(data[\"test_user3\"])\n\n    data[\"test_user4\"] = User(\n        name=\"User4\",\n        login=\"user4\",\n        email=\"user4@test.com\",\n        password=\"123456\",\n    )\n    DBSession.add(data[\"test_user4\"])\n\n    data[\"test_user5\"] = User(\n        name=\"User5\",\n        login=\"user5\",\n        email=\"user5@test.com\",\n        password=\"123456\",\n    )\n    DBSession.add(data[\"test_user5\"])\n\n    data[\"users_list\"] = [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n        data[\"test_user3\"],\n        data[\"test_user4\"],\n    ]\n\n    data[\"date_created\"] = data[\"date_updated\"] = datetime.datetime.now(pytz.utc)\n\n    data[\"kwargs\"] = {\n        \"name\": \"Test Department\",\n        \"description\": \"This is a department for testing purposes\",\n        \"created_by\": data[\"test_admin\"],\n        \"updated_by\": data[\"test_admin\"],\n        \"date_created\": data[\"date_created\"],\n        \"date_updated\": data[\"date_updated\"],\n        \"users\": data[\"users_list\"],\n    }\n\n    # create a default department object\n    data[\"test_department\"] = Department(**data[\"kwargs\"])\n    DBSession.add(data[\"test_department\"])\n    DBSession.commit()\n    return data\n\n\ndef test_user_role_attribute(setup_department_db_tests):\n    \"\"\"Automatic generation of the DepartmentUser class.\"\"\"\n    data = setup_department_db_tests\n    # assign a user to a department and search for a DepartmentUser\n    # representing that relation\n    DBSession.commit()\n    with DBSession.no_autoflush:\n        data[\"test_department\"].users.append(data[\"test_user5\"])\n\n    dus = (\n        DepartmentUser.query.filter(DepartmentUser.user == data[\"test_user5\"])\n        .filter(DepartmentUser.department == data[\"test_department\"])\n        .all()\n    )\n\n    assert len(dus) > 0\n    du = dus[0]\n    assert isinstance(du, DepartmentUser)\n    assert du.department == data[\"test_department\"]\n    assert du.user == data[\"test_user5\"]\n    assert du.role is None\n\n\ndef test_tjp_id_is_working_as_expected(setup_department_db_tests):\n    \"\"\"tjp_is working as expected.\"\"\"\n    data = setup_department_db_tests\n    dep = data[\"test_department\"]\n    assert dep.tjp_id == f\"Department_{dep.id}\"\n\n\ndef test_to_tjp_is_working_as_expected(setup_department_db_tests):\n    \"\"\"to_tjp property is working as expected.\"\"\"\n    data = setup_department_db_tests\n    expected_tjp = \"\"\"resource Department_33 \"Department_33\" {\n    resource User_28 \"User_28\" {\n        efficiency 1.0\n    }\n    resource User_29 \"User_29\" {\n        efficiency 1.0\n    }\n    resource User_30 \"User_30\" {\n        efficiency 1.0\n    }\n    resource User_31 \"User_31\" {\n        efficiency 1.0\n    }\n}\"\"\"\n    assert data[\"test_department\"].to_tjp == expected_tjp\n"
  },
  {
    "path": "tests/models/test_department_user.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the DepartmentUser class.\"\"\"\n\nimport pytest\n\nfrom stalker import Department, DepartmentUser, User\n\n\ndef test_role_argument_is_not_a_role_instance():\n    \"\"\"TypeError will be raised when the role argument is not a Role instance.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        DepartmentUser(\n            department=Department(name=\"Test Department\"),\n            user=User(\n                name=\"Test User\", login=\"tuser\", email=\"u@u.com\", password=\"secret\"\n            ),\n            role=\"not a role instance\",\n        )\n\n    assert str(cm.value) == (\n        \"DepartmentUser.role should be a stalker.models.auth.Role instance, \"\n        \"not str: 'not a role instance'\"\n    )\n"
  },
  {
    "path": "tests/models/test_dependency_target.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"DependencyTarget related tests are here.\"\"\"\nfrom enum import Enum\nimport sys\n\nimport pytest\n\nfrom stalker.models.enum import DependencyTarget, DependencyTargetDecorator\n\n\n@pytest.mark.parametrize(\n    \"target\",\n    [\n        DependencyTarget.OnStart,\n        DependencyTarget.OnEnd,\n    ],\n)\ndef test_it_is_an_enum(target):\n    \"\"\"DependencyTarget is an Enum.\"\"\"\n    assert isinstance(target, Enum)\n\n\n@pytest.mark.parametrize(\n    \"target,expected_value\",\n    [\n        [DependencyTarget.OnStart, \"onstart\"],\n        [DependencyTarget.OnEnd, \"onend\"],\n    ],\n)\ndef test_enum_values(target, expected_value):\n    \"\"\"Test enum values.\"\"\"\n    assert target.value == expected_value\n\n\n@pytest.mark.parametrize(\n    \"target,expected_name\",\n    [\n        [DependencyTarget.OnStart, \"OnStart\"],\n        [DependencyTarget.OnEnd, \"OnEnd\"],\n    ],\n)\ndef test_enum_names(target, expected_name):\n    \"\"\"Test enum names.\"\"\"\n    assert target.name == expected_name\n\n\n@pytest.mark.parametrize(\n    \"target,expected_value\",\n    [\n        [DependencyTarget.OnStart, \"onstart\"],\n        [DependencyTarget.OnEnd, \"onend\"],\n    ],\n)\ndef test_enum_as_str(target, expected_value):\n    \"\"\"Test enum names.\"\"\"\n    assert str(target) == expected_value\n\n\ndef test_to_target_target_is_skipped():\n    \"\"\"DependencyTarget.to_target() target is skipped.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = DependencyTarget.to_target()\n\n    py_error_message = {\n        8: \"to_target() missing 1 required positional argument: 'target'\",\n        9: \"to_target() missing 1 required positional argument: 'target'\",\n        10: \"DependencyTarget.to_target() missing 1 required positional argument: 'target'\",\n        11: \"DependencyTarget.to_target() missing 1 required positional argument: 'target'\",\n        12: \"DependencyTarget.to_target() missing 1 required positional argument: 'target'\",\n        13: \"DependencyTarget.to_target() missing 1 required positional argument: 'target'\",\n    }[sys.version_info.minor]\n    assert str(cm.value) == py_error_message\n\n\ndef test_to_target_target_is_none():\n    \"\"\"DependencyTarget.to_target() target is None.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = DependencyTarget.to_target(None)\n    assert str(cm.value) == (\n        \"target should be a DependencyTarget enum value or one of ['OnStart', 'OnEnd', 'onstart', 'onend'], not NoneType: 'None'\"\n    )\n\n\ndef test_to_target_target_is_not_a_str():\n    \"\"\"DependencyTarget.to_target() target is not a str.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = DependencyTarget.to_target(12334.123)\n\n    assert str(cm.value) == (\n        \"target should be a DependencyTarget enum value or one of ['OnStart', 'OnEnd', 'onstart', 'onend'], not float: '12334.123'\"\n    )\n\n\ndef test_to_target_target_is_not_a_valid_str():\n    \"\"\"DependencyTarget.to_target() target is not a valid str.\"\"\"\n    with pytest.raises(ValueError) as cm:\n        _ = DependencyTarget.to_target(\"not a valid value\")\n\n    assert str(cm.value) == (\n        \"target should be a DependencyTarget enum value or one of ['OnStart', \"\n        \"'OnEnd', 'onstart', 'onend'], not 'not a valid value'\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"target_name,target\",\n    [\n        # OnStart\n        [\"OnStart\", DependencyTarget.OnStart],\n        [\"onstart\", DependencyTarget.OnStart],\n        [\"ONSTART\", DependencyTarget.OnStart],\n        [\"oNsTART\", DependencyTarget.OnStart],\n        # OnEnd\n        [\"OnEnd\", DependencyTarget.OnEnd],\n        [\"onend\", DependencyTarget.OnEnd],\n        [\"ONEND\", DependencyTarget.OnEnd],\n        [\"oNeNd\", DependencyTarget.OnEnd],\n        [\"OnEnD\", DependencyTarget.OnEnd],\n    ],\n)\ndef test_to_target_is_working_properly(target_name, target):\n    \"\"\"DependencyTarget can parse dependency target names.\"\"\"\n    assert DependencyTarget.to_target(target_name) == target\n\n\ndef test_cache_ok_is_true_in_type_decorator():\n    \"\"\"DependencyTargetDecorator.cache_ok is True.\"\"\"\n    assert DependencyTargetDecorator.cache_ok is True\n"
  },
  {
    "path": "tests/models/test_entity.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Entity class.\"\"\"\n\nimport copy\n\nimport pytest\n\nfrom stalker import Entity, Note, Tag, User\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_entity_tests():\n    \"\"\"Set up Entity class test data.\"\"\"\n    data = dict()\n    # create a user\n    data[\"test_user\"] = User(\n        name=\"Test User\", login=\"testuser\", email=\"test@user.com\", password=\"test\"\n    )\n\n    # create some test Tag objects, not necessarily needed but create them\n    data[\"test_tag1\"] = Tag(name=\"Test Tag 1\")\n    data[\"test_tag2\"] = Tag(name=\"Test Tag 1\")  # make it equal to tag1\n    data[\"test_tag3\"] = Tag(name=\"Test Tag 3\")\n\n    data[\"tags\"] = [data[\"test_tag1\"], data[\"test_tag2\"]]\n\n    # create a couple of test Note objects\n    data[\"test_note1\"] = Note(name=\"test note1\", content=\"test note1\")\n    data[\"test_note2\"] = Note(name=\"test note2\", content=\"test note2\")\n    data[\"test_note3\"] = Note(name=\"test note3\", content=\"test note3\")\n\n    data[\"notes\"] = [data[\"test_note1\"], data[\"test_note2\"]]\n\n    data[\"kwargs\"] = {\n        \"name\": \"Test Entity\",\n        \"description\": \"This is a test entity, and this is a proper \\\n        description for it\",\n        \"created_by\": data[\"test_user\"],\n        \"updated_by\": data[\"test_user\"],\n        \"tags\": data[\"tags\"],\n        \"notes\": data[\"notes\"],\n    }\n\n    # create a proper SimpleEntity to use it later in the tests\n    data[\"test_entity\"] = Entity(**data[\"kwargs\"])\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_true():\n    \"\"\"__auto_name__ class attribute is set to False for Entity class.\"\"\"\n    assert Entity.__auto_name__ is True\n\n\ndef test_notes_argument_being_omitted(setup_entity_tests):\n    \"\"\"no error raised if omitted the notes argument.\"\"\"\n    data = setup_entity_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"notes\")\n    new_entity = Entity(**kwargs)\n    assert isinstance(new_entity, Entity)\n\n\ndef test_notes_argument_is_set_to_none(setup_entity_tests):\n    \"\"\"notes attr is set to an empty list if the notes argument is set to None.\"\"\"\n    data = setup_entity_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"notes\"] = None\n    new_entity = Entity(**kwargs)\n    assert new_entity.notes == []\n\n\ndef test_notes_attribute_is_set_to_none(setup_entity_tests):\n    \"\"\"TypeError is raised if the notes attribute is set to None.\"\"\"\n    data = setup_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_entity\"].notes = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_notes_argument_set_to_something_other_than_a_list(setup_entity_tests):\n    \"\"\"TypeError is raised if setting the notes argument something other than a list.\"\"\"\n    data = setup_entity_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"notes\"] = [\"a string note\"]\n    with pytest.raises(TypeError) as cm:\n        Entity(**kwargs)\n\n    assert str(cm.value) == (\n        \"Entity.note should be a stalker.models.note.Note instance, \"\n        \"not str: 'a string note'\"\n    )\n\n\ndef test_notes_attribute_set_to_something_other_than_a_list(setup_entity_tests):\n    \"\"\"TypeError is raised if setting the notes argument something other than a list.\"\"\"\n    data = setup_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_entity\"].notes = [\"a string note\"]\n\n    assert str(cm.value) == (\n        \"Entity.note should be a stalker.models.note.Note instance, \"\n        \"not str: 'a string note'\"\n    )\n\n\ndef test_notes_argument_set_to_a_list_of_other_objects(setup_entity_tests):\n    \"\"\"TypeError is raised if notes argument is a list of non-Note objects.\"\"\"\n    data = setup_entity_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"notes\"] = [1, 12.2, \"this is a string\"]\n\n    with pytest.raises(TypeError) as cm:\n        Entity(**kwargs)\n\n    assert str(cm.value) == (\n        \"Entity.note should be a stalker.models.note.Note instance, not int: '1'\"\n    )\n\n\ndef test_notes_attribute_set_to_a_list_of_other_objects(setup_entity_tests):\n    \"\"\"TypeError is raised if notes attr set to a list of non Note objects.\"\"\"\n    data = setup_entity_tests\n    test_value = [1, 12.2, \"this is a string\"]\n    with pytest.raises(TypeError) as cm:\n        data[\"test_entity\"].notes = test_value\n\n    assert str(cm.value) == (\n        \"Entity.note should be a stalker.models.note.Note instance, not int: '1'\"\n    )\n\n\ndef test_notes_attribute_works_as_expected(setup_entity_tests):\n    \"\"\"notes attribute works as expected,\"\"\"\n    data = setup_entity_tests\n    test_value = [data[\"test_note3\"]]\n    data[\"test_entity\"].notes = test_value\n    assert data[\"test_entity\"].notes == test_value\n\n\ndef test_notes_attribute_element_is_set_to_non_note_object(setup_entity_tests):\n    \"\"\"TypeError is raised if non-Note instance assigned to the notes list.\"\"\"\n    data = setup_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_entity\"].notes[0] = 0\n\n    assert str(cm.value) == (\n        \"Entity.note should be a stalker.models.note.Note instance, not int: '0'\"\n    )\n\n\ndef test_tags_argument_being_omitted(setup_entity_tests):\n    \"\"\"no error is raised if creating an entity without setting the tags argument.\"\"\"\n    data = setup_entity_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"tags\")\n    # this should work without errors\n    new_entity = Entity(**kwargs)\n    assert isinstance(new_entity, Entity)\n\n\ndef test_tags_argument_being_initialized_as_an_empty_list(setup_entity_tests):\n    \"\"\"nothing happens if tags argument an empty list.\"\"\"\n    data = setup_entity_tests\n    # this should work without errors\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"tags\")\n    new_entity = Entity(**kwargs)\n    expected_result = []\n    assert new_entity.tags == expected_result\n\n\ndef test_tags_argument_set_to_something_other_than_a_list(setup_entity_tests):\n    \"\"\"TypeError is raised if tags arg is not a list.\"\"\"\n    data = setup_entity_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"tags\"] = [\"a tag\", 1243, 12.12]\n    with pytest.raises(TypeError) as cm:\n        Entity(**kwargs)\n\n    assert str(cm.value) == (\n        \"Entity.tag should be a stalker.models.tag.Tag instance, not str: 'a tag'\"\n    )\n\n\ndef test_tags_attribute_works_as_expected(setup_entity_tests):\n    \"\"\"tags attribute works as expected.\"\"\"\n    data = setup_entity_tests\n    test_value = [data[\"test_tag1\"]]\n    data[\"test_entity\"].tags = test_value\n    assert data[\"test_entity\"].tags == test_value\n\n\ndef test_tags_attribute_element_is_set_to_non_tag_object(setup_entity_tests):\n    \"\"\"TypeError is raised if assign something to tags list that is not a Tag.\"\"\"\n    data = setup_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_entity\"].tags[0] = 0\n\n    assert str(cm.value) == (\n        \"Entity.tag should be a stalker.models.tag.Tag instance, not int: '0'\"\n    )\n\n\ndef test_tags_attribute_set_to_none(setup_entity_tests):\n    \"\"\"TypeError is raised if the tags attribute is set to None.\"\"\"\n    data = setup_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_entity\"].tags = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_equality(setup_entity_tests):\n    \"\"\"equality of two entities.\"\"\"\n    data = setup_entity_tests\n    # create two entities with same parameters and check for equality\n    kwargs = copy.copy(data[\"kwargs\"])\n\n    entity1 = Entity(**kwargs)\n    entity2 = Entity(**kwargs)\n\n    kwargs[\"name\"] = \"another entity\"\n    kwargs[\"tags\"] = [data[\"test_tag3\"]]\n    kwargs[\"notes\"] = []\n    entity3 = Entity(**kwargs)\n\n    assert entity1 == entity2\n    assert not entity1 == entity3\n\n\ndef test_inequality(setup_entity_tests):\n    \"\"\"inequality of two entities.\"\"\"\n    data = setup_entity_tests\n    # change the tags and test it again, expect False\n    kwargs = copy.copy(data[\"kwargs\"])\n    entity1 = Entity(**kwargs)\n    entity2 = Entity(**kwargs)\n\n    kwargs[\"name\"] = \"another entity\"\n    kwargs[\"tags\"] = [data[\"test_tag3\"]]\n    kwargs[\"notes\"] = []\n    entity3 = Entity(**kwargs)\n\n    assert not entity1 != entity2\n    assert entity1 != entity3\n"
  },
  {
    "path": "tests/models/test_entity_group.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the EntityGroup class.\"\"\"\n\nimport pytest\n\nfrom stalker import (\n    Asset,\n    EntityGroup,\n    Project,\n    Repository,\n    Status,\n    StatusList,\n    Task,\n    Type,\n    User,\n)\nfrom stalker.models.enum import TimeUnit\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_entity_group_tests():\n    \"\"\"Set up tests for the EntityGroup class.\"\"\"\n    data = dict()\n\n    # create a couple of task\n    data[\"status_new\"] = Status(name=\"Mew\", code=\"NEW\")\n    data[\"status_wfd\"] = Status(name=\"Waiting For Dependency\", code=\"WFD\")\n    data[\"status_rts\"] = Status(name=\"Ready To Start\", code=\"RTS\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_prev\"] = Status(name=\"Pending Review\", code=\"PREV\")\n    data[\"status_hrev\"] = Status(name=\"Has Revision\", code=\"HREV\")\n    data[\"status_drev\"] = Status(name=\"Dependency Has Revision\", code=\"DREV\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n\n    data[\"test_user1\"] = User(\n        name=\"User1\",\n        login=\"user1\",\n        email=\"user1@user.com\",\n        password=\"1234\",\n    )\n\n    data[\"test_user2\"] = User(\n        name=\"User2\",\n        login=\"user2\",\n        email=\"user2@user.com\",\n        password=\"1234\",\n    )\n\n    data[\"test_user3\"] = User(\n        name=\"User3\",\n        login=\"user3\",\n        email=\"user3@user.com\",\n        password=\"1234\",\n    )\n\n    data[\"project_status_list\"] = StatusList(\n        name=\"Project Status List\",\n        statuses=[data[\"status_new\"], data[\"status_wip\"], data[\"status_cmpl\"]],\n        target_entity_type=\"Project\",\n    )\n\n    data[\"repo\"] = Repository(\n        name=\"Test Repo\",\n        code=\"TR\",\n        linux_path=\"/mnt/M/JOBs\",\n        windows_path=\"M:/JOBs\",\n        macos_path=\"/Users/Shared/Servers/M\",\n    )\n\n    data[\"project1\"] = Project(\n        name=\"Tests Project\",\n        code=\"tp\",\n        status_list=data[\"project_status_list\"],\n        repository=data[\"repo\"],\n    )\n\n    data[\"char_asset_type\"] = Type(\n        name=\"Character Asset\", code=\"char\", target_entity_type=\"Asset\"\n    )\n\n    data[\"task_status_list\"] = StatusList(\n        name=\"Task Statuses\",\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n\n    data[\"asset_status_list\"] = StatusList(\n        name=\"Asset Statuses\",\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Asset\",\n    )\n\n    data[\"asset1\"] = Asset(\n        name=\"Char1\",\n        code=\"char1\",\n        type=data[\"char_asset_type\"],\n        project=data[\"project1\"],\n        responsible=[data[\"test_user1\"]],\n        status_list=data[\"asset_status_list\"],\n    )\n\n    data[\"task1\"] = Task(\n        name=\"Test Task\",\n        watchers=[data[\"test_user3\"]],\n        parent=data[\"asset1\"],\n        schedule_timing=5,\n        schedule_unit=TimeUnit.Hour,\n        bid_timing=52,\n        bid_unit=TimeUnit.Hour,\n        status_list=data[\"task_status_list\"],\n    )\n\n    data[\"child_task1\"] = Task(\n        name=\"Child Task 1\",\n        resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        parent=data[\"task1\"],\n        status_list=data[\"task_status_list\"],\n    )\n\n    data[\"child_task2\"] = Task(\n        name=\"Child Task 2\",\n        resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        parent=data[\"task1\"],\n        status_list=data[\"task_status_list\"],\n    )\n\n    data[\"task2\"] = Task(\n        name=\"Another Task\",\n        project=data[\"project1\"],\n        resources=[data[\"test_user1\"]],\n        responsible=[data[\"test_user2\"]],\n        status_list=data[\"task_status_list\"],\n    )\n\n    data[\"entity_group1\"] = EntityGroup(\n        name=\"My Tasks\", entities=[data[\"task1\"], data[\"child_task2\"], data[\"task2\"]]\n    )\n    return data\n\n\ndef test_entities_argument_is_skipped():\n    \"\"\"entities attribute is an empty list if the entities argument is skipped.\"\"\"\n    eg = EntityGroup()\n    assert eg.entities == []\n\n\ndef test_entities_argument_is_none():\n    \"\"\"entities attribute is an empty list if the entities argument is None.\"\"\"\n    eg = EntityGroup(entities=None)\n    assert eg.entities == []\n\n\ndef test_entities_argument_is_not_a_list():\n    \"\"\"TypeError is raised if the entities argument is not a list.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        EntityGroup(entities=\"not a list of SimpleEntities\")\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_entities_argument_is_not_a_list_of_simple_entity_instances():\n    \"\"\"TypeError is raised if the entities argument is not a list of SimpleEntities.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        EntityGroup(entities=[\"not\", 1, \"list\", \"of\", \"SimpleEntities\"])\n\n    assert str(cm.value) == (\n        \"EntityGroup.entities should be a list of SimpleEntities, not str: 'not'\"\n    )\n\n\ndef test_entities_argument_is_working_as_expected(setup_entity_group_tests):\n    \"\"\"entities argument value is correctly passed to the entities attribute.\"\"\"\n    data = setup_entity_group_tests\n    test_value = [data[\"project1\"], data[\"asset1\"], data[\"status_cmpl\"]]\n    eg = EntityGroup(entities=test_value)\n    assert eg.entities == test_value\n\n\ndef test__eq__is_working_as_expected_with_same_data(setup_entity_group_tests):\n    \"\"\"__eq__ is working as expected with same data.\"\"\"\n    data = setup_entity_group_tests\n    eg2 = EntityGroup(\n        name=\"My Tasks\", entities=[data[\"task1\"], data[\"child_task2\"], data[\"task2\"]]\n    )\n    assert (data[\"entity_group1\"] == eg2) is True\n\n\ndef test__eq__is_working_as_expected_with_different_data(setup_entity_group_tests):\n    \"\"\"__eq__ is working as expected with same data.\"\"\"\n    data = setup_entity_group_tests\n    eg2 = EntityGroup(name=\"My Tasks\", entities=[data[\"task1\"], data[\"child_task2\"]])\n    assert data[\"entity_group1\"].entities == [\n        data[\"task1\"],\n        data[\"child_task2\"],\n        data[\"task2\"],\n    ]\n    assert eg2.entities == [data[\"task1\"], data[\"child_task2\"]]\n    assert (data[\"entity_group1\"] == eg2) is False\n\n\ndef test__hash__is_working_as_expected(setup_entity_group_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_entity_group_tests\n    result = hash(data[\"entity_group1\"])\n    assert isinstance(result, int)\n    assert result == data[\"entity_group1\"].__hash__()\n"
  },
  {
    "path": "tests/models/test_file.py",
    "content": "# -*- coding: utf-8 -*-\n\nimport os\nimport sys\n\nimport pytest\n\nfrom stalker import File, Repository, Type\nfrom stalker.db.session import DBSession\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_file_tests():\n    \"\"\"Set up the test for the File class.\"\"\"\n    data = dict()\n\n    # create a Type object for Files\n    data[\"test_file_type1\"] = Type(\n        name=\"Test Type 1\",\n        code=\"test type1\",\n        target_entity_type=\"File\",\n    )\n    data[\"test_file_type2\"] = Type(\n        name=\"Test Type 2\",\n        code=\"test type2\",\n        target_entity_type=\"File\",\n    )\n\n    image_sequence_type = Type(\n        name=\"Image Sequence\",\n        code=\"ImSeq\",\n        target_entity_type=\"File\",\n    )\n\n    # a File for the input file\n    data[\"test_input_file1\"] = File(\n        name=\"Input File 1\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"Outputs/SH001_beauty_v001.###.exr\",\n        type=image_sequence_type,\n    )\n\n    data[\"test_input_file2\"] = File(\n        name=\"Input File 2\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"Outputs/SH001_occ_v001.###.exr\",\n        type=image_sequence_type,\n    )\n\n    data[\"kwargs\"] = {\n        \"name\": \"An Image File\",\n        \"full_path\": \"C:/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.exr\",\n        \"references\": [data[\"test_input_file1\"], data[\"test_input_file2\"]],\n        \"original_filename\": \"this_is_an_image.jpg\",\n        \"type\": data[\"test_file_type1\"],\n        \"created_with\": \"Houdini\",\n    }\n\n    data[\"test_file\"] = File(**data[\"kwargs\"])\n    yield data\n\n\ndef test___auto_name__class_attribute_is_set_to_true():\n    \"\"\"__auto_name__ class attribute is set to False for File class.\"\"\"\n    assert File.__auto_name__ is True\n\n\ndef test_full_path_argument_is_none(setup_file_tests):\n    \"\"\"full_path argument is None is set the full_path attribute to an empty string.\"\"\"\n    data = setup_file_tests\n    data[\"kwargs\"][\"full_path\"] = None\n    new_file = File(**data[\"kwargs\"])\n    assert new_file.full_path == \"\"\n\n\ndef test_full_path_attribute_is_set_to_none(setup_file_tests):\n    \"\"\"full_path attr to None is set its value to an empty string.\"\"\"\n    data = setup_file_tests\n    data[\"test_file\"].full_path = \"\"\n\n\ndef test_full_path_argument_is_empty_an_empty_string(setup_file_tests):\n    \"\"\"full_path attr is set to an empty str if full_path arg is an empty string.\"\"\"\n    data = setup_file_tests\n    data[\"kwargs\"][\"full_path\"] = \"\"\n    new_file = File(**data[\"kwargs\"])\n    assert new_file.full_path == \"\"\n\n\ndef test_full_path_attribute_is_set_to_empty_string(setup_file_tests):\n    \"\"\"full_path attr value is set to an empty string.\"\"\"\n    data = setup_file_tests\n    data[\"test_file\"].full_path = \"\"\n    assert data[\"test_file\"].full_path == \"\"\n\n\ndef test_full_path_argument_is_not_a_string(setup_file_tests):\n    \"\"\"TypeError is raised if the full_path argument is not a string.\"\"\"\n    data = setup_file_tests\n    test_value = 1\n    data[\"kwargs\"][\"full_path\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        File(**data[\"kwargs\"])\n    assert str(cm.value) == (\"File.full_path should be a str, not int: '1'\")\n\n\ndef test_full_path_attribute_is_not_a_string(setup_file_tests):\n    \"\"\"TypeError is raised if the full_path attribute is not a string instance.\"\"\"\n    data = setup_file_tests\n    test_value = 1\n    with pytest.raises(TypeError) as cm:\n        data[\"test_file\"].full_path = test_value\n\n    assert str(cm.value) == (\"File.full_path should be a str, not int: '1'\")\n\n\ndef test_full_path_windows_to_other_conversion(setup_file_tests):\n    \"\"\"full_path is stored in internal format.\"\"\"\n    data = setup_file_tests\n    windows_path = \"M:\\\\path\\\\to\\\\object\"\n    expected_result = \"M:/path/to/object\"\n    data[\"test_file\"].full_path = windows_path\n    assert data[\"test_file\"].full_path == expected_result\n\n\ndef test_original_filename_argument_is_none(setup_file_tests):\n    \"\"\"original_filename arg is None will set the attr to filename of the full_path.\"\"\"\n    data = setup_file_tests\n    data[\"kwargs\"][\"original_filename\"] = None\n    new_file = File(**data[\"kwargs\"])\n    filename = os.path.basename(new_file.full_path)\n    assert new_file.original_filename == filename\n\n\ndef test_original_filename_attribute_is_set_to_none(setup_file_tests):\n    \"\"\"original_filename is equal to the filename of the full_path if it is None.\"\"\"\n    data = setup_file_tests\n    data[\"test_file\"].original_filename = None\n    filename = os.path.basename(data[\"test_file\"].full_path)\n    assert data[\"test_file\"].original_filename == filename\n\n\ndef test_original_filename_argument_is_empty_string(setup_file_tests):\n    \"\"\"original_filename arg is empty str, attr is set to filename of the full_path.\"\"\"\n    data = setup_file_tests\n    data[\"kwargs\"][\"original_filename\"] = \"\"\n    new_file = File(**data[\"kwargs\"])\n    filename = os.path.basename(new_file.full_path)\n    assert new_file.original_filename == filename\n\n\ndef test_original_filename_attribute_set_to_empty_string(setup_file_tests):\n    \"\"\"original_filename attr is empty str, attr is set to filename of the full_path.\"\"\"\n    data = setup_file_tests\n    data[\"test_file\"].original_filename = \"\"\n    filename = os.path.basename(data[\"test_file\"].full_path)\n    assert data[\"test_file\"].original_filename == filename\n\n\ndef test_original_filename_argument_accepts_string_only(setup_file_tests):\n    \"\"\"original_filename arg accepts str only and raises TypeError for other types.\"\"\"\n    data = setup_file_tests\n    test_value = 1\n    data[\"kwargs\"][\"original_filename\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        File(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\"File.original_filename should be a str, not int: '1'\")\n\n\ndef test_original_filename_attribute_accepts_string_only(setup_file_tests):\n    \"\"\"original_filename attr accepts str only and raises TypeError for other types.\"\"\"\n    data = setup_file_tests\n    test_value = 1.1\n    with pytest.raises(TypeError) as cm:\n        data[\"test_file\"].original_filename = test_value\n\n    assert str(cm.value) == (\"File.original_filename should be a str, not float: '1.1'\")\n\n\ndef test_original_filename_argument_is_working_as_expected(setup_file_tests):\n    \"\"\"original_filename argument is working as expected.\"\"\"\n    data = setup_file_tests\n    assert data[\"kwargs\"][\"original_filename\"] == data[\"test_file\"].original_filename\n\n\ndef test_original_filename_attribute_is_working_as_expected(setup_file_tests):\n    \"\"\"original_filename attribute is working as expected.\"\"\"\n    data = setup_file_tests\n    new_value = \"this_is_the_original_filename.jpg\"\n    assert data[\"test_file\"].original_filename != new_value\n    data[\"test_file\"].original_filename = new_value\n    assert data[\"test_file\"].original_filename == new_value\n\n\ndef test_equality_of_two_files(setup_file_tests):\n    \"\"\"Equality operator.\"\"\"\n    data = setup_file_tests\n    # with same parameters\n    mock_file1 = File(**data[\"kwargs\"])\n    assert data[\"test_file\"] == mock_file1\n\n    # with different parameters\n    data[\"kwargs\"][\"type\"] = data[\"test_file_type2\"]\n    mock_file2 = File(**data[\"kwargs\"])\n\n    assert not data[\"test_file\"] == mock_file2\n\n\ndef test_inequality_of_two_files(setup_file_tests):\n    \"\"\"Inequality operator.\"\"\"\n    data = setup_file_tests\n    # with same parameters\n    mock_file1 = File(**data[\"kwargs\"])\n    assert data[\"test_file\"] == mock_file1\n\n    # with different parameters\n    data[\"kwargs\"][\"type\"] = data[\"test_file_type2\"]\n    mock_file2 = File(**data[\"kwargs\"])\n\n    assert not data[\"test_file\"] != mock_file1\n    assert data[\"test_file\"] != mock_file2\n\n\ndef test_plural_class_name(setup_file_tests):\n    \"\"\"Plural name of File class.\"\"\"\n    data = setup_file_tests\n    assert data[\"test_file\"].plural_class_name == \"Files\"\n\n\ndef test_path_attribute_is_set_to_none(setup_file_tests):\n    \"\"\"TypeError is raised if the path attribute is set to None.\"\"\"\n    data = setup_file_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_file\"].path = None\n    assert str(cm.value) == \"File.path cannot be set to None\"\n\n\ndef test_path_attribute_is_set_to_empty_string(setup_file_tests):\n    \"\"\"ValueError is raised if the path attribute is set to an empty string.\"\"\"\n    data = setup_file_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_file\"].path = \"\"\n    assert str(cm.value) == \"File.path cannot be an empty string\"\n\n\ndef test_path_attribute_is_set_to_a_value_other_then_string(setup_file_tests):\n    \"\"\"TypeError is raised if the path attribute is set to a value other than string.\"\"\"\n    data = setup_file_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_file\"].path = 1\n    assert str(cm.value) == \"File.path should be a str, not int: '1'\"\n\n\ndef test_path_attribute_value_comes_from_full_path(setup_file_tests):\n    \"\"\"path attribute value is calculated from the full_path attribute.\"\"\"\n    data = setup_file_tests\n    assert data[\"test_file\"].path == \"C:/A_NEW_PROJECT/td/dsdf\"\n\n\ndef test_path_attribute_updates_the_full_path_attribute(setup_file_tests):\n    \"\"\"path attribute is updating the full_path attribute.\"\"\"\n    data = setup_file_tests\n    test_value = \"/mnt/some/new/path\"\n    expected_full_path = \"/mnt/some/new/path/\" \"22-fdfffsd-32342-dsf2332-dsfd-3.exr\"\n\n    assert data[\"test_file\"].path != test_value\n    data[\"test_file\"].path = test_value\n    assert data[\"test_file\"].path == test_value\n    assert data[\"test_file\"].full_path == expected_full_path\n\n\ndef test_filename_attribute_is_set_to_none(setup_file_tests):\n    \"\"\"filename attribute is an empty string if it is set a None.\"\"\"\n    data = setup_file_tests\n    data[\"test_file\"].filename = None\n    assert data[\"test_file\"].filename == \"\"\n\n\ndef test_filename_attribute_is_set_to_a_value_other_then_string(setup_file_tests):\n    \"\"\"TypeError is raised if the filename attr is set to a value other than string.\"\"\"\n    data = setup_file_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_file\"].filename = 3\n    assert str(cm.value) == \"File.filename should be a str, not int: '3'\"\n\n\ndef test_filename_attribute_is_set_to_empty_string(setup_file_tests):\n    \"\"\"filename value can be set to an empty string.\"\"\"\n    data = setup_file_tests\n    data[\"test_file\"].filename = \"\"\n    assert data[\"test_file\"].filename == \"\"\n\n\ndef test_filename_attribute_value_comes_from_full_path(setup_file_tests):\n    \"\"\"filename attribute value is calculated from the full_path attribute.\"\"\"\n    data = setup_file_tests\n    assert data[\"test_file\"].filename == \"22-fdfffsd-32342-dsf2332-dsfd-3.exr\"\n\n\ndef test_filename_attribute_updates_the_full_path_attribute(setup_file_tests):\n    \"\"\"filename attribute is updating the full_path attribute.\"\"\"\n    data = setup_file_tests\n    test_value = \"new_filename.tif\"\n    assert data[\"test_file\"].filename != test_value\n    data[\"test_file\"].filename = test_value\n    assert data[\"test_file\"].filename == test_value\n    assert data[\"test_file\"].full_path == \"C:/A_NEW_PROJECT/td/dsdf/new_filename.tif\"\n\n\ndef test_filename_attribute_changes_also_the_extension(setup_file_tests):\n    \"\"\"filename attribute also changes the extension attribute.\"\"\"\n    data = setup_file_tests\n    assert data[\"test_file\"].extension != \".an_extension\"\n    data[\"test_file\"].filename = \"some_filename_and.an_extension\"\n    assert data[\"test_file\"].extension == \".an_extension\"\n\n\ndef test_extension_attribute_is_set_to_none(setup_file_tests):\n    \"\"\"extension is an empty string if it is set to None.\"\"\"\n    data = setup_file_tests\n    data[\"test_file\"].extension = None\n    assert data[\"test_file\"].extension == \"\"\n\n\ndef test_extension_attribute_is_set_to_empty_string(setup_file_tests):\n    \"\"\"extension attr can be set to an empty string.\"\"\"\n    data = setup_file_tests\n    data[\"test_file\"].extension = \"\"\n    assert data[\"test_file\"].extension == \"\"\n\n\ndef test_extension_attribute_is_set_to_a_value_other_then_string(setup_file_tests):\n    \"\"\"TypeError is raised if the extension attr is not str.\"\"\"\n    data = setup_file_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_file\"].extension = 123\n    assert str(cm.value) == (\"File.extension should be a str, not int: '123'\")\n\n\ndef test_extension_attribute_value_comes_from_full_path(setup_file_tests):\n    \"\"\"extension attribute value is calculated from the full_path attribute.\"\"\"\n    data = setup_file_tests\n    assert data[\"test_file\"].extension == \".exr\"\n\n\ndef test_extension_attribute_updates_the_full_path_attribute(setup_file_tests):\n    \"\"\"extension attribute is updating the full_path attribute.\"\"\"\n    data = setup_file_tests\n    test_value = \".iff\"\n    assert data[\"test_file\"].extension != test_value\n    data[\"test_file\"].extension = test_value\n    assert data[\"test_file\"].extension == test_value\n    assert (\n        data[\"test_file\"].full_path\n        == \"C:/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.iff\"\n    )\n\n\ndef test_extension_attribute_updates_the_full_path_attribute_with_the_dot(\n    setup_file_tests,\n):\n    \"\"\"full_path attr updated with the extension that doesn't have a dot.\"\"\"\n    data = setup_file_tests\n    test_value = \"iff\"\n    expected_value = \".iff\"\n    assert data[\"test_file\"].extension != test_value\n    data[\"test_file\"].extension = test_value\n    assert data[\"test_file\"].extension == expected_value\n    assert (\n        data[\"test_file\"].full_path\n        == \"C:/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.iff\"\n    )\n\n\ndef test_extension_attribute_is_also_change_the_filename_attribute(setup_file_tests):\n    \"\"\"extension attribute is updating the filename attribute.\"\"\"\n    data = setup_file_tests\n    test_value = \".targa\"\n    expected_value = \"22-fdfffsd-32342-dsf2332-dsfd-3.targa\"\n    assert data[\"test_file\"].filename != expected_value\n    data[\"test_file\"].extension = test_value\n    assert data[\"test_file\"].filename == expected_value\n\n\ndef test_format_path_converts_bytes_to_strings(setup_file_tests):\n    \"\"\"_format_path() converts bytes to strings.\"\"\"\n    data = setup_file_tests\n    test_value = b\"C:/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.iff\"\n    result = data[\"test_file\"]._format_path(test_value)\n    assert isinstance(result, str)\n    assert result == test_value.decode(\"utf-8\")\n\n\ndef test__hash__is_working_as_expected(setup_file_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_file_tests\n    result = hash(data[\"test_file\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_file\"].__hash__()\n\n\ndef test_references_arg_is_skipped(setup_file_tests):\n    \"\"\"references attr is an empty list if the references argument is skipped.\"\"\"\n    data = setup_file_tests\n    data[\"kwargs\"].pop(\"references\")\n    new_version = File(**data[\"kwargs\"])\n    assert new_version.references == []\n\n\ndef test_references_arg_is_none(setup_file_tests):\n    \"\"\"references attr is an empty list if the references argument is None.\"\"\"\n    data = setup_file_tests\n    data[\"kwargs\"][\"references\"] = None\n    new_file = File(**data[\"kwargs\"])\n    assert new_file.references == []\n\n\ndef test_references_attr_is_none(setup_file_tests):\n    \"\"\"TypeError raised if the references attr is set to None.\"\"\"\n    data = setup_file_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_file\"].references = None\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_references_arg_is_not_a_list_of_file_instances(setup_file_tests):\n    \"\"\"TypeError raised if the references arg is not a File instance.\"\"\"\n    data = setup_file_tests\n    test_value = [132, \"231123\"]\n    data[\"kwargs\"][\"references\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        File(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"File.references should only contain instances of \"\n        \"stalker.models.file.File, not int: '132'\"\n    )\n\n\ndef test_references_attr_is_not_a_list_of_file_instances(setup_file_tests):\n    \"\"\"TypeError raised if the references attr is set to something other than a File.\"\"\"\n    data = setup_file_tests\n    test_value = [132, \"231123\"]\n    with pytest.raises(TypeError) as cm:\n        data[\"test_file\"].references = test_value\n\n    assert str(cm.value) == (\n        \"File.references should only contain instances of \"\n        \"stalker.models.file.File, not int: '132'\"\n    )\n\n\ndef test_references_attr_is_working_as_expected(setup_file_tests):\n    \"\"\"references attr is working as expected.\"\"\"\n    data = setup_file_tests\n    data[\"kwargs\"].pop(\"references\")\n    new_file = File(**data[\"kwargs\"])\n    assert data[\"test_input_file1\"] not in new_file.references\n    assert data[\"test_input_file2\"] not in new_file.references\n\n    new_file.references = [data[\"test_input_file1\"], data[\"test_input_file2\"]]\n    assert data[\"test_input_file1\"] in new_file.references\n    assert data[\"test_input_file2\"] in new_file.references\n\n\ndef test_created_with_argument_can_be_skipped(setup_file_tests):\n    \"\"\"created_with argument can be skipped.\"\"\"\n    data = setup_file_tests\n    data[\"kwargs\"].pop(\"created_with\")\n    File(**data[\"kwargs\"])\n\n\ndef test_created_with_argument_can_be_none(setup_file_tests):\n    \"\"\"created_with argument can be None.\"\"\"\n    data = setup_file_tests\n    data[\"kwargs\"][\"created_with\"] = None\n    File(**data[\"kwargs\"])\n\n\ndef test_created_with_attribute_can_be_set_to_none(setup_file_tests):\n    \"\"\"created with attribute can be set to None.\"\"\"\n    data = setup_file_tests\n    data[\"test_file\"].created_with = None\n\n\ndef test_created_with_argument_accepts_only_string_or_none(setup_file_tests):\n    \"\"\"TypeError raised if the created_with arg is not a string or None.\"\"\"\n    data = setup_file_tests\n    data[\"kwargs\"][\"created_with\"] = 234\n    with pytest.raises(TypeError) as cm:\n        File(**data[\"kwargs\"])\n    assert str(cm.value) == (\n        \"File.created_with should be an instance of str, not int: '234'\"\n    )\n\n\ndef test_created_with_attribute_accepts_only_string_or_none(setup_file_tests):\n    \"\"\"TypeError raised if the created_with attr is not a str or None.\"\"\"\n    data = setup_file_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_file\"].created_with = 234\n\n    assert str(cm.value) == (\n        \"File.created_with should be an instance of str, not int: '234'\"\n    )\n\n\ndef test_created_with_argument_is_working_as_expected(setup_file_tests):\n    \"\"\"created_with argument value is passed to created_with attribute.\"\"\"\n    data = setup_file_tests\n    test_value = \"Maya\"\n    data[\"kwargs\"][\"created_with\"] = test_value\n    test_file = File(**data[\"kwargs\"])\n    assert test_file.created_with == test_value\n\n\ndef test_created_with_attribute_is_working_as_expected(setup_file_tests):\n    \"\"\"created_with attribute is working as expected.\"\"\"\n    data = setup_file_tests\n    test_value = \"Maya\"\n    assert data[\"test_file\"].created_with != test_value\n    data[\"test_file\"].created_with = test_value\n    assert data[\"test_file\"].created_with == test_value\n\n\ndef test_walk_references_is_working_as_expected_in_dfs_mode(setup_file_tests):\n    \"\"\"walk_references() method is working in DFS mode correctly.\"\"\"\n    # data = setup_file_tests\n\n    repr_type = Type(name=\"Representation\", code=\"Repr\", target_entity_type=\"File\")\n    # DBSession.add(repr_type)\n    # DBSession.commit()\n\n    # File 1\n    # v1 = Version(task=data[\"test_task1\"])\n    v1_base_repr = File(\n        name=\"Base Repr.\",\n        # full_path=str(v1.generate_path().with_suffix(\".ma\")),\n        type=repr_type,\n    )\n    # v1.files.append(v1_base_repr)\n\n    # Version 2\n    # v2 = Version(task=data[\"test_task1\"])\n    v2_base_repr = File(\n        name=\"Base Repr.\",\n        # full_path=str(v2.generate_path().with_suffix(\".ma\")),\n        type=repr_type,\n    )\n    # v2.files.append(v2_base_repr)\n\n    # Version 3\n    # v3 = Version(task=data[\"test_task1\"])\n    v3_base_repr = File(\n        name=\"Base Repr.\",\n        # full_path=str(v3.generate_path().with_suffix(\".ma\")),\n        type=repr_type,\n    )\n    # v3.files.append(v3_base_repr)\n\n    # v4 = Version(task=data[\"test_task1\"])\n    v4_base_repr = File(\n        name=\"Base Repr.\",\n        # full_path=str(v4.generate_path().with_suffix(\".ma\")),\n        type=repr_type,\n    )\n    # v4.files.append(v4_base_repr)\n\n    # v5 = Version(task=data[\"test_task1\"])\n    v5_base_repr = File(\n        name=\"Base Repr.\",\n        # full_path=str(v5.generate_path().with_suffix(\".ma\")),\n        type=repr_type,\n    )\n    # v5.files.append(v5_base_repr)\n\n    v5_base_repr.references = [v4_base_repr]\n    v4_base_repr.references = [v3_base_repr, v2_base_repr]\n    v3_base_repr.references = [v1_base_repr]\n    v2_base_repr.references = [v1_base_repr]\n\n    expected_result = [\n        v5_base_repr,\n        v4_base_repr,\n        v3_base_repr,\n        v1_base_repr,\n        v2_base_repr,\n        v1_base_repr,\n    ]\n    visited_versions = []\n    for v in v5_base_repr.walk_references():\n        visited_versions.append(v)\n\n    assert expected_result == visited_versions\n\n\ndef test_absolute_path_is_read_only(setup_file_tests):\n    \"\"\"absolute_path property is read-only.\"\"\"\n    data = setup_file_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_file\"].absolute_path = \"C:/A_NEW_PROJECT/td/dsdf\"\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'absolute_path'\",\n        11: \"property 'absolute_path' of 'File' object has no setter\",\n    }.get(sys.version_info.minor, \"property 'absolute_path' of 'File' object has no setter\")\n    assert str(cm.value) == error_message\n\n\ndef test_absolute_path_returns_the_absolute_path(setup_file_tests):\n    \"\"\"absolute_path property returns the absolute path of the full_path attribute.\"\"\"\n    data = setup_file_tests\n    file = data[\"test_file\"]\n    os.environ[\"REPOPR1\"] = \"/mnt/project_server/Projects\"\n    file.full_path = \"$REPOPR1/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.exr\"\n    expected_result = \"/mnt/project_server/Projects/A_NEW_PROJECT/td/dsdf\"\n    assert data[\"test_file\"].absolute_path == expected_result\n\n\ndef test_absolute_full_path_is_read_only(setup_file_tests):\n    \"\"\"absolute_full_path property is read-only.\"\"\"\n    data = setup_file_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_file\"].absolute_full_path = \"C:/A_NEW_PROJECT/td/dsdf\"\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'absolute_full_path'\",\n        11: \"property 'absolute_full_path' of 'File' object has no setter\",\n    }.get(sys.version_info.minor, \"property 'absolute_full_path' of 'File' object has no setter\")\n    assert str(cm.value) == error_message\n\n\ndef test_absolute_full_path_returns_the_absolute_full_path(setup_file_tests):\n    \"\"\"absolute_full_path property returns the absolute path of the full_path attribute.\"\"\"\n    data = setup_file_tests\n    os.environ[\"REPOPR1\"] = \"/mnt/project_server/Projects\"\n    file = data[\"test_file\"]\n    file.full_path = \"$REPOPR1/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.exr\"\n    expected_result = \"/mnt/project_server/Projects/A_NEW_PROJECT/td/dsdf/22-fdfffsd-32342-dsf2332-dsfd-3.exr\"\n    assert data[\"test_file\"].absolute_full_path == expected_result\n"
  },
  {
    "path": "tests/models/test_filename_template.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the stalker.models.template.FilenameTemplate class.\"\"\"\n\nimport sys\nimport pytest\n\nfrom stalker import (\n    Entity,\n    FilenameTemplate,\n    Project,\n    Sequence,\n    Shot,\n    Structure,\n    Task,\n    Type,\n    Version,\n)\nfrom stalker.db.session import DBSession\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_filename_template_tests():\n    \"\"\"Set up tests for the FilenameTemplate class.\"\"\"\n    data = dict()\n    data[\"kwargs\"] = {\n        \"name\": \"Test FilenameTemplate\",\n        \"type\": Type(\n            name=\"Test Type\", code=\"tt\", target_entity_type=\"FilenameTemplate\"\n        ),\n        \"path\": \"ASSETS/{{asset.code}}/{{task.type.code}}/\",\n        \"filename\": \"{{asset.code}}_{{task.type.code}}_\"\n        \"{{version.version}}_{{user.initials}}\",\n        \"output_path\": \"\",\n        \"target_entity_type\": \"Asset\",\n    }\n    data[\"filename_template\"] = FilenameTemplate(**data[\"kwargs\"])\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Asset class.\"\"\"\n    assert FilenameTemplate.__auto_name__ is False\n\n\ndef test_filename_template_is_not_strictly_typed(setup_filename_template_tests):\n    \"\"\"FilenameTemplate class is not strictly typed.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"].pop(\"type\")\n    # no errors\n    ft = FilenameTemplate(**data[\"kwargs\"])\n    assert isinstance(ft, FilenameTemplate)\n\n\ndef test_target_entity_type_argument_is_skipped(setup_filename_template_tests):\n    \"\"\"TypeError is raised if the target_entity_type argument is skipped.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"].pop(\"target_entity_type\")\n    with pytest.raises(TypeError) as cm:\n        FilenameTemplate(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"FilenameTemplate.target_entity_type cannot be None\"\n\n\ndef test_target_entity_type_argument_is_none(setup_filename_template_tests):\n    \"\"\"TypeError is raised if the target_entity_type argument is given as None.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"][\"target_entity_type\"] = None\n    with pytest.raises(TypeError) as cm:\n        FilenameTemplate(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"FilenameTemplate.target_entity_type cannot be None\"\n\n\ndef test_target_entity_type_attribute_is_read_only(setup_filename_template_tests):\n    \"\"\"AttributeError is raised if the target_entity_type attribute is set.\"\"\"\n    data = setup_filename_template_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"filename_template\"].target_entity_type = \"Asset\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'FilenameTemplate' object has no setter\",\n        12: \"property of 'FilenameTemplate' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_target_entity_type_getter' of 'FilenameTemplate' \"\n        \"object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_target_entity_type_argument_accepts_classes(setup_filename_template_tests):\n    \"\"\"target_entity_type can be set to a class directly.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"][\"target_entity_type\"] = \"Asset\"\n    _ = FilenameTemplate(**data[\"kwargs\"])\n\n\ndef test_target_entity_type_attribute_is_converted_to_a_string_if_given_as_a_class(\n    setup_filename_template_tests,\n):\n    \"\"\"target_entity_type attr is converted if the target_entity_type is a class.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"][\"target_entity_type\"] = \"Asset\"\n    ft = FilenameTemplate(**data[\"kwargs\"])\n    assert ft.target_entity_type == \"Asset\"\n\n\ndef test_path_argument_is_skipped(setup_filename_template_tests):\n    \"\"\"Nothing happens if the path argument is skipped.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"].pop(\"path\")\n    ft = FilenameTemplate(**data[\"kwargs\"])\n    assert isinstance(ft, FilenameTemplate)\n\n\ndef test_path_argument_skipped_path_attribute_is_empty_string(\n    setup_filename_template_tests,\n):\n    \"\"\"path attribute is an empty string if the path argument is skipped.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"].pop(\"path\")\n    ft = FilenameTemplate(**data[\"kwargs\"])\n    assert ft.path == \"\"\n\n\ndef test_path_argument_is_none_path_attribute_is_empty_string(\n    setup_filename_template_tests,\n):\n    \"\"\"path attribute is an empty string if the path argument is None.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"][\"path\"] = None\n    ft = FilenameTemplate(**data[\"kwargs\"])\n    assert ft.path == \"\"\n\n\ndef test_path_argument_is_empty_string(setup_filename_template_tests):\n    \"\"\"Nothing happens if the path argument is empty string.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"][\"path\"] = \"\"\n    ft = FilenameTemplate(**data[\"kwargs\"])\n    assert isinstance(ft, FilenameTemplate)\n\n\ndef test_path_attribute_is_empty_string(setup_filename_template_tests):\n    \"\"\"Nothing happens if the path attribute is set to empty string.\"\"\"\n    data = setup_filename_template_tests\n    data[\"filename_template\"].path = \"\"\n\n\ndef test_path_argument_is_not_string(setup_filename_template_tests):\n    \"\"\"TypeError is raised if the path argument is not a string.\"\"\"\n    data = setup_filename_template_tests\n    test_value = list(\"a list from a string\")\n    data[\"kwargs\"][\"path\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        FilenameTemplate(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"FilenameTemplate.path attribute should be string, not list: '['a', ' ', 'l', \"\n        \"'i', 's', 't', ' ', 'f', 'r', 'o', 'm', ' ', 'a', ' ', 's', 't', 'r', 'i', \"\n        \"'n', 'g']'\"\n    )\n\n\ndef test_path_attribute_is_not_string(setup_filename_template_tests):\n    \"\"\"TypeError is raised if the path attribute is not set to a string.\"\"\"\n    data = setup_filename_template_tests\n    test_value = list(\"a list from a string\")\n    with pytest.raises(TypeError) as cm:\n        data[\"filename_template\"].path = test_value\n\n    assert str(cm.value) == (\n        \"FilenameTemplate.path attribute should be string, not list: '['a', ' ', 'l', \"\n        \"'i', 's', 't', ' ', 'f', 'r', 'o', 'm', ' ', 'a', ' ', 's', 't', 'r', 'i', \"\n        \"'n', 'g']'\"\n    )\n\n\ndef test_filename_argument_is_skipped(setup_filename_template_tests):\n    \"\"\"Nothing happens if the filename argument is skipped.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"].pop(\"filename\")\n    ft = FilenameTemplate(**data[\"kwargs\"])\n    assert isinstance(ft, FilenameTemplate)\n\n\ndef test_filename_argument_skipped_filename_attribute_is_empty_string(\n    setup_filename_template_tests,\n):\n    \"\"\"filename attribute is an empty string if the filename argument is skipped.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"].pop(\"filename\")\n    ft = FilenameTemplate(**data[\"kwargs\"])\n    assert ft.filename == \"\"\n\n\ndef test_filename_argument_is_none_filename_attribute_is_empty_string(\n    setup_filename_template_tests,\n):\n    \"\"\"filename attribute is an empty string if the filename argument is None.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"][\"filename\"] = None\n    ft = FilenameTemplate(**data[\"kwargs\"])\n    assert ft.filename == \"\"\n\n\ndef test_filename_argument_is_empty_string(setup_filename_template_tests):\n    \"\"\"Nothing happens if the filename argument is empty string.\"\"\"\n    data = setup_filename_template_tests\n    data[\"kwargs\"][\"filename\"] = \"\"\n    ft = FilenameTemplate(**data[\"kwargs\"])\n    assert isinstance(ft, FilenameTemplate)\n\n\ndef test_filename_attribute_is_empty_string(setup_filename_template_tests):\n    \"\"\"Nothing happens if the filename attribute is set to empty string.\"\"\"\n    data = setup_filename_template_tests\n    data[\"filename_template\"].filename = \"\"\n\n\ndef test_filename_argument_is_not_string(setup_filename_template_tests):\n    \"\"\"TypeError is raised if filename argument is not string.\"\"\"\n    data = setup_filename_template_tests\n    test_value = list(\"a list from a string\")\n    data[\"kwargs\"][\"filename\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        FilenameTemplate(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"FilenameTemplate.filename attribute should be string, not list: '['a', ' ', \"\n        \"'l', 'i', 's', 't', ' ', 'f', 'r', 'o', 'm', ' ', 'a', ' ', 's', 't', 'r', \"\n        \"'i', 'n', 'g']'\"\n    )\n\n\ndef test_filename_attribute_is_not_string(setup_filename_template_tests):\n    \"\"\"Given value converted to string for the filename attribute.\"\"\"\n    data = setup_filename_template_tests\n    test_value = list(\"a list from a string\")\n    with pytest.raises(TypeError) as cm:\n        data[\"filename_template\"].filename = test_value\n\n    assert str(cm.value) == (\n        \"FilenameTemplate.filename attribute should be string, not list: \"\n        \"'['a', ' ', 'l', 'i', 's', 't', ' ', 'f', 'r', 'o', 'm', ' ', 'a', ' ', \"\n        \"'s', 't', 'r', 'i', 'n', 'g']'\"\n    )\n\n\ndef test_equality(setup_filename_template_tests):\n    \"\"\"Equality of FilenameTemplate objects.\"\"\"\n    data = setup_filename_template_tests\n    ft1 = FilenameTemplate(**data[\"kwargs\"])\n\n    new_entity = Entity(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"target_entity_type\"] = \"Entity\"\n    ft2 = FilenameTemplate(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"path\"] = \"different path\"\n    ft3 = FilenameTemplate(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"filename\"] = \"different filename\"\n    ft4 = FilenameTemplate(**data[\"kwargs\"])\n\n    assert data[\"filename_template\"] == ft1\n    assert not data[\"filename_template\"] == new_entity\n    assert not ft1 == ft2\n    assert not ft2 == ft3\n    assert not ft3 == ft4\n\n\ndef test_inequality(setup_filename_template_tests):\n    \"\"\"Inequality of FilenameTemplate objects.\"\"\"\n    data = setup_filename_template_tests\n    ft1 = FilenameTemplate(**data[\"kwargs\"])\n\n    new_entity = Entity(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"target_entity_type\"] = \"Entity\"\n    ft2 = FilenameTemplate(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"path\"] = \"different path\"\n    ft3 = FilenameTemplate(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"filename\"] = \"different filename\"\n    ft4 = FilenameTemplate(**data[\"kwargs\"])\n\n    assert not data[\"filename_template\"] != ft1\n    assert data[\"filename_template\"] != new_entity\n    assert ft1 != ft2\n    assert ft2 != ft3\n    assert ft3 != ft4\n\n\ndef test_naming_case(setup_postgresql_db):\n    \"\"\"Naming should contain both Sequence Shot and other stuff.\n\n    (this is based on https://github.com/eoyilmaz/anima/issues/23)\n    \"\"\"\n    ft = FilenameTemplate(\n        name=\"Normal Naming Convention\",\n        target_entity_type=\"Task\",\n        path=\"$REPO{{project.repository.id}}/{{project.code}}/{%- for parent_task in \"\n        \"parent_tasks -%}{{parent_task.nice_name}}/{%- endfor -%}\",\n        filename=\"\"\"{%- for p in parent_tasks -%}\n            {%- if p.entity_type == 'Sequence' -%}\n                {{p.name}}\n            {%- elif p.entity_type == 'Shot' -%}\n                _{{p.name}}{{p.children[0].name}}\n            {%- endif -%}\n        {%- endfor -%}\n        {%- set fx = parent_tasks[-2] -%}\n        _{{fx.name}}_v{{\"%02d\"|format(version.version_number)}}\"\"\",\n    )\n    DBSession.add(ft)\n\n    st = Structure(name=\"Normal Project Structure\", templates=[ft])\n    DBSession.add(st)\n\n    test_project = Project(name=\"test001\", code=\"test001\", structure=st)\n    DBSession.add(test_project)\n    DBSession.commit()\n\n    seq_task = Task(name=\"seq\", project=test_project)\n    DBSession.add(seq_task)\n\n    ep101 = Sequence(name=\"ep101\", code=\"ep101\", parent=seq_task)\n    DBSession.add(ep101)\n\n    shot_task = Task(name=\"shot\", parent=ep101)\n    DBSession.add(shot_task)\n\n    s001 = Shot(name=\"s001\", code=\"s001\", parent=shot_task)\n    DBSession.add(s001)\n\n    c001 = Task(name=\"c001\", parent=s001)\n    DBSession.add(c001)\n\n    effects_scene = Task(name=\"effect scene\", parent=c001)\n    DBSession.add(effects_scene)\n\n    fxA = Task(name=\"fxA\", parent=effects_scene)\n    DBSession.add(fxA)\n\n    maya = Task(name=\"maya\", parent=fxA)\n    DBSession.add(maya)\n    DBSession.commit()\n\n    v = Version(task=maya)\n    DBSession.add(v)\n    DBSession.commit()\n    path = v.generate_path(extension=\".ma\")\n    assert path.name == \"ep101_s001c001_fxA_v01.ma\"\n\n\ndef test__hash__is_working_as_expected(setup_filename_template_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_filename_template_tests\n    result = hash(data[\"filename_template\"])\n    assert isinstance(result, int)\n    assert result == data[\"filename_template\"].__hash__()\n"
  },
  {
    "path": "tests/models/test_generic.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests utility functions.\"\"\"\n\nimport datetime\n\nimport pytest\n\nimport pytz\n\nfrom stalker.utils import make_plural, utc_to_local, local_to_utc\n\n\n@pytest.mark.parametrize(\n    \"test_value,expected\",\n    [\n        (\"asset\", \"assets\"),\n        (\"client\", \"clients\"),\n        (\"department\", \"departments\"),\n        (\"entity\", \"entities\"),\n        (\"template\", \"templates\"),\n        (\"group\", \"groups\"),\n        (\"format\", \"formats\"),\n        (\"file\", \"files\"),\n        (\"session\", \"sessions\"),\n        (\"note\", \"notes\"),\n        (\"permission\", \"permissions\"),\n        (\"project\", \"projects\"),\n        (\"repository\", \"repositories\"),\n        (\"review\", \"reviews\"),\n        (\"scene\", \"scenes\"),\n        (\"sequence\", \"sequences\"),\n        (\"shot\", \"shots\"),\n        (\"status\", \"statuses\"),\n        (\"list\", \"lists\"),\n        (\"structure\", \"structures\"),\n        (\"studio\", \"studios\"),\n        (\"tag\", \"tags\"),\n        (\"task\", \"tasks\"),\n        (\"dependency\", \"dependencies\"),\n        (\"type\", \"types\"),\n        (\"bench\", \"benches\"),\n        (\"thief\", \"thieves\"),\n    ],\n)\ndef test_make_plural_is_working_as_expected(test_value, expected):\n    \"\"\"make_plural() is working as expected.\"\"\"\n    assert expected == make_plural(test_value)\n\n\ndef test_utc_to_local_is_working_as_expected():\n    \"\"\"utc_to_local() is working as expected.\"\"\"\n    local_now = datetime.datetime.now()\n    utc_now = datetime.datetime.now(pytz.utc)\n\n    utc_without_tz = datetime.datetime(\n        utc_now.year,\n        utc_now.month,\n        utc_now.day,\n        utc_now.hour,\n        utc_now.minute,\n    )\n    local_from_utc = utc_to_local(utc_without_tz)\n\n    assert local_from_utc.year == local_now.year\n    assert local_from_utc.month == local_now.month\n    assert local_from_utc.day == local_now.day\n    assert local_from_utc.hour == local_now.hour\n    assert local_from_utc.minute == local_now.minute\n\n\ndef test_local_to_utc_is_working_as_expected():\n    \"\"\"local_to_utc() is working as expected.\"\"\"\n    local_now = datetime.datetime.now()\n    utc_now = datetime.datetime.now(pytz.utc)\n\n    utc_without_tz = datetime.datetime(\n        utc_now.year,\n        utc_now.month,\n        utc_now.day,\n        utc_now.hour,\n        utc_now.minute,\n    )\n    utc_from_local = local_to_utc(local_now)\n\n    assert utc_from_local.year == utc_without_tz.year\n    assert utc_from_local.month == utc_without_tz.month\n    assert utc_from_local.day == utc_without_tz.day\n    assert utc_from_local.hour == utc_without_tz.hour\n    assert utc_from_local.minute == utc_without_tz.minute\n"
  },
  {
    "path": "tests/models/test_good.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Good class.\"\"\"\n\nimport pytest\n\nfrom stalker import Client, Good\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_good_tests():\n    \"\"\"Set up the test for the stalker.models.budget.Good class.\"\"\"\n    data = dict()\n    data[\"kwargs\"] = {\"name\": \"Comp\", \"cost\": 10, \"msrp\": 12, \"unit\": \"TL/hour\"}\n    return data\n\n\ndef test_cost_argument_is_skipped(setup_good_tests):\n    \"\"\"cost attribute value is 0.0 if the cost argument is skipped.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"].pop(\"cost\")\n    g = Good(**data[\"kwargs\"])\n    assert g.cost == 0\n\n\ndef test_cost_argument_is_none(setup_good_tests):\n    \"\"\"cost attribute value is 0.0 if the cost argument is None.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"cost\"] = None\n    g = Good(**data[\"kwargs\"])\n    assert g.cost == 0\n\n\ndef test_cost_attribute_is_none(setup_good_tests):\n    \"\"\"cost attribute is 0.0 if it is set to None.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    assert g.cost != 0\n    g.cost = None\n    assert g.cost == 0\n\n\ndef test_cost_argument_is_not_a_number(setup_good_tests):\n    \"\"\"TypeError is raised if cost argument is not a number.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"cost\"] = \"not a number\"\n    with pytest.raises(TypeError) as cm:\n        _ = Good(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Good.cost should be a non-negative number, not str: 'not a number'\"\n    )\n\n\ndef test_cost_attribute_is_not_a_number(setup_good_tests):\n    \"\"\"TypeError is raised if the cost attr is not a number.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        g.cost = \"not a number\"\n\n    assert str(cm.value) == (\n        \"Good.cost should be a non-negative number, not str: 'not a number'\"\n    )\n\n\ndef test_cost_argument_is_zero(setup_good_tests):\n    \"\"\"It is totally ok to set the cost to 0.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"cost\"] = 0\n    g = Good(**data[\"kwargs\"])\n    assert g.cost == 0.0\n\n\ndef test_cost_attribute_is_zero(setup_good_tests):\n    \"\"\"It is totally ok to test the cost attribute to 0.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    assert g.cost != 0.0\n    g.cost = 0.0\n    assert g.cost == 0.0\n\n\ndef test_cost_argument_is_negative(setup_good_tests):\n    \"\"\"ValueError is raised if the cost argument is a negative number.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"cost\"] = -10\n    with pytest.raises(ValueError) as cm:\n        _ = Good(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Good.cost should be a non-negative number\"\n\n\ndef test_cost_attribute_is_negative(setup_good_tests):\n    \"\"\"ValueError is raised if the cost attribute is set to a negative number.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    with pytest.raises(ValueError) as cm:\n        g.cost = -10\n\n    assert str(cm.value) == \"Good.cost should be a non-negative number\"\n\n\ndef test_cost_argument_is_working_as_expected(setup_good_tests):\n    \"\"\"cost argument value is passed to the cost attribute.\"\"\"\n    data = setup_good_tests\n    test_value = 113\n    data[\"kwargs\"][\"cost\"] = test_value\n    g = Good(**data[\"kwargs\"])\n    assert g.cost == test_value\n\n\ndef test_cost_attribute_is_working_as_expected(setup_good_tests):\n    \"\"\"cost attribute value can be changed.\"\"\"\n    data = setup_good_tests\n    test_value = 145\n    g = Good(**data[\"kwargs\"])\n    assert g.cost != test_value\n\n    g.cost = test_value\n    assert g.cost == test_value\n\n\ndef test_msrp_argument_is_skipped(setup_good_tests):\n    \"\"\"msrp attribute value is 0.0 if the msrp argument is skipped.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"].pop(\"msrp\")\n    g = Good(**data[\"kwargs\"])\n    assert g.msrp == 0\n\n\ndef test_msrp_argument_is_none(setup_good_tests):\n    \"\"\"msrp attribute value is 0.0 if the msrp argument is None.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"msrp\"] = None\n    g = Good(**data[\"kwargs\"])\n    assert g.msrp == 0\n\n\ndef test_msrp_attribute_is_none(setup_good_tests):\n    \"\"\"msrp attribute is 0.0 if it is set to None.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    assert g.msrp != 0\n    g.msrp = None\n    assert g.msrp == 0\n\n\ndef test_msrp_argument_is_not_a_number(setup_good_tests):\n    \"\"\"TypeError is raised if msrp argument is not a number.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"msrp\"] = \"not a number\"\n    with pytest.raises(TypeError) as cm:\n        _ = Good(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Good.msrp should be a non-negative number, not str: 'not a number'\"\n    )\n\n\ndef test_msrp_attribute_is_not_a_number(setup_good_tests):\n    \"\"\"TypeError is raised if the msrp attr is not a number.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        g.msrp = \"not a number\"\n\n    assert str(cm.value) == (\n        \"Good.msrp should be a non-negative number, not str: 'not a number'\"\n    )\n\n\ndef test_msrp_argument_is_zero(setup_good_tests):\n    \"\"\"It is totally ok to set the msrp to 0.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"msrp\"] = 0\n    g = Good(**data[\"kwargs\"])\n    assert g.msrp == 0.0\n\n\ndef test_msrp_attribute_is_zero(setup_good_tests):\n    \"\"\"It is totally ok to test the msrp attribute to 0.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    assert g.msrp != 0.0\n    g.msrp = 0.0\n    assert g.msrp == 0.0\n\n\ndef test_msrp_argument_is_negative(setup_good_tests):\n    \"\"\"ValueError is raised if the msrp argument is a negative number.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"msrp\"] = -10\n    with pytest.raises(ValueError) as cm:\n        _ = Good(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Good.msrp should be a non-negative number\"\n\n\ndef test_msrp_attribute_is_negative(setup_good_tests):\n    \"\"\"ValueError is raised if the msrp attribute is set to a negative number.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    with pytest.raises(ValueError) as cm:\n        g.msrp = -10\n\n    assert str(cm.value) == \"Good.msrp should be a non-negative number\"\n\n\ndef test_msrp_argument_is_working_as_expected(setup_good_tests):\n    \"\"\"msrp argument value is passed to the msrp attribute.\"\"\"\n    data = setup_good_tests\n    test_value = 113\n    data[\"kwargs\"][\"msrp\"] = test_value\n    g = Good(**data[\"kwargs\"])\n    assert g.msrp == test_value\n\n\ndef test_msrp_attribute_is_working_as_expected(setup_good_tests):\n    \"\"\"msrp attribute value can be changed.\"\"\"\n    data = setup_good_tests\n    test_value = 145\n    g = Good(**data[\"kwargs\"])\n    assert g.msrp != test_value\n\n    g.msrp = test_value\n    assert g.msrp == test_value\n\n\ndef test_unit_argument_is_skipped(setup_good_tests):\n    \"\"\"unit attribute is an empty string if the unit argument is skipped.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"].pop(\"unit\")\n    g = Good(**data[\"kwargs\"])\n    assert g.unit == \"\"\n\n\ndef test_unit_argument_is_none(setup_good_tests):\n    \"\"\"unit attribute is an empty string if the unit argument is None.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"unit\"] = None\n    g = Good(**data[\"kwargs\"])\n    assert g.unit == \"\"\n\n\ndef test_unit_attribute_is_set_to_none(setup_good_tests):\n    \"\"\"unit attribute is an empty string if it is set to None.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    assert g.unit != \"\"\n    g.unit = None\n    assert g.unit == \"\"\n\n\ndef test_unit_argument_is_not_a_string(setup_good_tests):\n    \"\"\"TypeError is raised if the unit argument is not a string.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"unit\"] = 12312\n    with pytest.raises(TypeError) as cm:\n        g = Good(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Good.unit should be a string, not int: '12312'\"\n\n\ndef test_unit_attribute_is_not_a_string(setup_good_tests):\n    \"\"\"TypeError is raised if the unit attr is set to a value which is not a string.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        g.unit = 2342\n\n    assert str(cm.value) == \"Good.unit should be a string, not int: '2342'\"\n\n\ndef test_unit_argument_is_working_as_expected(setup_good_tests):\n    \"\"\"unit argument value is passed to the unit attribute.\"\"\"\n    data = setup_good_tests\n    test_value = \"this is my unit\"\n    data[\"kwargs\"][\"unit\"] = test_value\n    g = Good(**data[\"kwargs\"])\n    assert g.unit == test_value\n\n\ndef test_unit_attribute_is_working_as_expected(setup_good_tests):\n    \"\"\"unit attribute value can be changed.\"\"\"\n    data = setup_good_tests\n    test_value = \"this is my unit\"\n    g = Good(**data[\"kwargs\"])\n    assert g.unit != test_value\n    g.unit = test_value\n    assert g.unit == test_value\n\n\ndef test_client_argument_is_skipped(setup_good_tests):\n    \"\"\"Good can be created without a Client.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"].pop(\"client\", None)\n    g = Good(**data[\"kwargs\"])\n    assert g is not None\n    assert isinstance(g, Good)\n\n\ndef test_client_argument_is_none(setup_good_tests):\n    \"\"\"Good can be created without a Client.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"client\"] = None\n    g = Good(**data[\"kwargs\"])\n    assert g is not None\n    assert isinstance(g, Good)\n\n\ndef test_client_argument_is_not_a_client_instance(setup_good_tests):\n    \"\"\"TypeError is raised if the client argument is not a Client instance.\"\"\"\n    data = setup_good_tests\n    data[\"kwargs\"][\"client\"] = \"not a client\"\n    with pytest.raises(TypeError) as cm:\n        Good(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Good.client attribute should be a stalker.models.client.Client instance, \"\n        \"not str: 'not a client'\"\n    )\n\n\ndef test_client_attribute_is_set_to_a_value_other_than_a_client(setup_good_tests):\n    \"\"\"TypeError is raised if the client attr is not a Client instance.\"\"\"\n    data = setup_good_tests\n    g = Good(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        g.client = \"not a client\"\n\n    assert str(cm.value) == (\n        \"Good.client attribute should be a stalker.models.client.Client instance, \"\n        \"not str: 'not a client'\"\n    )\n\n\ndef test_client_argument_is_working_as_expected(setup_good_tests):\n    \"\"\"client argument is working as expected.\"\"\"\n    data = setup_good_tests\n    client = Client(name=\"Test Client\")\n    data[\"kwargs\"][\"client\"] = client\n    g = Good(**data[\"kwargs\"])\n    assert g.client == client\n\n\ndef test_client_attribute_is_working_as_expected(setup_good_tests):\n    \"\"\"client attribute is working as expected.\"\"\"\n    data = setup_good_tests\n    client = Client(name=\"Test Client\")\n    g = Good(**data[\"kwargs\"])\n    assert g.client != client\n    g.client = client\n    assert g.client == client\n"
  },
  {
    "path": "tests/models/test_group.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Group class.\"\"\"\n\nimport pytest\n\nfrom stalker import Group, Permission, User\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_group_tests():\n    \"\"\"Set up the test for the Group class.\"\"\"\n    data = dict()\n    # create a couple of Users\n    data[\"test_user1\"] = User(\n        name=\"User1\",\n        login=\"user1\",\n        password=\"1234\",\n        email=\"user1@test.com\",\n    )\n\n    data[\"test_user2\"] = User(\n        name=\"User2\",\n        login=\"user2\",\n        password=\"1234\",\n        email=\"user1@test.com\",\n    )\n\n    data[\"test_user3\"] = User(\n        name=\"User3\",\n        login=\"user3\",\n        password=\"1234\",\n        email=\"user3@test.com\",\n    )\n\n    # create a test group\n    data[\"kwargs\"] = {\n        \"name\": \"Test Group\",\n        \"users\": [data[\"test_user1\"], data[\"test_user2\"], data[\"test_user3\"]],\n    }\n\n    data[\"test_group\"] = Group(**data[\"kwargs\"])\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Group class.\"\"\"\n    assert Group.__auto_name__ is False\n\n\ndef test_users_argument_is_skipped(setup_group_tests):\n    \"\"\"users argument is skipped the users attribute is an empty list.\"\"\"\n    data = setup_group_tests\n    data[\"kwargs\"].pop(\"users\")\n    new_group = Group(**data[\"kwargs\"])\n    assert new_group.users == []\n\n\ndef test_users_argument_is_not_a_list_of_user_instances(setup_group_tests):\n    \"\"\"TypeError is raised if the users argument is not a list of User instances.\"\"\"\n    data = setup_group_tests\n    data[\"kwargs\"][\"users\"] = [12, \"not a user\"]\n    with pytest.raises(TypeError) as cm:\n        Group(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Group.users should only contain instances of \"\n        \"stalker.models.auth.User, not int: '12'\"\n    )\n\n\ndef test_users_attribute_is_not_a_list_of_user_instances(setup_group_tests):\n    \"\"\"TypeError is raised if the users attribute is not a list of User instances.\"\"\"\n    data = setup_group_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_group\"].users = [12, \"not a user\"]\n\n    assert str(cm.value) == (\n        \"Group.users should only contain instances of \"\n        \"stalker.models.auth.User, not int: '12'\"\n    )\n\n\ndef test_users_argument_updates_the_groups_attribute_in_the_given_user_instances(\n    setup_group_tests,\n):\n    \"\"\"users arg will have the current Group instance in their groups attribute.\"\"\"\n    data = setup_group_tests\n    data[\"kwargs\"][\"name\"] = \"New Group\"\n    new_group = Group(**data[\"kwargs\"])\n\n    assert all(new_group in user.groups for user in data[\"kwargs\"][\"users\"])\n\n\ndef test_users_attribute_updates_the_groups_attribute_in_the_given_user_instances(\n    setup_group_tests,\n):\n    \"\"\"users attr will have the current Group instance in their groups attribute.\"\"\"\n    data = setup_group_tests\n    test_users = data[\"kwargs\"].pop(\"users\")\n    new_group = Group(**data[\"kwargs\"])\n    new_group.users = test_users\n    assert all(new_group in user.groups for user in test_users)\n\n\ndef test_permissions_argument_is_working_as_expected(setup_group_tests):\n    \"\"\"permissions can be added to the Group on __init__().\"\"\"\n    data = setup_group_tests\n    # create a couple of permissions\n    perm1 = Permission(\"Allow\", \"Create\", \"User\")\n    perm2 = Permission(\"Allow\", \"Read\", \"User\")\n    perm3 = Permission(\"Deny\", \"Delete\", \"User\")\n\n    new_group = Group(\n        name=\"Test Group\",\n        users=[data[\"test_user1\"], data[\"test_user2\"]],\n        permissions=[perm1, perm2, perm3],\n    )\n\n    assert new_group.permissions == [perm1, perm2, perm3]\n\n\ndef test_hash_value(setup_group_tests):\n    \"\"\"__hash__ returns the hash of the Group instance.\"\"\"\n    data = setup_group_tests\n    result = hash(data[\"test_group\"])\n    assert isinstance(result, int)\n"
  },
  {
    "path": "tests/models/test_image_format.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the ImageFormat class.\"\"\"\n\nimport sys\nimport pytest\n\nfrom stalker import ImageFormat\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_image_format_tests():\n    \"\"\"Set up test data for the ImageFormat.\"\"\"\n    data = dict()\n\n    # some proper values\n    data[\"kwargs\"] = {\n        \"name\": \"HD\",\n        \"width\": 1920,\n        \"height\": 1080,\n        \"pixel_aspect\": 1.0,\n        \"print_resolution\": 300,\n    }\n\n    data[\"test_image_format\"] = ImageFormat(**data[\"kwargs\"])\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for ImageFormat class.\"\"\"\n    assert ImageFormat.__auto_name__ is False\n\n\ndef test_width_argument_accepts_int_or_float_only(setup_image_format_tests):\n    \"\"\"TypeError is raised if the width argument is not integer or float.\"\"\"\n    data = setup_image_format_tests\n    # the width should be an integer or float\n    test_value = \"1920\"\n    data[\"kwargs\"][\"width\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value)\n        == \"ImageFormat.width should be an instance of int or float, not str: '1920'\"\n    )\n\n\ndef test_width_attribute_int_or_float(setup_image_format_tests):\n    \"\"\"TypeError is raised if the width attribute is not an integer or float.\"\"\"\n    data = setup_image_format_tests\n    test_value = \"1920\"\n    with pytest.raises(TypeError) as cm:\n        data[\"test_image_format\"].width = test_value\n\n    assert (\n        str(cm.value)\n        == \"ImageFormat.width should be an instance of int or float, not str: '1920'\"\n    )\n\n\ndef test_width_argument_float_to_int_conversion(setup_image_format_tests):\n    \"\"\"width argument is given as a float and converted to int successfully.\"\"\"\n    data = setup_image_format_tests\n    # the given floats should be converted to integer\n    data[\"kwargs\"][\"width\"] = 1920.0\n    an_image_format = ImageFormat(**data[\"kwargs\"])\n    assert isinstance(an_image_format.width, int)\n\n\ndef test_width_attribute_float_to_int_conversion(setup_image_format_tests):\n    \"\"\"width attribute against being converted to int successfully.\"\"\"\n    data = setup_image_format_tests\n    # the given floats should be converted to integer\n    data[\"test_image_format\"].width = 1920.0\n    assert isinstance(data[\"test_image_format\"].width, int)\n\n\ndef test_width_argument_being_zero(setup_image_format_tests):\n    \"\"\"ValueError is raised if the width argument is zero.\"\"\"\n    data = setup_image_format_tests\n    # could not be zero\n    data[\"kwargs\"][\"width\"] = 0\n    with pytest.raises(ValueError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"ImageFormat.width cannot be zero or negative\"\n\n\ndef test_width_attribute_being_zero(setup_image_format_tests):\n    \"\"\"ValueError is raised if the width attribute is zero.\"\"\"\n    data = setup_image_format_tests\n    # also test the attribute for this\n    with pytest.raises(ValueError) as cm:\n        data[\"test_image_format\"].width = 0\n\n    assert str(cm.value) == \"ImageFormat.width cannot be zero or negative\"\n\n\ndef test_width_argument_being_negative(setup_image_format_tests):\n    \"\"\"ValueError is raised if the width argument is negative.\"\"\"\n    data = setup_image_format_tests\n    data[\"kwargs\"][\"width\"] = -10\n    with pytest.raises(ValueError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"ImageFormat.width cannot be zero or negative\"\n\n\ndef test_width_attribute_being_negative(setup_image_format_tests):\n    \"\"\"ValueError is raised if the width attribute is negative.\"\"\"\n    data = setup_image_format_tests\n    # also test the attribute for this\n    with pytest.raises(ValueError) as cm:\n        data[\"test_image_format\"].width = -100\n\n    assert str(cm.value) == \"ImageFormat.width cannot be zero or negative\"\n\n\ndef test_height_argument_int_or_float(setup_image_format_tests):\n    \"\"\"TypeError is raised if the height argument is not an integer or float.\"\"\"\n    data = setup_image_format_tests\n    test_value = \"1080\"\n    data[\"kwargs\"][\"height\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"ImageFormat.height should be an instance of int or float, not str: '1080'\"\n    )\n\n\ndef test_height_attribute_int_or_float(setup_image_format_tests):\n    \"\"\"TypeError is raised if the height attribute is not an integer or float.\"\"\"\n    data = setup_image_format_tests\n    # test also the attribute\n    test_value = \"1080\"\n    with pytest.raises(TypeError) as cm:\n        data[\"test_image_format\"].height = test_value\n\n    assert str(cm.value) == (\n        \"ImageFormat.height should be an instance of int or float, not str: '1080'\"\n    )\n\n\ndef test_height_argument_float_to_int_conversion(setup_image_format_tests):\n    \"\"\"height argument given as float is converted to int successfully.\"\"\"\n    data = setup_image_format_tests\n    data[\"kwargs\"][\"height\"] = 1080.0\n    an_image_format = ImageFormat(**data[\"kwargs\"])\n    assert isinstance(an_image_format.height, int)\n\n\ndef test_height_attribute_float_to_int_conversion(setup_image_format_tests):\n    \"\"\"height attribute given as float being converted to int successfully.\"\"\"\n    data = setup_image_format_tests\n    # also test the attribute for this\n    data[\"test_image_format\"].height = 1080.0\n    assert isinstance(data[\"test_image_format\"].height, int)\n\n\ndef test_height_argument_being_zero(setup_image_format_tests):\n    \"\"\"ValueError is raised if the height argument is zero.\"\"\"\n    data = setup_image_format_tests\n    data[\"kwargs\"][\"height\"] = 0\n    with pytest.raises(ValueError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n    assert str(cm.value) == \"ImageFormat.height cannot be zero or negative\"\n\n\ndef test_height_attribute_being_zero(setup_image_format_tests):\n    \"\"\"ValueError is raised if the height attribute is zero.\"\"\"\n    data = setup_image_format_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_image_format\"].height = 0\n    assert str(cm.value) == \"ImageFormat.height cannot be zero or negative\"\n\n\ndef test_height_argument_being_negative(setup_image_format_tests):\n    \"\"\"ValueError is raised if the height argument is negative.\"\"\"\n    data = setup_image_format_tests\n    data[\"kwargs\"][\"height\"] = -10\n    with pytest.raises(ValueError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n    assert str(cm.value) == \"ImageFormat.height cannot be zero or negative\"\n\n\ndef test_height_attribute_being_negative(setup_image_format_tests):\n    \"\"\"ValueError is raised if the height attribute is negative.\"\"\"\n    data = setup_image_format_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_image_format\"].height = -100\n    assert str(cm.value) == \"ImageFormat.height cannot be zero or negative\"\n\n\ndef test_device_aspect_attribute_float(setup_image_format_tests):\n    \"\"\"device aspect ratio is calculated as a float value.\"\"\"\n    data = setup_image_format_tests\n    assert isinstance(data[\"test_image_format\"].device_aspect, float)\n\n\ndef test_device_aspect_ratio_correctly_calculated_1(setup_image_format_tests):\n    \"\"\"device aspect ratio is correctly calculated.\"\"\"\n    data = setup_image_format_tests\n    # the device aspect is something calculated using width, height and\n    # the pixel aspect ratio\n\n    # Test HD\n    data[\"kwargs\"].update(\n        {\n            \"name\": \"HD\",\n            \"width\": 1920,\n            \"height\": 1080,\n            \"pixel_aspect\": 1.0,\n            \"print_resolution\": 300,\n        }\n    )\n\n    an_image_format = ImageFormat(**data[\"kwargs\"])\n\n    # the device aspect for this setup should be around 1.7778\n    assert \"%1.4g\" % an_image_format.device_aspect == \"%1.4g\" % 1.7778\n\n\ndef test_device_aspect_ratio_correctly_calculated_2(setup_image_format_tests):\n    \"\"\"device aspect ratio is correctly calculated.\"\"\"\n    data = setup_image_format_tests\n\n    # test PAL\n    data[\"kwargs\"].update(\n        {\n            \"name\": \"PAL\",\n            \"width\": 720,\n            \"height\": 576,\n            \"pixel_aspect\": 1.0667,\n            \"print_resolution\": 300,\n        }\n    )\n\n    an_image_format = ImageFormat(**data[\"kwargs\"])\n\n    # the device aspect for this setup should be around 4/3\n    assert \"%1.4g\" % an_image_format.device_aspect == \"%1.4g\" % 1.3333\n\n\ndef test_device_aspect_attribute_updates(setup_image_format_tests):\n    \"\"\"device_aspect_ratio attr is updated if width, height or pixel_aspect updated.\"\"\"\n    data = setup_image_format_tests\n    # just changing one of the width or height should be causing an update\n    # in device_aspect\n\n    # start with PAL\n    data[\"kwargs\"].update(\n        {\n            \"name\": \"PAL\",\n            \"width\": 720,\n            \"height\": 576,\n            \"pixel_aspect\": 1.0667,\n            \"print_resolution\": 300,\n        }\n    )\n    an_image_format = ImageFormat(**data[\"kwargs\"])\n    previous_device_aspect = an_image_format.device_aspect\n\n    # change to HD\n    an_image_format.width = 1920\n    an_image_format.height = 1080\n    an_image_format.pixel_aspect = 1.0\n\n    assert abs(an_image_format.device_aspect - 1.77778) < 0.001\n    assert an_image_format.device_aspect != previous_device_aspect\n\n\ndef test_device_aspect_attribute_write_protected(setup_image_format_tests):\n    \"\"\"device_aspect attribute is write protected.\"\"\"\n    data = setup_image_format_tests\n    # the device aspect should be write protected\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_image_format\"].device_aspect = 10\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'device_aspect'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'device_aspect' of 'ImageFormat' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_pixel_aspect_int_float(setup_image_format_tests):\n    \"\"\"TypeError is raised if the pixel aspect ratio is not an integer or float.\"\"\"\n    data = setup_image_format_tests\n    # the pixel aspect ratio should be a given as float or integer number\n    # any other variable type than int and float is not ok\n    data[\"kwargs\"][\"pixel_aspect\"] = \"1.0\"\n    with pytest.raises(TypeError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"ImageFormat.pixel_aspect should be an instance of int or float, \"\n        \"not str: '1.0'\"\n    )\n\n\ndef test_pixel_aspect_int_float_2(setup_image_format_tests):\n    \"\"\"TypeError is raised if the pixel aspect ratio is not an integer or float.\"\"\"\n    data = setup_image_format_tests\n    # float is ok\n    data[\"kwargs\"][\"pixel_aspect\"] = 1.0\n    ImageFormat(**data[\"kwargs\"])\n\n\ndef test_pixel_aspect_int_float_3(setup_image_format_tests):\n    \"\"\"TypeError is raised if the pixel aspect ratio is not an integer or float.\"\"\"\n    data = setup_image_format_tests\n    # int is ok\n    data[\"kwargs\"][\"pixel_aspect\"] = 2\n    ImageFormat(**data[\"kwargs\"])\n\n\ndef test_pixel_aspect_float_conversion(setup_image_format_tests):\n    \"\"\"pixel aspect ratio converted to float.\"\"\"\n    data = setup_image_format_tests\n    # given an integer for the pixel aspect ratio,\n    # the returned pixel aspect ratio should be a float\n    data[\"kwargs\"][\"pixel_aspect\"] = 1\n    an_image_format = ImageFormat(**data[\"kwargs\"])\n    assert isinstance(an_image_format.pixel_aspect, float)\n\n\ndef test_pixel_aspect_argument_zero(setup_image_format_tests):\n    \"\"\"ValueError is raised if the pixel_aspect argument is zero.\"\"\"\n    data = setup_image_format_tests\n    # the pixel aspect ratio cannot be zero\n    data[\"kwargs\"][\"pixel_aspect\"] = 0\n    with pytest.raises(ValueError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n    assert (\n        str(cm.value) == \"ImageFormat.pixel_aspect cannot be zero or a negative value\"\n    )\n\n\ndef test_pixel_aspect_attribute_zero(setup_image_format_tests):\n    \"\"\"ValueError is raised if the pixel_aspect attribute is zero.\"\"\"\n    data = setup_image_format_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_image_format\"].pixel_aspect = 0\n    assert (\n        str(cm.value) == \"ImageFormat.pixel_aspect cannot be zero or a negative value\"\n    )\n\n\ndef test_pixel_aspect_argument_negative_float(setup_image_format_tests):\n    \"\"\"ValueError is raised if pixel_aspect argument is negative.\"\"\"\n    data = setup_image_format_tests\n    # the pixel aspect ratio cannot be negative\n    data[\"kwargs\"][\"pixel_aspect\"] = -1.0\n    with pytest.raises(ValueError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n    assert (\n        str(cm.value) == \"ImageFormat.pixel_aspect cannot be zero or a negative value\"\n    )\n\n\ndef test_pixel_aspect_argument_negative_int(setup_image_format_tests):\n    \"\"\"ValueError is raised if pixel_aspect argument is negative.\"\"\"\n    data = setup_image_format_tests\n    # the pixel aspect ratio cannot be negative\n    data[\"kwargs\"][\"pixel_aspect\"] = -1\n    with pytest.raises(ValueError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n    assert (\n        str(cm.value) == \"ImageFormat.pixel_aspect cannot be zero or a negative value\"\n    )\n\n\ndef test_pixel_aspect_attribute_negative_integer(setup_image_format_tests):\n    \"\"\"ValueError is raised if pixel_aspect attribute is negative.\"\"\"\n    data = setup_image_format_tests\n    # also test the attribute\n    with pytest.raises(ValueError) as cm:\n        data[\"test_image_format\"].pixel_aspect = -1.0\n    assert (\n        str(cm.value) == \"ImageFormat.pixel_aspect cannot be zero or a negative value\"\n    )\n\n\ndef test_pixel_aspect_attribute_negative_float(setup_image_format_tests):\n    \"\"\"ValueError is raised if pixel_aspect attribute is negative.\"\"\"\n    data = setup_image_format_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_image_format\"].pixel_aspect = -1\n    assert (\n        str(cm.value) == \"ImageFormat.pixel_aspect cannot be zero or a negative value\"\n    )\n\n\ndef test_pixel_aspect_attribute_if_being_initialized_correctly(\n    setup_image_format_tests,\n):\n    \"\"\"pixel_aspect attr is correctly initialized to its default value if omitted.\"\"\"\n    data = setup_image_format_tests\n    data[\"kwargs\"].pop(\"pixel_aspect\")\n    an_image_format = ImageFormat(**data[\"kwargs\"])\n    default_value = 1.0\n    assert an_image_format.pixel_aspect == default_value\n\n\ndef test_print_resolution_omit(setup_image_format_tests):\n    \"\"\"print_resolution against being omitted.\"\"\"\n    data = setup_image_format_tests\n    # the print timing_resolution can be omitted\n    data[\"kwargs\"].pop(\"print_resolution\")\n    imf = ImageFormat(**data[\"kwargs\"])\n\n    # and the default value should be a float instance\n    assert isinstance(imf.print_resolution, float)\n\n\ndef test_print_resolution_argument_accepts_int_float_only(setup_image_format_tests):\n    \"\"\"TypeError is raised if the print_resolution arg is not an integer or float.\"\"\"\n    data = setup_image_format_tests\n    # the print timing_resolution should be initialized with an integer or a float\n    data[\"kwargs\"][\"print_resolution\"] = \"300.0\"\n\n    with pytest.raises(TypeError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"ImageFormat.print_resolution should be an instance of int or float, \"\n        \"not str: '300.0'\"\n    )\n\n\ndef test_print_resolution_argument_accepts_int_float_only_2(setup_image_format_tests):\n    \"\"\"TypeError is raised if the print_resolution arg is not an integer or float.\"\"\"\n    # the print timing_resolution should be initialized with an integer or a float\n    data = setup_image_format_tests\n    data[\"kwargs\"][\"print_resolution\"] = 300\n    imf = ImageFormat(**data[\"kwargs\"])\n    assert isinstance(imf.print_resolution, float)\n\n\ndef test_print_resolution_argument_accepts_int_float_only_3(setup_image_format_tests):\n    \"\"\"TypeError is raised if the print_resolution arg is not an integer or float.\"\"\"\n    data = setup_image_format_tests\n    # the print timing_resolution should be initialized with an integer or\n    # a float\n    data[\"kwargs\"][\"print_resolution\"] = 300.0\n    imf = ImageFormat(**data[\"kwargs\"])\n    assert isinstance(imf.print_resolution, float)\n\n\ndef test_print_resolution_argument_zero(setup_image_format_tests):\n    \"\"\"ValueError is raised if the print_resolution argument is zero.\"\"\"\n    data = setup_image_format_tests\n    data[\"kwargs\"][\"print_resolution\"] = 0\n\n    # the print timing_resolution cannot be zero\n    with pytest.raises(ValueError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"ImageFormat.print_resolution cannot be zero or negative\"\n\n\ndef test_print_resolution_attribute_zero(setup_image_format_tests):\n    \"\"\"ValueError is raised if the print_resolution attribute is zero.\"\"\"\n    data = setup_image_format_tests\n    # also test the attribute\n    with pytest.raises(ValueError) as cm:\n        data[\"test_image_format\"].print_resolution = 0\n\n    assert str(cm.value) == \"ImageFormat.print_resolution cannot be zero or negative\"\n\n\ndef test_print_resolution_argument_negative_int(setup_image_format_tests):\n    \"\"\"ValueError is raised if the print_resolution argument is negative.\"\"\"\n    data = setup_image_format_tests\n    # the print timing_resolution cannot be negative\n    data[\"kwargs\"][\"print_resolution\"] = -300\n    with pytest.raises(ValueError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"ImageFormat.print_resolution cannot be zero or negative\"\n\n\ndef test_print_resolution_argument_negative_float(setup_image_format_tests):\n    \"\"\"ValueError is raised if the print_resolution argument is negative.\"\"\"\n    data = setup_image_format_tests\n    # the print timing_resolution cannot be negative\n    data[\"kwargs\"][\"print_resolution\"] = -300.0\n    with pytest.raises(ValueError) as cm:\n        ImageFormat(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"ImageFormat.print_resolution cannot be zero or negative\"\n\n\ndef test_print_resolution_attribute_negative_int(setup_image_format_tests):\n    \"\"\"ValueError is raised if the print_resolution attribute is negative.\"\"\"\n    data = setup_image_format_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_image_format\"].print_resolution = -300\n\n    assert str(cm.value) == \"ImageFormat.print_resolution cannot be zero or negative\"\n\n\ndef test_print_resolution_attribute_negative_float(setup_image_format_tests):\n    \"\"\"ValueError is raised if the print_resolution attribute is negative.\"\"\"\n    data = setup_image_format_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_image_format\"].print_resolution = -300.0\n\n    assert str(cm.value) == \"ImageFormat.print_resolution cannot be zero or negative\"\n\n\ndef test_equality(setup_image_format_tests):\n    \"\"\"Equality operator.\"\"\"\n    data = setup_image_format_tests\n    image_format1 = ImageFormat(**data[\"kwargs\"])\n    image_format2 = ImageFormat(**data[\"kwargs\"])\n\n    data[\"kwargs\"].update(\n        {\n            \"width\": 720,\n            \"height\": 480,\n            \"pixel_aspect\": 0.888888,\n        }\n    )\n    image_format3 = ImageFormat(**data[\"kwargs\"])\n\n    assert image_format1 == image_format2\n    assert not image_format1 == image_format3\n\n\ndef test_inequality(setup_image_format_tests):\n    \"\"\"Inequality operator.\"\"\"\n    data = setup_image_format_tests\n    image_format1 = ImageFormat(**data[\"kwargs\"])\n    image_format2 = ImageFormat(**data[\"kwargs\"])\n\n    data[\"kwargs\"].update(\n        {\n            \"name\": \"NTSC\",\n            \"description\": \"The famous NTSC image format\",\n            \"width\": 720,\n            \"height\": 480,\n            \"pixel_aspect\": 0.888888,\n        }\n    )\n    image_format3 = ImageFormat(**data[\"kwargs\"])\n\n    assert not image_format1 != image_format2\n    assert image_format1 != image_format3\n\n\ndef test_plural_class_name(setup_image_format_tests):\n    \"\"\"Plural name of ImageFormat class.\"\"\"\n    data = setup_image_format_tests\n    assert data[\"test_image_format\"].plural_class_name == \"ImageFormats\"\n\n\ndef test_hash_value(setup_image_format_tests):\n    \"\"\"hash value is correctly calculated.\"\"\"\n    data = setup_image_format_tests\n    assert data[\"test_image_format\"].__hash__() == hash(\n        \"{}:{}:{}\".format(\n            data[\"test_image_format\"].id,\n            data[\"test_image_format\"].name,\n            data[\"test_image_format\"].entity_type,\n        )\n    )\n"
  },
  {
    "path": "tests/models/test_invoice.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Invoice class.\"\"\"\n\nimport datetime\n\nimport pytest\n\nimport pytz\n\nfrom stalker import (\n    Budget,\n    Client,\n    Invoice,\n    Project,\n    Repository,\n    Status,\n    StatusList,\n    Type,\n    User,\n)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_invoice_tests():\n    \"\"\"Set up invoice class related tests.\"\"\"\n    data = dict()\n    data[\"status_new\"] = Status(name=\"Mew\", code=\"NEW\")\n    data[\"status_wfd\"] = Status(name=\"Waiting For Dependency\", code=\"WFD\")\n    data[\"status_rts\"] = Status(name=\"Ready To Start\", code=\"RTS\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_prev\"] = Status(name=\"Pending Review\", code=\"PREV\")\n    data[\"status_hrev\"] = Status(name=\"Has Revision\", code=\"HREV\")\n    data[\"status_drev\"] = Status(name=\"Dependency Has Revision\", code=\"DREV\")\n    data[\"status_oh\"] = Status(name=\"On Hold\", code=\"OH\")\n    data[\"status_stop\"] = Status(name=\"Stopped\", code=\"STOP\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n    data[\"status_new\"] = Status(name=\"New\", code=\"NEW\")\n    data[\"status_app\"] = Status(name=\"Approved\", code=\"APP\")\n\n    data[\"budget_status_list\"] = StatusList(\n        name=\"Budget Statuses\",\n        target_entity_type=\"Budget\",\n        statuses=[data[\"status_new\"], data[\"status_prev\"], data[\"status_app\"]],\n    )\n\n    data[\"task_status_list\"] = StatusList(\n        name=\"Task Statuses\",\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_oh\"],\n            data[\"status_stop\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n\n    data[\"test_project_status_list\"] = StatusList(\n        name=\"Project Statuses\",\n        statuses=[data[\"status_wip\"], data[\"status_prev\"], data[\"status_cmpl\"]],\n        target_entity_type=Project,\n    )\n\n    data[\"test_movie_project_type\"] = Type(\n        name=\"Movie Project\",\n        code=\"movie\",\n        target_entity_type=\"Project\",\n    )\n\n    data[\"test_repository_type\"] = Type(\n        name=\"Test Repository Type\",\n        code=\"test\",\n        target_entity_type=\"Repository\",\n    )\n\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"test_repository_type\"],\n    )\n\n    data[\"test_user1\"] = User(\n        name=\"User1\", login=\"user1\", email=\"user1@user1.com\", password=\"1234\"\n    )\n\n    data[\"test_user2\"] = User(\n        name=\"User2\", login=\"user2\", email=\"user2@user2.com\", password=\"1234\"\n    )\n\n    data[\"test_user3\"] = User(\n        name=\"User3\", login=\"user3\", email=\"user3@user3.com\", password=\"1234\"\n    )\n\n    data[\"test_user4\"] = User(\n        name=\"User4\", login=\"user4\", email=\"user4@user4.com\", password=\"1234\"\n    )\n\n    data[\"test_user5\"] = User(\n        name=\"User5\", login=\"user5\", email=\"user5@user5.com\", password=\"1234\"\n    )\n\n    data[\"test_client\"] = Client(\n        name=\"Test Client\",\n    )\n\n    data[\"test_project\"] = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=data[\"test_movie_project_type\"],\n        status_list=data[\"test_project_status_list\"],\n        repository=data[\"test_repository\"],\n        clients=[data[\"test_client\"]],\n    )\n\n    data[\"test_budget\"] = Budget(\n        project=data[\"test_project\"],\n        name=\"Test Budget 1\",\n        status_list=data[\"budget_status_list\"],\n    )\n    return data\n\n\ndef test_creating_an_invoice_instance(setup_invoice_tests):\n    \"\"\"Creation of an Invoice instance.\"\"\"\n    data = setup_invoice_tests\n    invoice = Invoice(\n        budget=data[\"test_budget\"],\n        amount=1500,\n        unit=\"TL\",\n        client=data[\"test_client\"],\n        date_created=datetime.datetime(2016, 11, 7, tzinfo=pytz.utc),\n    )\n    assert isinstance(invoice, Invoice)\n\n\ndef test_budget_argument_is_skipped(setup_invoice_tests):\n    \"\"\"TypeError is raised if the budget argument is skipped.\"\"\"\n    data = setup_invoice_tests\n    with pytest.raises(TypeError) as cm:\n        Invoice(client=data[\"test_client\"], amount=1500, unit=\"TRY\")\n\n    assert str(cm.value) == (\n        \"Invoice.budget should be a Budget instance, not NoneType: 'None'\"\n    )\n\n\ndef test_budget_argument_is_none(setup_invoice_tests):\n    \"\"\"TypeError is raised if the budget argument is None.\"\"\"\n    data = setup_invoice_tests\n    with pytest.raises(TypeError) as cm:\n        Invoice(budget=None, client=data[\"test_client\"], amount=1500, unit=\"TRY\")\n    assert str(cm.value) == (\n        \"Invoice.budget should be a Budget instance, not NoneType: 'None'\"\n    )\n\n\ndef test_budget_attribute_is_set_to_none(setup_invoice_tests):\n    \"\"\"TypeError is raised if the budget attribute is set to None.\"\"\"\n    data = setup_invoice_tests\n    test_invoice = Invoice(\n        budget=data[\"test_budget\"], client=data[\"test_client\"], amount=1500, unit=\"TRY\"\n    )\n    with pytest.raises(TypeError) as cm:\n        test_invoice.budget = None\n\n    assert str(cm.value) == (\n        \"Invoice.budget should be a Budget instance, not NoneType: 'None'\"\n    )\n\n\ndef test_budget_argument_is_not_a_budget_instance(setup_invoice_tests):\n    \"\"\"TypeError is raised if the Budget argument is not a Budget instance.\"\"\"\n    data = setup_invoice_tests\n    with pytest.raises(TypeError) as cm:\n        Invoice(\n            budget=\"Not a budget instance\",\n            client=data[\"test_client\"],\n            amount=1500,\n            unit=\"TRY\",\n        )\n    assert str(cm.value) == (\n        \"Invoice.budget should be a Budget instance, not str: 'Not a budget instance'\"\n    )\n\n\ndef test_budget_attribute_is_set_to_a_value_other_than_a_budget_instance(\n    setup_invoice_tests,\n):\n    \"\"\"TypeError is raised if the Budget attr is not a Budget instance.\"\"\"\n    data = setup_invoice_tests\n    test_invoice = Invoice(\n        budget=data[\"test_budget\"], client=data[\"test_client\"], amount=1500, unit=\"TRY\"\n    )\n    with pytest.raises(TypeError) as cm:\n        test_invoice.budget = \"Not a budget instance\"\n\n    assert str(cm.value) == (\n        \"Invoice.budget should be a Budget instance, not str: 'Not a budget instance'\"\n    )\n\n\ndef test_budget_argument_is_working_as_expected(setup_invoice_tests):\n    \"\"\"budget argument value is passed to the budget attribute.\"\"\"\n    data = setup_invoice_tests\n    test_invoice = Invoice(\n        budget=data[\"test_budget\"], client=data[\"test_client\"], amount=1500, unit=\"TRY\"\n    )\n    assert test_invoice.budget == data[\"test_budget\"]\n\n\ndef test_client_argument_is_skipped(setup_invoice_tests):\n    \"\"\"TypeError is raised if the client argument is skipped.\"\"\"\n    data = setup_invoice_tests\n    with pytest.raises(TypeError) as cm:\n        Invoice(budget=data[\"test_budget\"], amount=100, unit=\"TRY\")\n    assert str(cm.value) == (\n        \"Invoice.client should be a Client instance, not NoneType: 'None'\"\n    )\n\n\ndef test_client_argument_is_none(setup_invoice_tests):\n    \"\"\"TypeError is raised if the client argument is None.\"\"\"\n    data = setup_invoice_tests\n    with pytest.raises(TypeError) as cm:\n        Invoice(budget=data[\"test_budget\"], client=None, amount=100, unit=\"TRY\")\n    assert str(cm.value) == (\n        \"Invoice.client should be a Client instance, not NoneType: 'None'\"\n    )\n\n\ndef test_client_attribute_is_set_to_none(setup_invoice_tests):\n    \"\"\"TypeError is raised if the client attribute is set to None.\"\"\"\n    data = setup_invoice_tests\n    test_invoice = Invoice(\n        budget=data[\"test_budget\"], client=data[\"test_client\"], amount=100, unit=\"TRY\"\n    )\n    with pytest.raises(TypeError) as cm:\n        test_invoice.client = None\n    assert str(cm.value) == (\n        \"Invoice.client should be a Client instance, not NoneType: 'None'\"\n    )\n\n\ndef test_client_argument_is_not_a_client_instance(setup_invoice_tests):\n    \"\"\"TypeError is raised if the client argument is not a Client instance.\"\"\"\n    data = setup_invoice_tests\n    with pytest.raises(TypeError) as cm:\n        Invoice(\n            budget=data[\"test_budget\"],\n            client=\"not a client instance\",\n            amount=100,\n            unit=\"TRY\",\n        )\n    assert str(cm.value) == (\n        \"Invoice.client should be a Client instance, not str: 'not a client instance'\"\n    )\n\n\ndef test_client_attribute_is_set_to_a_value_other_than_a_client_instance(\n    setup_invoice_tests,\n):\n    \"\"\"TypeError is raised if the client attr is set to a non Client instance.\"\"\"\n    data = setup_invoice_tests\n    test_invoice = Invoice(\n        budget=data[\"test_budget\"], client=data[\"test_client\"], amount=100, unit=\"TRY\"\n    )\n    with pytest.raises(TypeError) as cm:\n        test_invoice.client = \"not a client instance\"\n    assert str(cm.value) == (\n        \"Invoice.client should be a Client instance, not str: 'not a client instance'\"\n    )\n\n\ndef test_client_argument_is_working_as_expected(setup_invoice_tests):\n    \"\"\"client argument value is correctly passed to the client attribute.\"\"\"\n    data = setup_invoice_tests\n    test_invoice = Invoice(\n        budget=data[\"test_budget\"], client=data[\"test_client\"], amount=100, unit=\"TRY\"\n    )\n    assert test_invoice.client == data[\"test_client\"]\n\n\ndef test_client_attribute_is_working_as_expected(setup_invoice_tests):\n    \"\"\"client attribute value an be changed.\"\"\"\n    data = setup_invoice_tests\n    test_invoice = Invoice(\n        budget=data[\"test_budget\"], client=data[\"test_client\"], amount=100, unit=\"TRY\"\n    )\n    test_client = Client(name=\"Test Client 2\")\n    assert test_invoice.client != test_client\n    test_invoice.client = test_client\n    assert test_invoice.client == test_client\n"
  },
  {
    "path": "tests/models/test_local_session.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the LocalSession class.\"\"\"\n\nimport datetime\nimport json\nimport os\nimport shutil\nimport tempfile\n\nimport pytest\n\nimport pytz\n\nfrom stalker import LocalSession, User, defaults\nfrom stalker.db.session import DBSession\nfrom stalker.utils import datetime_to_millis\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_local_session_tester():\n    \"\"\"Set up the LocalSession related tests.\"\"\"\n    defaults[\"local_storage_path\"] = tempfile.mktemp()\n    yield\n    shutil.rmtree(defaults.local_storage_path, True)\n\n\ndef test_save_serializes_the_class_itself(setup_local_session_tester):\n    \"\"\"save function serializes the class to the filesystem.\"\"\"\n    new_local_session = LocalSession()\n    new_local_session.save()\n\n    # check if a file is created in the users local storage\n    assert os.path.exists(\n        os.path.join(defaults.local_storage_path, defaults.local_session_data_file_name)\n    )\n\n\ndef test_save_serializes_the_class_itself_with_real_data(setup_local_session_tester):\n    \"\"\"save function serializes the class to the filesystem.\"\"\"\n    new_local_session = LocalSession()\n    new_local_session.logged_in_user_id = 1\n    new_local_session.save()\n\n    # check if a file is created in the users local storage\n    assert os.path.exists(\n        os.path.join(defaults.local_storage_path, defaults.local_session_data_file_name)\n    )\n\n\ndef test_local_session_initialized_with_previous_session_data(\n    setup_local_session_tester,\n):\n    \"\"\"new LocalSession instance the class is restored from previous session.\"\"\"\n    # test data\n    logged_in_user_id = -10\n\n    # create a local_session\n    local_session = LocalSession()\n\n    # store some data\n    local_session.logged_in_user_id = logged_in_user_id\n    local_session.save()\n\n    # now create a new LocalSession\n    local_session2 = LocalSession()\n\n    # now try to get the data back\n    assert local_session2.logged_in_user_id == logged_in_user_id\n\n\ndef test_delete_will_delete_the_session_cache(setup_local_session_tester):\n    \"\"\"LocalSession.delete() will delete the current cache file.\"\"\"\n    # create a new user\n    new_user = User(\n        name=\"Test User\",\n        login=\"test_user\",\n        email=\"test_user@users.com\",\n        password=\"secret\",\n    )\n\n    # save it to the Database\n    new_user.id = 1023\n    assert new_user.id is not None\n\n    # save it to the local storage\n    local_session = LocalSession()\n    local_session.store_user(new_user)\n\n    # save the session\n    local_session.save()\n\n    # check if the file is created\n    # check if a file is created in the users local storage\n    assert os.path.exists(\n        os.path.join(defaults.local_storage_path, defaults.local_session_data_file_name)\n    )\n\n    # now delete the session by calling delete()\n    local_session.delete()\n\n    # check if the file is gone\n    # check if a file is created in the users local storage\n    assert not os.path.exists(\n        os.path.join(defaults.local_storage_path, defaults.local_session_data_file_name)\n    )\n\n    # delete a second time\n    # this should not raise an OSError\n    local_session.delete()\n\n\ndef test_local_session_will_not_use_the_stored_data_if_it_is_invalid(\n    setup_postgresql_db,\n):\n    \"\"\"LocalSession will not use the stored session if it is not valid anymore.\"\"\"\n    # create a new user\n    new_user = User(\n        name=\"Test User\",\n        login=\"test_user\",\n        email=\"test_user@users.com\",\n        password=\"secret\",\n    )\n\n    # save it to the Database\n    DBSession.add(new_user)\n    DBSession.commit()\n    assert new_user.id is not None\n\n    # save it to the local storage\n    local_session = LocalSession()\n    local_session.store_user(new_user)\n\n    # save the session\n    local_session.save()\n\n    # set the valid time to an early date\n    local_session.valid_to = datetime.datetime.now(pytz.utc) - datetime.timedelta(10)\n\n    # pickle the data\n    data = json.dumps(\n        {\n            \"valid_to\": datetime_to_millis(local_session.valid_to),\n            \"logged_in_user_id\": -1,\n        },\n    )\n    local_session._write_data(data)\n\n    # now get it back with a new local_session\n    local_session2 = LocalSession()\n\n    assert local_session2.logged_in_user_id is None\n    assert local_session2.logged_in_user is None\n\n\ndef test_logged_in_user_returns_the_stored_user_instance_from_last_time(\n    setup_postgresql_db,\n):\n    \"\"\"logged_in_user returns the logged in user.\"\"\"\n    # create a new user\n    new_user = User(\n        name=\"Test User\",\n        login=\"test_user\",\n        email=\"test_user@users.com\",\n        password=\"secret\",\n    )\n\n    # save it to the Database\n    DBSession.add(new_user)\n    DBSession.commit()\n    assert new_user.id is not None\n\n    # save it to the local storage\n    local_session = LocalSession()\n    local_session.store_user(new_user)\n\n    # save the session\n    local_session.save()\n\n    # now get it back with a new local_session\n    local_session2 = LocalSession()\n\n    assert local_session2.logged_in_user_id == new_user.id\n    assert local_session2.logged_in_user == new_user\n"
  },
  {
    "path": "tests/models/test_message.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests related to the Message class.\"\"\"\n\nfrom stalker import Message, Status, StatusList\n\n\ndef test_message_instance_creation():\n    \"\"\"message instance creation.\"\"\"\n    status_unread = Status(name=\"Unread\", code=\"UR\")\n    status_read = Status(name=\"Read\", code=\"READ\")\n    status_replied = Status(name=\"Replied\", code=\"REP\")\n\n    message_status_list = StatusList(\n        name=\"Message Statuses\",\n        statuses=[status_unread, status_read, status_replied],\n        target_entity_type=\"Message\",\n    )\n\n    new_message = Message(\n        description=\"This is a test message\", status_list=message_status_list\n    )\n    assert new_message.description == \"This is a test message\"\n"
  },
  {
    "path": "tests/models/test_note.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Not class.\"\"\"\n\nimport pytest\n\nfrom stalker import Note\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_note_tests():\n    \"\"\"Set up the test Note related tests.\"\"\"\n    data = dict()\n    data[\"kwargs\"] = {\n        \"name\": \"Note to something\",\n        \"description\": \"this is a simple note\",\n        \"content\": \"this is a note content\",\n    }\n    # create a Note object\n    data[\"test_note\"] = Note(**data[\"kwargs\"])\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_true():\n    \"\"\"__auto_name__ class attribute is set to True for Note class.\"\"\"\n    assert Note.__auto_name__ is True\n\n\ndef test_content_argument_is_missing(setup_note_tests):\n    \"\"\"Nothing is going to happen if no content argument is given.\"\"\"\n    data = setup_note_tests\n    data[\"kwargs\"].pop(\"content\")\n    new_note = Note(**data[\"kwargs\"])\n    assert isinstance(new_note, Note)\n\n\ndef test_content_argument_is_set_to_none(setup_note_tests):\n    \"\"\"nothing is going to happen if content argument is given as None.\"\"\"\n    data = setup_note_tests\n    data[\"kwargs\"][\"content\"] = None\n    new_note = Note(**data[\"kwargs\"])\n    assert isinstance(new_note, Note)\n\n\ndef test_content_attribute_is_set_to_none(setup_note_tests):\n    \"\"\"nothing is going to happen if content attribute is set to None.\"\"\"\n    data = setup_note_tests\n    # nothing should happen\n    data[\"test_note\"].content = None\n\n\ndef test_content_argument_is_set_to_empty_string(setup_note_tests):\n    \"\"\"nothing is going to happen if content argument is given as an empty string.\"\"\"\n    data = setup_note_tests\n    data[\"kwargs\"][\"content\"] = \"\"\n    Note(**data[\"kwargs\"])\n\n\ndef test_content_attribute_is_set_to_empty_string(setup_note_tests):\n    \"\"\"nothing is going to happen if content argument is set to an empty string.\"\"\"\n    data = setup_note_tests\n    # nothing should happen\n    data[\"test_note\"].content = \"\"\n\n\ndef test_content_argument_is_set_to_something_other_than_a_string(setup_note_tests):\n    \"\"\"TypeError is raised if content arg is not a str.\"\"\"\n    data = setup_note_tests\n    test_value = 1.24\n    data[\"kwargs\"][\"content\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        Note(**data[\"kwargs\"])\n    assert str(cm.value) == \"Note.description should be a string, not float: '1.24'\"\n\n\ndef test_content_attribute_is_set_to_something_other_than_a_string(setup_note_tests):\n    \"\"\"TypeError is raised if content attr is not a string.\"\"\"\n    data = setup_note_tests\n    test_value = 1\n    with pytest.raises(TypeError) as cm:\n        data[\"test_note\"].content = test_value\n    assert str(cm.value) == \"Note.description should be a string, not int: '1'\"\n\n\ndef test_content_attribute_is_working_as_expected(setup_note_tests):\n    \"\"\"content attribute is working as expected.\"\"\"\n    data = setup_note_tests\n    new_content = (\n        \"This is my new content for the note, and I expect it to \"\n        \"work fine if I assign it to a Note object\"\n    )\n    data[\"test_note\"].content = new_content\n    assert data[\"test_note\"].content == new_content\n\n\ndef test_equality_operator(setup_note_tests):\n    \"\"\"Equality operator.\"\"\"\n    data = setup_note_tests\n    note1 = Note(**data[\"kwargs\"])\n    note2 = Note(**data[\"kwargs\"])\n    data[\"kwargs\"][\"content\"] = \"this is a different content\"\n    note3 = Note(**data[\"kwargs\"])\n    assert note1 == note2\n    assert not note1 == note3\n\n\ndef test_inequality_operator(setup_note_tests):\n    \"\"\"Inequality operator.\"\"\"\n    data = setup_note_tests\n    note1 = Note(**data[\"kwargs\"])\n    note2 = Note(**data[\"kwargs\"])\n    data[\"kwargs\"][\"content\"] = \"this is a different content\"\n    note3 = Note(**data[\"kwargs\"])\n    assert not note1 != note2\n    assert note1 != note3\n\n\ndef test_plural_class_name(setup_note_tests):\n    \"\"\"plural name of Note class.\"\"\"\n    data = setup_note_tests\n    assert data[\"test_note\"].plural_class_name == \"Notes\"\n\n\ndef test__hash__is_working_as_expected(setup_note_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_note_tests\n    result = hash(data[\"test_note\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_note\"].__hash__()\n"
  },
  {
    "path": "tests/models/test_payment.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Payment class.\"\"\"\n\nimport pytest\n\nfrom stalker import (\n    Budget,\n    Client,\n    Invoice,\n    Payment,\n    Project,\n    Repository,\n    Status,\n    StatusList,\n    Type,\n    User,\n)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_payment_tests():\n    \"\"\"Payment class related tests.\"\"\"\n    data = dict()\n    data[\"status_new\"] = Status(name=\"Mew\", code=\"NEW\")\n    data[\"status_wfd\"] = Status(name=\"Waiting For Dependency\", code=\"WFD\")\n    data[\"status_rts\"] = Status(name=\"Ready To Start\", code=\"RTS\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_prev\"] = Status(name=\"Pending Review\", code=\"PREV\")\n    data[\"status_hrev\"] = Status(name=\"Has Revision\", code=\"HREV\")\n    data[\"status_drev\"] = Status(name=\"Dependency Has Revision\", code=\"DREV\")\n    data[\"status_oh\"] = Status(name=\"On Hold\", code=\"OH\")\n    data[\"status_stop\"] = Status(name=\"Stopped\", code=\"STOP\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n    data[\"status_new\"] = Status(name=\"New\", code=\"NEW\")\n    data[\"status_app\"] = Status(name=\"Approved\", code=\"APP\")\n\n    data[\"budget_status_list\"] = StatusList(\n        name=\"Budget Statuses\",\n        target_entity_type=\"Budget\",\n        statuses=[data[\"status_new\"], data[\"status_prev\"], data[\"status_app\"]],\n    )\n\n    data[\"task_status_list\"] = StatusList(\n        name=\"Task Statuses\",\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_oh\"],\n            data[\"status_stop\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n\n    data[\"test_project_status_list\"] = StatusList(\n        name=\"Project Statuses\",\n        statuses=[data[\"status_wip\"], data[\"status_prev\"], data[\"status_cmpl\"]],\n        target_entity_type=Project,\n    )\n\n    data[\"test_movie_project_type\"] = Type(\n        name=\"Movie Project\",\n        code=\"movie\",\n        target_entity_type=Project,\n    )\n\n    data[\"test_repository_type\"] = Type(\n        name=\"Test Repository Type\",\n        code=\"test\",\n        target_entity_type=Repository,\n    )\n\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"test_repository_type\"],\n    )\n\n    data[\"test_user1\"] = User(\n        name=\"User1\", login=\"user1\", email=\"user1@user1.com\", password=\"1234\"\n    )\n\n    data[\"test_user2\"] = User(\n        name=\"User2\", login=\"user2\", email=\"user2@user2.com\", password=\"1234\"\n    )\n\n    data[\"test_user3\"] = User(\n        name=\"User3\", login=\"user3\", email=\"user3@user3.com\", password=\"1234\"\n    )\n\n    data[\"test_user4\"] = User(\n        name=\"User4\", login=\"user4\", email=\"user4@user4.com\", password=\"1234\"\n    )\n\n    data[\"test_user5\"] = User(\n        name=\"User5\", login=\"user5\", email=\"user5@user5.com\", password=\"1234\"\n    )\n\n    data[\"test_client\"] = Client(\n        name=\"Test Client\",\n    )\n\n    data[\"test_project\"] = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=data[\"test_movie_project_type\"],\n        status_list=data[\"test_project_status_list\"],\n        repository=data[\"test_repository\"],\n        clients=[data[\"test_client\"]],\n    )\n\n    data[\"test_budget\"] = Budget(\n        project=data[\"test_project\"],\n        name=\"Test Budget 1\",\n        status_list=data[\"budget_status_list\"],\n    )\n\n    data[\"test_invoice\"] = Invoice(\n        budget=data[\"test_budget\"], client=data[\"test_client\"], amount=1500, unit=\"TRY\"\n    )\n    return data\n\n\ndef test_creating_a_payment_instance(setup_payment_tests):\n    \"\"\"Payment instance creation.\"\"\"\n    data = setup_payment_tests\n    payment = Payment(invoice=data[\"test_invoice\"], amount=1000, unit=\"TRY\")\n    assert isinstance(payment, Payment)\n\n\ndef test_invoice_argument_is_skipped(setup_payment_tests):\n    \"\"\"TypeError is raised if the invoice argument is skipped.\"\"\"\n    data = setup_payment_tests\n    with pytest.raises(TypeError) as cm:\n        _ = Payment(amount=1499, unit=\"TRY\")\n\n    assert str(cm.value) == (\n        \"Payment.invoice should be an Invoice instance, not NoneType: 'None'\"\n    )\n\n\ndef test_invoice_argument_is_none(setup_payment_tests):\n    \"\"\"TypeError is raised if the invoice argument is None.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = Payment(invoice=None, amount=1499, unit=\"TRY\")\n\n    assert str(cm.value) == (\n        \"Payment.invoice should be an Invoice instance, not NoneType: 'None'\"\n    )\n\n\ndef test_invoice_attribute_is_none(setup_payment_tests):\n    \"\"\"TypeError is raised if the invoice attribute is set to None.\"\"\"\n    data = setup_payment_tests\n    p = Payment(invoice=data[\"test_invoice\"], amount=1499, unit=\"TRY\")\n\n    with pytest.raises(TypeError) as cm:\n        p.invoice = None\n\n    assert str(cm.value) == (\n        \"Payment.invoice should be an Invoice instance, not NoneType: 'None'\"\n    )\n\n\ndef test_invoice_argument_is_not_an_invoice_instance():\n    \"\"\"TypeError is raised if the invoice argument is not an Invoice instance.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = Payment(invoice=\"not an invoice instance\", amount=1499, unit=\"TRY\")\n\n    assert str(cm.value) == (\n        \"Payment.invoice should be an Invoice instance, \"\n        \"not str: 'not an invoice instance'\"\n    )\n\n\ndef test_invoice_attribute_is_set_to_a_value_other_than_an_invoice_instance(\n    setup_payment_tests,\n):\n    \"\"\"TypeError is raised if the invoice attr is not an Invoice instance.\"\"\"\n    data = setup_payment_tests\n    p = Payment(invoice=data[\"test_invoice\"], amount=1499, unit=\"TRY\")\n\n    with pytest.raises(TypeError) as cm:\n        p.invoice = \"not an invoice instance\"\n\n    assert str(cm.value) == (\n        \"Payment.invoice should be an Invoice instance, \"\n        \"not str: 'not an invoice instance'\"\n    )\n\n\ndef test_invoice_argument_is_working_as_expected(setup_payment_tests):\n    \"\"\"invoice argument value is correctly passed to the invoice attribute.\"\"\"\n    data = setup_payment_tests\n    p = Payment(invoice=data[\"test_invoice\"], amount=1499, unit=\"TRY\")\n    assert p.invoice == data[\"test_invoice\"]\n\n\ndef test_invoice_attribute_is_working_as_expected(setup_payment_tests):\n    \"\"\"invoice attribute value can be correctly changed.\"\"\"\n    data = setup_payment_tests\n    p = Payment(invoice=data[\"test_invoice\"], amount=1499, unit=\"TRY\")\n    new_invoice = Invoice(\n        budget=data[\"test_budget\"], client=data[\"test_client\"], amount=2500, unit=\"TRY\"\n    )\n    assert p.invoice != new_invoice\n    p.invoice = new_invoice\n    assert p.invoice == new_invoice\n"
  },
  {
    "path": "tests/models/test_permission.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Permission class tests.\"\"\"\n\nimport sys\n\nimport pytest\n\nfrom stalker.models.auth import Permission\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_permission_tests():\n    \"\"\"Set up the tests for the Permission class.\"\"\"\n    data = dict()\n    data[\"kwargs\"] = {\"access\": \"Allow\", \"action\": \"Create\", \"class_name\": \"Project\"}\n    data[\"test_permission\"] = Permission(**data[\"kwargs\"])\n    return data\n\n\ndef test_access_argument_is_skipped(setup_permission_tests):\n    \"\"\"TypeError is raised if the access argument is skipped.\"\"\"\n    data = setup_permission_tests\n    data[\"kwargs\"].pop(\"access\")\n    with pytest.raises(TypeError) as cm:\n        Permission(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"__init__() missing 1 required positional argument: 'access'\"\n    )\n\n\ndef test_access_argument_is_none(setup_permission_tests):\n    \"\"\"TypeError is raised if the access argument is None.\"\"\"\n    data = setup_permission_tests\n    data[\"kwargs\"][\"access\"] = None\n    with pytest.raises(TypeError) as cm:\n        Permission(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Permission.access should be an instance of str, not NoneType: 'None'\"\n    )\n\n\ndef test_access_argument_accepts_only_allow_or_deny_as_value(setup_permission_tests):\n    \"\"\"ValueError is raised if access is something other than 'Allow' or 'Deny'.\"\"\"\n    data = setup_permission_tests\n    data[\"kwargs\"][\"access\"] = \"Allowed\"\n    with pytest.raises(ValueError) as cm:\n        Permission(**data[\"kwargs\"])\n    assert str(cm.value) == 'Permission.access should be \"Allow\" or \"Deny\" not Allowed'\n\n\ndef test_access_argument_is_setting_access_attribute_value(setup_permission_tests):\n    \"\"\"access argument is setting the access attribute value correctly.\"\"\"\n    data = setup_permission_tests\n    assert data[\"kwargs\"][\"access\"] == data[\"test_permission\"].access\n\n\ndef test_access_attribute_is_read_only(setup_permission_tests):\n    \"\"\"access attribute is read only.\"\"\"\n    data = setup_permission_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_permission\"].access = \"Deny\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'Permission' object has no setter\",\n        12: \"property of 'Permission' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_access_getter' of 'Permission' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_action_argument_is_skipped_will_raise_a_type_error(setup_permission_tests):\n    \"\"\"TypeError is raised if the action argument is skipped.\"\"\"\n    data = setup_permission_tests\n    data[\"kwargs\"].pop(\"action\")\n    with pytest.raises(TypeError) as cm:\n        Permission(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"__init__() missing 1 required positional argument: 'action'\"\n    )\n\n\ndef test_action_argument_is_none(setup_permission_tests):\n    \"\"\"TypeError is raised if the action argument is set to None.\"\"\"\n    data = setup_permission_tests\n    data[\"kwargs\"][\"action\"] = None\n    with pytest.raises(TypeError) as cm:\n        Permission(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Permission.action should be an instance of str, not NoneType: 'None'\"\n    )\n\n\ndef test_action_argument_accepts_default_values_only(setup_permission_tests):\n    \"\"\"ValueError is raised if the action arg is not in the defaults.DEFAULT_ACTIONS.\"\"\"\n    data = setup_permission_tests\n    data[\"kwargs\"][\"action\"] = \"Add\"\n    with pytest.raises(ValueError) as cm:\n        Permission(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"Permission.action should be one of the values of ['Create', \"\n        \"'Read', 'Update', 'Delete', 'List'] not 'Add'\"\n    )\n\n\ndef test_action_argument_is_setting_the_argument_attribute(setup_permission_tests):\n    \"\"\"action argument is setting the argument attribute value.\"\"\"\n    data = setup_permission_tests\n    assert data[\"kwargs\"][\"action\"] == data[\"test_permission\"].action\n\n\ndef test_action_attribute_is_read_only(setup_permission_tests):\n    \"\"\"action attribute is read only.\"\"\"\n    data = setup_permission_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_permission\"].action = \"Add\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'Permission' object has no setter\",\n        12: \"property of 'Permission' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_action_getter' of 'Permission' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_class_name_argument_skipped(setup_permission_tests):\n    \"\"\"TypeError is raised if the class_name argument is skipped.\"\"\"\n    data = setup_permission_tests\n    data[\"kwargs\"].pop(\"class_name\")\n    with pytest.raises(TypeError) as cm:\n        Permission(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"__init__() missing 1 required positional argument: \"\n        \"'class_name'\"\n    )\n\n\ndef test_class_name_argument_is_none(setup_permission_tests):\n    \"\"\"TypeError is raised if the class_name argument is None.\"\"\"\n    data = setup_permission_tests\n    data[\"kwargs\"][\"class_name\"] = None\n    with pytest.raises(TypeError) as cm:\n        Permission(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Permission.class_name should be an instance of str, not NoneType: 'None'\"\n    )\n\n\ndef test_class_name_argument_is_not_a_string(setup_permission_tests):\n    \"\"\"TypeError is raised if the class_name argument is not a string instance.\"\"\"\n    data = setup_permission_tests\n    data[\"kwargs\"][\"class_name\"] = 10\n    with pytest.raises(TypeError) as cm:\n        Permission(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Permission.class_name should be an instance of str, not int: '10'\"\n    )\n\n\ndef test_class_name_argument_is_setting_the_class_name_attribute_value(\n    setup_permission_tests,\n):\n    \"\"\"class_name argument value is correctly passed to the class_name attribute.\"\"\"\n    data = setup_permission_tests\n    assert data[\"test_permission\"].class_name == data[\"kwargs\"][\"class_name\"]\n\n\ndef test_class_name_attribute_is_read_only(setup_permission_tests):\n    \"\"\"class_name attribute is read only.\"\"\"\n    data = setup_permission_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_permission\"].class_name = \"Asset\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'Permission' object has no setter\",\n        12: \"property of 'Permission' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_class_name_getter' of 'Permission' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_hash_value(setup_permission_tests):\n    \"\"\"__hash__ returns the hash of the Permission instance.\"\"\"\n    data = setup_permission_tests\n    result = hash(data[\"test_permission\"])\n    assert isinstance(result, int)\n"
  },
  {
    "path": "tests/models/test_price_list.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the PriceList class.\"\"\"\n\nimport pytest\n\nfrom stalker import Good, PriceList\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_price_list_tests():\n    \"\"\"Set up tests for the PriceList class.\"\"\"\n    data = dict()\n    data[\"kwargs\"] = {\n        \"name\": \"Test Price List\",\n    }\n    return data\n\n\ndef test_goods_argument_is_skipped(setup_price_list_tests):\n    \"\"\"goods attribute is an empty list if the goods argument is skipped.\"\"\"\n    data = setup_price_list_tests\n    p = PriceList(**data[\"kwargs\"])\n    assert p.goods == []\n\n\ndef test_goods_argument_is_none(setup_price_list_tests):\n    \"\"\"goods attribute is an empty list if the goods argument is None.\"\"\"\n    data = setup_price_list_tests\n    data[\"kwargs\"][\"goods\"] = None\n    p = PriceList(**data[\"kwargs\"])\n    assert p.goods == []\n\n\ndef test_goods_attribute_is_none(setup_price_list_tests):\n    \"\"\"TypeError is raised if the goods attribute is set to None.\"\"\"\n    data = setup_price_list_tests\n    g1 = Good(name=\"Test Good\")\n    data[\"kwargs\"][\"goods\"] = [g1]\n    p = PriceList(**data[\"kwargs\"])\n    assert p.goods == [g1]\n\n    with pytest.raises(TypeError) as cm:\n        p.goods = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_goods_argument_is_not_a_list(setup_price_list_tests):\n    \"\"\"TypeError is raised if the goods argument value is not a list.\"\"\"\n    data = setup_price_list_tests\n    data[\"kwargs\"][\"goods\"] = \"this is not a list\"\n    with pytest.raises(TypeError) as cm:\n        PriceList(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_goods_attribute_is_not_a_list(setup_price_list_tests):\n    \"\"\"TypeError is raised if the goods attribute is not a list.\"\"\"\n    data = setup_price_list_tests\n    g1 = Good(name=\"Test Good\")\n    data[\"kwargs\"][\"goods\"] = [g1]\n    p = PriceList(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        p.goods = \"this is not a list\"\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_goods_argument_is_a_list_of_objects_which_are_not_goods(\n    setup_price_list_tests,\n):\n    \"\"\"TypeError is raised if the goods argument is not a list of Good instances.\"\"\"\n    data = setup_price_list_tests\n    data[\"kwargs\"][\"goods\"] = [\"not\", 1, \"good\", \"instances\"]\n    with pytest.raises(TypeError) as cm:\n        PriceList(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"PriceList.goods should only contain instances of \"\n        \"stalker.model.budget.Good, not str: 'not'\"\n    )\n\n\ndef test_good_attribute_is_a_list_of_objects_which_are_not_goods(\n    setup_price_list_tests,\n):\n    \"\"\"TypeError is raised if the goods attribute is not a list of Good instances.\"\"\"\n    data = setup_price_list_tests\n    p = PriceList(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        p.goods = [\"not\", 1, \"good\", \"instances\"]\n\n    assert str(cm.value) == (\n        \"PriceList.goods should only contain instances of \"\n        \"stalker.model.budget.Good, not str: 'not'\"\n    )\n\n\ndef test_good_argument_is_working_as_expected(setup_price_list_tests):\n    \"\"\"good argument value is passed to the good attribute.\"\"\"\n    data = setup_price_list_tests\n    g1 = Good(name=\"Good1\")\n    g2 = Good(name=\"Good2\")\n    g3 = Good(name=\"Good3\")\n    test_value = [g1, g2, g3]\n    data[\"kwargs\"][\"goods\"] = test_value\n    p = PriceList(**data[\"kwargs\"])\n    assert p.goods == test_value\n\n\ndef test_good_attribute_is_working_as_expected(setup_price_list_tests):\n    \"\"\"good attribute value can be set.\"\"\"\n    data = setup_price_list_tests\n    g1 = Good(name=\"Good1\")\n    g2 = Good(name=\"Good2\")\n    g3 = Good(name=\"Good3\")\n    test_value = [g1, g2, g3]\n    p = PriceList(**data[\"kwargs\"])\n    assert p.goods != test_value\n    p.goods = test_value\n    assert p.goods == test_value\n"
  },
  {
    "path": "tests/models/test_project.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Project class.\"\"\"\n\nimport datetime\nimport logging\nimport re\nimport sys\n\nimport pytest\n\nimport pytz\n\nfrom stalker import (\n    log,\n    Asset,\n    Entity,\n    Client,\n    ImageFormat,\n    File,\n    Project,\n    Repository,\n    Sequence,\n    Shot,\n    Status,\n    StatusList,\n    Structure,\n    Task,\n    Ticket,\n    TimeLog,\n    Type,\n    User,\n)\nfrom stalker.db.session import DBSession\nfrom stalker.models.ticket import FIXED, CANTFIX, INVALID\n\nlogger = logging.getLogger(\"stalker.models.project\")\nlog.register_logger(logger)\n\n\ndef condition_tjp_output(data: str) -> str:\n    \"\"\"Conditions the tjp output.\n\n    Args:\n        data (str): The data.\n\n    Returns:\n        str: The formatted data.\n    \"\"\"\n    assert isinstance(data, str)\n    data_out = re.subn(r\"[\\s]+\", \" \", data)[0]\n    return data_out\n\n\n@pytest.mark.parametrize(\n    \"get_data_file\",\n    [[\"project_to_tjp_output_rendered\", \"project_to_tjp_output_formatted\"]],\n    indirect=True,\n)\ndef test_condition_tjp_output_is_working_as_expected(get_data_file):\n    \"\"\"condition_tjp_output_is_working_as_expected.\"\"\"\n    test_data = get_data_file\n    rendered_path = test_data[0]\n    formatted_path = test_data[1]\n    with open(rendered_path, \"r\") as f:\n        rendered_data = f.read()\n    with open(formatted_path, \"r\") as f:\n        formatted_data = f.read()\n\n    assert condition_tjp_output(rendered_data) == formatted_data\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_project_db_test(setup_postgresql_db):\n    \"\"\"Set up the Project class tests.\"\"\"\n    data = dict()\n\n    # create test objects\n    data[\"start\"] = datetime.datetime(2016, 11, 17, tzinfo=pytz.utc)\n    data[\"end\"] = data[\"start\"] + datetime.timedelta(days=20)\n\n    data[\"test_lead\"] = User(\n        name=\"lead\", login=\"lead\", email=\"lead@lead.com\", password=\"lead\"\n    )\n\n    data[\"test_user1\"] = User(\n        name=\"User1\", login=\"user1\", email=\"user1@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user2\"] = User(\n        name=\"User2\", login=\"user2\", email=\"user2@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user3\"] = User(\n        name=\"User3\", login=\"user3\", email=\"user3@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user4\"] = User(\n        name=\"User4\", login=\"user4\", email=\"user4@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user5\"] = User(\n        name=\"User5\", login=\"user5\", email=\"user5@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user6\"] = User(\n        name=\"User6\", login=\"user6\", email=\"user6@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user7\"] = User(\n        name=\"User7\", login=\"user7\", email=\"user7@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user8\"] = User(\n        name=\"User8\", login=\"user8\", email=\"user8@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user9\"] = User(\n        name=\"user9\", login=\"user9\", email=\"user9@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user10\"] = User(\n        name=\"User10\", login=\"user10\", email=\"user10@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user_client\"] = User(\n        name=\"User Client\",\n        login=\"userClient\",\n        email=\"user@client.com\",\n        password=\"123456\",\n    )\n    DBSession.save(\n        [\n            data[\"test_lead\"],\n            data[\"test_user1\"],\n            data[\"test_user2\"],\n            data[\"test_user3\"],\n            data[\"test_user4\"],\n            data[\"test_user5\"],\n            data[\"test_user6\"],\n            data[\"test_user7\"],\n            data[\"test_user8\"],\n            data[\"test_user9\"],\n            data[\"test_user10\"],\n            data[\"test_user_client\"],\n        ]\n    )\n\n    data[\"test_image_format\"] = ImageFormat(\n        name=\"HD\",\n        width=1920,\n        height=1080,\n    )\n\n    # type for project\n    data[\"test_project_type\"] = Type(\n        name=\"Project Type 1\", code=\"projt1\", target_entity_type=\"Project\"\n    )\n\n    data[\"test_project_type2\"] = Type(\n        name=\"Project Type 2\", code=\"projt2\", target_entity_type=\"Project\"\n    )\n\n    # type for structure\n    data[\"test_structure_type1\"] = Type(\n        name=\"Structure Type 1\", code=\"struct1\", target_entity_type=\"Structure\"\n    )\n\n    data[\"test_structure_type2\"] = Type(\n        name=\"Structure Type 2\", code=\"struct2\", target_entity_type=\"Structure\"\n    )\n\n    data[\"test_project_structure\"] = Structure(\n        name=\"Project Structure 1\",\n        type=data[\"test_structure_type1\"],\n    )\n\n    data[\"test_project_structure2\"] = Structure(\n        name=\"Project Structure 2\",\n        type=data[\"test_structure_type2\"],\n    )\n\n    data[\"test_repo1\"] = Repository(\n        name=\"Commercials Repository 1\",\n        code=\"CR1\",\n    )\n\n    data[\"test_repo2\"] = Repository(name=\"Commercials Repository 2\", code=\"CR2\")\n\n    data[\"test_client\"] = Client(name=\"Test Company\", users=[data[\"test_user_client\"]])\n    DBSession.save(\n        [\n            data[\"test_image_format\"],\n            data[\"test_project_type\"],\n            data[\"test_project_type2\"],\n            data[\"test_structure_type1\"],\n            data[\"test_structure_type2\"],\n            data[\"test_project_structure\"],\n            data[\"test_project_structure2\"],\n            data[\"test_repo1\"],\n            data[\"test_repo2\"],\n            data[\"test_client\"],\n        ]\n    )\n\n    # create a project object\n    data[\"kwargs\"] = {\n        \"name\": \"Test Project\",\n        \"code\": \"tp\",\n        \"description\": \"This is a project object for testing purposes\",\n        \"image_format\": data[\"test_image_format\"],\n        \"fps\": 25,\n        \"type\": data[\"test_project_type\"],\n        \"structure\": data[\"test_project_structure\"],\n        \"repositories\": [data[\"test_repo1\"], data[\"test_repo2\"]],\n        \"is_stereoscopic\": False,\n        \"display_width\": 15,\n        \"start\": data[\"start\"],\n        \"end\": data[\"end\"],\n        \"clients\": [data[\"test_client\"]],\n    }\n\n    data[\"test_project\"] = Project(**data[\"kwargs\"])\n    DBSession.add(data[\"test_project\"])\n    DBSession.commit()\n\n    # sequences without tasks\n    data[\"test_seq1\"] = Sequence(\n        name=\"Seq1\",\n        code=\"Seq1\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_user1\"]],\n        responsible=[data[\"test_user1\"]],\n    )\n\n    data[\"test_seq2\"] = Sequence(\n        name=\"Seq2\",\n        code=\"Seq2\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_user2\"]],\n        responsible=[data[\"test_user2\"]],\n    )\n\n    data[\"test_seq3\"] = Sequence(\n        name=\"Seq3\",\n        code=\"Seq3\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_user3\"]],\n        responsible=[data[\"test_user1\"]],\n    )\n\n    # sequences with tasks\n    data[\"test_seq4\"] = Sequence(\n        name=\"Seq4\",\n        code=\"Seq4\",\n        project=data[\"test_project\"],\n        responsible=[data[\"test_user1\"]],\n    )\n\n    data[\"test_seq5\"] = Sequence(\n        name=\"Seq5\",\n        code=\"Seq5\",\n        project=data[\"test_project\"],\n        responsible=[data[\"test_user1\"]],\n    )\n\n    # sequences without tasks but with shots\n    data[\"test_seq6\"] = Sequence(\n        name=\"Seq6\",\n        code=\"Seq6\",\n        project=data[\"test_project\"],\n        responsible=[data[\"test_user1\"]],\n    )\n\n    data[\"test_seq7\"] = Sequence(\n        name=\"Seq7\",\n        code=\"Seq7\",\n        project=data[\"test_project\"],\n        responsible=[data[\"test_user1\"]],\n    )\n\n    # shots\n    data[\"test_shot1\"] = Shot(\n        code=\"SH001\",\n        project=data[\"test_project\"],\n        sequence=data[\"test_seq6\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    data[\"test_shot2\"] = Shot(\n        code=\"SH002\",\n        project=data[\"test_project\"],\n        sequence=data[\"test_seq6\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    data[\"test_shot3\"] = Shot(\n        code=\"SH003\",\n        project=data[\"test_project\"],\n        sequence=data[\"test_seq7\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    data[\"test_shot4\"] = Shot(\n        code=\"SH004\",\n        project=data[\"test_project\"],\n        sequence=data[\"test_seq7\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    # asset types\n    data[\"asset_type\"] = Type(\n        name=\"Character\",\n        code=\"char\",\n        target_entity_type=\"Asset\",\n    )\n\n    # assets without tasks\n    data[\"test_asset1\"] = Asset(\n        name=\"Test Asset 1\",\n        code=\"ta1\",\n        type=data[\"asset_type\"],\n        project=data[\"test_project\"],\n        resources=[data[\"test_user2\"]],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    data[\"test_asset2\"] = Asset(\n        name=\"Test Asset 2\",\n        code=\"ta2\",\n        type=data[\"asset_type\"],\n        project=data[\"test_project\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    data[\"test_asset3\"] = Asset(\n        name=\"Test Asset 3\",\n        code=\"ta3\",\n        type=data[\"asset_type\"],\n        project=data[\"test_project\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    # assets with tasks\n    data[\"test_asset4\"] = Asset(\n        name=\"Test Asset 4\",\n        code=\"ta4\",\n        type=data[\"asset_type\"],\n        project=data[\"test_project\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    data[\"test_asset5\"] = Asset(\n        name=\"Test Asset 5\",\n        code=\"ta5\",\n        type=data[\"asset_type\"],\n        project=data[\"test_project\"],\n    )\n\n    # for project\n    data[\"test_task1\"] = Task(\n        name=\"Test Task 1\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_user1\"]],\n    )\n\n    data[\"test_task2\"] = Task(\n        name=\"Test Task 2\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_user2\"]],\n    )\n\n    data[\"test_task3\"] = Task(\n        name=\"Test Task 3\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_user3\"]],\n    )\n\n    # for sequence4\n    data[\"test_task4\"] = Task(\n        name=\"Test Task 4\",\n        parent=data[\"test_seq4\"],\n        resources=[data[\"test_user4\"]],\n    )\n\n    data[\"test_task5\"] = Task(\n        name=\"Test Task 5\",\n        parent=data[\"test_seq4\"],\n        resources=[data[\"test_user5\"]],\n    )\n\n    data[\"test_task6\"] = Task(\n        name=\"Test Task 6\",\n        parent=data[\"test_seq4\"],\n        resources=[data[\"test_user6\"]],\n    )\n\n    # for sequence5\n    data[\"test_task7\"] = Task(\n        name=\"Test Task 7\",\n        parent=data[\"test_seq5\"],\n        resources=[data[\"test_user7\"]],\n    )\n\n    data[\"test_task8\"] = Task(\n        name=\"Test Task 8\",\n        parent=data[\"test_seq5\"],\n        resources=[data[\"test_user8\"]],\n    )\n\n    data[\"test_task9\"] = Task(\n        name=\"Test Task 9\",\n        parent=data[\"test_seq5\"],\n        resources=[data[\"test_user9\"]],\n    )\n\n    # for shot1 of sequence6\n    data[\"test_task10\"] = Task(\n        name=\"Test Task 10\",\n        parent=data[\"test_shot1\"],\n        resources=[data[\"test_user10\"]],\n        schedule_timing=10,\n    )\n\n    data[\"test_task11\"] = Task(\n        name=\"Test Task 11\",\n        parent=data[\"test_shot1\"],\n        resources=[data[\"test_user1\"], data[\"test_user2\"]],\n    )\n\n    data[\"test_task12\"] = Task(\n        name=\"Test Task 12\",\n        parent=data[\"test_shot1\"],\n        resources=[data[\"test_user3\"], data[\"test_user4\"]],\n    )\n\n    # for shot2 of sequence6\n    data[\"test_task13\"] = Task(\n        name=\"Test Task 13\",\n        parent=data[\"test_shot2\"],\n        resources=[data[\"test_user5\"], data[\"test_user6\"]],\n    )\n\n    data[\"test_task14\"] = Task(\n        name=\"Test Task 14\",\n        parent=data[\"test_shot2\"],\n        resources=[data[\"test_user7\"], data[\"test_user8\"]],\n    )\n\n    data[\"test_task15\"] = Task(\n        name=\"Test Task 15\",\n        parent=data[\"test_shot2\"],\n        resources=[data[\"test_user9\"], data[\"test_user10\"]],\n    )\n\n    # for shot3 of sequence7\n    data[\"test_task16\"] = Task(\n        name=\"Test Task 16\",\n        parent=data[\"test_shot3\"],\n        resources=[data[\"test_user1\"], data[\"test_user2\"], data[\"test_user3\"]],\n    )\n\n    data[\"test_task17\"] = Task(\n        name=\"Test Task 17\",\n        parent=data[\"test_shot3\"],\n        resources=[data[\"test_user4\"], data[\"test_user5\"], data[\"test_user6\"]],\n    )\n\n    data[\"test_task18\"] = Task(\n        name=\"Test Task 18\",\n        parent=data[\"test_shot3\"],\n        resources=[data[\"test_user7\"], data[\"test_user8\"], data[\"test_user9\"]],\n    )\n\n    # for shot4 of sequence7\n    data[\"test_task19\"] = Task(\n        name=\"Test Task 19\",\n        parent=data[\"test_shot4\"],\n        resources=[data[\"test_user10\"], data[\"test_user1\"], data[\"test_user2\"]],\n    )\n\n    data[\"test_task20\"] = Task(\n        name=\"Test Task 20\",\n        parent=data[\"test_shot4\"],\n        resources=[data[\"test_user3\"], data[\"test_user4\"], data[\"test_user5\"]],\n    )\n\n    data[\"test_task21\"] = Task(\n        name=\"Test Task 21\",\n        parent=data[\"test_shot4\"],\n        resources=[data[\"test_user6\"], data[\"test_user7\"], data[\"test_user8\"]],\n    )\n\n    # for asset4\n    data[\"test_task22\"] = Task(\n        name=\"Test Task 22\",\n        parent=data[\"test_asset4\"],\n        resources=[data[\"test_user9\"], data[\"test_user10\"], data[\"test_user1\"]],\n    )\n\n    data[\"test_task23\"] = Task(\n        name=\"Test Task 23\",\n        parent=data[\"test_asset4\"],\n        resources=[data[\"test_user2\"], data[\"test_user3\"]],\n    )\n\n    data[\"test_task24\"] = Task(\n        name=\"Test Task 24\",\n        parent=data[\"test_asset4\"],\n        resources=[data[\"test_user4\"], data[\"test_user5\"]],\n    )\n\n    # for asset5\n    data[\"test_task25\"] = Task(\n        name=\"Test Task 25\",\n        parent=data[\"test_asset5\"],\n        resources=[data[\"test_user6\"], data[\"test_user7\"]],\n    )\n\n    data[\"test_task26\"] = Task(\n        name=\"Test Task 26\",\n        parent=data[\"test_asset5\"],\n        resources=[data[\"test_user8\"], data[\"test_user9\"]],\n    )\n\n    data[\"test_task27\"] = Task(\n        name=\"Test Task 27\",\n        parent=data[\"test_asset5\"],\n        resources=[data[\"test_user10\"], data[\"test_user1\"]],\n    )\n\n    # final task hierarchy\n    # test_seq1\n    # test_seq2\n    # test_seq3\n    #\n    # test_seq4\n    #     test_task4\n    #     test_task5\n    #     test_task6\n    # test_seq5\n    #     test_task7\n    #     test_task8\n    #     test_task9\n    # test_seq6\n    # test_seq7\n    #\n    # test_shot1\n    #     test_task10\n    #     test_task11\n    #     test_task12\n    # test_shot2\n    #     test_task13\n    #     test_task14\n    #     test_task15\n    # test_shot3\n    #     test_task16\n    #     test_task17\n    #     test_task18\n    # test_shot4\n    #     test_task19\n    #     test_task20\n    #     test_task21\n    #\n    # test_asset1\n    # test_asset2\n    # test_asset3\n    # test_asset4\n    #     test_task22\n    #     test_task23\n    #     test_task24\n    # test_asset5\n    #     test_task25\n    #     test_task26\n    #     test_task27\n    #\n    # test_task1\n    # test_task2\n    # test_task3\n    DBSession.save(\n        [\n            data[\"test_seq1\"],\n            data[\"test_seq2\"],\n            data[\"test_seq3\"],\n            data[\"test_seq4\"],\n            data[\"test_seq5\"],\n            data[\"test_seq6\"],\n            data[\"test_seq7\"],\n            data[\"test_shot1\"],\n            data[\"test_shot2\"],\n            data[\"test_shot3\"],\n            data[\"test_shot4\"],\n            data[\"test_asset1\"],\n            data[\"test_asset2\"],\n            data[\"test_asset3\"],\n            data[\"test_asset4\"],\n            data[\"test_asset5\"],\n            data[\"test_task1\"],\n            data[\"test_task2\"],\n            data[\"test_task3\"],\n            data[\"test_task4\"],\n            data[\"test_task5\"],\n            data[\"test_task6\"],\n            data[\"test_task7\"],\n            data[\"test_task8\"],\n            data[\"test_task9\"],\n            data[\"test_task10\"],\n            data[\"test_task11\"],\n            data[\"test_task12\"],\n            data[\"test_task13\"],\n            data[\"test_task14\"],\n            data[\"test_task15\"],\n            data[\"test_task16\"],\n            data[\"test_task17\"],\n            data[\"test_task18\"],\n            data[\"test_task19\"],\n            data[\"test_task20\"],\n            data[\"test_task21\"],\n            data[\"test_task22\"],\n            data[\"test_task23\"],\n            data[\"test_task24\"],\n            data[\"test_task25\"],\n            data[\"test_task26\"],\n            data[\"test_task27\"],\n        ]\n    )\n\n    DBSession.add(data[\"test_project\"])\n    DBSession.commit()\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Project class.\"\"\"\n    assert Project.__auto_name__ is False\n\n\ndef test_setup_is_working_correctly(setup_project_db_test):\n    \"\"\"Setup is done correctly.\"\"\"\n    data = setup_project_db_test\n    assert isinstance(data[\"test_project_type\"], Type)\n    assert isinstance(data[\"test_project_type2\"], Type)\n\n\ndef test_sequences_attribute_is_read_only(setup_project_db_test):\n    \"\"\"Sequence attribute is read-only.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_project\"].sequences = [\"some non sequence related data\"]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'sequences'\",\n    }.get(\n        sys.version_info.minor, \"property 'sequences' of 'Project' object has no setter\"\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_assets_attribute_is_read_only(setup_project_db_test):\n    \"\"\"assets attribute is read only.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(AttributeError) as _:\n        data[\"test_project\"].assets = [\"some list\"]\n\n\ndef test_image_format_argument_is_skipped(setup_project_db_test):\n    \"\"\"image_format attribute is None if the image_format argument is skipped.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"].pop(\"image_format\")\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.image_format is None\n\n\ndef test_image_format_argument_is_none(setup_project_db_test):\n    \"\"\"nothing is going to happen if the image_format is set to None.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"image_format\"] = None\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.image_format is None\n\n\ndef test_image_format_attribute_is_set_to_none(setup_project_db_test):\n    \"\"\"nothing will happen if the image_format attribute is set to None.\"\"\"\n    data = setup_project_db_test\n    data[\"test_project\"].image_format = None\n\n\ndef test_image_format_argument_accepts_image_format_only(setup_project_db_test):\n    \"\"\"TypeError is raised if the image_format argument value is not an ImageFormat.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"image_format\"] = \"a str\"\n    with pytest.raises(TypeError) as cm:\n        Project(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Project.image_format should be an instance of \"\n        \"stalker.models.format.ImageFormat, not str: 'a str'\"\n    )\n\n\ndef test_image_format_argument_is_working_as_expected(setup_project_db_test):\n    \"\"\"image_format argument value is correctly passed to the image_format attribute.\"\"\"\n    data = setup_project_db_test\n    # and a proper image format\n    data[\"kwargs\"][\"image_format\"] = data[\"test_image_format\"]\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.image_format == data[\"test_image_format\"]\n\n\n@pytest.mark.parametrize(\"test_value\", [1, 1.2, \"a str\", [\"a\", \"list\"], {\"a\": \"dict\"}])\ndef test_image_format_attribute_accepts_image_format_only(\n    setup_project_db_test, test_value\n):\n    \"\"\"TypeError is raised if the image_format attr set not to an ImageFormat.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].image_format = test_value\n\n    # and a proper image format\n    data[\"test_project\"].image_format = data[\"test_image_format\"]\n\n\ndef test_image_format_attribute_works_as_expected(setup_project_db_test):\n    \"\"\"image_format attribute is working as expected.\"\"\"\n    data = setup_project_db_test\n    new_image_format = ImageFormat(name=\"Foo Image Format\", width=10, height=10)\n    data[\"test_project\"].image_format = new_image_format\n    assert data[\"test_project\"].image_format == new_image_format\n\n\ndef test_fps_argument_is_skipped(setup_project_db_test):\n    \"\"\"Default value is used if fps is skipped.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"].pop(\"fps\")\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.fps == 25.0\n\n\ndef test_fps_attribute_is_set_to_none(setup_project_db_test):\n    \"\"\"TypeError is raised if the fps attribute is set to None.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"fps\"] = None\n    with pytest.raises(TypeError) as cm:\n        Project(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Project.fps should be a positive float or int, not NoneType: 'None'\"\n    )\n\n\ndef test_fps_argument_is_given_as_non_float_or_integer_1(setup_project_db_test):\n    \"\"\"TypeError is raised if the fps arg is not a float, int.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"fps\"] = \"a str\"\n    with pytest.raises(TypeError) as cm:\n        Project(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Project.fps should be a positive float or int, not str: 'a str'\"\n    )\n\n\ndef test_fps_argument_is_given_as_non_float_or_integer_2(setup_project_db_test):\n    \"\"\"TypeError is raised if the fps arg not a float or int.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"fps\"] = [\"a\", \"list\"]\n    with pytest.raises(TypeError) as cm:\n        Project(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Project.fps should be a positive float or int, not list: '['a', 'list']'\"\n    )\n\n\ndef test_fps_attribute_is_given_as_non_float_or_integer_1(setup_project_db_test):\n    \"\"\"TypeError is raised if the fps attr set not to a float, int.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].fps = \"a str\"\n\n    assert str(cm.value) == (\n        \"Project.fps should be a positive float or int, not str: 'a str'\"\n    )\n\n\ndef test_fps_attribute_is_given_as_non_float_or_integer_2(setup_project_db_test):\n    \"\"\"TypeError is raised if the fps attr set not to a float, int.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].fps = [\"a\", \"list\"]\n\n    assert str(cm.value) == (\n        \"Project.fps should be a positive float or int, not list: '['a', 'list']'\"\n    )\n\n\ndef test_fps_argument_string_to_float_conversion(setup_project_db_test):\n    \"\"\"TypeError is raised if a string containing a float has been passed.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"fps\"] = \"2.3\"\n    with pytest.raises(TypeError) as cm:\n        Project(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Project.fps should be a positive float or int, not str: '2.3'\"\n    )\n\n\ndef test_fps_attribute_string_to_float_conversion(setup_project_db_test):\n    \"\"\"TypeError is raised if the fps attr is set to a string containing a float.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].fps = \"2.3\"\n\n    assert str(cm.value) == (\n        \"Project.fps should be a positive float or int, not str: '2.3'\"\n    )\n\n\ndef test_fps_attribute_float_conversion(setup_project_db_test):\n    \"\"\"fps attr is converted to float if the float argument is given as an integer.\"\"\"\n    data = setup_project_db_test\n    test_value = 1\n    data[\"kwargs\"][\"fps\"] = test_value\n    new_project = Project(**data[\"kwargs\"])\n    assert isinstance(new_project.fps, float)\n    assert new_project.fps == float(test_value)\n\n\ndef test_fps_attribute_float_conversion_2(setup_project_db_test):\n    \"\"\"fps attribute is converted to float if it is set to an integer value.\"\"\"\n    data = setup_project_db_test\n    test_value = 1\n    data[\"test_project\"].fps = test_value\n    assert isinstance(data[\"test_project\"].fps, float)\n    assert data[\"test_project\"].fps == float(test_value)\n\n\ndef test_fps_argument_is_zero(setup_project_db_test):\n    \"\"\"ValueError is raised if the fps is 0.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"fps\"] = 0\n    with pytest.raises(ValueError) as cm:\n        Project(**data[\"kwargs\"])\n    assert str(cm.value) == \"Project.fps should be a positive float or int, not 0.0\"\n\n\ndef test_fps_attribute_is_set_to_zero(setup_project_db_test):\n    \"\"\"value error is raised if the fps attribute is set to zero.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(ValueError) as cm:\n        data[\"test_project\"].fps = 0\n    assert str(cm.value) == \"Project.fps should be a positive float or int, not 0.0\"\n\n\ndef test_fps_argument_is_negative(setup_project_db_test):\n    \"\"\"ValueError is raised if the fps argument is negative.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"fps\"] = -1.0\n    with pytest.raises(ValueError) as cm:\n        Project(**data[\"kwargs\"])\n    assert str(cm.value) == \"Project.fps should be a positive float or int, not -1.0\"\n\n\ndef test_fps_attribute_is_negative(setup_project_db_test):\n    \"\"\"ValueError is raised if the fps attribute is set to a negative value.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(ValueError) as cm:\n        data[\"test_project\"].fps = -1\n    assert str(cm.value) == \"Project.fps should be a positive float or int, not -1.0\"\n\n\ndef test_repositories_argument_is_skipped(setup_project_db_test):\n    \"\"\"repositories attr is an empty list if the repositories argument is skipped.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"].pop(\"repositories\")\n    p = Project(**data[\"kwargs\"])\n    assert p.repositories == []\n\n\ndef test_repositories_argument_is_none(setup_project_db_test):\n    \"\"\"the repositories attr is an empty list if the repositories argument is None.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"repositories\"] = None\n    p = Project(**data[\"kwargs\"])\n    assert p.repositories == []\n\n\ndef test_repositories_attribute_is_set_to_none(setup_project_db_test):\n    \"\"\"TypeError is raised if the repositories attribute is set to None.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].repositories = None\n\n    assert str(cm.value) == \"'NoneType' object is not iterable\"\n\n\ndef test_repositories_argument_is_not_a_list(setup_project_db_test):\n    \"\"\"TypeError is raised if the repositories argument value is not a list.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"repositories\"] = \"not a list\"\n    with pytest.raises(TypeError) as cm:\n        Project(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"ProjectRepository.repositories should be a list of \"\n        \"stalker.models.repository.Repository instances or derivatives, \"\n        \"not str: 'n'\"\n    )\n\n\ndef test_repositories_attribute_is_not_a_list(setup_project_db_test):\n    \"\"\"TypeError raised if the repositories attr is set to not a list.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].repositories = \"not a list\"\n\n    assert str(cm.value) == (\n        \"ProjectRepository.repositories should be a list of \"\n        \"stalker.models.repository.Repository instances or derivatives, \"\n        \"not str: 'n'\"\n    )\n\n\ndef test_repositories_argument_is_not_a_list_of_repository_instances(\n    setup_project_db_test,\n):\n    \"\"\"TypeError raised if the repositories arg is not Repository instances.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"repositories\"] = [\"not\", 1, \"list\", \"of\", Repository, \"instances\"]\n    with pytest.raises(TypeError) as cm:\n        Project(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"ProjectRepository.repositories should be a list of \"\n        \"stalker.models.repository.Repository instances or derivatives, \"\n        \"not str: 'not'\"\n    )\n\n\ndef test_repositories_attribute_is_not_a_list_of_repository_instances(\n    setup_project_db_test,\n):\n    \"\"\"TypeError raised if the repositories attr is set to a list of non Repository.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].repositories = [\n            \"not\",\n            1,\n            \"list\",\n            \"of\",\n            Repository,\n            \"instances\",\n        ]\n\n    assert str(cm.value) == (\n        \"ProjectRepository.repositories should be a list of \"\n        \"stalker.models.repository.Repository instances or derivatives, not str: 'not'\"\n    )\n\n\ndef test_repositories_argument_is_working_as_expected(setup_project_db_test):\n    \"\"\"repositories argument value is passed to the repositories attr.\"\"\"\n    data = setup_project_db_test\n    assert data[\"test_project\"].repositories == data[\"kwargs\"][\"repositories\"]\n\n\ndef test_repositories_attribute_is_working_as_expected(setup_project_db_test):\n    \"\"\"repository attribute is working as expected.\"\"\"\n    data = setup_project_db_test\n    new_repo1 = Repository(\n        name=\"Some Random Repo\",\n        code=\"SRP\",\n        linux_path=\"/mnt/S/random/repo\",\n        windows_path=\"S:/random/repo\",\n        macos_path=\"/Volumes/S/random/repo\",\n    )\n\n    assert data[\"test_project\"].repositories != [new_repo1]\n    data[\"test_project\"].repositories = [new_repo1]\n    assert data[\"test_project\"].repositories == [new_repo1]\n\n\ndef test_repositories_attribute_value_order_is_not_changing(setup_project_db_test):\n    \"\"\"Order of the repositories attribute is preserved.\"\"\"\n    data = setup_project_db_test\n    repo1 = Repository(name=\"Repo1\", code=\"R1\")\n    repo2 = Repository(name=\"Repo2\", code=\"R1\")\n    repo3 = Repository(name=\"Repo3\", code=\"R1\")\n\n    DBSession.add_all([repo1, repo2, repo3])\n    DBSession.commit()\n\n    test_value = [repo3, repo1, repo2]\n    data[\"test_project\"].repositories = test_value\n    DBSession.commit()\n\n    for i in range(10):\n        db_proj = Project.query.first()\n        assert db_proj.repositories == test_value\n        DBSession.commit()\n\n\ndef test_is_stereoscopic_argument_skipped(setup_project_db_test):\n    \"\"\"is_stereoscopic will set the is_stereoscopic attribute to False.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"].pop(\"is_stereoscopic\")\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.is_stereoscopic is False\n\n\n@pytest.mark.parametrize(\"test_value\", [0, 1, 1.2, \"\", \"str\", [\"a\", \"list\"]])\ndef test_is_stereoscopic_argument_bool_conversion(test_value, setup_project_db_test):\n    \"\"\"is_stereoscopic arg is converted to a bool value.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"is_stereoscopic\"] = test_value\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.is_stereoscopic == bool(test_value)\n\n\n@pytest.mark.parametrize(\"test_value\", [0, 1, 1.2, \"\", \"str\", [\"a\", \"list\"]])\ndef test_is_stereoscopic_attribute_bool_conversion(test_value, setup_project_db_test):\n    \"\"\"is_stereoscopic attr is converted to a bool value correctly.\"\"\"\n    data = setup_project_db_test\n    data[\"test_project\"].is_stereoscopic = test_value\n    assert data[\"test_project\"].is_stereoscopic == bool(test_value)\n\n\ndef test_structure_argument_is_none(setup_project_db_test):\n    \"\"\"structure argument can be None.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"structure\"] = None\n    new_project = Project(**data[\"kwargs\"])\n    assert isinstance(new_project, Project)\n\n\ndef test_structure_attribute_is_none(setup_project_db_test):\n    \"\"\"structure attribute can be set to None.\"\"\"\n    data = setup_project_db_test\n    data[\"test_project\"].structure = None\n\n\ndef test_structure_argument_not_instance_of_structure(setup_project_db_test):\n    \"\"\"TypeError is raised if the structure argument is not an instance of Structure.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"structure\"] = 1.215\n    with pytest.raises(TypeError) as cm:\n        Project(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"Project.structure should be an instance of \"\n        \"stalker.models.structure.Structure, not float: '1.215'\"\n    )\n\n\ndef test_structure_attribute_not_instance_of_structure(setup_project_db_test):\n    \"\"\"TypeError raised if the structure attr is not a Structure.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].structure = 1.2\n\n    assert (\n        str(cm.value) == \"Project.structure should be an instance of \"\n        \"stalker.models.structure.Structure, not float: '1.2'\"\n    )\n\n\ndef test_structure_attribute_is_working_as_expected(setup_project_db_test):\n    \"\"\"structure attribute is working as expected.\"\"\"\n    data = setup_project_db_test\n    data[\"test_project\"].structure = data[\"test_project_structure2\"]\n    assert data[\"test_project\"].structure == data[\"test_project_structure2\"]\n\n\ndef test_equality(setup_project_db_test):\n    \"\"\"Equality of two projects.\"\"\"\n    data = setup_project_db_test\n    # create a new project with the same arguments\n    new_project1 = Project(**data[\"kwargs\"])\n\n    # create a new entity with the same arguments\n    new_entity = Entity(**data[\"kwargs\"])\n\n    # create another project with different name\n    data[\"kwargs\"][\"name\"] = \"a different project\"\n    new_project2 = Project(**data[\"kwargs\"])\n\n    assert not data[\"test_project\"] != new_project1\n    assert data[\"test_project\"] != new_project2\n    assert data[\"test_project\"] != new_entity\n\n\ndef test_inequality(setup_project_db_test):\n    \"\"\"Inequality of two projects\"\"\"\n    data = setup_project_db_test\n    # create a new project with the same arguments\n    new_project1 = Project(**data[\"kwargs\"])\n\n    # create a new entity with the same arguments\n    new_entity = Entity(**data[\"kwargs\"])\n\n    # create another project with different name\n    data[\"kwargs\"][\"name\"] = \"a different project\"\n    new_project2 = Project(**data[\"kwargs\"])\n\n    assert not data[\"test_project\"] != new_project1\n    assert data[\"test_project\"] != new_project2\n    assert data[\"test_project\"] != new_entity\n\n\ndef test_reference_mixin_initialization(setup_project_db_test):\n    \"\"\"ReferenceMixin part is initialized correctly.\"\"\"\n    data = setup_project_db_test\n    file_type_1 = Type(name=\"Image\", code=\"image\", target_entity_type=\"File\")\n\n    file1 = File(\n        name=\"Artwork 1\",\n        full_path=\"/mnt/M/JOBs/TEST_PROJECT\",\n        filename=\"a.jpg\",\n        type=file_type_1,\n    )\n\n    file2 = File(\n        name=\"Artwork 2\",\n        full_path=\"/mnt/M/JOBs/TEST_PROJECT\",\n        filename=\"b.jbg\",\n        type=file_type_1,\n    )\n\n    references = [file1, file2]\n\n    data[\"kwargs\"][\"references\"] = references\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.references == references\n\n\ndef test_status_mixin_initialization(setup_project_db_test):\n    \"\"\"StatusMixin part is initialized correctly.\"\"\"\n    data = setup_project_db_test\n    status_list = StatusList.query.filter_by(target_entity_type=\"Project\").first()\n    data[\"kwargs\"][\"status\"] = 0\n    data[\"kwargs\"][\"status_list\"] = status_list\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.status_list == status_list\n\n\ndef test_schedule_mixin_initialization(setup_project_db_test):\n    \"\"\"DateRangeMixin part is initialized correctly.\"\"\"\n    data = setup_project_db_test\n    start = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc) + datetime.timedelta(\n        days=25\n    )\n    end = start + datetime.timedelta(days=12)\n\n    data[\"kwargs\"][\"start\"] = start\n    data[\"kwargs\"][\"end\"] = end\n\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.start == start\n    assert new_project.end == end\n    assert new_project.duration == end - start\n\n\ndef test___strictly_typed___is_false(setup_project_db_test):\n    \"\"\"__strictly_typed__ is True for Project class.\"\"\"\n    assert Project.__strictly_typed__ is False\n\n\ndef test___strictly_typed___not_forces_type_initialization(setup_project_db_test):\n    \"\"\"Project cannot be created without defining a type for it.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"].pop(\"type\")\n    Project(**data[\"kwargs\"])  # should be possible\n\n\n@pytest.mark.parametrize(\n    \"test_data\",\n    [\n        \"test_task1\",\n        \"test_task2\",\n        \"test_task3\",\n        \"test_task4\",\n        \"test_task5\",\n        \"test_task6\",\n        \"test_task7\",\n        \"test_task8\",\n        \"test_task9\",\n        \"test_task10\",\n        \"test_task11\",\n        \"test_task12\",\n        \"test_task13\",\n        \"test_task14\",\n        \"test_task15\",\n        \"test_task16\",\n        \"test_task17\",\n        \"test_task18\",\n        \"test_task19\",\n        \"test_task20\",\n        \"test_task21\",\n        \"test_task22\",\n        \"test_task23\",\n        \"test_task24\",\n        \"test_task25\",\n        \"test_task26\",\n        \"test_task27\",\n        \"test_seq1\",\n        \"test_seq2\",\n        \"test_seq3\",\n        \"test_seq4\",\n        \"test_seq5\",\n        \"test_seq6\",\n        \"test_seq7\",\n        \"test_asset1\",\n        \"test_asset2\",\n        \"test_asset3\",\n        \"test_asset4\",\n        \"test_asset5\",\n        \"test_shot1\",\n        \"test_shot2\",\n        \"test_shot3\",\n        \"test_shot4\",\n    ],\n)\ndef test_tasks_attribute_returns_the_tasks_instances_related_to_this_project(\n    test_data, setup_project_db_test\n):\n    \"\"\"tasks attr returns a list of Task instances related to this Project instance.\"\"\"\n    data = setup_project_db_test\n    # test if we are going to get all the Tasks for project.tasks\n    assert len(data[\"test_project\"].tasks) == 43\n    assert data[test_data] in data[\"test_project\"].tasks\n\n\n@pytest.mark.parametrize(\n    \"test_data\",\n    [\n        \"test_task1\",\n        \"test_task2\",\n        \"test_task3\",\n        \"test_seq1\",\n        \"test_seq2\",\n        \"test_seq3\",\n        \"test_seq4\",\n        \"test_seq5\",\n        \"test_seq6\",\n        \"test_seq7\",\n        \"test_asset1\",\n        \"test_asset2\",\n        \"test_asset3\",\n        \"test_asset4\",\n        \"test_asset5\",\n        \"test_shot1\",\n        \"test_shot2\",\n        \"test_shot3\",\n        \"test_shot4\",\n    ],\n)\ndef test_root_tasks_attribute_returns_the_tasks_instances_with_no_parent_in_this_project(\n    test_data, setup_project_db_test\n):\n    \"\"\"root_tasks attr returns Task instances on this Project that has no parent.\"\"\"\n    data = setup_project_db_test\n    # test if we are going to get all the Tasks for project.tasks\n    root_tasks = data[\"test_project\"].root_tasks\n    assert len(root_tasks) == 19\n    assert data[test_data] in root_tasks\n\n\ndef test_users_argument_is_skipped(setup_project_db_test):\n    \"\"\"users attribute is an empty list if the users argument is skipped.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"name\"] = \"New Project Name\"\n    try:\n        data[\"kwargs\"].pop(\"users\")\n    except KeyError:\n        pass\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.users == []\n\n\ndef test_users_argument_is_none(setup_project_db_test):\n    \"\"\"the users attribute is an empty list if the users argument is set to None.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"name\"] = \"New Project Name\"\n    data[\"kwargs\"][\"users\"] = None\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.users == []\n\n\ndef test_users_attribute_is_set_to_none(setup_project_db_test):\n    \"\"\"TypeError is raised if the users attribute is set to None.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].users = None\n\n    assert str(cm.value) == \"'NoneType' object is not iterable\"\n\n\ndef test_users_argument_is_not_a_list_of_user_instances(setup_project_db_test):\n    \"\"\"TypeError is raised if the users argument is not a list of Users.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"name\"] = \"New Project Name\"\n    data[\"kwargs\"][\"users\"] = [\"not a list of User instances\"]\n    with pytest.raises(TypeError) as cm:\n        Project(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"ProjectUser.user should be a stalker.models.auth.User \"\n        \"instance, not str: 'not a list of User instances'\"\n    )\n\n\ndef test_users_attribute_is_set_to_a_value_which_is_not_a_list_of_User_instances(\n    setup_project_db_test,\n):\n    \"\"\"TypeError raised if the user attribute is set to not a list of User instances.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].users = [\"not a list of Users\"]\n\n    assert (\n        str(cm.value) == \"ProjectUser.user should be a stalker.models.auth.User \"\n        \"instance, not str: 'not a list of Users'\"\n    )\n\n\ndef test_users_argument_is_working_as_expected(setup_project_db_test):\n    \"\"\"users argument value is passed to the users attribute.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"users\"] = [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n        data[\"test_user3\"],\n    ]\n    new_proj = Project(**data[\"kwargs\"])\n    assert sorted(data[\"kwargs\"][\"users\"], key=lambda x: x.name) == sorted(\n        new_proj.users, key=lambda x: x.name\n    )\n\n\ndef test_users_attribute_is_working_as_expected(setup_project_db_test):\n    \"\"\"users attribute is working as expected.\"\"\"\n    data = setup_project_db_test\n    users = [data[\"test_user1\"], data[\"test_user2\"], data[\"test_user3\"]]\n    data[\"test_project\"].users = users\n    assert sorted(users, key=lambda x: x.name) == sorted(\n        data[\"test_project\"].users, key=lambda x: x.name\n    )\n\n\ndef test_tjp_id_is_working_as_expected(setup_project_db_test):\n    \"\"\"tjp_id attribute is working as expected.\"\"\"\n    data = setup_project_db_test\n    data[\"test_project\"].id = 654654\n    assert data[\"test_project\"].tjp_id == \"Project_654654\"\n\n\n@pytest.mark.parametrize(\n    \"entity_name\",\n    [\n        \"test_task1\",\n        \"test_task2\",\n        \"test_task3\",\n        \"test_task4\",\n        \"test_task5\",\n        \"test_task6\",\n        \"test_task7\",\n        \"test_task8\",\n        \"test_task9\",\n        \"test_task10\",\n        \"test_task11\",\n        \"test_task12\",\n        \"test_task13\",\n        \"test_task14\",\n        \"test_task15\",\n        \"test_task16\",\n        \"test_task17\",\n        \"test_task18\",\n        \"test_task19\",\n        \"test_task20\",\n        \"test_task21\",\n        \"test_task22\",\n        \"test_task23\",\n        \"test_task24\",\n        \"test_task25\",\n        \"test_task26\",\n        \"test_task27\",\n        \"test_asset1\",\n        \"test_asset2\",\n        \"test_asset3\",\n        \"test_asset4\",\n        \"test_asset5\",\n        \"test_shot1\",\n        \"test_shot2\",\n        \"test_shot3\",\n        \"test_shot4\",\n        \"test_seq1\",\n        \"test_seq2\",\n        \"test_seq3\",\n        \"test_seq4\",\n        \"test_seq5\",\n        \"test_seq6\",\n        \"test_seq7\",\n    ],\n)\ndef test_to_tjp_is_working_as_expected(setup_project_db_test, entity_name):\n    \"\"\"to_tjp attribute is working as expected.\"\"\"\n    data = setup_project_db_test\n    # because of the randomness in the order of the test data being created,\n    # we can't exactly know the output, so it might be better to check if the\n    # tjp output starts with the correct line and every single child is\n    # represented in the tjp output.\n    result = data[\"test_project\"].to_tjp\n    # format the output so that it is more predictable\n    result = condition_tjp_output(result)\n    assert result.startswith(\n        'task Project_{id} \"Project_{id}\" {{'.format(id=data[\"test_project\"].id)\n    )\n    entity = data[entity_name]\n    assert condition_tjp_output(entity.to_tjp) in result\n\n\ndef test_project_instance_does_not_have_active_attribute(setup_project_db_test):\n    \"\"\"Project instances does not have active attribute.\"\"\"\n    data = setup_project_db_test\n    new_project = Project(**data[\"kwargs\"])\n    assert hasattr(new_project, \"active\") is False\n\n\n@pytest.mark.parametrize(\n    \"status, expected\",\n    [\n        [\"RTS\", False],\n        [\"WIP\", True],\n        [\"CMPL\", False],\n    ],\n)\ndef test_is_active_property_depends_on_the_status(\n    setup_project_db_test, status, expected\n):\n    \"\"\"is_active property depends on the Project.status.\"\"\"\n    data = setup_project_db_test\n    new_project = Project(**data[\"kwargs\"])\n    status_ins = Status.query.filter_by(code=status).first()\n    assert status_ins is not None\n    new_project.status = status_ins\n    assert new_project.is_active is expected\n\n\ndef test_is_active_is_read_only(setup_project_db_test):\n    \"\"\"is_active is a read only property.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_project\"].is_active = True\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'is_active'\",\n    }.get(\n        sys.version_info.minor, \"property 'is_active' of 'Project' object has no setter\"\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_total_logged_seconds_attribute_is_read_only(setup_project_db_test):\n    \"\"\"total_logged_seconds attribute is a read-only attribute.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_project\"].total_logged_seconds = 32.3\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'total_logged_seconds'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'total_logged_seconds' of 'Project' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_total_logged_seconds_is_0_for_a_project_with_no_child_tasks(\n    setup_project_db_test,\n):\n    \"\"\"total_logged_seconds.\"\"\"\n    data = setup_project_db_test\n    new_project = Project(**data[\"kwargs\"])\n    DBSession.save(new_project)\n    assert new_project.total_logged_seconds == 0\n\n\ndef test_total_logged_seconds_attribute_is_working_as_expected(setup_project_db_test):\n    \"\"\"total_logged_seconds attribute is working as expected.\"\"\"\n    data = setup_project_db_test\n    # create some time logs\n    t1 = TimeLog(\n        task=data[\"test_task1\"],\n        resource=data[\"test_task1\"].resources[0],\n        start=datetime.datetime(2013, 8, 1, 1, 0, tzinfo=pytz.utc),\n        duration=datetime.timedelta(hours=1),\n    )\n    DBSession.save(t1)\n    assert data[\"test_project\"].total_logged_seconds == 3600\n\n    # add more time logs\n    t2 = TimeLog(\n        task=data[\"test_seq1\"],\n        resource=data[\"test_seq1\"].resources[0],\n        start=datetime.datetime(2013, 8, 1, 2, 0, tzinfo=pytz.utc),\n        duration=datetime.timedelta(hours=1),\n    )\n    DBSession.save(t2)\n    assert data[\"test_project\"].total_logged_seconds == 7200\n\n    # create more deeper time logs\n    t3 = TimeLog(\n        task=data[\"test_task10\"],\n        resource=data[\"test_task10\"].resources[0],\n        start=datetime.datetime(2013, 8, 1, 3, 0, tzinfo=pytz.utc),\n        duration=datetime.timedelta(hours=3),\n    )\n    DBSession.save(t3)\n    assert data[\"test_project\"].total_logged_seconds == 18000\n\n    # create a time log for one asset\n    t4 = TimeLog(\n        task=data[\"test_asset1\"],\n        resource=data[\"test_asset1\"].resources[0],\n        start=datetime.datetime(2013, 8, 1, 6, 0, tzinfo=pytz.utc),\n        duration=datetime.timedelta(hours=10),\n    )\n    DBSession.save(t4)\n    assert data[\"test_project\"].total_logged_seconds == 15 * 3600\n\n\ndef test_schedule_seconds_attribute_is_read_only(setup_project_db_test):\n    \"\"\"schedule_seconds is a read-only attribute.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_project\"].schedule_seconds = 3\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'schedule_seconds'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'schedule_seconds' of 'Project' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_schedule_seconds_attribute_value_is_0_for_a_project_with_no_tasks(\n    setup_project_db_test,\n):\n    \"\"\"schedule_seconds attribute value is 0 for a project with no tasks.\"\"\"\n    data = setup_project_db_test\n    new_project = Project(**data[\"kwargs\"])\n    DBSession.add(new_project)\n    DBSession.commit()\n    assert new_project.schedule_seconds == 0\n\n\n@pytest.mark.parametrize(\n    \"test_entity,expected_value\",\n    [\n        [\"test_seq1\", 3600],\n        [\"test_seq2\", 3600],\n        [\"test_seq3\", 3600],\n        [\"test_seq4\", 3 * 3600],\n        [\"test_seq5\", 3 * 3600],\n        [\"test_seq6\", 3600],\n        [\"test_seq7\", 3600],\n        [\"test_shot1\", 12 * 3600],\n        [\"test_shot2\", 3 * 3600],\n        [\"test_shot3\", 3 * 3600],\n        [\"test_shot4\", 3 * 3600],\n        [\"test_asset1\", 3600],\n        [\"test_asset2\", 3600],\n        [\"test_asset3\", 3600],\n        [\"test_asset4\", 3 * 3600],\n        [\"test_asset5\", 3 * 3600],\n        [\"test_task1\", 3600],\n        [\"test_task2\", 3600],\n        [\"test_task3\", 3600],\n        [\"test_task4\", 3600],\n        [\"test_task5\", 3600],\n        [\"test_task6\", 3600],\n        [\"test_task7\", 3600],\n        [\"test_task8\", 3600],\n        [\"test_task9\", 3600],\n        [\"test_task10\", 10 * 3600],\n        [\"test_task11\", 3600],\n        [\"test_task12\", 3600],\n        [\"test_task13\", 3600],\n        [\"test_task14\", 3600],\n        [\"test_task15\", 3600],\n        [\"test_task16\", 3600],\n        [\"test_task17\", 3600],\n        [\"test_task18\", 3600],\n        [\"test_task19\", 3600],\n        [\"test_task20\", 3600],\n        [\"test_task21\", 3600],\n        [\"test_task22\", 3600],\n        [\"test_task23\", 3600],\n        [\"test_task24\", 3600],\n        [\"test_task25\", 3600],\n        [\"test_task26\", 3600],\n        [\"test_task27\", 3600],\n        [\"test_project\", 44 * 3600],\n    ],\n)\ndef test_schedule_seconds_attribute_is_working_as_expected(\n    test_entity, expected_value, setup_project_db_test\n):\n    \"\"\"schedule_seconds attribute value is gathered from the child tasks.\"\"\"\n    data = setup_project_db_test\n    assert data[\"test_shot1\"].is_container\n    assert data[\"test_task10\"].parent == data[\"test_shot1\"]\n\n    assert data[test_entity].schedule_seconds == expected_value\n\n\ndef test_percent_complete_attribute_is_read_only(setup_project_db_test):\n    \"\"\"percent_complete is a read-only attribute.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_project\"].percent_complete = 32.3\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'percent_complete'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'percent_complete' of 'Project' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_percent_complete_is_0_for_a_project_with_no_tasks(setup_project_db_test):\n    \"\"\"percent_complete attribute value is 0 for a project with no tasks.\"\"\"\n    data = setup_project_db_test\n    new_project = Project(**data[\"kwargs\"])\n    DBSession.add(new_project)\n    DBSession.commit()\n    assert new_project.percent_complete == 0\n\n\ndef test_percent_complete_attribute_is_working_as_expected(setup_project_db_test):\n    \"\"\"percent_complete attribute is working as expected\"\"\"\n    data = setup_project_db_test\n    assert data[\"test_project\"].percent_complete == 0\n    assert data[\"test_shot1\"].is_container is True\n    assert data[\"test_task10\"].parent == data[\"test_shot1\"]\n    assert data[\"test_task10\"].schedule_seconds == 36000\n    assert data[\"test_task11\"].schedule_seconds == 3600\n    assert data[\"test_task12\"].schedule_seconds == 3600\n    assert data[\"test_shot1\"].schedule_seconds == 12 * 3600\n\n    # create some time logs\n    t = TimeLog(\n        task=data[\"test_task1\"],\n        resource=data[\"test_task1\"].resources[0],\n        start=datetime.datetime(2013, 8, 1, 1, 0, tzinfo=pytz.utc),\n        duration=datetime.timedelta(hours=1),\n    )\n    DBSession.add(t)\n    DBSession.commit()\n\n    assert data[\"test_project\"].percent_complete == (1.0 / 44.0 * 100)\n\n\ndef test_clients_argument_is_skipped(setup_project_db_test):\n    \"\"\"clients attribute is set to None if the clients argument is skipped.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"name\"] = \"New Project Name\"\n    try:\n        data[\"kwargs\"].pop(\"clients\")\n    except KeyError:\n        pass\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.clients == []\n\n\ndef test_clients_argument_is_none(setup_project_db_test):\n    \"\"\"clients argument can be None.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"clients\"] = None\n    new_project = Project(**data[\"kwargs\"])\n    assert new_project.clients == []\n\n\ndef test_clients_attribute_is_set_to_none(setup_project_db_test):\n    \"\"\"it a TypeError is raised if the clients attribute is set to None.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].clients = None\n\n    assert str(cm.value) == \"'NoneType' object is not iterable\"\n\n\ndef test_clients_argument_is_given_as_something_other_than_a_client(\n    setup_project_db_test,\n):\n    \"\"\"TypeError raised if the client arg is not a Client.\"\"\"\n    data = setup_project_db_test\n    data[\"kwargs\"][\"clients\"] = \"a user\"\n    with pytest.raises(TypeError) as cm:\n        Project(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"ProjectClient.client should be an instance of \"\n        \"stalker.models.auth.Client, not str: 'a'\"\n    )\n\n\ndef test_clients_attribute_is_not_a_client_instance(setup_project_db_test):\n    \"\"\"TypeError raised if the client attribute is not a Client.\"\"\"\n    data = setup_project_db_test\n    with pytest.raises(TypeError) as cm:\n        data[\"test_project\"].clients = \"a user\"\n\n    assert str(cm.value) == (\n        \"ProjectClient.client should be an instance of stalker.models.auth.Client, \"\n        \"not str: 'a'\"\n    )\n\n\n# def test_client_argument_is_working_as_expected(setup_project_db_test):\n#     \"\"\"client argument value is correctly passed to the client attribute.\"\"\"\n#     data = setup_project_db_test\n#     assert data[\"test_project\"].client == data[\"kwargs\"]['client']\n\n\ndef test_clients_attribute_is_working_as_expected(setup_project_db_test):\n    \"\"\"clients attribute value can be updated correctly.\"\"\"\n    data = setup_project_db_test\n    new_client = Client(name=\"New Client\")\n    assert data[\"test_project\"].clients != [new_client]\n    data[\"test_project\"].clients = [new_client]\n    assert data[\"test_project\"].clients == [new_client]\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_project_tickets_db_tests(setup_postgresql_db):\n    \"\"\"Set up the tests for the Project <-> Ticket relation.\"\"\"\n    data = dict()\n\n    # create test objects\n    data[\"start\"] = datetime.datetime(2016, 11, 17, tzinfo=pytz.utc)\n    data[\"end\"] = data[\"start\"] + datetime.timedelta(days=20)\n\n    data[\"test_lead\"] = User(\n        name=\"lead\", login=\"lead\", email=\"lead@lead.com\", password=\"lead\"\n    )\n\n    data[\"test_user1\"] = User(\n        name=\"User1\", login=\"user1\", email=\"user1@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user2\"] = User(\n        name=\"User2\", login=\"user2\", email=\"user2@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user3\"] = User(\n        name=\"User3\", login=\"user3\", email=\"user3@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user4\"] = User(\n        name=\"User4\", login=\"user4\", email=\"user4@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user5\"] = User(\n        name=\"User5\", login=\"user5\", email=\"user5@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user6\"] = User(\n        name=\"User6\", login=\"user6\", email=\"user6@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user7\"] = User(\n        name=\"User7\", login=\"user7\", email=\"user7@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user8\"] = User(\n        name=\"User8\", login=\"user8\", email=\"user8@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user9\"] = User(\n        name=\"user9\", login=\"user9\", email=\"user9@users.com\", password=\"123456\"\n    )\n\n    data[\"test_user10\"] = User(\n        name=\"User10\", login=\"user10\", email=\"user10@users.com\", password=\"123456\"\n    )\n    DBSession.save(\n        [\n            data[\"test_lead\"],\n            data[\"test_user1\"],\n            data[\"test_user2\"],\n            data[\"test_user3\"],\n            data[\"test_user4\"],\n            data[\"test_user5\"],\n            data[\"test_user6\"],\n            data[\"test_user7\"],\n            data[\"test_user8\"],\n            data[\"test_user9\"],\n            data[\"test_user10\"],\n        ]\n    )\n\n    data[\"test_image_format\"] = ImageFormat(\n        name=\"HD\",\n        width=1920,\n        height=1080,\n    )\n\n    # type for project\n    data[\"test_project_type\"] = Type(\n        name=\"Project Type 1\", code=\"projt1\", target_entity_type=\"Project\"\n    )\n\n    data[\"test_project_type2\"] = Type(\n        name=\"Project Type 2\", code=\"projt2\", target_entity_type=\"Project\"\n    )\n\n    # type for structure\n    data[\"test_structure_type1\"] = Type(\n        name=\"Structure Type 1\", code=\"struct1\", target_entity_type=\"Structure\"\n    )\n\n    data[\"test_structure_type2\"] = Type(\n        name=\"Structure Type 2\", code=\"struct2\", target_entity_type=\"Structure\"\n    )\n\n    data[\"test_project_structure\"] = Structure(\n        name=\"Project Structure 1\",\n        type=data[\"test_structure_type1\"],\n    )\n\n    data[\"test_project_structure2\"] = Structure(\n        name=\"Project Structure 2\",\n        type=data[\"test_structure_type2\"],\n    )\n\n    data[\"test_repo\"] = Repository(\n        name=\"Commercials Repository\",\n        code=\"CR\",\n    )\n\n    # create a project object\n    data[\"kwargs\"] = {\n        \"name\": \"Test Project\",\n        \"code\": \"tp\",\n        \"description\": \"This is a project object for testing purposes\",\n        \"image_format\": data[\"test_image_format\"],\n        \"fps\": 25,\n        \"type\": data[\"test_project_type\"],\n        \"structure\": data[\"test_project_structure\"],\n        \"repository\": data[\"test_repo\"],\n        \"is_stereoscopic\": False,\n        \"display_width\": 15,\n        \"start\": data[\"start\"],\n        \"end\": data[\"end\"],\n    }\n\n    data[\"test_project\"] = Project(**data[\"kwargs\"])\n\n    # *********************************************************************\n    # Tickets\n    # *********************************************************************\n\n    # no need to create status list for tickets cause we have a database\n    # set up an running so it is automatically linked\n\n    # tickets for version1\n    data[\"test_ticket1\"] = Ticket(project=data[\"test_project\"])\n\n    DBSession.add(data[\"test_ticket1\"])\n    # set it to closed\n    data[\"test_ticket1\"].resolve()\n    DBSession.commit()\n\n    # create a new ticket and leave it open\n    data[\"test_ticket2\"] = Ticket(project=data[\"test_project\"])\n    DBSession.add(data[\"test_ticket2\"])\n    DBSession.commit()\n\n    # create a new ticket and close and then reopen it\n    data[\"test_ticket3\"] = Ticket(project=data[\"test_project\"])\n    DBSession.add(data[\"test_ticket3\"])\n    data[\"test_ticket3\"].resolve()\n    data[\"test_ticket3\"].reopen()\n    DBSession.commit()\n\n    # *********************************************************************\n    # tickets for version2\n    # create a new ticket and leave it open\n    data[\"test_ticket4\"] = Ticket(project=data[\"test_project\"])\n    DBSession.add(data[\"test_ticket4\"])\n    DBSession.commit()\n\n    # create a new Ticket and close it\n    data[\"test_ticket5\"] = Ticket(project=data[\"test_project\"])\n    DBSession.add(data[\"test_ticket5\"])\n    data[\"test_ticket5\"].resolve()\n    DBSession.commit()\n\n    # create a new Ticket and close it\n    data[\"test_ticket6\"] = Ticket(project=data[\"test_project\"])\n    DBSession.add(data[\"test_ticket6\"])\n    data[\"test_ticket6\"].resolve()\n    DBSession.commit()\n\n    # *********************************************************************\n    # tickets for version3\n    # create a new ticket and close it\n    data[\"test_ticket7\"] = Ticket(project=data[\"test_project\"])\n    DBSession.add(data[\"test_ticket7\"])\n    data[\"test_ticket7\"].resolve()\n    DBSession.commit()\n\n    # create a new ticket and close it\n    data[\"test_ticket8\"] = Ticket(project=data[\"test_project\"])\n    DBSession.add(data[\"test_ticket8\"])\n    data[\"test_ticket8\"].resolve()\n    DBSession.commit()\n\n    # *********************************************************************\n    # tickets for version4\n    # create a new ticket and close it\n    data[\"test_ticket9\"] = Ticket(project=data[\"test_project\"])\n    DBSession.add(data[\"test_ticket9\"])\n\n    data[\"test_ticket9\"].resolve()\n    DBSession.commit()\n\n    # *********************************************************************\n\n    DBSession.add(data[\"test_project\"])\n    DBSession.commit()\n    return data\n\n\ndef test_tickets_attribute_is_an_empty_list_by_default(setup_project_tickets_db_tests):\n    \"\"\"Project.tickets is an empty list by default.\"\"\"\n    data = setup_project_tickets_db_tests\n    project1 = Project(**data[\"kwargs\"])\n    assert project1.tickets == []\n\n\ndef test_open_tickets_attribute_is_an_empty_list_by_default(\n    setup_project_tickets_db_tests,\n):\n    \"\"\"Project.open_tickets is an empty list by default.\"\"\"\n    data = setup_project_tickets_db_tests\n    project1 = Project(**data[\"kwargs\"])\n    DBSession.add(project1)\n    DBSession.commit()\n    assert project1.open_tickets == []\n\n\ndef test_open_tickets_attribute_is_read_only(setup_project_tickets_db_tests):\n    \"\"\"Project.open_tickets attribute is a read only attribute.\"\"\"\n    data = setup_project_tickets_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_project\"].open_tickets = []\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'open_tickets'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'open_tickets' of 'Project' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_tickets_attribute_returns_all_tickets_in_this_project(\n    setup_project_tickets_db_tests,\n):\n    \"\"\"Project.tickets returns all the tickets in this project.\"\"\"\n    data = setup_project_tickets_db_tests\n    # there should be tickets in this project already\n    assert data[\"test_project\"].tickets != []\n\n    # now we should have some tickets\n    assert len(data[\"test_project\"].tickets) > 0\n\n    # now check for exact items\n    assert sorted(data[\"test_project\"].tickets, key=lambda x: x.name) == sorted(\n        [\n            data[\"test_ticket1\"],\n            data[\"test_ticket2\"],\n            data[\"test_ticket3\"],\n            data[\"test_ticket4\"],\n            data[\"test_ticket5\"],\n            data[\"test_ticket6\"],\n            data[\"test_ticket7\"],\n            data[\"test_ticket8\"],\n            data[\"test_ticket9\"],\n        ],\n        key=lambda x: x.name,\n    )\n\n\ndef test_open_tickets_attribute_returns_all_open_tickets_owned_by_this_user(\n    setup_project_tickets_db_tests,\n):\n    \"\"\"User.open_tickets returns all the open tickets owned by this user.\"\"\"\n    data = setup_project_tickets_db_tests\n    # there should be tickets in this project already\n    assert data[\"test_project\"].open_tickets != []\n\n    # assign the user to some tickets\n    data[\"test_ticket1\"].reopen(data[\"test_user1\"])\n    data[\"test_ticket2\"].reopen(data[\"test_user1\"])\n    data[\"test_ticket3\"].reopen(data[\"test_user1\"])\n    data[\"test_ticket4\"].reopen(data[\"test_user1\"])\n    data[\"test_ticket5\"].reopen(data[\"test_user1\"])\n    data[\"test_ticket6\"].reopen(data[\"test_user1\"])\n    data[\"test_ticket7\"].reopen(data[\"test_user1\"])\n    data[\"test_ticket8\"].reopen(data[\"test_user1\"])\n\n    # be careful not all of these are open tickets\n    data[\"test_ticket1\"].reassign(data[\"test_user1\"], data[\"test_user1\"])\n    data[\"test_ticket2\"].reassign(data[\"test_user1\"], data[\"test_user1\"])\n    data[\"test_ticket3\"].reassign(data[\"test_user1\"], data[\"test_user1\"])\n    data[\"test_ticket4\"].reassign(data[\"test_user1\"], data[\"test_user1\"])\n    data[\"test_ticket5\"].reassign(data[\"test_user1\"], data[\"test_user1\"])\n    data[\"test_ticket6\"].reassign(data[\"test_user1\"], data[\"test_user1\"])\n    data[\"test_ticket7\"].reassign(data[\"test_user1\"], data[\"test_user1\"])\n    data[\"test_ticket8\"].reassign(data[\"test_user1\"], data[\"test_user1\"])\n\n    # now we should have some open tickets\n    assert len(data[\"test_project\"].open_tickets) > 0\n\n    # now check for exact items\n    assert sorted(data[\"test_project\"].open_tickets, key=lambda x: x.name) == sorted(\n        [\n            data[\"test_ticket1\"],\n            data[\"test_ticket2\"],\n            data[\"test_ticket3\"],\n            data[\"test_ticket4\"],\n            data[\"test_ticket5\"],\n            data[\"test_ticket6\"],\n            data[\"test_ticket7\"],\n            data[\"test_ticket8\"],\n        ],\n        key=lambda x: x.name,\n    )\n\n    # close a couple of them\n    data[\"test_ticket1\"].resolve(data[\"test_user1\"], FIXED)\n    data[\"test_ticket2\"].resolve(data[\"test_user1\"], INVALID)\n    data[\"test_ticket3\"].resolve(data[\"test_user1\"], CANTFIX)\n\n    # new check again\n    assert sorted(data[\"test_project\"].open_tickets, key=lambda x: x.name) == sorted(\n        [\n            data[\"test_ticket4\"],\n            data[\"test_ticket5\"],\n            data[\"test_ticket6\"],\n            data[\"test_ticket7\"],\n            data[\"test_ticket8\"],\n        ],\n        key=lambda x: x.name,\n    )\n\n\ndef test__hash__is_working_as_expected(setup_project_db_test):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_project_db_test\n    result = hash(data[\"test_project\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_project\"].__hash__()\n"
  },
  {
    "path": "tests/models/test_project_client.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests related to the ProjectClient class.\"\"\"\n\nimport pytest\n\nfrom stalker import Client, Project, ProjectClient, Repository, Role, Status, User\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_project_client_db_test(setup_postgresql_db):\n    \"\"\"Set the test up ProjectClient class tests with a DB.\"\"\"\n    data = dict()\n    data[\"test_repo\"] = Repository(name=\"Test Repo\", code=\"TR\")\n    data[\"status_new\"] = Status(name=\"New\", code=\"NEW\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n\n    data[\"test_user1\"] = User(\n        name=\"Test User 1\",\n        login=\"testuser1\",\n        email=\"testuser1@users.com\",\n        password=\"secret\",\n    )\n\n    data[\"test_client\"] = Client(name=\"Test Client\")\n\n    data[\"test_project\"] = Project(\n        name=\"Test Project 1\",\n        code=\"TP1\",\n        repositories=[data[\"test_repo\"]],\n    )\n\n    data[\"test_role\"] = Role(name=\"Test Client\")\n    return data\n\n\ndef test_project_client_creation(setup_project_client_db_test):\n    \"\"\"Project client creation.\"\"\"\n    data = setup_project_client_db_test\n    ProjectClient(\n        project=data[\"test_project\"], client=data[\"test_client\"], role=data[\"test_role\"]\n    )\n\n    assert data[\"test_client\"] in data[\"test_project\"].clients\n\n\ndef test_role_argument_is_not_a_role_instance(setup_project_client_db_test):\n    \"\"\"TypeError will be raised when the role argument is not a Role instance.\"\"\"\n    data = setup_project_client_db_test\n    with pytest.raises(TypeError) as cm:\n        ProjectClient(\n            project=data[\"test_project\"],\n            client=data[\"test_client\"],\n            role=\"not a role instance\",\n        )\n\n    assert str(cm.value) == (\n        \"ProjectClient.role should be a stalker.models.auth.Role \"\n        \"instance, not str: 'not a role instance'\"\n    )\n"
  },
  {
    "path": "tests/models/test_project_user.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests related to the ProjectUser class.\"\"\"\n\nimport pytest\n\nfrom stalker import Project, ProjectUser, Repository, Role, User\nfrom stalker.db.session import DBSession\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_project_user_db_tests(setup_postgresql_db):\n    \"\"\"Set up the tests database and data for the ProjectUser class related tests.\"\"\"\n    data = dict()\n    data[\"test_repo\"] = Repository(name=\"Test Repo\", code=\"TR\")\n\n    DBSession.add(data[\"test_repo\"])\n    DBSession.commit()\n\n    data[\"test_user1\"] = User(\n        name=\"Test User 1\",\n        login=\"testuser1\",\n        email=\"testuser1@users.com\",\n        password=\"secret\",\n    )\n    DBSession.add(data[\"test_user1\"])\n\n    data[\"test_project\"] = Project(\n        name=\"Test Project 1\",\n        code=\"TP1\",\n        repositories=[data[\"test_repo\"]],\n    )\n    DBSession.add(data[\"test_project\"])\n\n    data[\"test_role\"] = Role(name=\"Test User\")\n    DBSession.add(data[\"test_role\"])\n    DBSession.commit()\n    return data\n\n\ndef test_project_user_creation(setup_project_user_db_tests):\n    \"\"\"project user creation.\"\"\"\n    data = setup_project_user_db_tests\n    puser = ProjectUser(\n        project=data[\"test_project\"], user=data[\"test_user1\"], role=data[\"test_role\"]\n    )\n    DBSession.save(puser)\n    assert data[\"test_user1\"] in data[\"test_project\"].users\n\n\ndef test_role_argument_is_not_a_role_instance(setup_project_user_db_tests):\n    \"\"\"TypeError will be raised if the role argument is not a Role instance.\"\"\"\n    data = setup_project_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        ProjectUser(\n            project=data[\"test_project\"],\n            user=data[\"test_user1\"],\n            role=\"not a role instance\",\n        )\n\n    assert str(cm.value) == (\n        \"ProjectUser.role should be a stalker.models.auth.Role instance, \"\n        \"not str: 'not a role instance'\"\n    )\n\n\ndef test_rate_attribute_is_copied_from_user(setup_project_user_db_tests):\n    \"\"\"rate attribute value is copied from the user on init.\"\"\"\n    data = setup_project_user_db_tests\n    data[\"test_user1\"].rate = 100.0\n    project_user1 = ProjectUser(\n        project=data[\"test_project\"], user=data[\"test_user1\"], role=data[\"test_role\"]\n    )\n    assert data[\"test_user1\"].rate == project_user1.rate\n\n\ndef test_rate_attribute_initialization_through_user(setup_project_user_db_tests):\n    \"\"\"rate attribute initialization through ``user.projects`` attribute.\"\"\"\n    data = setup_project_user_db_tests\n    data[\"test_user1\"].rate = 102.0\n    data[\"test_user1\"].projects = [data[\"test_project\"]]\n    assert data[\"test_project\"].user_role[0].rate == data[\"test_user1\"].rate\n\n\ndef test_rate_attribute_initialization_through_project(setup_project_user_db_tests):\n    \"\"\"rate attribute initialization through ``project.users`` attribute.\"\"\"\n    data = setup_project_user_db_tests\n    data[\"test_user1\"].rate = 102.0\n    data[\"test_project\"].users = [data[\"test_user1\"]]\n\n    assert data[\"test_project\"].user_role[0].rate == data[\"test_user1\"].rate\n"
  },
  {
    "path": "tests/models/test_repository.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests related to the Repository class.\"\"\"\n\nimport os\nimport sys\n\nimport pytest\n\nfrom stalker import CodeMixin, Repository, Tag, defaults\nfrom stalker.db.session import DBSession\n\nfrom tests.utils import PlatformPatcher\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_repository_db_tests(setup_postgresql_db):\n    \"\"\"Set up the tests for the Repository class with a DB.\"\"\"\n    data = dict()\n    data[\"patcher\"] = PlatformPatcher()\n\n    # create a couple of test tags\n    data[\"test_tag1\"] = Tag(name=\"test tag 1\")\n    data[\"test_tag2\"] = Tag(name=\"test tag 2\")\n\n    data[\"kwargs\"] = {\n        \"name\": \"a repository\",\n        \"code\": \"R1\",\n        \"description\": \"this is for testing purposes\",\n        \"tags\": [data[\"test_tag1\"], data[\"test_tag2\"]],\n        \"linux_path\": \"/mnt/M/Projects\",\n        \"macos_path\": \"/Volumes/M/Projects\",\n        \"windows_path\": \"M:/Projects\",\n    }\n\n    repo = Repository(**data[\"kwargs\"])\n    data[\"test_repo\"] = repo\n    DBSession.add(data[\"test_repo\"])\n    DBSession.commit()\n    yield data\n    data[\"patcher\"].restore()\n\n\ndef test_code_mixin_as_super(setup_repository_db_tests):\n    \"\"\"CodeMixin is one of the supers of the Repository class.\"\"\"\n    data = setup_repository_db_tests\n    repo = Repository(**data[\"kwargs\"])\n    assert isinstance(repo, CodeMixin)\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Repository class.\"\"\"\n    assert Repository.__auto_name__ is False\n\n\n@pytest.mark.parametrize(\"test_value\", [123123, 123.1231, [], {}])\ndef test_linux_path_argument_accepts_only_strings(\n    test_value, setup_repository_db_tests\n):\n    \"\"\"linux_path argument accepts only string values.\"\"\"\n    data = setup_repository_db_tests\n    data[\"kwargs\"][\"linux_path\"] = test_value\n    with pytest.raises(TypeError):\n        Repository(**data[\"kwargs\"])\n\n\n@pytest.mark.parametrize(\"test_value\", [123123, 123.1231, [], {}])\ndef test_linux_path_attribute_accepts_only_strings(\n    test_value, setup_repository_db_tests\n):\n    \"\"\"linux_path attribute accepts only string values.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError):\n        data[\"test_repo\"].linux_path = test_value\n\n\ndef test_linux_path_attribute_is_working_as_expected(setup_repository_db_tests):\n    \"\"\"linux_path attribute is working as expected.\"\"\"\n    data = setup_repository_db_tests\n    test_value = \"~/newRepoPath/Projects/\"\n    data[\"test_repo\"].linux_path = test_value\n    assert data[\"test_repo\"].linux_path == test_value\n\n\ndef test_linux_path_attribute_finishes_with_a_slash(setup_repository_db_tests):\n    \"\"\"linux_path attr is finished with a slash even it is not supplied by default.\"\"\"\n    data = setup_repository_db_tests\n    test_value = \"/mnt/T\"\n    expected_value = \"/mnt/T/\"\n    data[\"test_repo\"].linux_path = test_value\n    assert data[\"test_repo\"].linux_path == expected_value\n\n\n@pytest.mark.parametrize(\"test_value\", [123123, 123.1231, [], {}])\ndef test_windows_path_argument_accepts_only_strings(\n    test_value, setup_repository_db_tests\n):\n    \"\"\"windows_path argument accepts only string values.\"\"\"\n    data = setup_repository_db_tests\n    data[\"kwargs\"][\"windows_path\"] = test_value\n    with pytest.raises(TypeError):\n        Repository(**data[\"kwargs\"])\n\n\ndef test_windows_path_attribute_accepts_only_strings(setup_repository_db_tests):\n    \"\"\"windows_path attribute accepts only string values.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"].windows_path = 123123\n\n    assert str(cm.value) == (\n        \"Repository.windows_path should be an instance of string, not int: '123123'\"\n    )\n\n\ndef test_windows_path_attribute_is_working_as_expected(setup_repository_db_tests):\n    \"\"\"windows_path attribute is working as expected.\"\"\"\n    data = setup_repository_db_tests\n    test_value = \"~/newRepoPath/Projects/\"\n    data[\"test_repo\"].windows_path = test_value\n    assert data[\"test_repo\"].windows_path == test_value\n\n\ndef test_windows_path_attribute_finishes_with_a_slash(setup_repository_db_tests):\n    \"\"\"windows_path attr is finished with a slash even it is not supplied by default.\"\"\"\n    data = setup_repository_db_tests\n    test_value = \"T:\"\n    expected_value = \"T:/\"\n    data[\"test_repo\"].windows_path = test_value\n    assert data[\"test_repo\"].windows_path == expected_value\n\n\ndef test_macos_path_argument_accepts_only_strings(setup_repository_db_tests):\n    \"\"\"macos_path argument accepts only string values.\"\"\"\n    data = setup_repository_db_tests\n    data[\"kwargs\"][\"macos_path\"] = 123123\n    with pytest.raises(TypeError) as cm:\n        Repository(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Repository.macos_path should be an instance of string, not int: '123123'\"\n    )\n\n\ndef test_macos_path_attribute_accepts_only_strings(setup_repository_db_tests):\n    \"\"\"macos_path attribute accepts only string values.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"].macos_path = 123123\n\n    assert str(cm.value) == (\n        \"Repository.macos_path should be an instance of string, not int: '123123'\"\n    )\n\n\ndef test_macos_path_attribute_is_working_as_expected(setup_repository_db_tests):\n    \"\"\"macos_path attribute is working as expected.\"\"\"\n    data = setup_repository_db_tests\n    test_value = \"~/newRepoPath/Projects/\"\n    data[\"test_repo\"].macos_path = test_value\n    assert data[\"test_repo\"].macos_path == test_value\n\n\ndef test_macos_path_attribute_finishes_with_a_slash(setup_repository_db_tests):\n    \"\"\"macos_path attr is finished with a slash even it is not supplied by default.\"\"\"\n    data = setup_repository_db_tests\n    test_value = \"/Volumes/T\"\n    expected_value = \"/Volumes/T/\"\n    data[\"test_repo\"].macos_path = test_value\n    assert data[\"test_repo\"].macos_path == expected_value\n\n\ndef test_path_returns_properly_for_windows(setup_repository_db_tests):\n    \"\"\"path returns the correct value for the os.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Windows\")\n    assert data[\"test_repo\"].path == data[\"test_repo\"].windows_path\n\n\ndef test_path_returns_properly_for_linux(setup_repository_db_tests):\n    \"\"\"path returns the correct value for the os.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n    assert data[\"test_repo\"].path == data[\"test_repo\"].linux_path\n\n\ndef test_path_returns_properly_for_macos(setup_repository_db_tests):\n    \"\"\"path returns the correct value for the os.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Darwin\")\n    assert data[\"test_repo\"].path == data[\"test_repo\"].macos_path\n\n\ndef test_path_attribute_sets_correct_path_for_windows(setup_repository_db_tests):\n    \"\"\"path property sets the correct attribute in windows.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Windows\")\n    test_value = \"S:/Projects/\"\n    assert data[\"test_repo\"].path != test_value\n    assert data[\"test_repo\"].windows_path != test_value\n    data[\"test_repo\"].path = test_value\n    assert data[\"test_repo\"].windows_path == test_value\n    assert data[\"test_repo\"].path == test_value\n\n\ndef test_path_attribute_sets_correct_path_for_linux(setup_repository_db_tests):\n    \"\"\"path property sets the correct attribute in linux.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n    test_value = \"/mnt/S/Projects/\"\n    assert data[\"test_repo\"].path != test_value\n    assert data[\"test_repo\"].linux_path != test_value\n    data[\"test_repo\"].path = test_value\n    assert data[\"test_repo\"].linux_path == test_value\n    assert data[\"test_repo\"].path == test_value\n\n\ndef test_path_attribute_sets_correct_path_for_macos(setup_repository_db_tests):\n    \"\"\"path property sets the correct attribute in macos.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Darwin\")\n    test_value = \"/Volumes/S/Projects/\"\n    assert data[\"test_repo\"].path != test_value\n    assert data[\"test_repo\"].macos_path != test_value\n    data[\"test_repo\"].path = test_value\n    assert data[\"test_repo\"].macos_path == test_value\n    assert data[\"test_repo\"].path == test_value\n\n\ndef test_equality(setup_repository_db_tests):\n    \"\"\"equality of two repositories.\"\"\"\n    data = setup_repository_db_tests\n    repo1 = Repository(**data[\"kwargs\"])\n    repo2 = Repository(**data[\"kwargs\"])\n\n    data[\"kwargs\"].update(\n        {\n            \"name\": \"a repository\",\n            \"description\": \"this is the commercial repository\",\n            \"linux_path\": \"/mnt/commercialServer/Projects\",\n            \"macos_path\": \"/Volumes/commercialServer/Projects\",\n            \"windows_path\": \"Z:\\\\Projects\",\n        }\n    )\n\n    repo3 = Repository(**data[\"kwargs\"])\n\n    assert repo1 == repo2\n    assert not repo1 == repo3\n\n\ndef test_inequality(setup_repository_db_tests):\n    \"\"\"inequality of two repositories.\"\"\"\n    data = setup_repository_db_tests\n    repo1 = Repository(**data[\"kwargs\"])\n    repo2 = Repository(**data[\"kwargs\"])\n\n    data[\"kwargs\"].update(\n        {\n            \"name\": \"a repository\",\n            \"description\": \"this is the commercial repository\",\n            \"linux_path\": \"/mnt/commercialServer/Projects\",\n            \"macos_path\": \"/Volumes/commercialServer/Projects\",\n            \"windows_path\": \"Z:\\\\Projects\",\n        }\n    )\n\n    repo3 = Repository(**data[\"kwargs\"])\n\n    assert not repo1 != repo2\n    assert repo1 != repo3\n\n\ndef test_plural_class_name(setup_repository_db_tests):\n    \"\"\"plural name of Repository class.\"\"\"\n    data = setup_repository_db_tests\n    assert data[\"test_repo\"].plural_class_name == \"Repositories\"\n\n\ndef test_linux_path_argument_backward_slashes_are_converted_to_forward_slashes(\n    setup_repository_db_tests,\n):\n    \"\"\"backward slashes are converted to forward slashes in the linux_path argument.\"\"\"\n    data = setup_repository_db_tests\n    data[\"kwargs\"][\"linux_path\"] = r\"\\mnt\\M\\Projects\"\n    new_repo = Repository(**data[\"kwargs\"])\n    assert \"\\\\\" not in new_repo.linux_path\n    assert new_repo.linux_path == \"/mnt/M/Projects/\"\n\n\ndef test_linux_path_attribute_backward_slashes_are_converted_to_forward_slashes(\n    setup_repository_db_tests,\n):\n    \"\"\"backward slashes are converted to forward slashes in the linux_path attribute.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = r\"\\mnt\\M\\Projects\"\n    assert \"\\\\\" not in data[\"test_repo\"].linux_path\n    assert data[\"test_repo\"].linux_path == \"/mnt/M/Projects/\"\n\n\ndef test_macos_path_argument_backward_slashes_are_converted_to_forward_slashes(\n    setup_repository_db_tests,\n):\n    \"\"\"backward slashes are converted to forward slashes in the macos_path argument.\"\"\"\n    data = setup_repository_db_tests\n    data[\"kwargs\"][\"macos_path\"] = r\"\\Volumes\\M\\Projects\"\n    new_repo = Repository(**data[\"kwargs\"])\n    assert \"\\\\\" not in new_repo.linux_path\n    assert new_repo.macos_path == \"/Volumes/M/Projects/\"\n\n\ndef test_macos_path_attribute_backward_slashes_are_converted_to_forward_slashes(\n    setup_repository_db_tests,\n):\n    \"\"\"backward slashes are converted to forward slashes in the macos_path attribute.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].macos_path = r\"\\Volumes\\M\\Projects\"\n    assert \"\\\\\" not in data[\"test_repo\"].macos_path\n    assert data[\"test_repo\"].macos_path == \"/Volumes/M/Projects/\"\n\n\ndef test_windows_path_argument_backward_slashes_are_converted_to_forward_slashes(\n    setup_repository_db_tests,\n):\n    \"\"\"backward slashes are converted to forward slashes in the windows_path arg.\"\"\"\n    data = setup_repository_db_tests\n    data[\"kwargs\"][\"windows_path\"] = r\"M:\\Projects\"\n    new_repo = Repository(**data[\"kwargs\"])\n    assert \"\\\\\" not in new_repo.linux_path\n    assert new_repo.windows_path == \"M:/Projects/\"\n\n\ndef test_windows_path_attribute_backward_slashes_are_converted_to_forward_slashes(\n    setup_repository_db_tests,\n):\n    \"\"\"backward slashes are converted to forward slashes in the windows_path attr.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = r\"M:\\Projects\"\n    assert \"\\\\\" not in data[\"test_repo\"].windows_path\n    assert data[\"test_repo\"].windows_path == \"M:/Projects/\"\n\n\ndef test_windows_path_with_more_than_one_slashes_converted_to_single_slash_1(\n    setup_repository_db_tests,\n):\n    \"\"\"windows_path is set with more than one slashes is converted to single slash.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = r\"M://\"\n    assert data[\"test_repo\"].windows_path == \"M:/\"\n\n\ndef test_windows_path_with_more_than_one_slashes_converted_to_single_slash_2(\n    setup_repository_db_tests,\n):\n    \"\"\"windows_path is set with more than one slashes is converted to single slash.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = r\"M://////////\"\n    assert data[\"test_repo\"].windows_path == \"M:/\"\n\n\ndef test_to_linux_path_returns_the_linux_version_of_the_given_windows_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_linux_path returns the linux version of the given windows path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].to_linux_path(test_windows_path) == test_linux_path\n\n\ndef test_to_linux_path_returns_the_linux_version_of_the_given_linux_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_linux_path returns the linux version of the given linux path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].to_linux_path(test_linux_path) == test_linux_path\n\n\ndef test_to_linux_path_returns_the_linux_version_of_the_given_macos_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_linux_path returns the linux version of the given macos path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].to_linux_path(test_macos_path) == test_linux_path\n\n\ndef test_to_linux_path_returns_the_linux_version_of_the_given_reverse_windows_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_linux_path returns the linux version of the given reverse windows path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_windows_path_reverse = (\n        \"T:\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].to_linux_path(test_windows_path_reverse) == test_linux_path\n\n\ndef test_to_linux_path_returns_the_linux_version_of_the_given_reverse_linux_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_linux_path returns the linux version of the given reverse linux path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_linux_path_reverse = (\n        \"\\\\mnt\\\\T\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].to_linux_path(test_linux_path_reverse) == test_linux_path\n\n\ndef test_to_linux_path_returns_the_linux_version_of_the_given_reverse_macos_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_linux_path returns the linux version of the given reverse macos path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_macos_path_reverse = (\n        \"\\\\Volumes\\\\T\\\\Stalker_Projects\\\\Sero\\\\\" \"Task1\\\\Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].to_linux_path(test_macos_path_reverse) == test_linux_path\n\n\ndef test_to_linux_path_returns_the_linux_version_of_the_given_path_with_env_vars(\n    setup_repository_db_tests,\n):\n    \"\"\"to_linux_path returns the linux version of the given path contains env vars.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].id = 1\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    os.environ[\"REPOR1\"] = \"/mnt/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_path_with_env_var = \"$REPOR1/Sero/Task1/Task2/Some_file.ma\"\n    assert data[\"test_repo\"].to_linux_path(test_path_with_env_var) == test_linux_path\n\n\ndef test_to_linux_path_raises_type_error_if_path_is_none(setup_repository_db_tests):\n    \"\"\"to_linux_path raises TypeError if path is None.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"].to_linux_path(None)\n    assert str(cm.value) == (\n        \"path should be a string containing a file path, not NoneType: 'None'\"\n    )\n\n\ndef test_to_linux_path_raises_type_error_if_path_is_not_a_string(\n    setup_repository_db_tests,\n):\n    \"\"\"to_linux_path raises TypeError if path is None.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"].to_linux_path(123)\n    assert str(cm.value) == (\n        \"path should be a string containing a file path, not int: '123'\"\n    )\n\n\ndef test_to_windows_path_returns_the_windows_version_of_the_given_windows_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_windows_path returns the windows version of the given windows path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    assert data[\"test_repo\"].to_windows_path(test_windows_path) == test_windows_path\n\n\ndef test_to_windows_path_returns_the_windows_version_of_the_given_linux_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_windows_path returns the windows version of the given linux path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    assert data[\"test_repo\"].to_windows_path(test_linux_path) == test_windows_path\n\n\ndef test_to_windows_path_returns_the_windows_version_of_the_given_macos_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_windows_path returns the windows version of the given macos path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    assert data[\"test_repo\"].to_windows_path(test_macos_path) == test_windows_path\n\n\ndef test_to_windows_path_returns_the_windows_version_of_the_given_reverse_windows_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_windows_path returns the windows version of the given reverse windows path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    test_windows_path_reverse = (\n        \"T:\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    assert (\n        data[\"test_repo\"].to_windows_path(test_windows_path_reverse)\n        == test_windows_path\n    )\n\n\ndef test_to_windows_path_returns_the_windows_version_of_the_given_reverse_linux_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_windows_path returns the windows version of the given reverse linux path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    test_linux_path_reverse = (\n        \"\\\\mnt\\\\T\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    assert (\n        data[\"test_repo\"].to_windows_path(test_linux_path_reverse) == test_windows_path\n    )\n\n\ndef test_to_windows_path_returns_the_windows_version_of_the_given_reverse_macos_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_windows_path returns the windows version of the given reverse macos path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    test_macos_path_reverse = (\n        \"\\\\Volumes\\\\T\\\\Stalker_Projects\\\\Sero\\\\\" \"Task1\\\\Task2\\\\Some_file.ma\"\n    )\n    assert (\n        data[\"test_repo\"].to_windows_path(test_macos_path_reverse) == test_windows_path\n    )\n\n\ndef test_to_windows_path_returns_the_windows_version_of_the_given_path_with_env_vars(\n    setup_repository_db_tests,\n):\n    \"\"\"to_windows_path returns the windows version of the given path which env vars.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].id = 1\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    os.environ[\"REPOR1\"] = data[\"test_repo\"].linux_path\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_path_with_env_var = \"$REPOR1/Sero/Task1/Task2/Some_file.ma\"\n    assert (\n        data[\"test_repo\"].to_windows_path(test_path_with_env_var) == test_windows_path\n    )\n\n\ndef test_to_windows_path_raises_type_error_if_path_is_none(setup_repository_db_tests):\n    \"\"\"to_windows_path raises TypeError if path is None.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"].to_windows_path(None)\n    assert str(cm.value) == (\n        \"path should be a string containing a file path, not NoneType: 'None'\"\n    )\n\n\ndef test_to_windows_path_raises_type_error_if_path_is_not_a_string(\n    setup_repository_db_tests,\n):\n    \"\"\"to_windows_path raises TypeError if path is None.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"].to_windows_path(123)\n    assert str(cm.value) == (\n        \"path should be a string containing a file path, not int: '123'\"\n    )\n\n\ndef test_to_macos_path_returns_the_macos_version_of_the_given_windows_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_macos_path returns the macos version of the given windows path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].to_macos_path(test_windows_path) == test_macos_path\n\n\ndef test_to_macos_path_returns_the_macos_version_of_the_given_linux_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_macos_path returns the macOS version of the given linux path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].to_macos_path(test_linux_path) == test_macos_path\n\n\ndef test_to_macos_path_returns_the_macos_version_of_the_given_macos_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_macos_path returns the macos version of the given macos path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].to_macos_path(test_macos_path) == test_macos_path\n\n\ndef test_to_macos_path_returns_the_macos_version_of_the_given_reverse_windows_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_macos_path returns the macos version of the given reverse windows path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_windows_path_reverse = (\n        \"T:\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].to_macos_path(test_windows_path_reverse) == test_macos_path\n\n\ndef test_to_macos_path_returns_the_macos_version_of_the_given_reverse_linux_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_macos_path returns the macos version of the given reverse linux path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_linux_path_reverse = (\n        \"\\\\mnt\\\\T\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].to_macos_path(test_linux_path_reverse) == test_macos_path\n\n\ndef test_to_macos_path_returns_the_macos_version_of_the_given_reverse_macos_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_macos_path returns the macos version of the given reverse macos path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_macos_path_reverse = (\n        \"\\\\Volumes\\\\T\\\\Stalker_Projects\\\\Sero\\\\\" \"Task1\\\\Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].to_macos_path(test_macos_path_reverse) == test_macos_path\n\n\ndef test_to_macos_path_returns_the_macos_version_of_the_given_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_macos_path returns the macos version of the given path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n\n    test_windows_path_reverse = (\n        \"T:\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    test_linux_path_reverse = (\n        \"\\\\mnt\\\\T\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    test_macos_path_reverse = (\n        \"\\\\Volumes\\\\T\\\\Stalker_Projects\\\\Sero\\\\\" \"Task1\\\\Task2\\\\Some_file.ma\"\n    )\n\n    assert data[\"test_repo\"].to_macos_path(test_windows_path) == test_macos_path\n    assert data[\"test_repo\"].to_macos_path(test_linux_path) == test_macos_path\n    assert data[\"test_repo\"].to_macos_path(test_macos_path) == test_macos_path\n    assert data[\"test_repo\"].to_macos_path(test_windows_path_reverse) == test_macos_path\n    assert data[\"test_repo\"].to_macos_path(test_linux_path_reverse) == test_macos_path\n    assert data[\"test_repo\"].to_macos_path(test_macos_path_reverse) == test_macos_path\n\n\ndef test_to_macos_path_returns_the_macos_version_of_the_given_path_with_env_vars(\n    setup_repository_db_tests,\n):\n    \"\"\"to_macos_path returns the macos version of the given path which contains env vars.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].id = 1\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    os.environ[\"REPOR1\"] = data[\"test_repo\"].windows_path\n    test_windows_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_path_with_env_var = \"$REPOR1/Sero/Task1/Task2/Some_file.ma\"\n    assert data[\"test_repo\"].to_macos_path(test_path_with_env_var) == test_windows_path\n\n\ndef test_to_macos_path_raises_type_error_if_path_is_none(setup_repository_db_tests):\n    \"\"\"to_macos_path raises TypeError if path is None.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"].to_macos_path(None)\n    assert str(cm.value) == (\n        \"path should be a string containing a file path, not NoneType: 'None'\"\n    )\n\n\ndef test_to_macos_path_raises_type_error_if_path_is_not_a_string(\n    setup_repository_db_tests,\n):\n    \"\"\"to_macos_path raises TypeError if path is None.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"].to_macos_path(123)\n    assert str(cm.value) == (\n        \"path should be a string containing a file path, not int: '123'\"\n    )\n\n\ndef test_to_native_path_returns_the_native_version_of_the_given_linux_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_native_path returns the native version of the given linux path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].to_native_path(test_linux_path) == test_linux_path\n\n\ndef test_to_native_path_returns_the_native_version_of_the_given_windows_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_native_path returns the native version of the given windows path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    assert (\n        data[\"test_repo\"].to_native_path(test_windows_path)\n        == \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    )\n\n\ndef test_to_native_path_returns_the_native_version_of_the_given_macos_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_native_path returns the native version of the given macos path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].to_native_path(test_macos_path) == test_linux_path\n\n\ndef test_to_native_path_returns_the_native_version_of_the_given_reverse_windows_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_native_path returns the native version of the given reverse windows path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    test_windows_path_reverse = (\n        \"T:\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert (\n        data[\"test_repo\"].to_native_path(test_windows_path_reverse) == test_linux_path\n    )\n\n\ndef test_to_native_path_returns_the_native_version_of_the_given_reverse_linux_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_native_path returns the native version of the given reverse linux path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_linux_path_reverse = (\n        \"\\\\mnt\\\\T\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].to_native_path(test_linux_path_reverse) == test_linux_path\n\n\ndef test_to_native_path_returns_the_native_version_of_the_given_reverse_macos_path(\n    setup_repository_db_tests,\n):\n    \"\"\"to_native_path returns the native version of the given reverse macos path.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    test_macos_path_reverse = (\n        \"\\\\Volumes\\\\T\\\\Stalker_Projects\\\\Sero\\\\\" \"Task1\\\\Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].to_native_path(test_macos_path_reverse) == test_linux_path\n\n\ndef test_to_native_path_raises_type_error_if_path_is_none(setup_repository_db_tests):\n    \"\"\"to_native_path raises TypeError if path is None.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"].to_native_path(None)\n    assert str(cm.value) == (\n        \"path should be a string containing a file path, not NoneType: 'None'\"\n    )\n\n\ndef test_to_native_path_raises_type_error_if_path_is_not_a_string(\n    setup_repository_db_tests,\n):\n    \"\"\"to_native_path raises TypeError if path is None.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"].to_native_path(123)\n    assert str(cm.value) == (\n        \"path should be a string containing a file path, not int: '123'\"\n    )\n\n\ndef test_is_in_repo_returns_true_if_the_given_linux_path_is_in_this_repo(\n    setup_repository_db_tests,\n):\n    \"\"\"is_in_repo returns True if linux path is in this repo or False otherwise.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    test_linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].is_in_repo(test_linux_path)\n\n\ndef test_is_in_repo_returns_true_if_the_given_linux_reverse_path_is_in_this_repo(\n    setup_repository_db_tests,\n):\n    \"\"\"is_in_repo returns True if linux path with reverse slashes is in this repo.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    test_linux_path_reverse = (\n        \"\\\\mnt\\\\T\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].is_in_repo(test_linux_path_reverse)\n\n\ndef test_is_in_repo_returns_false_if_the_given_linux_path_is_not_in_this_repo(\n    setup_repository_db_tests,\n):\n    \"\"\"is_in_repo returns False if linux path is not in this repo or False otherwise.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    test_not_in_path_linux_path = (\n        \"/mnt/T/Other_Projects/Sero/Task1/\" \"Task2/Some_file.ma\"\n    )\n    assert data[\"test_repo\"].is_in_repo(test_not_in_path_linux_path) is False\n\n\ndef test_is_in_repo_returns_true_if_the_given_windows_path_is_in_this_repo(\n    setup_repository_db_tests,\n):\n    \"\"\"is_in_repo returns True if windows path is in this repo or False otherwise.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    test_windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    assert data[\"test_repo\"].is_in_repo(test_windows_path)\n\n\ndef test_is_in_repo_returns_true_if_the_given_windows_reverse_path_is_in_this_repo(\n    setup_repository_db_tests,\n):\n    \"\"\"is_in_repo returns True if windows path is in this repo or False otherwise.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    test_windows_path_reverse = (\n        \"T:\\\\Stalker_Projects\\\\Sero\\\\Task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].is_in_repo(test_windows_path_reverse)\n\n\ndef test_is_in_repo_is_case_insensitive_under_windows(setup_repository_db_tests):\n    \"\"\"is_in_repo is case-insensitive under windows.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    test_windows_path_reverse = (\n        \"t:\\\\stalKer_ProjectS\\\\sErO\\\\task1\\\\\" \"Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].is_in_repo(test_windows_path_reverse)\n\n\ndef test_is_in_repo_returns_false_if_the_given_windows_path_is_not_in_this_repo(\n    setup_repository_db_tests,\n):\n    \"\"\"is_in_repo returns False if windows path is not in this repo.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    test_not_in_path_windows_path = \"T:/Other_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].is_in_repo(test_not_in_path_windows_path) is False\n\n\ndef test_is_in_repo_returns_true_if_the_given_macos_path_is_in_this_repo(\n    setup_repository_db_tests,\n):\n    \"\"\"is_in_repo returns True if the given macos path is in this repo.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/\" \"Some_file.ma\"\n    assert data[\"test_repo\"].is_in_repo(test_macos_path)\n\n\ndef test_is_in_repo_returns_true_if_the_given_macos_reverse_path_is_in_this_repo(\n    setup_repository_db_tests,\n):\n    \"\"\"is_in_repo returns True if the macos reverse path is in this repo.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_macos_path_reverse = (\n        \"\\\\Volumes\\\\T\\\\Stalker_Projects\\\\Sero\\\\\" \"Task1\\\\Task2\\\\Some_file.ma\"\n    )\n    assert data[\"test_repo\"].is_in_repo(test_macos_path_reverse)\n\n\ndef test_is_in_repo_returns_false_if_the_given_macos_path_is_not_in_this_repo(\n    setup_repository_db_tests,\n):\n    \"\"\"is_in_repo returns False if macos path is not in this repo.\"\"\"\n    data = setup_repository_db_tests\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    test_not_in_path_macos_path = (\n        \"/Volumes/T/Other_Projects/Sero/Task1/\" \"Task2/Some_file.ma\"\n    )\n    assert not data[\"test_repo\"].is_in_repo(test_not_in_path_macos_path)\n\n\ndef test_make_relative_converts_the_given_linux_path_to_relative_to_repo_root(\n    setup_repository_db_tests,\n):\n    \"\"\"make_relative() will convert the Linux path to repository root relative path.\"\"\"\n    data = setup_repository_db_tests\n    # a Linux Path\n    linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    result = data[\"test_repo\"].make_relative(linux_path)\n    assert result == \"Sero/Task1/Task2/Some_file.ma\"\n\n\ndef test_make_relative_converts_the_given_macos_path_to_relative_to_repo_root(\n    setup_repository_db_tests,\n):\n    \"\"\"make_relative() will convert macos path to repository root relative path.\"\"\"\n    data = setup_repository_db_tests\n    # a macos Path\n    macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    result = data[\"test_repo\"].make_relative(macos_path)\n    assert result == \"Sero/Task1/Task2/Some_file.ma\"\n\n\ndef test_make_relative_converts_the_given_windows_path_to_relative_to_repo_root(\n    setup_repository_db_tests,\n):\n    \"\"\"make_relative() will convert Windows path to repository root relative path.\"\"\"\n    data = setup_repository_db_tests\n    # a Windows Path\n    windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    data[\"test_repo\"].macos_path = \"T:/Stalker_Projects\"\n    result = data[\"test_repo\"].make_relative(windows_path)\n    assert result == \"Sero/Task1/Task2/Some_file.ma\"\n\n\ndef test_make_relative_converts_the_given_path_with_env_variable_to_native_path(\n    setup_repository_db_tests,\n):\n    \"\"\"make_relative() converts path with env vars to repository root relative path.\"\"\"\n    data = setup_repository_db_tests\n    # so we should have the env var to be configured\n    # now create a path with env var\n    path = \"$REPO{}/Sero/Task1/Task2/Some_file.ma\".format(data[\"test_repo\"].code)\n    result = data[\"test_repo\"].make_relative(path)\n    assert result == \"Sero/Task1/Task2/Some_file.ma\"\n\n\ndef test_to_os_independent_path_is_working_as_expected(setup_repository_db_tests):\n    \"\"\"to_os_independent_path() is working as expected.\"\"\"\n    data = setup_repository_db_tests\n    DBSession.add(data[\"test_repo\"])\n    DBSession.commit()\n    relative_part = \"some/path/to/a/file.ma\"\n    test_path = \"{}/{}\".format(data[\"test_repo\"].path, relative_part)\n    assert Repository.to_os_independent_path(test_path) == \"$REPO{}/{}\".format(\n        data[\"test_repo\"].code,\n        relative_part,\n    )\n\n\ndef test_to_os_independent_path_converts_the_given_linux_path_to_universal(\n    setup_repository_db_tests,\n):\n    \"\"\"to_os_independent_path() converts Linux path to an OS independent path.\"\"\"\n    data = setup_repository_db_tests\n    # a Linux Path\n    linux_path = \"/mnt/T/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    data[\"test_repo\"].linux_path = \"/mnt/T/Stalker_Projects\"\n    data[\"test_repo\"].windows_path = \"T:/Stalker_Projects\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    result = data[\"test_repo\"].to_os_independent_path(linux_path)\n    assert result == (\n        \"$REPO{}/Sero/Task1/Task2/Some_file.ma\".format(data[\"test_repo\"].code)\n    )\n\n\ndef test_to_os_independent_path_converts_the_given_macos_path_to_universal(\n    setup_repository_db_tests,\n):\n    \"\"\"to_os_independent_path() converts macos path to an os independent path.\"\"\"\n    data = setup_repository_db_tests\n    # an macOS Path\n    macos_path = \"/Volumes/T/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    data[\"test_repo\"].macos_path = \"/Volumes/T/Stalker_Projects\"\n    result = data[\"test_repo\"].to_os_independent_path(macos_path)\n    assert result == (\n        \"$REPO{}/Sero/Task1/Task2/Some_file.ma\".format(data[\"test_repo\"].code)\n    )\n\n\ndef test_to_os_independent_path_converts_the_given_windows_path_to_universal(\n    setup_repository_db_tests,\n):\n    \"\"\"to_os_independent_path() converts Windows path to an os independent path.\"\"\"\n    data = setup_repository_db_tests\n    # a Windows Path\n    windows_path = \"T:/Stalker_Projects/Sero/Task1/Task2/Some_file.ma\"\n    data[\"test_repo\"].macos_path = \"T:/Stalker_Projects\"\n    result = data[\"test_repo\"].to_os_independent_path(windows_path)\n    assert result == \"$REPO{}/Sero/Task1/Task2/Some_file.ma\".format(\n        data[\"test_repo\"].code\n    )\n\n\ndef test_to_os_independent_path_not_change_the_path_with_env_variable(\n    setup_repository_db_tests,\n):\n    \"\"\"to_os_independent_path() do not change the given path with env var.\"\"\"\n    data = setup_repository_db_tests\n    # so we should have the env var to be configured\n    # now create a path with env var\n    path = \"$REPO{}/Sero/Task1/Task2/Some_file.ma\".format(data[\"test_repo\"].code)\n    result = data[\"test_repo\"].to_os_independent_path(path)\n    assert result == \"$REPO{}/Sero/Task1/Task2/Some_file.ma\".format(\n        data[\"test_repo\"].code\n    )\n\n\ndef test_to_os_independent_path_cannot_convert_the_given_path_with_old_env_variable_new_env_variable(\n    setup_repository_db_tests,\n):\n    \"\"\"to_os_independent_path cannot convert path with old env var to new env var.\"\"\"\n    data = setup_repository_db_tests\n    # so we should have the env var to be configured\n    # now create a path with env var\n    path = \"$REPO{}/Sero/Task1/Task2/Some_file.ma\".format(data[\"test_repo\"].id)\n    result = data[\"test_repo\"].to_os_independent_path(path)\n    assert result != \"$REPO{}/Sero/Task1/Task2/Some_file.ma\".format(\n        data[\"test_repo\"].code\n    )\n\n\ndef test_to_os_independent_path_repo_cannot_be_found(setup_repository_db_tests):\n    \"\"\"to_os_independent_path() repo cannot be found returns the path back.\"\"\"\n    data = setup_repository_db_tests\n    path = \"/not/on/a/particular/repo/file.ma\"\n    result = data[\"test_repo\"].to_os_independent_path(path)\n    assert result == path\n\n\ndef test_find_repo_is_working_as_expected(setup_repository_db_tests):\n    \"\"\"find_repo() is working as expected.\"\"\"\n    data = setup_repository_db_tests\n    DBSession.add(data[\"test_repo\"])\n    DBSession.commit()\n\n    # add some other repositories\n    new_repo1 = Repository(\n        name=\"New Repository\",\n        code=\"NR\",\n        linux_path=\"/mnt/T/Projects\",\n        macos_path=\"/Volumes/T/Projects\",\n        windows_path=\"T:/Projects\",\n    )\n    DBSession.add(new_repo1)\n    DBSession.commit()\n\n    test_path = \"{}/some/path/to/a/file.ma\".format(data[\"test_repo\"].path)\n    assert Repository.find_repo(test_path) == data[\"test_repo\"]\n\n    test_path = \"{}/some/path/to/a/file.ma\".format(new_repo1.windows_path)\n    assert Repository.find_repo(test_path) == new_repo1\n\n\ndef test_find_repo_is_case_insensitive_under_windows(\n    setup_repository_db_tests, monkeypatch\n):\n    \"\"\"find_repo() is case-insensitive under windows.\"\"\"\n\n    def patched_platform_system():\n        \"\"\"Patch the platform.system to always return Windows.\"\"\"\n        return \"Windows\"\n\n    monkeypatch.setattr(\n        \"stalker.models.repository.platform.system\", patched_platform_system\n    )\n\n    data = setup_repository_db_tests\n    DBSession.add(data[\"test_repo\"])\n    DBSession.commit()\n\n    # add some other repositories\n    new_repo1 = Repository(\n        name=\"New Repository\",\n        code=\"NR\",\n        linux_path=\"/mnt/T/Projects\",\n        macos_path=\"/Volumes/T/Projects\",\n        windows_path=\"T:/Projects\",\n    )\n    DBSession.add(new_repo1)\n    DBSession.commit()\n\n    test_path = \"{}/some/path/to/a/file.ma\".format(data[\"test_repo\"].path.lower())\n    assert Repository.find_repo(test_path) == data[\"test_repo\"]\n\n    test_path = \"{}/some/path/to/a/file.ma\".format(new_repo1.windows_path.lower())\n    assert Repository.find_repo(test_path) == new_repo1\n\n\ndef test_find_repo_is_working_as_expected_with_reverse_slashes(\n    setup_repository_db_tests,\n):\n    \"\"\"find_repo class works as expected with paths that contains reverse slashes.\"\"\"\n    data = setup_repository_db_tests\n    DBSession.add(data[\"test_repo\"])\n    DBSession.commit()\n\n    # add some other repositories\n    new_repo1 = Repository(\n        name=\"New Repository\",\n        code=\"NR\",\n        linux_path=\"/mnt/T/Projects\",\n        macos_path=\"/Volumes/T/Projects\",\n        windows_path=\"T:/Projects\",\n    )\n    DBSession.add(new_repo1)\n    DBSession.commit()\n\n    test_path = \"{}\\\\some\\\\path\\\\to\\\\a\\\\file.ma\".format(data[\"test_repo\"].path)\n    test_path.replace(\"/\", \"\\\\\")\n    assert Repository.find_repo(test_path) == data[\"test_repo\"]\n\n    test_path = \"{}\\\\some\\\\path\\\\to\\\\a\\\\file.ma\".format(new_repo1.windows_path.lower())\n    test_path.replace(\"/\", \"\\\\\")\n    assert Repository.find_repo(test_path) == new_repo1\n\n\ndef test_find_repo_is_working_as_expected_with_env_vars(setup_repository_db_tests):\n    \"\"\"find_repo is working as expected with paths containing env vars.\"\"\"\n    data = setup_repository_db_tests\n    DBSession.add(data[\"test_repo\"])\n    DBSession.commit()\n\n    # add some other repositories\n    new_repo1 = Repository(\n        name=\"New Repository\",\n        code=\"NR\",\n        linux_path=\"/mnt/T/Projects\",\n        macos_path=\"/Volumes/T/Projects\",\n        windows_path=\"T:/Projects\",\n    )\n    DBSession.add(new_repo1)\n    DBSession.commit()\n\n    # Test with env var\n    test_path = \"$REPO{}/some/path/to/a/file.ma\".format(data[\"test_repo\"].code)\n    assert Repository.find_repo(test_path) == data[\"test_repo\"]\n\n    test_path = f\"$REPO{new_repo1.code}/some/path/to/a/file.ma\"\n    assert Repository.find_repo(test_path) == new_repo1\n\n\ndef test_find_repo_returns_none_if_a_repo_cannot_be_found(setup_repository_db_tests):\n    \"\"\"find_repo() returns None if a repo cannot be found.\"\"\"\n    data = setup_repository_db_tests\n    result = data[\"test_repo\"].find_repo(\"not a repo path\")\n    assert result is None\n\n\ndef test_env_var_property_is_working_as_expected(setup_repository_db_tests):\n    \"\"\"env_var property is working as expected.\"\"\"\n    data = setup_repository_db_tests\n    assert data[\"test_repo\"].env_var == \"REPOR1\"\n\n\ndef test_creating_and_committing_a_new_repository_instance_will_create_env_var(\n    setup_repository_db_tests,\n):\n    \"\"\"environment variable is created if a new repository is created.\"\"\"\n    repo = Repository(\n        name=\"Test Repo\",\n        code=\"TR\",\n        linux_path=\"/mnt/T\",\n        macos_path=\"/Volumes/T\",\n        windows_path=\"T:/\",\n    )\n    DBSession.add(repo)\n    DBSession.commit()\n\n    assert defaults.repo_env_var_template.format(code=repo.code) in os.environ\n\n\ndef test_updating_a_repository_will_update_repo_path(setup_repository_db_tests):\n    \"\"\"environment variable is updated if the repository path is updated.\"\"\"\n    repo = Repository(\n        name=\"Test Repo\",\n        code=\"TR\",\n        linux_path=\"/mnt/T\",\n        macos_path=\"/Volumes/T\",\n        windows_path=\"T:/\",\n    )\n    DBSession.add(repo)\n    DBSession.commit()\n\n    assert defaults.repo_env_var_template.format(code=repo.code) in os.environ\n\n    # now update the repository\n    test_value = \"/mnt/S/\"\n    repo.path = test_value\n\n    # expect the environment variable is also updated\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)] == test_value\n    )\n\n\ndef test_updating_windows_path_only_update_repo_path_if_on_windows(\n    setup_repository_db_tests,\n):\n    \"\"\"updating the windows path will only update the path if the system is windows.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n    repo = Repository(\n        name=\"Test Repo\",\n        code=\"TR\",\n        linux_path=\"/mnt/T\",\n        macos_path=\"/Volumes/T\",\n        windows_path=\"T:/\",\n    )\n    DBSession.add(repo)\n    DBSession.commit()\n\n    assert defaults.repo_env_var_template.format(code=repo.code) in os.environ\n\n    # now update the repository\n    test_value = \"S:/\"\n    repo.windows_path = test_value\n\n    # expect the environment variable not updated\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)] != test_value\n    )\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)]\n        == repo.linux_path\n    )\n\n    # make it windows\n    data[\"patcher\"].patch(\"Windows\")\n\n    # now update the repository\n    test_value = \"S:/\"\n    repo.windows_path = test_value\n\n    # expect the environment variable not updated\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)] == test_value\n    )\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)]\n        == repo.windows_path\n    )\n\n\ndef test_updating_macos_path_only_update_repo_path_if_on_macos(\n    setup_repository_db_tests,\n):\n    \"\"\"updating the macos path will only update the path if the system is macos.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Windows\")\n\n    repo = Repository(\n        name=\"Test Repo\",\n        code=\"TR\",\n        linux_path=\"/mnt/T\",\n        macos_path=\"/Volumes/T\",\n        windows_path=\"T:/\",\n    )\n    DBSession.add(repo)\n    DBSession.commit()\n\n    assert defaults.repo_env_var_template.format(code=repo.code) in os.environ\n\n    # now update the repository\n    test_value = \"/Volumes/S/\"\n    repo.macos_path = test_value\n\n    # expect the environment variable not updated\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)] != test_value\n    )\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)]\n        == repo.windows_path\n    )\n\n    # make it macos\n    data[\"patcher\"].patch(\"Darwin\")\n\n    # now update the repository\n    test_value = \"/Volumes/S/\"\n    repo.macos_path = test_value\n\n    # expect the environment variable not updated\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)] == test_value\n    )\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)]\n        == repo.macos_path\n    )\n\n\ndef test_updating_linux_path_only_update_repo_path_if_on_linux(\n    setup_repository_db_tests,\n):\n    \"\"\"updating the linux path will only update the path if the system is linux.\"\"\"\n    data = setup_repository_db_tests\n    data[\"patcher\"].patch(\"Darwin\")\n\n    repo = Repository(\n        name=\"Test Repo\",\n        code=\"TR\",\n        linux_path=\"/mnt/T\",\n        macos_path=\"/Volumes/T\",\n        windows_path=\"T:/\",\n    )\n    DBSession.add(repo)\n    DBSession.commit()\n\n    assert defaults.repo_env_var_template.format(code=repo.code) in os.environ\n\n    # now update the repository\n    test_value = \"/mnt/S/\"\n    repo.linux_path = test_value\n\n    # expect the environment variable not updated\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)] != test_value\n    )\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)]\n        == repo.macos_path\n    )\n\n    # make it linux\n    data[\"patcher\"].patch(\"Linux\")\n\n    # now update the repository\n    test_value = \"/mnt/S/\"\n    repo.linux_path = test_value\n\n    # expect the environment variable not updated\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)] == test_value\n    )\n    assert (\n        os.environ[defaults.repo_env_var_template.format(code=repo.code)]\n        == repo.linux_path\n    )\n\n\ndef test_to_path_path_is_none(setup_repository_db_tests):\n    \"\"\"_to_path() path is None raises TypeError.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"]._to_path(None, \"C:/\")\n\n    assert str(cm.value) == (\n        \"path should be a string containing a file path, not NoneType: 'None'\"\n    )\n\n\ndef test_to_path_path_is_not_a_str(setup_repository_db_tests):\n    \"\"\"_to_path() path is not a str raises TypeError.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"]._to_path(1234, \"C:/\")\n\n    assert str(cm.value) == (\n        \"path should be a string containing a file path, not int: '1234'\"\n    )\n\n\ndef test_to_path_path_is_not_starting_with_a_repo_path_returns_the_path(\n    setup_repository_db_tests,\n):\n    \"\"\"_to_path() path is not starting with a repo path returns the path.\"\"\"\n    data = setup_repository_db_tests\n    test_value = \"not_starting_with_any_repo_path\"\n    result = data[\"test_repo\"]._to_path(test_value, \"C:/\")\n    assert result == test_value\n\n\ndef test_to_path_replace_with_is_none(setup_repository_db_tests):\n    \"\"\"_to_path() replace_with is None raises TypeError.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"]._to_path(\"some_path\", None)\n\n    assert str(cm.value) == (\n        \"replace_with should be a string containing a file path, not NoneType: 'None'\"\n    )\n\n\ndef test_to_path_replace_with_is_not_a_str(setup_repository_db_tests):\n    \"\"\"_to_path() replace_with is not a str raises TypeError.\"\"\"\n    data = setup_repository_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_repo\"]._to_path(\"some_path\", 1234)\n\n    assert str(cm.value) == (\n        \"replace_with should be a string containing a file path, not int: '1234'\"\n    )\n\n\ndef test__hash__is_working_as_expected(setup_repository_db_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_repository_db_tests\n    result = hash(data[\"test_repo\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_repo\"].__hash__()\n"
  },
  {
    "path": "tests/models/test_review.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests related to Review class.\"\"\"\n\nimport datetime\nimport sys\n\nimport pytest\n\nimport pytz\n\nfrom stalker import Project, Repository, Review, Status, Structure, Task, User, Version\nfrom stalker.db.session import DBSession\nfrom stalker.models.enum import TimeUnit\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_review_db_test(setup_postgresql_db):\n    \"\"\"Set up the tests for stalker.models.review.Review class with a DB.\"\"\"\n    data = dict()\n    data[\"user1\"] = User(\n        name=\"Test User 1\",\n        login=\"test_user1\",\n        email=\"test1@user.com\",\n        password=\"secret\",\n    )\n    DBSession.add(data[\"user1\"])\n\n    data[\"user2\"] = User(\n        name=\"Test User 2\",\n        login=\"test_user2\",\n        email=\"test2@user.com\",\n        password=\"secret\",\n    )\n    DBSession.add(data[\"user2\"])\n\n    data[\"user3\"] = User(\n        name=\"Test User 2\",\n        login=\"test_user3\",\n        email=\"test3@user.com\",\n        password=\"secret\",\n    )\n    DBSession.add(data[\"user3\"])\n\n    # Review Statuses\n    with DBSession.no_autoflush:\n        data[\"status_new\"] = Status.query.filter_by(code=\"NEW\").first()\n        data[\"status_rrev\"] = Status.query.filter_by(code=\"RREV\").first()\n        data[\"status_app\"] = Status.query.filter_by(code=\"APP\").first()\n\n        # Task Statuses\n        data[\"status_wfd\"] = Status.query.filter_by(code=\"WFD\").first()\n        data[\"status_rts\"] = Status.query.filter_by(code=\"RTS\").first()\n        data[\"status_wip\"] = Status.query.filter_by(code=\"WIP\").first()\n        data[\"status_prev\"] = Status.query.filter_by(code=\"PREV\").first()\n        data[\"status_hrev\"] = Status.query.filter_by(code=\"HREV\").first()\n        data[\"status_drev\"] = Status.query.filter_by(code=\"DREV\").first()\n        data[\"status_cmpl\"] = Status.query.filter_by(code=\"CMPL\").first()\n\n    data[\"repo\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        linux_path=\"/mnt/T/\",\n        windows_path=\"T:/\",\n        macos_path=\"/Volumes/T/\",\n    )\n    DBSession.add(data[\"repo\"])\n\n    data[\"structure\"] = Structure(name=\"Test Project Structure\")\n    DBSession.add(data[\"structure\"])\n\n    data[\"project\"] = Project(name=\"Test Project\", code=\"TP\", repository=data[\"repo\"])\n    DBSession.add(data[\"project\"])\n\n    data[\"task1\"] = Task(\n        name=\"Test Task 1\",\n        project=data[\"project\"],\n        resources=[data[\"user1\"]],\n        responsible=[data[\"user2\"]],\n    )\n    DBSession.add(data[\"task1\"])\n\n    data[\"task2\"] = Task(\n        name=\"Test Task 2\", project=data[\"project\"], responsible=[data[\"user1\"]]\n    )\n    DBSession.add(data[\"task2\"])\n\n    data[\"task3\"] = Task(\n        name=\"Test Task 3\", parent=data[\"task2\"], resources=[data[\"user1\"]]\n    )\n    DBSession.add(data[\"task3\"])\n\n    data[\"task4\"] = Task(\n        name=\"Test Task 4\",\n        project=data[\"project\"],\n        resources=[data[\"user1\"]],\n        depends_on=[data[\"task3\"]],\n        responsible=[data[\"user2\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Hour,\n    )\n    DBSession.add(data[\"task4\"])\n\n    data[\"task5\"] = Task(\n        name=\"Test Task 5\",\n        project=data[\"project\"],\n        resources=[data[\"user2\"]],\n        depends_on=[data[\"task3\"]],\n        responsible=[data[\"user2\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Hour,\n    )\n    DBSession.add(data[\"task5\"])\n\n    data[\"task6\"] = Task(\n        name=\"Test Task 6\",\n        project=data[\"project\"],\n        resources=[data[\"user3\"]],\n        depends_on=[data[\"task3\"]],\n        responsible=[data[\"user2\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Hour,\n    )\n    DBSession.add(data[\"task6\"])\n    data[\"kwargs\"] = {\"task\": data[\"task1\"], \"reviewer\": data[\"user1\"]}\n    # add everything to the db\n    DBSession.commit()\n    return data\n\n\ndef test_task_argument_is_not_a_task_instance(setup_review_db_test):\n    \"\"\"TypeError is raised if the task argument value is not a Task instance.\"\"\"\n    data = setup_review_db_test\n    data[\"kwargs\"][\"task\"] = \"not a Task instance\"\n    with pytest.raises(TypeError) as cm:\n        Review(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value)\n        == \"Review.task should be an instance of stalker.models.task.Task, \"\n        \"not str: 'not a Task instance'\"\n    )\n\n\ndef test_task_argument_is_not_a_leaf_task(setup_review_db_test):\n    \"\"\"ValueError is raised if the task given in task argument is not a leaf task.\"\"\"\n    data = setup_review_db_test\n    task1 = Task(name=\"Task1\", project=data[\"project\"])\n    task2 = Task(name=\"Task2\", parent=task1)\n    data[\"kwargs\"][\"task\"] = task1\n    with pytest.raises(ValueError) as cm:\n        Review(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"It is only possible to create a review for a leaf tasks, and \"\n        \"<Task1 (Task)> is not a leaf task.\"\n    )\n\n\ndef test_task_argument_can_be_skipped_if_version_is_given(setup_review_db_test):\n    \"\"\"task argument can be skipped if the version arg is given.\"\"\"\n    data = setup_review_db_test\n    task1 = Task(name=\"Task1\", project=data[\"project\"])\n    DBSession.save(task1)\n    version = Version(task=task1)\n    DBSession.save(version)\n    data[\"kwargs\"][\"version\"] = version\n    data[\"kwargs\"].pop(\"task\")\n    review = Review(**data[\"kwargs\"])\n    assert review.task == task1\n\n\ndef test_task_argument_is_working_as_expected(setup_review_db_test):\n    \"\"\"task argument value is passed to the task argument.\"\"\"\n    data = setup_review_db_test\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"task1\"].resources[0],\n        start=now,\n        end=now + datetime.timedelta(hours=1),\n    )\n    reviews = data[\"task1\"].request_review()\n    assert reviews[0].task == data[\"task1\"]\n\n\ndef test_auto_name_is_true():\n    \"\"\"review instances are named automatically.\"\"\"\n    assert Review.__auto_name__ is True\n\n\ndef test_status_is_new_for_a_newly_created_review_instance(setup_review_db_test):\n    \"\"\"status is NEW for a newly created review instance.\"\"\"\n    data = setup_review_db_test\n    review = Review(**data[\"kwargs\"])\n    assert review.status == data[\"status_new\"]\n\n\ndef test_review_number_attribute_is_a_read_only_attribute(setup_review_db_test):\n    \"\"\"review_number attribute is a read only attribute.\"\"\"\n    data = setup_review_db_test\n    review = Review(**data[\"kwargs\"])\n    with pytest.raises(AttributeError) as cm:\n        review.review_number = 2\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'Review' object has no setter\",\n        12: \"property of 'Review' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_review_number_getter' of 'Review' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_review_number_attribute_is_initialized_to_the_task_review_number_plus_1(\n    setup_review_db_test,\n):\n    \"\"\"review_number attribute is initialized with task.review_number + 1.\"\"\"\n    data = setup_review_db_test\n    review = Review(**data[\"kwargs\"])\n    assert review.review_number == 1\n\n\ndef test_review_number_for_multiple_responsible_task_is_equal_to_each_other(\n    setup_review_db_test,\n):\n    \"\"\"Review.review_number for a task with multiple responsible equal to each other.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"], data[\"user3\"]]\n\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"task1\"].resources[0],\n        start=now,\n        end=now + datetime.timedelta(hours=1),\n    )\n    reviews = data[\"task1\"].request_review()\n    expected_review_number = data[\"task1\"].review_number + 1\n\n    assert len(reviews) == 3\n    assert reviews[0].review_number == expected_review_number\n    assert reviews[1].review_number == expected_review_number\n    assert reviews[2].review_number == expected_review_number\n\n\ndef test_reviewer_argument_is_skipped(setup_review_db_test):\n    \"\"\"TypeError is raised if the reviewer argument is skipped.\"\"\"\n    data = setup_review_db_test\n    data[\"kwargs\"].pop(\"reviewer\")\n    with pytest.raises(TypeError) as cm:\n        Review(**data[\"kwargs\"])\n    assert (\n        str(cm.value) == \"Review.reviewer should be set to a stalker.models.auth.User \"\n        \"instance, not NoneType: 'None'\"\n    )\n\n\ndef test_reviewer_argument_is_none(setup_review_db_test):\n    \"\"\"TypeError is raised if the reviewer argument is None.\"\"\"\n    data = setup_review_db_test\n    data[\"kwargs\"][\"reviewer\"] = None\n    with pytest.raises(TypeError) as cm:\n        Review(**data[\"kwargs\"])\n    assert (\n        str(cm.value) == \"Review.reviewer should be set to a stalker.models.auth.User \"\n        \"instance, not NoneType: 'None'\"\n    )\n\n\ndef test_reviewer_attribute_is_set_to_none(setup_review_db_test):\n    \"\"\"TypeError is raised if the reviewer attribute is set to None.\"\"\"\n    data = setup_review_db_test\n    review = Review(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        review.reviewer = None\n    assert (\n        str(cm.value) == \"Review.reviewer should be set to a stalker.models.auth.User \"\n        \"instance, not NoneType: 'None'\"\n    )\n\n\ndef test_reviewer_argument_is_not_a_user_instance(setup_review_db_test):\n    \"\"\"TypeError is raised if the reviewer argument is not a User instance.\"\"\"\n    data = setup_review_db_test\n    data[\"kwargs\"][\"reviewer\"] = \"not a user instance\"\n    with pytest.raises(TypeError) as cm:\n        Review(**data[\"kwargs\"])\n    assert (\n        str(cm.value) == \"Review.reviewer should be set to a stalker.models.auth.User \"\n        \"instance, not str: 'not a user instance'\"\n    )\n\n\ndef test_reviewer_attribute_is_not_a_user_instance(setup_review_db_test):\n    \"\"\"TypeError is raised if the reviewer attr is not a User instance.\"\"\"\n    data = setup_review_db_test\n    review = Review(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        review.reviewer = \"not a user\"\n    assert (\n        str(cm.value) == \"Review.reviewer should be set to a stalker.models.auth.User \"\n        \"instance, not str: 'not a user'\"\n    )\n\n\ndef test_reviewer_argument_is_not_in_task_responsible_list(setup_review_db_test):\n    \"\"\"A user not listed in Task.responsible can be reviewer.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"]]\n    data[\"kwargs\"][\"reviewer\"] = data[\"user2\"]\n    review = Review(**data[\"kwargs\"])\n    assert review.reviewer == data[\"user2\"]\n\n\ndef test_reviewer_attribute_is_not_in_task_responsible_list(setup_review_db_test):\n    \"\"\"A user not listed in Task.responsible can be reviewer.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"]]\n    data[\"kwargs\"][\"reviewer\"] = data[\"user1\"]\n    review = Review(**data[\"kwargs\"])\n    review.reviewer = data[\"user2\"]\n    assert review.reviewer == data[\"user2\"]\n\n\ndef test_reviewer_argument_is_working_as_expected(setup_review_db_test):\n    \"\"\"reviewer argument value is correctly passed to reviewer attribute.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"]]\n    data[\"kwargs\"][\"reviewer\"] = data[\"user1\"]\n    review = Review(**data[\"kwargs\"])\n    assert review.reviewer == data[\"user1\"]\n\n\ndef test_reviewer_attribute_is_working_as_expected(setup_review_db_test):\n    \"\"\"reviewer attribute is working as expected.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"]]\n    data[\"kwargs\"][\"reviewer\"] = data[\"user1\"]\n    review = Review(**data[\"kwargs\"])\n    review.reviewer = data[\"user2\"]\n    assert review.reviewer == data[\"user2\"]\n\n\n# TODO: Add tests for the same user is being the reviewer for all reviews at the same\n#       level with same task.\n\n\ndef test_approve_method_updates_task_status_correctly_for_a_single_responsible_task(\n    setup_review_db_test,\n):\n    \"\"\"approve() updates status correctly for a task with only one responsible.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"]]\n    data[\"kwargs\"][\"reviewer\"] = data[\"user1\"]\n    assert data[\"task1\"].status != data[\"status_cmpl\"]\n    review = Review(**data[\"kwargs\"])\n    review.approve()\n    assert data[\"task1\"].status == data[\"status_cmpl\"]\n\n\ndef test_approve_method_updates_task_status_correctly_for_a_multi_responsible_task_if_all_approve(\n    setup_review_db_test,\n):\n    \"\"\"approve() updates status correctly for a task with multiple responsible.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + datetime.timedelta(hours=1)\n    )\n\n    reviews = data[\"task1\"].request_review()\n    review1 = reviews[0]\n    review2 = reviews[1]\n\n    review1.approve()\n    # still pending review\n    assert data[\"task1\"].status == data[\"status_prev\"]\n\n    # first reviewer\n    review2.approve()\n    assert data[\"task1\"].status == data[\"status_cmpl\"]\n\n\ndef test_approve_method_updates_task_parent_status(setup_review_db_test):\n    \"\"\"approve() updates the task parent status.\"\"\"\n    data = setup_review_db_test\n    data[\"task3\"].status = data[\"status_rts\"]\n    now = datetime.datetime.now(pytz.utc)\n    td = datetime.timedelta\n    data[\"task3\"].create_time_log(\n        resource=data[\"task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    reviews = data[\"task3\"].request_review()\n    assert data[\"task3\"].status == data[\"status_prev\"]\n\n    review1 = reviews[0]\n    review1.approve()\n\n    assert data[\"task3\"].status == data[\"status_cmpl\"]\n    assert data[\"task2\"].status == data[\"status_cmpl\"]\n\n\ndef test_approve_method_updates_task_dependent_statuses(setup_review_db_test):\n    \"\"\"approve() updates the task dependent statuses.\"\"\"\n    data = setup_review_db_test\n    data[\"task3\"].status = data[\"status_rts\"]\n    now = datetime.datetime.now(pytz.utc)\n    td = datetime.timedelta\n    data[\"task3\"].create_time_log(\n        resource=data[\"task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    reviews = data[\"task3\"].request_review()\n    assert data[\"task3\"].status == data[\"status_prev\"]\n    review1 = reviews[0]\n    review1.approve()\n\n    assert data[\"task3\"].status == data[\"status_cmpl\"]\n    assert data[\"task4\"].status == data[\"status_rts\"]\n    assert data[\"task5\"].status == data[\"status_rts\"]\n    assert data[\"task6\"].status == data[\"status_rts\"]\n\n    # create time logs for task4 to make it wip\n    data[\"task4\"].create_time_log(\n        resource=data[\"task4\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    assert data[\"task4\"].status == data[\"status_wip\"]\n\n    # now request revision to task3\n    data[\"task3\"].request_revision(reviewer=data[\"task3\"].responsible[0])\n\n    # check statuses of task4 and task4\n    assert data[\"task4\"].status == data[\"status_drev\"]\n    assert data[\"task5\"].status == data[\"status_wfd\"]\n    assert data[\"task6\"].status == data[\"status_wfd\"]\n\n    # now approve task3\n    reviews = data[\"task3\"].review_set()\n    for rev in reviews:\n        rev.approve()\n\n    # check the task statuses again\n    assert data[\"task4\"].status == data[\"status_hrev\"]\n    assert data[\"task5\"].status == data[\"status_rts\"]\n    assert data[\"task5\"].status == data[\"status_rts\"]\n\n\ndef test_approve_method_updates_task_dependent_timings(setup_review_db_test):\n    \"\"\"approve updates the task dependent timings for DREV tasks with no effort left.\"\"\"\n    data = setup_review_db_test\n    data[\"task3\"].status = data[\"status_rts\"]\n    now = datetime.datetime.now(pytz.utc)\n    td = datetime.timedelta\n    tlog = data[\"task3\"].create_time_log(\n        resource=data[\"task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n    DBSession.add(tlog)\n    reviews = data[\"task3\"].request_review()\n    DBSession.add_all(reviews)\n    assert data[\"task3\"].status == data[\"status_prev\"]\n\n    review1 = reviews[0]\n    review1.approve()\n\n    assert data[\"task3\"].status == data[\"status_cmpl\"]\n    assert data[\"task4\"].status == data[\"status_rts\"]\n    assert data[\"task5\"].status == data[\"status_rts\"]\n    assert data[\"task6\"].status == data[\"status_rts\"]\n\n    # create time logs for task4 and task5 to make them wip\n    tlog = data[\"task4\"].create_time_log(\n        resource=data[\"task4\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n    DBSession.add(tlog)\n\n    tlog = data[\"task5\"].create_time_log(\n        resource=data[\"task5\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n    DBSession.add(tlog)\n\n    # no time log for task6\n    assert data[\"task4\"].status == data[\"status_wip\"]\n    assert data[\"task5\"].status == data[\"status_wip\"]\n    assert data[\"task6\"].status == data[\"status_rts\"]\n\n    # now request revision to task3\n    review = data[\"task3\"].request_revision(reviewer=data[\"task3\"].responsible[0])\n    DBSession.add(review)\n\n    # check statuses of task4 and task4\n    assert data[\"task4\"].status == data[\"status_drev\"]\n    assert data[\"task5\"].status == data[\"status_drev\"]\n    assert data[\"task6\"].status == data[\"status_wfd\"]\n\n    # TODO: add a new dependent task with schedule_model is not 'effort'\n    # enter a new time log for task4 to complete its allowed time\n    tlog = data[\"task4\"].create_time_log(\n        resource=data[\"task4\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n    DBSession.save(tlog)\n\n    # the task should have not effort left\n    assert data[\"task4\"].schedule_seconds == data[\"task4\"].total_logged_seconds\n\n    # task5 should have an extra time\n    assert data[\"task5\"].schedule_seconds == data[\"task5\"].total_logged_seconds + 3600\n\n    # task6 should be intact\n    assert data[\"task6\"].total_logged_seconds == 0\n\n    # now approve task3\n    reviews = data[\"task3\"].review_set()\n    for rev in reviews:\n        rev.approve()\n    DBSession.commit()\n\n    # check the task statuses again\n    assert data[\"task4\"].status == data[\"status_hrev\"]\n    assert data[\"task5\"].status == data[\"status_hrev\"]\n    assert data[\"task6\"].status == data[\"status_rts\"]\n\n    # and check if task4 is expanded by the timing resolution\n    assert data[\"task4\"].schedule_seconds == data[\"task4\"].total_logged_seconds + 3600\n\n    # and task5 still has 1 hours\n    assert data[\"task4\"].schedule_seconds == data[\"task4\"].total_logged_seconds + 3600\n\n    # and task6 intact\n    assert data[\"task6\"].total_logged_seconds == 0\n\n\ndef test_approve_method_updates_task_timings(setup_review_db_test):\n    \"\"\"approve method will also update the task timings.\"\"\"\n    data = setup_review_db_test\n    data[\"task3\"].status = data[\"status_rts\"]\n    now = datetime.datetime.now(pytz.utc)\n    td = datetime.timedelta\n\n    data[\"task3\"].schedule_timing = 2\n    data[\"task3\"].schedule_unit = TimeUnit.Hour\n    data[\"task3\"].create_time_log(\n        resource=data[\"task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    reviews = data[\"task3\"].request_review()\n    assert data[\"task3\"].status == data[\"status_prev\"]\n    assert data[\"task3\"].total_logged_seconds != data[\"task3\"].schedule_seconds\n\n    review1 = reviews[0]\n    review1.approve()\n\n    assert data[\"task3\"].status == data[\"status_cmpl\"]\n    assert data[\"task3\"].total_logged_seconds == data[\"task3\"].schedule_seconds\n\n\ndef test_approve_method_updates_task_status_correctly_for_a_multi_responsible_task_if_one_approve(\n    setup_review_db_test,\n):\n    \"\"\"Review.approve() updates the task status for a task with multiple responsible.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"]]\n    now = datetime.datetime.now(pytz.utc)\n    td = datetime.timedelta\n    data[\"task1\"].create_time_log(\n        resource=data[\"task1\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    reviews = data[\"task1\"].request_review()\n    review1 = reviews[0]\n    review2 = reviews[1]\n\n    review1.request_revision()\n    # one request review should be enough to set the status to hrev,\n    # note that this is another tests duty to check\n    assert data[\"task1\"].status == data[\"status_prev\"]\n\n    # first reviewer\n    review2.approve()\n    assert data[\"task1\"].status == data[\"status_hrev\"]\n\n\ndef test_request_revision_method_updates_task_status_correctly_for_a_single_responsible_task(\n    setup_review_db_test,\n):\n    \"\"\"request_revision updates status to HREV for a Task with only one responsible.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"task1\"].resources[0],\n        start=now,\n        end=now + datetime.timedelta(hours=1),\n    )\n\n    reviews = data[\"task1\"].request_review()\n    review = reviews[0]\n    review.request_revision()\n    assert data[\"task1\"].status == data[\"status_hrev\"]\n\n\ndef test_request_revision_method_updates_task_status_correctly_for_a_multi_responsible_task_if_one_request_revision(\n    setup_review_db_test,\n):\n    \"\"\"request_revision updates status for a Task with multiple responsible.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"task1\"].resources[0],\n        start=now,\n        end=now + datetime.timedelta(hours=1),\n    )\n\n    # first reviewer requests a revision\n    reviews = data[\"task1\"].request_review()\n\n    review1 = reviews[0]\n    review2 = reviews[1]\n\n    review1.approve()\n    assert data[\"task1\"].status == data[\"status_prev\"]\n\n    review2.request_revision()\n    assert data[\"task1\"].status == data[\"status_hrev\"]\n\n\ndef test_request_revision_method_updates_task_status_correctly_for_a_multi_responsible_task_if_all_request_revision(\n    setup_review_db_test,\n):\n    \"\"\"request_revision updates status for a Task with multiple responsible.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"task1\"].resources[0],\n        start=now,\n        end=now + datetime.timedelta(hours=1),\n    )\n\n    # first reviewer requests a revision\n    reviews = data[\"task1\"].request_review()\n\n    review1 = reviews[0]\n    review2 = reviews[1]\n\n    review1.request_revision()\n    assert data[\"task1\"].status == data[\"status_prev\"]\n\n    # first reviewer\n    review2.request_revision()\n    assert data[\"task1\"].status == data[\"status_hrev\"]\n\n\n@pytest.mark.parametrize(\"schedule_unit\", [\"h\", TimeUnit.Hour])\ndef test_request_revision_method_updates_task_timing_correctly_for_a_multi_responsible_task_if_all_request_revision(\n    setup_review_db_test, schedule_unit\n):\n    \"\"\"request_revision updates task timing for a Task with multiple responsible.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"]]\n    data[\"task1\"].schedule_timing = 3\n    data[\"task1\"].schedule_unit = schedule_unit\n\n    assert data[\"task1\"].status == data[\"status_rts\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    # create 1 hour time log\n    tlog1 = data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + td(hours=1)\n    )\n    DBSession.add(tlog1)\n    DBSession.commit()\n\n    # first reviewer requests a revision\n    reviews = data[\"task1\"].request_review()\n    DBSession.add_all(reviews)\n    assert len(reviews) == 2\n\n    review1 = reviews[0]\n    review2 = reviews[1]\n\n    review1.request_revision(\n        schedule_timing=2,\n        schedule_unit=schedule_unit,\n        description=\"do some 2 hours extra work\",\n    )\n    assert data[\"task1\"].status == data[\"status_prev\"]\n\n    # first reviewer\n    review2.request_revision(\n        schedule_timing=5,\n        schedule_unit=schedule_unit,\n        description=\"do some 5 hours extra work\",\n    )\n\n    assert data[\"task1\"].status == data[\"status_hrev\"]\n\n    # check the timing values\n    assert data[\"task1\"].schedule_timing == 8\n    assert data[\"task1\"].schedule_unit == TimeUnit.Hour\n\n\n@pytest.mark.parametrize(\"schedule_unit\", [\"h\", TimeUnit.Hour])\ndef test_request_revision_method_updates_task_timing_correctly_for_a_multi_responsible_task_with_exactly_the_same_amount_of_schedule_timing_as_the_given_revision_timing(\n    setup_review_db_test, schedule_unit\n):\n    \"\"\"request_revision updates the task timing for a Task with multiple responsible.\n\n    And has the same amount of schedule timing left with the given revision without\n    expanding the task more then the total amount of revision requested.\n    \"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"]]\n    data[\"task1\"].schedule_timing = 8\n    data[\"task1\"].schedule_unit = schedule_unit\n\n    assert data[\"task1\"].status == data[\"status_rts\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    # create 1 hour time log\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + td(hours=1)\n    )\n\n    # we should have 7 hours left\n\n    # first reviewer requests a revision\n    reviews = data[\"task1\"].request_review()\n\n    assert len(reviews) == 2\n\n    review1 = reviews[0]\n    review2 = reviews[1]\n\n    review1.request_revision(\n        schedule_timing=2,\n        schedule_unit=schedule_unit,\n        description=\"do some 2 hours extra work\",\n    )\n    assert data[\"task1\"].status == data[\"status_prev\"]\n\n    # first reviewer\n    review2.request_revision(\n        schedule_timing=5,\n        schedule_unit=schedule_unit,\n        description=\"do some 5 hours extra work\",\n    )\n\n    assert data[\"task1\"].status == data[\"status_hrev\"]\n\n    # check the timing values\n    assert data[\"task1\"].schedule_timing == 8\n    assert data[\"task1\"].schedule_unit == TimeUnit.Hour\n\n\n@pytest.mark.parametrize(\"schedule_unit\", [\"h\", TimeUnit.Hour])\ndef test_request_revision_method_updates_task_timing_correctly_for_a_multi_responsible_task_with_more_schedule_timing_then_given_revision_timing(\n    setup_review_db_test, schedule_unit\n):\n    \"\"\"request_revision updates the task timing for a Task with multiple responsible.\n\n    And still has more schedule timing then the given revision without expanding the\n    task more then the total amount of revision requested.\n    \"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"]]\n    data[\"task1\"].schedule_timing = 100\n    data[\"task1\"].schedule_unit = schedule_unit\n\n    assert data[\"task1\"].status == data[\"status_rts\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    # create 1 hour time log\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + td(hours=1)\n    )\n\n    # we should have 8 hours left\n\n    # first reviewer requests a revision\n    reviews = data[\"task1\"].request_review()\n\n    assert len(reviews) == 2\n\n    review1 = reviews[0]\n    review2 = reviews[1]\n\n    review1.request_revision(\n        schedule_timing=2,\n        schedule_unit=schedule_unit,\n        description=\"do some 2 hours extra work\",\n    )\n    assert data[\"task1\"].status == data[\"status_prev\"]\n\n    # first reviewer\n    review2.request_revision(\n        schedule_timing=5,\n        schedule_unit=schedule_unit,\n        description=\"do some 5 hours extra work\",\n    )\n\n    assert data[\"task1\"].status == data[\"status_hrev\"]\n\n    # check the timing values\n    assert data[\"task1\"].schedule_timing == 100\n    assert data[\"task1\"].schedule_unit == TimeUnit.Hour\n\n\ndef test_review_set_property_return_all_the_revision_instances_with_same_review_number(\n    setup_review_db_test,\n):\n    \"\"\"review_set returns all the Reviews of the task with the same review_number.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"], data[\"user3\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + datetime.timedelta(hours=1)\n    )\n    data[\"task1\"].status = data[\"status_wip\"]\n    reviews = data[\"task1\"].request_review()\n    review1 = reviews[0]\n    review2 = reviews[1]\n    review3 = reviews[2]\n\n    assert review1.review_number == 1\n    assert review2.review_number == 1\n    assert review3.review_number == 1\n\n    review1.approve()\n    review2.approve()\n    review3.approve()\n\n    review4 = data[\"task1\"].request_revision(reviewer=data[\"user1\"])\n\n    data[\"task1\"].status = data[\"status_wip\"]\n    assert review4.review_number == 2\n\n    # enter new time log to turn it into WIP\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"],\n        start=now + datetime.timedelta(hours=1),\n        end=now + datetime.timedelta(hours=2),\n    )\n\n    review_set2 = data[\"task1\"].request_review()\n    review5 = review_set2[0]\n    review6 = review_set2[1]\n    review7 = review_set2[2]\n\n    assert review5.review_number == 3\n    assert review6.review_number == 3\n    assert review7.review_number == 3\n\n\ndef test_review__init__version_arg_is_skipped(setup_review_db_test):\n    \"\"\"Review.__init__() version arg can be skipped.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"], data[\"user3\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + datetime.timedelta(hours=1)\n    )\n    data[\"task1\"].status = data[\"status_wip\"]\n    review = Review(task=data[\"task1\"], reviewer=data[\"task1\"].responsible[0])\n    assert isinstance(review, Review)\n\n\ndef test_review__init__version_arg_is_none(setup_review_db_test):\n    \"\"\"Review.__init__() version arg can be None.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"], data[\"user3\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + datetime.timedelta(hours=1)\n    )\n    data[\"task1\"].status = data[\"status_wip\"]\n    review = Review(\n        task=data[\"task1\"], version=None, reviewer=data[\"task1\"].responsible[0]\n    )\n    assert isinstance(review, Review)\n\n\ndef test_review__init__version_arg_is_not_a_version_instance(setup_review_db_test):\n    \"\"\"Review.__init__() version arg is not a Version instance.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"], data[\"user3\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + datetime.timedelta(hours=1)\n    )\n    data[\"task1\"].status = data[\"status_wip\"]\n    with pytest.raises(TypeError) as cm:\n        _ = Review(\n            task=data[\"task1\"],\n            version=\"not a version\",\n            reviewer=data[\"task1\"].responsible[0],\n        )\n    assert str(cm.value) == (\n        \"Review.version should be a Version instance, \" \"not str: 'not a version'\"\n    )\n\n\ndef test_review__init__version_arg_is_not_related_to_the_given_task(\n    setup_review_db_test,\n):\n    \"\"\"Review.__init__() raises ValueError if the version is not matching the task.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"], data[\"user3\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + datetime.timedelta(hours=1)\n    )\n    data[\"task1\"].status = data[\"status_wip\"]\n    version = Version(task=data[\"task2\"])\n\n    with pytest.raises(ValueError) as cm:\n        _ = Review(\n            task=data[\"task1\"], version=version, reviewer=data[\"task1\"].responsible[0]\n        )\n    assert str(cm.value) == (\n        \"Review.version should be a Version instance \"\n        f\"related to this Task: {version}\"\n    )\n\n\ndef test_review___init__accepts_a_version_with_version_argument(setup_review_db_test):\n    \"\"\"Review.__init__() accepts a Version instance with version argument.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"], data[\"user3\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + datetime.timedelta(hours=1)\n    )\n    data[\"task1\"].status = data[\"status_wip\"]\n    version = Version(task=data[\"task1\"])\n\n    review = Review(\n        task=data[\"task1\"], version=version, reviewer=data[\"task1\"].responsible[0]\n    )\n    assert isinstance(review, Review)\n\n\ndef test_review___init__version_arg_value_passed_to_version_attr(setup_review_db_test):\n    \"\"\"Review.__init__() version arg value is passed to the version attr.\"\"\"\n    data = setup_review_db_test\n    data[\"task1\"].responsible = [data[\"user1\"], data[\"user2\"], data[\"user3\"]]\n    now = datetime.datetime.now(pytz.utc)\n    data[\"task1\"].create_time_log(\n        resource=data[\"user1\"], start=now, end=now + datetime.timedelta(hours=1)\n    )\n    data[\"task1\"].status = data[\"status_wip\"]\n    version = Version(task=data[\"task1\"])\n\n    review = Review(\n        task=data[\"task1\"], version=version, reviewer=data[\"task1\"].responsible[0]\n    )\n    assert review.version == version\n"
  },
  {
    "path": "tests/models/test_role.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Role class.\"\"\"\n\nfrom stalker import Role\n\n\ndef test_role_class_generic():\n    \"\"\"creation of a Role instance.\"\"\"\n    r = Role(name=\"Lead\")\n    assert isinstance(r, Role)\n    assert r.name == \"Lead\"\n"
  },
  {
    "path": "tests/models/test_scene.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Scene class.\"\"\"\n\nimport pytest\n\nfrom stalker import (\n    Entity,\n    Project,\n    Repository,\n    Scene,\n    Status,\n    StatusList,\n    Task,\n    Type,\n    User,\n)\nfrom stalker.db.session import DBSession\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_scene_db_tests(setup_postgresql_db):\n    \"\"\"Set up the Scene tests with a DB.\"\"\"\n    data = dict()\n    # create a test project, user and a couple of shots\n    data[\"project_type\"] = Type(\n        name=\"Test Project Type\",\n        code=\"test\",\n        target_entity_type=\"Project\",\n    )\n\n    # create a repository\n    data[\"repository_type\"] = Type(\n        name=\"Test Type\", code=\"test\", target_entity_type=\"Repository\"\n    )\n\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"repository_type\"],\n    )\n\n    # create projects\n    data[\"test_project\"] = Project(\n        name=\"Test Project 1\",\n        code=\"tp1\",\n        type=data[\"project_type\"],\n        repository=data[\"test_repository\"],\n    )\n\n    data[\"test_project2\"] = Project(\n        name=\"Test Project 2\",\n        code=\"tp2\",\n        type=data[\"project_type\"],\n        repository=data[\"test_repository\"],\n    )\n\n    # the parameters\n    data[\"kwargs\"] = {\n        \"name\": \"Test Scene\",\n        \"code\": \"tsce\",\n        \"description\": \"A test scene\",\n        \"project\": data[\"test_project\"],\n    }\n\n    # the test sequence\n    data[\"test_scene\"] = Scene(**data[\"kwargs\"])\n    DBSession.add(data[\"test_scene\"])\n    DBSession.commit()\n    return data\n\n\ndef test_scene_is_deriving_from_task():\n    \"\"\"Scene is deriving from Task class.\"\"\"\n    assert Task in Scene.__mro__\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Scene class.\"\"\"\n    assert Scene.__auto_name__ is False\n\n\ndef test_shots_attribute_defaults_to_empty_list(setup_scene_db_tests):\n    \"\"\"shots attribute defaults to an empty list.\"\"\"\n    data = setup_scene_db_tests\n    new_scene = Scene(**data[\"kwargs\"])\n    assert new_scene.shots == []\n\n\ndef test_shots_attribute_is_set_to_none(setup_scene_db_tests):\n    \"\"\"TypeError is raised if the shots attribute is set to None.\"\"\"\n    data = setup_scene_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_scene\"].shots = None\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_shots_attribute_is_set_to_other_than_a_list(setup_scene_db_tests):\n    \"\"\"TypeError is raised if the shots attr is not a list.\"\"\"\n    data = setup_scene_db_tests\n    test_value = [1, 1.2, \"a string\"]\n    with pytest.raises(TypeError) as cm:\n        data[\"test_scene\"].shots = test_value\n    assert str(cm.value) == (\n        \"Scene.shots should only contain instances of \"\n        \"stalker.models.shot.Shot, not int: '1'\"\n    )\n\n\ndef test_shots_attribute_is_a_list_of_other_objects(setup_scene_db_tests):\n    \"\"\"TypeError raised if the shots argument is a list of other type of objects.\"\"\"\n    data = setup_scene_db_tests\n    test_value = [1, 1.2, \"a string\"]\n    with pytest.raises(TypeError) as cm:\n        data[\"test_scene\"].shots = test_value\n    assert str(cm.value) == (\n        \"Scene.shots should only contain instances of \"\n        \"stalker.models.shot.Shot, not int: '1'\"\n    )\n\n\ndef test_shots_attribute_elements_tried_to_be_set_to_non_shot_object(\n    setup_scene_db_tests,\n):\n    \"\"\"TypeError raised if shots list appended a not Shot instance.\"\"\"\n    data = setup_scene_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_scene\"].shots.append(\"a string\")\n    assert str(cm.value) == (\n        \"Scene.shots should only contain instances of \"\n        \"stalker.models.shot.Shot, not str: 'a string'\"\n    )\n\n\ndef test_equality(setup_scene_db_tests):\n    \"\"\"equality of scene instances.\"\"\"\n    data = setup_scene_db_tests\n    new_seq1 = Scene(**data[\"kwargs\"])\n    new_seq2 = Scene(**data[\"kwargs\"])\n    new_entity = Entity(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"name\"] = \"a different scene\"\n    new_seq3 = Scene(**data[\"kwargs\"])\n\n    assert new_seq1 == new_seq2\n    assert not new_seq1 == new_seq3\n    assert not new_seq1 == new_entity\n\n\ndef test_inequality(setup_scene_db_tests):\n    \"\"\"inequality of scene instances.\"\"\"\n    data = setup_scene_db_tests\n    new_seq1 = Scene(**data[\"kwargs\"])\n    new_seq2 = Scene(**data[\"kwargs\"])\n    new_entity = Entity(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"name\"] = \"a different scene\"\n    new_seq3 = Scene(**data[\"kwargs\"])\n\n    assert not new_seq1 != new_seq2\n    assert new_seq1 != new_seq3\n    assert new_seq1 != new_entity\n\n\ndef test_project_mixin_initialization(setup_scene_db_tests):\n    \"\"\"ProjectMixin part is initialized correctly.\"\"\"\n    data = setup_scene_db_tests\n    project_type = Type(name=\"Commercial\", code=\"comm\", target_entity_type=\"Project\")\n\n    new_project = Project(\n        name=\"Test Project\",\n        code=\"tp\",\n        type=project_type,\n        repository=data[\"test_repository\"],\n    )\n\n    data[\"kwargs\"][\"project\"] = new_project\n    new_scene = Scene(**data[\"kwargs\"])\n    assert new_scene.project == new_project\n\n\ndef test___strictly_typed___is_false():\n    \"\"\"__strictly_typed__ class attribute is False for Scene class.\"\"\"\n    assert Scene.__strictly_typed__ is False\n\n\ndef test__hash__is_working_as_expected(setup_scene_db_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_scene_db_tests\n    result = hash(data[\"test_scene\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_scene\"].__hash__()\n\n\ndef test_can_be_used_in_a_task_hierarchy(setup_scene_db_tests):\n    \"\"\"Scene can be used in a Task hierarchy.\"\"\"\n    data = setup_scene_db_tests\n\n    task1 = Task(name=\"Parent Task\", project=data[\"test_project\"])\n    data[\"test_scene\"].parent = task1\n\n    assert data[\"test_scene\"] in task1.children\n\n\ndef test_scenes_can_use_task_status_list():\n    \"\"\"It is possible to use TaskStatus lists with Shots.\"\"\"\n    # users\n    test_user1 = User(\n        name=\"User1\", login=\"user1\", password=\"12345\", email=\"user1@user1.com\"\n    )\n    # statuses\n    status_wip = Status(code=\"WIP\", name=\"Work In Progress\")\n    status_cmpl = Status(code=\"CMPL\", name=\"Complete\")\n\n    # Just create a StatusList for Tasks\n    task_status_list = StatusList(\n        statuses=[status_wip, status_cmpl], target_entity_type=\"Task\"\n    )\n    project_status_list = StatusList(\n        statuses=[status_wip, status_cmpl], target_entity_type=\"Project\"\n    )\n\n    # types\n    commercial_project_type = Type(\n        name=\"Commercial Project\",\n        code=\"commproj\",\n        target_entity_type=\"Project\",\n    )\n    # project\n    project1 = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=commercial_project_type,\n        status_list=project_status_list,\n    )\n    # sequence\n    test_scene = Scene(\n        name=\"Test Scene\",\n        code=\"tsce\",\n        project=project1,\n        status_list=task_status_list,\n        responsible=[test_user1],\n    )\n    assert test_scene.status_list == task_status_list\n"
  },
  {
    "path": "tests/models/test_schedule_constraint.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"ScheduleConstraint related tests are here.\"\"\"\nfrom enum import IntEnum\nimport sys\n\nimport pytest\n\nfrom stalker.models.enum import ScheduleConstraint, ScheduleConstraintDecorator\n\n\n@pytest.mark.parametrize(\n    \"schedule_constraint\",\n    [\n        ScheduleConstraint.NONE,\n        ScheduleConstraint.Start,\n        ScheduleConstraint.End,\n        ScheduleConstraint.Both,\n    ],\n)\ndef test_it_is_an_int_enum(schedule_constraint):\n    \"\"\"ScheduleConstraint is an IntEnum.\"\"\"\n    assert isinstance(schedule_constraint, IntEnum)\n\n\n@pytest.mark.parametrize(\n    \"schedule_constraint,expected_value\",\n    [\n        [ScheduleConstraint.NONE, 0],\n        [ScheduleConstraint.Start, 1],\n        [ScheduleConstraint.End, 2],\n        [ScheduleConstraint.Both, 3],\n    ],\n)\ndef test_enum_values(schedule_constraint, expected_value):\n    \"\"\"Test enum values.\"\"\"\n    assert schedule_constraint == expected_value\n\n\n@pytest.mark.parametrize(\n    \"schedule_constraint,expected_value\",\n    [\n        [ScheduleConstraint.NONE, \"None\"],\n        [ScheduleConstraint.Start, \"Start\"],\n        [ScheduleConstraint.End, \"End\"],\n        [ScheduleConstraint.Both, \"Both\"],\n    ],\n)\ndef test_enum_names(schedule_constraint, expected_value):\n    \"\"\"Test enum names.\"\"\"\n    assert str(schedule_constraint) == expected_value\n\n\ndef test_to_constraint_constraint_is_skipped():\n    \"\"\"ScheduleConstraint.to_constraint() constraint is skipped.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = ScheduleConstraint.to_constraint()\n\n    py_error_message = {\n        8: \"to_constraint() missing 1 required positional argument: 'constraint'\",\n        9: \"to_constraint() missing 1 required positional argument: 'constraint'\",\n        10: \"ScheduleConstraint.to_constraint() missing 1 required positional argument: 'constraint'\",\n        11: \"ScheduleConstraint.to_constraint() missing 1 required positional argument: 'constraint'\",\n        12: \"ScheduleConstraint.to_constraint() missing 1 required positional argument: 'constraint'\",\n        13: \"ScheduleConstraint.to_constraint() missing 1 required positional argument: 'constraint'\",\n    }[sys.version_info.minor]\n    assert str(cm.value) == py_error_message\n\n\ndef test_to_constraint_constraint_is_none():\n    \"\"\"ScheduleConstraint.to_constraint() constraint is None.\"\"\"\n    constraint = ScheduleConstraint.to_constraint(None)\n    assert constraint == ScheduleConstraint.NONE\n\n\ndef test_to_constraint_constraint_is_not_a_str():\n    \"\"\"ScheduleConstraint.to_constraint() constraint is not an int or str.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = ScheduleConstraint.to_constraint(12334.123)\n\n    assert str(cm.value) == (\n        \"constraint should be a ScheduleConstraint enum value or an int or a \"\n        \"str, not float: '12334.123'\"\n    )\n\n\ndef test_to_constraint_constraint_is_not_a_valid_str():\n    \"\"\"ScheduleConstraint.to_constraint() constraint is not a valid str.\"\"\"\n    with pytest.raises(ValueError) as cm:\n        _ = ScheduleConstraint.to_constraint(\"not a valid value\")\n\n    assert str(cm.value) == (\n        \"constraint should be a ScheduleConstraint enum value or one of \"\n        \"['None', 'Start', 'End', 'Both'], not 'not a valid value'\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"constraint_name,constraint\",\n    [\n        # None\n        [\"None\", ScheduleConstraint.NONE],\n        [\"none\", ScheduleConstraint.NONE],\n        [\"NONE\", ScheduleConstraint.NONE],\n        [\"NoNe\", ScheduleConstraint.NONE],\n        [\"nONe\", ScheduleConstraint.NONE],\n        [0, ScheduleConstraint.NONE],\n        # Start\n        [\"Start\", ScheduleConstraint.Start],\n        [\"start\", ScheduleConstraint.Start],\n        [\"START\", ScheduleConstraint.Start],\n        [\"StaRt\", ScheduleConstraint.Start],\n        [\"STaRt\", ScheduleConstraint.Start],\n        [\"StARt\", ScheduleConstraint.Start],\n        [1, ScheduleConstraint.Start],\n        # End\n        [\"End\", ScheduleConstraint.End],\n        [\"end\", ScheduleConstraint.End],\n        [\"END\", ScheduleConstraint.End],\n        [\"eNd\", ScheduleConstraint.End],\n        [\"eND\", ScheduleConstraint.End],\n        [2, ScheduleConstraint.End],\n        # Both\n        [\"Both\", ScheduleConstraint.Both],\n        [\"both\", ScheduleConstraint.Both],\n        [\"BOTH\", ScheduleConstraint.Both],\n        [\"bOth\", ScheduleConstraint.Both],\n        [\"boTh\", ScheduleConstraint.Both],\n        [\"BotH\", ScheduleConstraint.Both],\n        [\"BOtH\", ScheduleConstraint.Both],\n        [3, ScheduleConstraint.Both],\n    ],\n)\ndef test_to_constraint_is_working_properly(constraint_name, constraint):\n    \"\"\"ScheduleConstraint can parse schedule constraint names.\"\"\"\n    assert ScheduleConstraint.to_constraint(constraint_name) == constraint\n\n\ndef test_cache_ok_is_true_in_type_decorator():\n    \"\"\"ScheduleConstraintDecorator.cache_ok is True.\"\"\"\n    assert ScheduleConstraintDecorator.cache_ok is True\n"
  },
  {
    "path": "tests/models/test_schedule_model.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"ScheduleModel related tests are here.\"\"\"\nfrom enum import Enum\nimport sys\n\nimport pytest\n\nfrom stalker.models.enum import ScheduleModel, ScheduleModelDecorator\n\n\n@pytest.mark.parametrize(\n    \"model\",\n    [\n        ScheduleModel.Effort,\n        ScheduleModel.Duration,\n        ScheduleModel.Length,\n    ],\n)\ndef test_it_is_an_enum(model):\n    \"\"\"ScheduleModel is an Enum.\"\"\"\n    assert isinstance(model, Enum)\n\n\n@pytest.mark.parametrize(\n    \"model,expected_value\",\n    [\n        [ScheduleModel.Effort, \"effort\"],\n        [ScheduleModel.Duration, \"duration\"],\n        [ScheduleModel.Length, \"length\"],\n    ],\n)\ndef test_enum_values(model, expected_value):\n    \"\"\"Test enum values.\"\"\"\n    assert model.value == expected_value\n\n\n@pytest.mark.parametrize(\n    \"model,expected_name\",\n    [\n        [ScheduleModel.Effort, \"Effort\"],\n        [ScheduleModel.Duration, \"Duration\"],\n        [ScheduleModel.Length, \"Length\"],\n    ],\n)\ndef test_enum_names(model, expected_name):\n    \"\"\"Test enum names.\"\"\"\n    assert model.name == expected_name\n\n\n@pytest.mark.parametrize(\n    \"model,expected_value\",\n    [\n        [ScheduleModel.Effort, \"effort\"],\n        [ScheduleModel.Duration, \"duration\"],\n        [ScheduleModel.Length, \"length\"],\n    ],\n)\ndef test_enum_as_str(model, expected_value):\n    \"\"\"Test enum names.\"\"\"\n    assert str(model) == expected_value\n\n\ndef test_to_model_model_is_skipped():\n    \"\"\"ScheduleModel.to_model() model is skipped.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = ScheduleModel.to_model()\n\n    py_error_message = {\n        8: \"to_model() missing 1 required positional argument: 'model'\",\n        9: \"to_model() missing 1 required positional argument: 'model'\",\n        10: \"ScheduleModel.to_model() missing 1 required positional argument: 'model'\",\n        11: \"ScheduleModel.to_model() missing 1 required positional argument: 'model'\",\n        12: \"ScheduleModel.to_model() missing 1 required positional argument: 'model'\",\n        13: \"ScheduleModel.to_model() missing 1 required positional argument: 'model'\",\n    }[sys.version_info.minor]\n    assert str(cm.value) == py_error_message\n\n\ndef test_to_model_model_is_none():\n    \"\"\"ScheduleModel.to_model() model is None.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = ScheduleModel.to_model(None)\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test_to_model_model_is_not_a_str():\n    \"\"\"ScheduleModel.to_model() model is not a str.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = ScheduleModel.to_model(12334.123)\n\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], \"\n        \"not float: '12334.123'\"\n    )\n\n\ndef test_to_model_model_is_not_a_valid_str():\n    \"\"\"ScheduleModel.to_model() model is not a valid str.\"\"\"\n    with pytest.raises(ValueError) as cm:\n        _ = ScheduleModel.to_model(\"not a valid value\")\n\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], \"\n        \"not 'not a valid value'\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"model_name,model\",\n    [\n        # Effort\n        [\"Effort\", ScheduleModel.Effort],\n        [\"effort\", ScheduleModel.Effort],\n        [\"EFFORT\", ScheduleModel.Effort],\n        [\"EfFoRt\", ScheduleModel.Effort],\n        # Duration\n        [\"Duration\", ScheduleModel.Duration],\n        [\"duration\", ScheduleModel.Duration],\n        [\"DURATION\", ScheduleModel.Duration],\n        [\"DuRaTiOn\", ScheduleModel.Duration],\n        [\"dUrAtIoN\", ScheduleModel.Duration],\n        # Length\n        [\"Length\", ScheduleModel.Length],\n        [\"length\", ScheduleModel.Length],\n        [\"LENGTH\", ScheduleModel.Length],\n        [\"LeNgTh\", ScheduleModel.Length],\n        [\"lEnGtH\", ScheduleModel.Length],\n    ],\n)\ndef test_to_model_is_working_properly(model_name, model):\n    \"\"\"ScheduleModel can parse schedule model names.\"\"\"\n    assert ScheduleModel.to_model(model_name) == model\n\n\ndef test_cache_ok_is_true_in_type_decorator():\n    \"\"\"ScheduleModelDecorator.cache_ok is True.\"\"\"\n    assert ScheduleModelDecorator.cache_ok is True\n"
  },
  {
    "path": "tests/models/test_schedulers.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the SchedulerBase class.\"\"\"\n\nimport pytest\n\nfrom stalker import SchedulerBase, Studio\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_scheduler_base_tests():\n    \"\"\"Set up the tests for stalker.models.scheduler.SchedulerBase class.\"\"\"\n    data = dict()\n    data[\"test_studio\"] = Studio(name=\"Test Studio\")\n    data[\"kwargs\"] = {\"studio\": data[\"test_studio\"]}\n    data[\"test_scheduler_base\"] = SchedulerBase(**data[\"kwargs\"])\n    return data\n\n\ndef test_studio_argument_is_skipped(setup_scheduler_base_tests):\n    \"\"\"studio attribute None if the studio argument is skipped.\"\"\"\n    data = setup_scheduler_base_tests\n    data[\"kwargs\"].pop(\"studio\")\n    new_scheduler_base = SchedulerBase(**data[\"kwargs\"])\n    assert new_scheduler_base.studio is None\n\n\ndef test_studio_argument_is_none(setup_scheduler_base_tests):\n    \"\"\"studio attribute None if the studio argument is None.\"\"\"\n    data = setup_scheduler_base_tests\n    data[\"kwargs\"][\"studio\"] = None\n    new_scheduler_base = SchedulerBase(**data[\"kwargs\"])\n    assert new_scheduler_base.studio is None\n\n\ndef test_studio_attribute_is_none(setup_scheduler_base_tests):\n    \"\"\"studio argument can be set to None.\"\"\"\n    data = setup_scheduler_base_tests\n    data[\"test_scheduler_base\"].studio = None\n    assert data[\"test_scheduler_base\"].studio is None\n\n\ndef test_studio_argument_is_not_a_studio_instance(setup_scheduler_base_tests):\n    \"\"\"TypeError raised if the studio argument is not Studio instance.\"\"\"\n    data = setup_scheduler_base_tests\n    data[\"kwargs\"][\"studio\"] = \"not a studio instance\"\n    with pytest.raises(TypeError) as cm:\n        SchedulerBase(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"SchedulerBase.studio should be an instance of \"\n        \"stalker.models.studio.Studio, not str: 'not a studio instance'\"\n    )\n\n\ndef test_studio_attribute_is_not_a_studio_instance(setup_scheduler_base_tests):\n    \"\"\"TypeError raised if the studio attr is not a Studio instance.\"\"\"\n    data = setup_scheduler_base_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_scheduler_base\"].studio = \"not a studio instance\"\n\n    assert (\n        str(cm.value) == \"SchedulerBase.studio should be an instance of \"\n        \"stalker.models.studio.Studio, not str: 'not a studio instance'\"\n    )\n\n\ndef test_studio_argument_is_working_as_expected(setup_scheduler_base_tests):\n    \"\"\"studio argument value is correctly passed to the studio attribute.\"\"\"\n    data = setup_scheduler_base_tests\n    assert data[\"test_scheduler_base\"].studio == data[\"kwargs\"][\"studio\"]\n\n\ndef test_studio_attribute_is_working_as_expected(setup_scheduler_base_tests):\n    \"\"\"studio attribute is working as expected.\"\"\"\n    data = setup_scheduler_base_tests\n    new_studio = Studio(name=\"Test Studio 2\")\n    data[\"test_scheduler_base\"].studio = new_studio\n    assert data[\"test_scheduler_base\"].studio == new_studio\n\n\ndef test_schedule_method_will_raise_not_implemented_error():\n    \"\"\"schedule() method will raise a NotImplementedError.\"\"\"\n    base = SchedulerBase()\n    with pytest.raises(NotImplementedError) as cm:\n        base.schedule()\n\n    assert str(cm.value) == \"\"\n"
  },
  {
    "path": "tests/models/test_sequence.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Sequence class.\"\"\"\n\nimport pytest\n\nfrom stalker import (\n    Entity,\n    File,\n    Project,\n    Repository,\n    Sequence,\n    Status,\n    StatusList,\n    Task,\n    Type,\n    User,\n)\nfrom stalker.db.session import DBSession\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_sequence_db_tests(setup_postgresql_db):\n    \"\"\"Set up the tests for the Sequence class with a DB.\"\"\"\n    data = dict()\n    # create a test project, user and a couple of shots\n    data[\"project_type\"] = Type(\n        name=\"Test Project Type\",\n        code=\"test\",\n        target_entity_type=\"Project\",\n    )\n    DBSession.add(data[\"project_type\"])\n\n    # create a repository\n    data[\"repository_type\"] = Type(\n        name=\"Test Type\", code=\"test\", target_entity_type=\"Repository\"\n    )\n    DBSession.add(data[\"repository_type\"])\n\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"repository_type\"],\n    )\n    DBSession.add(data[\"test_repository\"])\n\n    # create projects\n    data[\"test_project\"] = Project(\n        name=\"Test Project 1\",\n        code=\"tp1\",\n        type=data[\"project_type\"],\n        repository=data[\"test_repository\"],\n    )\n    DBSession.add(data[\"test_project\"])\n\n    data[\"test_project2\"] = Project(\n        name=\"Test Project 2\",\n        code=\"tp2\",\n        type=data[\"project_type\"],\n        repository=data[\"test_repository\"],\n    )\n    DBSession.add(data[\"test_project2\"])\n\n    # the parameters\n    data[\"kwargs\"] = {\n        \"name\": \"Test Sequence\",\n        \"code\": \"tseq\",\n        \"description\": \"A test sequence\",\n        \"project\": data[\"test_project\"],\n    }\n\n    # the test sequence\n    data[\"test_sequence\"] = Sequence(**data[\"kwargs\"])\n    DBSession.commit()\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false(setup_sequence_db_tests):\n    \"\"\"__auto_name__ class attribute is set to False for Sequence class.\"\"\"\n    assert Sequence.__auto_name__ is False\n\n\ndef test_plural_class_name(setup_sequence_db_tests):\n    \"\"\"plural name of Sequence class.\"\"\"\n    data = setup_sequence_db_tests\n    assert data[\"test_sequence\"].plural_class_name == \"Sequences\"\n\n\ndef test___strictly_typed___is_False():\n    \"\"\"__strictly_typed__ class attribute is False for Sequence class.\"\"\"\n    assert Sequence.__strictly_typed__ is False\n\n\ndef test_shots_attribute_defaults_to_empty_list(setup_sequence_db_tests):\n    \"\"\"shots attribute defaults to an empty list.\"\"\"\n    data = setup_sequence_db_tests\n    new_sequence = Sequence(**data[\"kwargs\"])\n    assert new_sequence.shots == []\n\n\ndef test_shots_attribute_is_set_none(setup_sequence_db_tests):\n    \"\"\"TypeError raised if the shots attribute set to None.\"\"\"\n    data = setup_sequence_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_sequence\"].shots = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_shots_attribute_is_set_to_other_than_a_list(setup_sequence_db_tests):\n    \"\"\"TypeError raised if the shots attr is set to something other than a list.\"\"\"\n    data = setup_sequence_db_tests\n    test_value = \"a string\"\n    with pytest.raises(TypeError) as cm:\n        data[\"test_sequence\"].shots = test_value\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_shots_attribute_is_a_list_of_other_objects(setup_sequence_db_tests):\n    \"\"\"TypeError raised if the shots argument is a list of other type of objects.\"\"\"\n    data = setup_sequence_db_tests\n    test_value = [1, 1.2, \"a string\"]\n    with pytest.raises(TypeError) as cm:\n        data[\"test_sequence\"].shots = test_value\n\n    assert str(cm.value) == (\n        \"Sequence.shots should only contain instances of \"\n        \"stalker.models.shot.Shot, not int: '1'\"\n    )\n\n\ndef test_shots_attribute_elements_tried_to_be_set_to_non_Shot_object(\n    setup_sequence_db_tests,\n):\n    \"\"\"TypeError raised if the shots attr appended not a Shot instance.\"\"\"\n    data = setup_sequence_db_tests\n    test_value = \"a string\"\n    with pytest.raises(TypeError) as cm:\n        data[\"test_sequence\"].shots.append(test_value)\n\n    assert str(cm.value) == (\n        \"Sequence.shots should only contain instances of \"\n        \"stalker.models.shot.Shot, not str: 'a string'\"\n    )\n\n\ndef test_equality(setup_sequence_db_tests):\n    \"\"\"equality of sequences.\"\"\"\n    data = setup_sequence_db_tests\n    new_seq1 = Sequence(**data[\"kwargs\"])\n    new_seq2 = Sequence(**data[\"kwargs\"])\n    new_entity = Entity(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"name\"] = \"a different sequence\"\n    new_seq3 = Sequence(**data[\"kwargs\"])\n\n    assert new_seq1 == new_seq2\n    assert not new_seq1 == new_seq3\n    assert not new_seq1 == new_entity\n\n\ndef test_inequality(setup_sequence_db_tests):\n    \"\"\"inequality of sequences.\"\"\"\n    data = setup_sequence_db_tests\n    new_seq1 = Sequence(**data[\"kwargs\"])\n    new_seq2 = Sequence(**data[\"kwargs\"])\n    new_entity = Entity(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"name\"] = \"a different sequence\"\n    new_seq3 = Sequence(**data[\"kwargs\"])\n\n    assert not new_seq1 != new_seq2\n    assert new_seq1 != new_seq3\n    assert new_seq1 != new_entity\n\n\ndef test_reference_mixin_initialization(setup_sequence_db_tests):\n    \"\"\"ReferenceMixin part is initialized correctly.\"\"\"\n    data = setup_sequence_db_tests\n    file_type_1 = Type(name=\"Image\", code=\"image\", target_entity_type=\"File\")\n\n    file1 = File(\n        name=\"Artwork 1\",\n        full_path=\"/mnt/M/JOBs/TEST_PROJECT\",\n        filename=\"a.jpg\",\n        type=file_type_1,\n    )\n    file2 = File(\n        name=\"Artwork 2\",\n        full_path=\"/mnt/M/JOBs/TEST_PROJECT\",\n        filename=\"b.jbg\",\n        type=file_type_1,\n    )\n    references = [file1, file2]\n    data[\"kwargs\"][\"references\"] = references\n    new_sequence = Sequence(**data[\"kwargs\"])\n    assert new_sequence.references == references\n\n\ndef test_initialization_of_task_part(setup_sequence_db_tests):\n    \"\"\"Task part is initialized correctly.\"\"\"\n    data = setup_sequence_db_tests\n    project_type = Type(name=\"Commercial\", code=\"comm\", target_entity_type=\"Project\")\n\n    new_project = Project(\n        name=\"Commercial\",\n        code=\"comm\",\n        type=project_type,\n        repository=data[\"test_repository\"],\n    )\n\n    data[\"kwargs\"][\"project\"] = new_project\n    new_sequence = Sequence(**data[\"kwargs\"])\n\n    task1 = Task(\n        name=\"Modeling\",\n        status=0,\n        project=new_project,\n        parent=new_sequence,\n    )\n\n    task2 = Task(\n        name=\"Lighting\",\n        status=0,\n        project=new_project,\n        parent=new_sequence,\n    )\n\n    tasks = [task1, task2]\n\n    assert sorted(new_sequence.tasks, key=lambda x: x.name) == sorted(\n        tasks, key=lambda x: x.name\n    )\n\n\ndef test_project_mixin_initialization(setup_sequence_db_tests):\n    \"\"\"ProjectMixin part is initialized correctly.\"\"\"\n    data = setup_sequence_db_tests\n    project_type = Type(name=\"Commercial\", code=\"comm\", target_entity_type=\"Project\")\n\n    new_project = Project(\n        name=\"Test Project\",\n        code=\"tp\",\n        type=project_type,\n        repository=data[\"test_repository\"],\n    )\n\n    data[\"kwargs\"][\"project\"] = new_project\n    new_sequence = Sequence(**data[\"kwargs\"])\n    assert new_sequence.project == new_project\n\n\ndef test__hash__is_working_as_expected(setup_sequence_db_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_sequence_db_tests\n    result = hash(data[\"test_sequence\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_sequence\"].__hash__()\n\n\ndef test_sequences_can_use_task_status_list():\n    \"\"\"It is possible to use TaskStatus lists with Shots.\"\"\"\n    # users\n    test_user1 = User(\n        name=\"User1\", login=\"user1\", password=\"12345\", email=\"user1@user1.com\"\n    )\n    # statuses\n    status_wip = Status(code=\"WIP\", name=\"Work In Progress\")\n    status_cmpl = Status(code=\"CMPL\", name=\"Complete\")\n\n    # Just create a StatusList for Tasks\n    task_status_list = StatusList(\n        statuses=[status_wip, status_cmpl], target_entity_type=\"Task\"\n    )\n    project_status_list = StatusList(\n        statuses=[status_wip, status_cmpl], target_entity_type=\"Project\"\n    )\n\n    # types\n    commercial_project_type = Type(\n        name=\"Commercial Project\",\n        code=\"commproj\",\n        target_entity_type=\"Project\",\n    )\n    # project\n    project1 = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=commercial_project_type,\n        status_list=project_status_list,\n    )\n    # sequence\n    test_seq1 = Sequence(\n        name=\"Test Sequence\",\n        code=\"tseq\",\n        project=project1,\n        status_list=task_status_list,\n        responsible=[test_user1],\n    )\n    assert test_seq1.status_list == task_status_list\n"
  },
  {
    "path": "tests/models/test_shot.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Shot class.\"\"\"\n\nimport sys\nimport pytest\n\nfrom stalker import (\n    Asset,\n    Entity,\n    ImageFormat,\n    File,\n    Project,\n    Repository,\n    Scene,\n    Sequence,\n    Shot,\n    Status,\n    StatusList,\n    Task,\n    Type,\n    User,\n)\nfrom stalker.db.session import DBSession\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_shot_db_tests(setup_postgresql_db):\n    \"\"\"Set up the tests for the Shot class with a DB.\"\"\"\n    data = dict()\n    data[\"database_settings\"] = setup_postgresql_db\n    # statuses\n    # types\n    data[\"test_commercial_project_type\"] = Type(\n        name=\"Commercial Project\",\n        code=\"comm\",\n        target_entity_type=\"Project\",\n    )\n    DBSession.add(data[\"test_commercial_project_type\"])\n\n    data[\"test_character_asset_type\"] = Type(\n        name=\"Character\",\n        code=\"char\",\n        target_entity_type=\"Asset\",\n    )\n    DBSession.add(data[\"test_character_asset_type\"])\n\n    data[\"test_repository_type\"] = Type(\n        name=\"Test Repository Type\", code=\"test\", target_entity_type=\"Repository\"\n    )\n    DBSession.add(data[\"test_repository_type\"])\n\n    # repository\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"test_repository_type\"],\n    )\n    DBSession.add(data[\"test_repository\"])\n\n    # image format\n    data[\"test_image_format1\"] = ImageFormat(\n        name=\"Test Image Format 1\", width=1920, height=1080, pixel_aspect=1.0\n    )\n    DBSession.add(data[\"test_image_format1\"])\n\n    data[\"test_image_format2\"] = ImageFormat(\n        name=\"Test Image Format 2\", width=1280, height=720, pixel_aspect=1.0\n    )\n    DBSession.add(data[\"test_image_format2\"])\n\n    # project and sequences\n    data[\"test_project1\"] = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=data[\"test_commercial_project_type\"],\n        repository=data[\"test_repository\"],\n        image_format=data[\"test_image_format1\"],\n    )\n    DBSession.add(data[\"test_project1\"])\n    DBSession.commit()\n\n    data[\"test_project2\"] = Project(\n        name=\"Test Project2\",\n        code=\"tp2\",\n        type=data[\"test_commercial_project_type\"],\n        repository=data[\"test_repository\"],\n        image_format=data[\"test_image_format1\"],\n    )\n    DBSession.add(data[\"test_project2\"])\n    DBSession.commit()\n\n    data[\"test_sequence1\"] = Sequence(\n        name=\"Test Seq1\",\n        code=\"ts1\",\n        project=data[\"test_project1\"],\n    )\n    DBSession.add(data[\"test_sequence1\"])\n    DBSession.commit()\n\n    data[\"test_sequence2\"] = Sequence(\n        name=\"Test Seq2\",\n        code=\"ts2\",\n        project=data[\"test_project1\"],\n    )\n    DBSession.add(data[\"test_sequence2\"])\n    DBSession.commit()\n\n    data[\"test_sequence3\"] = Sequence(\n        name=\"Test Seq3\",\n        code=\"ts3\",\n        project=data[\"test_project1\"],\n    )\n    DBSession.add(data[\"test_sequence3\"])\n    DBSession.commit()\n\n    data[\"test_scene1\"] = Scene(\n        name=\"Test Sce1\",\n        code=\"tsc1\",\n        project=data[\"test_project1\"],\n    )\n    DBSession.add(data[\"test_scene1\"])\n    DBSession.commit()\n\n    data[\"test_scene2\"] = Scene(\n        name=\"Test Sce2\",\n        code=\"tsc2\",\n        project=data[\"test_project1\"],\n    )\n    DBSession.add(data[\"test_scene2\"])\n    DBSession.commit()\n\n    data[\"test_scene3\"] = Scene(\n        name=\"Test Sce3\", code=\"tsc3\", project=data[\"test_project1\"]\n    )\n    DBSession.add(data[\"test_scene3\"])\n    DBSession.commit()\n\n    data[\"test_asset1\"] = Asset(\n        name=\"Test Asset1\",\n        code=\"ta1\",\n        project=data[\"test_project1\"],\n        type=data[\"test_character_asset_type\"],\n    )\n    DBSession.add(data[\"test_asset1\"])\n    DBSession.commit()\n\n    data[\"test_asset2\"] = Asset(\n        name=\"Test Asset2\",\n        code=\"ta2\",\n        project=data[\"test_project1\"],\n        type=data[\"test_character_asset_type\"],\n    )\n    DBSession.add(data[\"test_asset2\"])\n    DBSession.commit()\n\n    data[\"test_asset3\"] = Asset(\n        name=\"Test Asset3\",\n        code=\"ta3\",\n        project=data[\"test_project1\"],\n        type=data[\"test_character_asset_type\"],\n    )\n    DBSession.add(data[\"test_asset3\"])\n    DBSession.commit()\n\n    data[\"kwargs\"] = dict(\n        name=\"SH123\",\n        code=\"SH123\",\n        description=\"This is a test Shot\",\n        project=data[\"test_project1\"],\n        sequence=data[\"test_sequence1\"],\n        scene=data[\"test_scene1\"],\n        cut_in=112,\n        cut_out=149,\n        source_in=120,\n        source_out=140,\n        record_in=85485,\n        status=0,\n        image_format=data[\"test_image_format2\"],\n    )\n\n    # create a mock shot object\n    data[\"test_shot\"] = Shot(**data[\"kwargs\"])\n    DBSession.add(data[\"test_shot\"])\n    DBSession.commit()\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_true():\n    \"\"\"__auto_name__ class attribute is set to True for Shot class.\"\"\"\n    assert Shot.__auto_name__ is True\n\n\ndef test___hash___value_is_correctly_calculated(setup_shot_db_tests):\n    \"\"\"__hash__ value is correctly calculated.\"\"\"\n    data = setup_shot_db_tests\n    assert data[\"test_shot\"].__hash__() == hash(\n        \"{}:{}:{}\".format(\n            data[\"test_shot\"].id, data[\"test_shot\"].name, data[\"test_shot\"].entity_type\n        )\n    )\n\n\ndef test_project_argument_is_skipped(setup_shot_db_tests):\n    \"\"\"TypeError raised if the project argument is skipped.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"].pop(\"project\")\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"Shot.project should be an instance of \"\n        \"stalker.models.project.Project, not NoneType: 'None'.\\n\\nOr please supply \"\n        \"a stalker.models.task.Task with the parent argument, so \"\n        \"Stalker can use the project of the supplied parent task\"\n    )\n\n\ndef test_project_argument_is_None(setup_shot_db_tests):\n    \"\"\"TypeError raised if the project argument is None.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"project\"] = None\n\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"Shot.project should be an instance of \"\n        \"stalker.models.project.Project, not NoneType: 'None'.\\n\\nOr please supply \"\n        \"a stalker.models.task.Task with the parent argument, so \"\n        \"Stalker can use the project of the supplied parent task\"\n    )\n\n\n@pytest.mark.parametrize(\"test_value\", [1, 1.2, \"project\", [\"a\", \"project\"]])\ndef test_project_argument_is_not_project_instance(test_value, setup_shot_db_tests):\n    \"\"\"TypeError raised if the given project argument is not a Project instance.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"project\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        Shot(data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.project should be an instance of stalker.models.project.Project, not \"\n        \"NoneType: 'None'.\\n\\nOr please supply a stalker.models.task.Task with the parent \"\n        \"argument, so Stalker can use the project of the supplied parent task\"\n    )\n\n\ndef test_project_already_has_a_shot_with_the_same_code(setup_shot_db_tests):\n    \"\"\"ValueError raised if project argument already has a shot with the same code.\"\"\"\n    data = setup_shot_db_tests\n    # let's try to assign the shot to the same sequence2 which has another\n    # shot with the same code\n    assert data[\"kwargs\"][\"code\"] == data[\"test_shot\"].code\n    with pytest.raises(ValueError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"There is a Shot with the same code: SH123\"\n\n    # this should not raise a ValueError\n    data[\"kwargs\"][\"code\"] = \"DifferentCode\"\n    new_shot2 = Shot(**data[\"kwargs\"])\n    assert isinstance(new_shot2, Shot)\n\n\ndef test_code_attribute_is_set_to_the_same_value(setup_shot_db_tests):\n    \"\"\"ValueError will NOT be raised if the shot.code is set to the same value.\"\"\"\n    data = setup_shot_db_tests\n    data[\"test_shot\"].code = data[\"test_shot\"].code\n\n\ndef test_project_attribute_is_read_only(setup_shot_db_tests):\n    \"\"\"project attribute is read only.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_shot\"].project = data[\"test_project2\"]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'Shot' object has no setter\",\n        12: \"property of 'Shot' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_project_getter' of 'Shot' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_project_contains_shots(setup_shot_db_tests):\n    \"\"\"shot is added to the Project.shots list.\"\"\"\n    data = setup_shot_db_tests\n    assert data[\"test_shot\"] in data[\"test_shot\"].project.shots\n\n\ndef test_project_argument_is_working_as_expected(setup_shot_db_tests):\n    \"\"\"project argument is working as expected.\"\"\"\n    data = setup_shot_db_tests\n    assert data[\"test_shot\"].project == data[\"kwargs\"][\"project\"]\n\n\ndef test_sequence_argument_is_skipped(setup_shot_db_tests):\n    \"\"\"sequence attribute a None if the sequence argument is skipped.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"].pop(\"sequence\")\n    data[\"kwargs\"][\"code\"] = \"DifferentCode\"\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.sequence == None\n\n\ndef test_sequence_argument_is_none(setup_shot_db_tests):\n    \"\"\"sequence attribute is None if the sequence argument is set to None.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"sequence\"] = None\n    data[\"kwargs\"][\"code\"] = \"NewCode\"\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.sequence is None\n\n\ndef test_sequence_attribute_is_set_to_none(setup_shot_db_tests):\n    \"\"\"No TypeError raised if the sequence attribute is set to None.\"\"\"\n    data = setup_shot_db_tests\n    data[\"test_shot\"].sequence = None\n\n\ndef test_sequence_argument_is_not_a_list(setup_shot_db_tests):\n    \"\"\"TypeError raised if the sequence argument is not a Sequence.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"sequence\"] = \"not a sequence\"\n    data[\"kwargs\"][\"code\"] = \"NewCode\"\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n    assert str(cm.value) == (\n        \"Shot.sequence should be a stalker.models.sequence.Sequence instance, \"\n        \"not str: 'not a sequence'\"\n    )\n\n\ndef test_sequence_attribute_is_not_a_sequence(setup_shot_db_tests):\n    \"\"\"TypeError raised if the sequence attribute is not a Sequence.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].sequence = \"not a sequence\"\n\n    assert str(cm.value) == (\n        \"Shot.sequence should be a stalker.models.sequence.Sequence instance, \"\n        \"not str: 'not a sequence'\"\n    )\n\n\ndef test_sequence_argument_is_a_list_of_sequence_instances(setup_shot_db_tests):\n    \"\"\"TypeError raised if the sequence argument is a list of Sequences.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"NewShot\"\n    seq1 = Sequence(\n        name=\"seq1\",\n        code=\"seq1\",\n        project=data[\"test_project1\"],\n    )\n    seq2 = Sequence(\n        name=\"seq2\",\n        code=\"seq2\",\n        project=data[\"test_project1\"],\n    )\n    seq3 = Sequence(\n        name=\"seq3\",\n        code=\"seq3\",\n        project=data[\"test_project1\"],\n    )\n\n    seqs = [seq1, seq2, seq3]\n    data[\"kwargs\"][\"sequence\"] = seqs\n    with pytest.raises(TypeError) as cm:\n        _ = Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.sequence should be a stalker.models.sequence.Sequence instance, \"\n        \"not list: '[<seq1 (Sequence)>, <seq2 (Sequence)>, <seq3 (Sequence)>]'\"\n    )\n\n\ndef test_sequence_attribute_is_a_list_of_Sequence_instances(setup_shot_db_tests):\n    \"\"\"TypeError raised if the sequence attr is a list of Sequence instances.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"NewShot\"\n    seq1 = Sequence(\n        name=\"seq1\",\n        code=\"seq1\",\n        project=data[\"test_project1\"],\n    )\n    seq2 = Sequence(\n        name=\"seq2\",\n        code=\"seq2\",\n        project=data[\"test_project1\"],\n    )\n    seq3 = Sequence(\n        name=\"seq3\",\n        code=\"seq3\",\n        project=data[\"test_project1\"],\n    )\n\n    new_shot = Shot(**data[\"kwargs\"])\n\n    with pytest.raises(TypeError) as cm:\n        new_shot.sequence = [seq1, seq2, seq3]\n\n    assert str(cm.value) == (\n        \"Shot.sequence should be a stalker.models.sequence.Sequence instance, \"\n        \"not list: '[<seq1 (Sequence)>, <seq2 (Sequence)>, <seq3 (Sequence)>]'\"\n    )\n\n\ndef test_sequence_argument_is_working_as_expected(setup_shot_db_tests):\n    \"\"\"sequence attribute is working as expected.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"NewShot\"\n    seq1 = Sequence(\n        name=\"seq1\",\n        code=\"seq1\",\n        project=data[\"test_project1\"],\n    )\n    seq2 = Sequence(\n        name=\"seq2\",\n        code=\"seq2\",\n        project=data[\"test_project1\"],\n    )\n    seq3 = Sequence(\n        name=\"seq3\",\n        code=\"seq3\",\n        project=data[\"test_project1\"],\n    )\n\n    data[\"kwargs\"][\"sequence\"] = seq2\n    new_shot = Shot(**data[\"kwargs\"])\n\n    assert new_shot.sequence == seq2\n\n\ndef test_sequence_attribute_is_working_as_expected(setup_shot_db_tests):\n    \"\"\"sequence attribute is working as expected.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"NewShot\"\n    seq1 = Sequence(\n        name=\"seq1\",\n        code=\"seq1\",\n        project=data[\"test_project1\"],\n    )\n    seq2 = Sequence(\n        name=\"seq2\",\n        code=\"seq2\",\n        project=data[\"test_project1\"],\n    )\n    seq3 = Sequence(\n        name=\"seq3\",\n        code=\"seq3\",\n        project=data[\"test_project1\"],\n    )\n\n    new_shot = Shot(**data[\"kwargs\"])\n\n    new_shot.sequence = seq2\n    assert new_shot.sequence == seq2\n\n\ndef test_scene_argument_is_skipped(setup_shot_db_tests):\n    \"\"\"scene attribute is None if the scene argument is skipped.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"].pop(\"scene\")\n    data[\"kwargs\"][\"code\"] = \"DifferentCode\"\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.scene is None\n\n\ndef test_scene_argument_is_None(setup_shot_db_tests):\n    \"\"\"scene attribute is None if the scene argument is set to None.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"scene\"] = None\n    data[\"kwargs\"][\"code\"] = \"NewCode\"\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.scene is None\n\n\ndef test_scene_attribute_is_set_to_None(setup_shot_db_tests):\n    \"\"\"TypeError is not raised if the scene attribute is set to None.\"\"\"\n    data = setup_shot_db_tests\n    assert data[\"test_shot\"].scene is not None\n    # no error should be raised\n    data[\"test_shot\"].scene = None\n    assert data[\"test_shot\"].scene is None\n\n\ndef test_scene_argument_is_not_a_scene(setup_shot_db_tests):\n    \"\"\"TypeError raised if the scene argument is not a scene.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"scene\"] = \"not a scene\"\n    data[\"kwargs\"][\"code\"] = \"NewCode\"\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.scene should be a stalker.models.scene.Scene instance, not str: \"\n        \"'not a scene'\"\n    )\n\n\ndef test_scene_attribute_is_not_a_scene(setup_shot_db_tests):\n    \"\"\"TypeError raised if the scene attribute is not a Scene.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].scene = \"not a scene\"\n\n    assert str(cm.value) == (\n        \"Shot.scene should be a stalker.models.scene.Scene instance, not str: \"\n        \"'not a scene'\"\n    )\n\n\ndef test_scene_argument_is_a_list_of_scene_instances(setup_shot_db_tests):\n    \"\"\"TypeError raised if the scene argument is a list of Scene instances.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"scene\"] = [\n        Scene(name=\"sce1\", code=\"sce1\", project=data[\"test_project1\"]),\n        Scene(name=\"sce2\", code=\"sce2\", project=data[\"test_project1\"]),\n        Scene(name=\"sce3\", code=\"sce3\", project=data[\"test_project1\"]),\n    ]\n    data[\"kwargs\"][\"code\"] = \"NewShot\"\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.scene should be a stalker.models.scene.Scene instance, \"\n        \"not list: '[<sce1 (Scene)>, <sce2 (Scene)>, <sce3 (Scene)>]'\"\n    )\n\n\ndef test_scene_attribute_is_a_list_of_Scene_instances(setup_shot_db_tests):\n    \"\"\"TypeError raised if the scene attribute is not a list of Scene instances.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].scene = [\n            Scene(name=\"sce1\", code=\"sce1\", project=data[\"test_project1\"]),\n            Scene(name=\"sce2\", code=\"sce2\", project=data[\"test_project1\"]),\n            Scene(name=\"sce3\", code=\"sce3\", project=data[\"test_project1\"]),\n        ]\n\n    assert str(cm.value) == (\n        \"Shot.scene should be a stalker.models.scene.Scene instance, \"\n        \"not list: '[<sce1 (Scene)>, <sce2 (Scene)>, <sce3 (Scene)>]'\"\n    )\n\n\ndef test_scene_argument_is_working_as_expected(setup_shot_db_tests):\n    \"\"\"scene argument value is passed to scene attribute as expected.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"NewShot\"\n    sce1 = Scene(name=\"sce1\", code=\"sce1\", project=data[\"test_project1\"])\n    sce2 = Scene(name=\"sce2\", code=\"sce2\", project=data[\"test_project1\"])\n    sce3 = Scene(name=\"sce3\", code=\"sce3\", project=data[\"test_project1\"])\n\n    DBSession.add_all([sce1, sce2, sce3])\n    data[\"kwargs\"][\"scene\"] = sce1\n    new_shot = Shot(**data[\"kwargs\"])\n    DBSession.add(new_shot)\n\n    assert new_shot.scene == sce1\n\n\ndef test_scene_attribute_is_working_as_expected(setup_shot_db_tests):\n    \"\"\"scene attribute is working as expected.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"NewShot\"\n\n    sce1 = Scene(name=\"sce1\", code=\"sce1\", project=data[\"test_project1\"])\n    sce2 = Scene(name=\"sce2\", code=\"sce2\", project=data[\"test_project1\"])\n    sce3 = Scene(name=\"sce3\", code=\"sce3\", project=data[\"test_project1\"])\n    DBSession.add_all([sce1, sce2, sce3])\n    data[\"kwargs\"][\"scene\"] = sce1\n\n    new_shot = Shot(**data[\"kwargs\"])\n    DBSession.add(new_shot)\n\n    assert new_shot.scene != sce2\n    new_shot.scene = sce2\n    assert new_shot.scene == sce2\n\n\ndef test_cut_in_argument_is_skipped(setup_shot_db_tests):\n    \"\"\"cut_in arg skipped the cut_in arg is calculated from cut_out arg.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"].pop(\"cut_in\")\n    data[\"kwargs\"][\"source_in\"] = None\n    data[\"kwargs\"][\"source_out\"] = None\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.cut_out == data[\"kwargs\"][\"cut_out\"]\n    assert new_shot.cut_in == new_shot.cut_out\n\n\ndef test_cut_in_argument_is_none(setup_shot_db_tests):\n    \"\"\"cut_in attr is calculated from the cut_out attr if the cut_in arg is None.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"cut_in\"] = None\n    data[\"kwargs\"][\"source_in\"] = None\n    data[\"kwargs\"][\"source_out\"] = None\n    shot = Shot(**data[\"kwargs\"])\n    assert shot.cut_out == data[\"kwargs\"][\"cut_out\"]\n    assert shot.cut_in == shot.cut_out\n\n\ndef test_cut_in_attribute_is_set_to_none(setup_shot_db_tests):\n    \"\"\"TypeError raised if the cut_in attribute is set to None.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].cut_in = None\n    assert str(cm.value) == \"Shot.cut_in should be an int, not NoneType: 'None'\"\n\n\ndef test_cut_in_argument_is_not_integer(setup_shot_db_tests):\n    \"\"\"TypeError raised if the cut_in argument is not an instance of int.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"cut_in\"] = \"a string\"\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n    assert str(cm.value) == \"Shot.cut_in should be an int, not str: 'a string'\"\n\n\ndef test_cut_in_attribute_is_not_integer(setup_shot_db_tests):\n    \"\"\"TypeError raised if the cut_in attr not an integer.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].cut_in = \"a string\"\n    assert str(cm.value) == \"Shot.cut_in should be an int, not str: 'a string'\"\n\n\ndef test_cut_in_argument_is_bigger_than_cut_out_argument(setup_shot_db_tests):\n    \"\"\"cut_out offset if the cut_in arg value is bigger than the cut_out arg value.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"cut_in\"] = data[\"kwargs\"][\"cut_out\"] + 10\n    data[\"kwargs\"][\"source_in\"] = None\n    data[\"kwargs\"][\"source_out\"] = None\n    shot = Shot(**data[\"kwargs\"])\n    assert shot.cut_in == 149\n    assert shot.cut_out == 149\n\n\ndef test_cut_in_attribute_is_bigger_than_cut_out_attribute(setup_shot_db_tests):\n    \"\"\"the cut_out attr offset if the cut_in is set bigger than cut_out.\"\"\"\n    data = setup_shot_db_tests\n    data[\"test_shot\"].cut_in = data[\"test_shot\"].cut_out + 10\n    assert data[\"test_shot\"].cut_in == 159\n    assert data[\"test_shot\"].cut_out == data[\"test_shot\"].cut_in\n\n\ndef test_cut_out_argument_is_skipped(setup_shot_db_tests):\n    \"\"\"cut_out attr calculated from cut_in arg value if the cut_out arg is skipped.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"].pop(\"cut_out\")\n    data[\"kwargs\"][\"source_in\"] = None\n    data[\"kwargs\"][\"source_out\"] = None\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.cut_in == data[\"kwargs\"][\"cut_in\"]\n    assert new_shot.cut_out == new_shot.cut_in\n\n\ndef test_cut_out_argument_is_set_to_none(setup_shot_db_tests):\n    \"\"\"cut_out arg is set to None the cut_out attr calculated from cut_in arg value.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"cut_out\"] = None\n    data[\"kwargs\"][\"source_in\"] = None\n    data[\"kwargs\"][\"source_out\"] = None\n\n    shot = Shot(**data[\"kwargs\"])\n    assert shot.cut_in == data[\"kwargs\"][\"cut_in\"]\n    assert shot.cut_out == shot.cut_in\n\n\ndef test_cut_out_attribute_is_set_to_none(setup_shot_db_tests):\n    \"\"\"TypeError raised if the cut_out attribute is set to None.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].cut_out = None\n    assert str(cm.value) == \"Shot.cut_out should be an int, not NoneType: 'None'\"\n\n\ndef test_cut_out_argument_is_not_integer(setup_shot_db_tests):\n    \"\"\"TypeError raised if the cut_out argument is not an integer.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"cut_out\"] = \"a string\"\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n    assert str(cm.value) == \"Shot.cut_out should be an int, not str: 'a string'\"\n\n\ndef test_cut_out_attribute_is_not_integer(setup_shot_db_tests):\n    \"\"\"TypeError raised if the cut_out attr is set to a value other than an integer.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].cut_out = \"a string\"\n    assert str(cm.value) == \"Shot.cut_out should be an int, not str: 'a string'\"\n\n\ndef test_cut_out_argument_is_smaller_than_cut_in_argument(setup_shot_db_tests):\n    \"\"\"cut_out attr is updated if the cut_out arg is smaller than cut_in arg.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"cut_out\"] = data[\"kwargs\"][\"cut_in\"] - 10\n    data[\"kwargs\"][\"source_in\"] = None\n    data[\"kwargs\"][\"source_out\"] = None\n    shot = Shot(**data[\"kwargs\"])\n    assert shot.cut_in == 102\n    assert shot.cut_out == 102\n\n\ndef test_cut_out_attribute_is_smaller_than_cut_in_attribute(setup_shot_db_tests):\n    \"\"\"cut_out attribute is updated if it is smaller than cut_in attribute.\"\"\"\n    data = setup_shot_db_tests\n    data[\"test_shot\"].cut_out = data[\"test_shot\"].cut_in - 10\n    assert data[\"test_shot\"].cut_in == 102\n    assert data[\"test_shot\"].cut_out == 102\n\n\ndef test_cut_duration_attribute_is_not_instance_of_int(setup_shot_db_tests):\n    \"\"\"TypeError raised if the cut_duration attr is set to a value other than an int.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].cut_duration = \"a string\"\n    assert str(cm.value) == (\n        \"Shot.cut_duration should be a positive integer value, not str: 'a string'\"\n    )\n\n\ndef test_cut_duration_attribute_will_be_updated_if_cut_in_attribute_changed(\n    setup_shot_db_tests,\n):\n    \"\"\"cut_duration attribute updated if the cut_in attribute changed.\"\"\"\n    data = setup_shot_db_tests\n    data[\"test_shot\"].cut_in = 1\n    assert (\n        data[\"test_shot\"].cut_duration\n        == data[\"test_shot\"].cut_out - data[\"test_shot\"].cut_in + 1\n    )\n\n\ndef test_cut_duration_attribute_will_be_updated_if_cut_out_attribute_changed(\n    setup_shot_db_tests,\n):\n    \"\"\"cut_duration attribute updated if the cut_out attribute changed.\"\"\"\n    data = setup_shot_db_tests\n    data[\"test_shot\"].cut_out = 1000\n    assert (\n        data[\"test_shot\"].cut_duration\n        == data[\"test_shot\"].cut_out - data[\"test_shot\"].cut_in + 1\n    )\n\n\ndef test_cut_duration_attribute_changes_cut_out_attribute(setup_shot_db_tests):\n    \"\"\"changes in cut_duration attribute will also affect cut_out value.\"\"\"\n    data = setup_shot_db_tests\n    first_cut_out = data[\"test_shot\"].cut_out\n    data[\"test_shot\"].cut_duration = 245\n    assert data[\"test_shot\"].cut_out != first_cut_out\n    assert (\n        data[\"test_shot\"].cut_out\n        == data[\"test_shot\"].cut_in + data[\"test_shot\"].cut_duration - 1\n    )\n\n\ndef test_cut_duration_attribute_is_zero(setup_shot_db_tests):\n    \"\"\"ValueError raised if the cut_duration attribute is set to zero.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_shot\"].cut_duration = 0\n    assert str(cm.value) == (\n        \"Shot.cut_duration cannot be set to zero or a negative value\"\n    )\n\n\ndef test_cut_duration_attribute_is_negative(setup_shot_db_tests):\n    \"\"\"ValueError raised if the cut_duration attribute is set to a negative value.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_shot\"].cut_duration = -100\n\n    assert str(cm.value) == (\n        \"Shot.cut_duration cannot be set to zero or a negative value\"\n    )\n\n\ndef test_source_in_argument_is_skipped(setup_shot_db_tests):\n    \"\"\"source_in arg is skipped the source_in arg equal to cut_in attr value.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"].pop(\"source_in\")\n    shot = Shot(**data[\"kwargs\"])\n    assert shot.source_in == shot.cut_in\n\n\ndef test_source_in_argument_is_none(setup_shot_db_tests):\n    \"\"\"source_in attr equal to the cut_in attr if the source_in arg is None.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"source_in\"] = None\n    shot = Shot(**data[\"kwargs\"])\n    assert shot.source_in == shot.cut_in\n\n\ndef test_source_in_attribute_is_set_to_none(setup_shot_db_tests):\n    \"\"\"TypeError raised if the source_in attribute is set to None.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].source_in = None\n\n    assert str(cm.value) == \"Shot.source_in should be an int, not NoneType: 'None'\"\n\n\ndef test_source_in_argument_is_not_integer(setup_shot_db_tests):\n    \"\"\"TypeError raised if the source_in argument is not an instance of int.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"source_in\"] = \"a string\"\n\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Shot.source_in should be an int, not str: 'a string'\"\n\n\ndef test_source_in_attribute_is_not_integer(setup_shot_db_tests):\n    \"\"\"TypeError raised if the source_in attr is set to a value other than an int.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].source_in = \"a string\"\n\n    assert str(cm.value) == \"Shot.source_in should be an int, not str: 'a string'\"\n\n\ndef test_source_in_argument_is_bigger_than_source_out_argument(setup_shot_db_tests):\n    \"\"\"ValueError raised if the source_in arg is bigger than source_out arg value.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"source_out\"] = data[\"kwargs\"][\"cut_out\"] - 10\n    data[\"kwargs\"][\"source_in\"] = data[\"kwargs\"][\"source_out\"] + 5\n    with pytest.raises(ValueError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.source_out cannot be smaller than Shot.source_in, source_in: 144 where \"\n        \"as source_out: 139\"\n    )\n\n\ndef test_source_in_attribute_is_bigger_than_source_out_attribute(setup_shot_db_tests):\n    \"\"\"ValueError raised if the source_in attr is set to bigger than source out.\"\"\"\n    data = setup_shot_db_tests\n    # give it a little bit of room, to be sure that the ValueError is not\n    # due to the cut_out\n    data[\"test_shot\"].source_out -= 5\n    with pytest.raises(ValueError) as cm:\n        data[\"test_shot\"].source_in = data[\"test_shot\"].source_out + 1\n\n    assert str(cm.value) == (\n        \"Shot.source_in cannot be bigger than Shot.source_out, \"\n        \"source_in: 136 where as source_out: 135\"\n    )\n\n\ndef test_source_in_argument_is_smaller_than_cut_in(setup_shot_db_tests):\n    \"\"\"ValueError raised if the source_in arg is smaller than cut_in attr value.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"source_in\"] = data[\"kwargs\"][\"cut_in\"] - 10\n    with pytest.raises(ValueError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.source_in cannot be smaller than Shot.cut_in, cut_in: \"\n        \"112 where as source_in: 102\"\n    )\n\n\ndef test_source_in_argument_is_bigger_than_cut_out(setup_shot_db_tests):\n    \"\"\"ValueError raised if the source_in arg is bigger than cut_out attr value.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"source_in\"] = data[\"kwargs\"][\"cut_out\"] + 10\n    with pytest.raises(ValueError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.source_in cannot be bigger than Shot.cut_out, cut_out: \"\n        \"149 where as source_in: 159\"\n    )\n\n\ndef test_source_out_argument_is_skipped(setup_shot_db_tests):\n    \"\"\"source_out attr equal to cut_out arg value if the source_out arg is skipped.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"].pop(\"source_out\")\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.source_out == new_shot.cut_out\n\n\ndef test_source_out_argument_is_none(setup_shot_db_tests):\n    \"\"\"source_out attr value equal to cut_out if the source_out arg value is None.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"source_out\"] = None\n    shot = Shot(**data[\"kwargs\"])\n    assert shot.source_out == shot.cut_out\n\n\ndef test_source_out_attribute_is_set_to_none(setup_shot_db_tests):\n    \"\"\"TypeError raised if the source_out attribute is set to None.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].source_out = None\n    assert str(cm.value) == \"Shot.source_out should be an int, not NoneType: 'None'\"\n\n\ndef test_source_out_argument_is_not_integer(setup_shot_db_tests):\n    \"\"\"TypeError raised if the source_out argument is not an integer.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"source_out\"] = \"a string\"\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Shot.source_out should be an int, not str: 'a string'\"\n\n\ndef test_source_out_attribute_is_not_integer(setup_shot_db_tests):\n    \"\"\"TypeError raised if the source_out attr is set to a value other than an int.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].source_out = \"a string\"\n    assert str(cm.value) == \"Shot.source_out should be an int, not str: 'a string'\"\n\n\ndef test_source_out_argument_is_smaller_than_source_in_argument(setup_shot_db_tests):\n    \"\"\"ValueError raised if the source_out arg is smaller than the source_in attr.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"source_in\"] = data[\"kwargs\"][\"cut_in\"] + 15\n    data[\"kwargs\"][\"source_out\"] = data[\"kwargs\"][\"source_in\"] - 10\n    with pytest.raises(ValueError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.source_out cannot be smaller than Shot.source_in, \"\n        \"source_in: 127 where as source_out: 117\"\n    )\n\n\ndef test_source_out_attribute_is_smaller_than_source_in_attribute(setup_shot_db_tests):\n    \"\"\"ValueError raised if the source_out attr is set to smaller than source_in.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_shot\"].source_out = data[\"test_shot\"].source_in - 2\n\n    assert str(cm.value) == (\n        \"Shot.source_out cannot be smaller than Shot.source_in, \"\n        \"source_in: 120 where as source_out: 118\"\n    )\n\n\ndef test_source_out_argument_is_smaller_than_cut_in_argument(setup_shot_db_tests):\n    \"\"\"ValueError raised if the source_out arg is smaller than the cut_in attr.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"source_in\"] = data[\"kwargs\"][\"cut_in\"] + 15\n    data[\"kwargs\"][\"source_out\"] = data[\"kwargs\"][\"cut_in\"] - 10\n    with pytest.raises(ValueError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.source_out cannot be smaller than Shot.cut_in, \"\n        \"cut_in: 112 where as source_out: 102\"\n    )\n\n\ndef test_source_out_attribute_is_smaller_than_cut_in_attribute(setup_shot_db_tests):\n    \"\"\"ValueError raised if the source_out attr is set to smaller than cut_in.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_shot\"].source_out = data[\"test_shot\"].cut_in - 2\n\n    assert str(cm.value) == (\n        \"Shot.source_out cannot be smaller than Shot.cut_in, \"\n        \"cut_in: 112 where as source_out: 110\"\n    )\n\n\ndef test_source_out_argument_is_bigger_than_cut_out_argument(setup_shot_db_tests):\n    \"\"\"ValueError raised if the source_out arg is bigger than the cut_out attr.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    data[\"kwargs\"][\"source_in\"] = data[\"kwargs\"][\"cut_in\"] + 2\n    data[\"kwargs\"][\"source_out\"] = data[\"kwargs\"][\"cut_out\"] + 20\n    with pytest.raises(ValueError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.source_out cannot be bigger than Shot.cut_out, \"\n        \"cut_out: 149 where as source_out: 169\"\n    )\n\n\ndef test_source_out_attribute_is_smaller_than_cut_out_attribute(setup_shot_db_tests):\n    \"\"\"ValueError raised if the source_out attr is set to bigger than cut_out.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_shot\"].source_out = data[\"test_shot\"].cut_out + 2\n\n    assert str(cm.value) == (\n        \"Shot.source_out cannot be bigger than Shot.cut_out, \"\n        \"cut_out: 149 where as source_out: 151\"\n    )\n\n\ndef test_image_format_argument_is_skipped(setup_shot_db_tests):\n    \"\"\"image_format is copied from the Project if the image_format arg is skipped.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"].pop(\"image_format\")\n    data[\"kwargs\"][\"code\"] = \"TestShot\"\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.image_format == data[\"kwargs\"][\"project\"].image_format\n\n\ndef test_image_format_argument_is_none(setup_shot_db_tests):\n    \"\"\"image format is copied from the Project if the image_format arg is None.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"image_format\"] = None\n    data[\"kwargs\"][\"code\"] = \"newShot\"\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.image_format == data[\"kwargs\"][\"project\"].image_format\n\n\ndef test_image_format_attribute_is_none(setup_shot_db_tests):\n    \"\"\"image format is copied from the Project if the image_format attr is None.\"\"\"\n    data = setup_shot_db_tests\n    # test start conditions\n    assert data[\"test_shot\"].image_format != data[\"test_shot\"].project.image_format\n    data[\"test_shot\"].image_format = None\n    assert data[\"test_shot\"].image_format == data[\"test_shot\"].project.image_format\n\n\ndef test_image_format_argument_is_not_a_image_format_instance_and_not_none(\n    setup_shot_db_tests,\n):\n    \"\"\"TypeError raised if the image_format arg is not a ImageFormat and not None.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"new_shot\"\n    data[\"kwargs\"][\"image_format\"] = \"not an image format instance\"\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.image_format should be an instance of \"\n        \"stalker.models.format.ImageFormat, not str: 'not an image format instance'\"\n    )\n\n\ndef test_image_format_attribute_is_not_a_ImageFormat_instance_and_not_none(\n    setup_shot_db_tests,\n):\n    \"\"\"TypeError raised if the image_format attr is not a ImageFormat and not None.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].image_format = \"not an image f\"\n\n    assert str(cm.value) == (\n        \"Shot.image_format should be an instance of \"\n        \"stalker.models.format.ImageFormat, not str: 'not an image f'\"\n    )\n\n\ndef test_image_format_argument_is_working_as_expected(setup_shot_db_tests):\n    \"\"\"image_format argument value is passed to the image_format attribute correctly.\"\"\"\n    data = setup_shot_db_tests\n    assert data[\"kwargs\"][\"image_format\"] == data[\"test_shot\"].image_format\n\n\ndef test_image_format_attribute_is_working_as_expected(setup_shot_db_tests):\n    \"\"\"image_format attribute is working as expected.\"\"\"\n    data = setup_shot_db_tests\n    assert data[\"test_shot\"].image_format != data[\"test_image_format1\"]\n    data[\"test_shot\"].image_format = data[\"test_image_format1\"]\n    assert data[\"test_shot\"].image_format == data[\"test_image_format1\"]\n\n\ndef test_equality(setup_shot_db_tests):\n    \"\"\"equality of shot objects.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    new_shot1 = Shot(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"project\"] = data[\"test_project2\"]\n    new_shot2 = Shot(**data[\"kwargs\"])\n    # an entity with the same parameters\n    # just set the name to the code too\n    data[\"kwargs\"][\"name\"] = data[\"kwargs\"][\"code\"]\n    new_entity = Entity(**data[\"kwargs\"])\n\n    # another shot with different code\n    data[\"kwargs\"][\"code\"] = \"SHAnotherShot\"\n    new_shot3 = Shot(**data[\"kwargs\"])\n\n    assert not new_shot1 == new_shot2\n    assert not new_shot1 == new_entity\n    assert not new_shot1 == new_shot3\n\n\ndef test_inequality(setup_shot_db_tests):\n    \"\"\"inequality of shot objects.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"code\"] = \"SH123A\"\n    new_shot1 = Shot(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"project\"] = data[\"test_project2\"]\n    new_shot2 = Shot(**data[\"kwargs\"])\n    # an entity with the same parameters\n    # just set the name to the code too\n    data[\"kwargs\"][\"name\"] = data[\"kwargs\"][\"code\"]\n    new_entity = Entity(**data[\"kwargs\"])\n\n    # another shot with different code\n    data[\"kwargs\"][\"code\"] = \"SHAnotherShot\"\n    new_shot3 = Shot(**data[\"kwargs\"])\n\n    assert new_shot1 != new_shot2\n    assert new_shot1 != new_entity\n    assert new_shot1 != new_shot3\n\n\ndef test_ReferenceMixin_initialization(setup_shot_db_tests):\n    \"\"\"ReferenceMixin part is initialized correctly.\"\"\"\n    data = setup_shot_db_tests\n    file_type_1 = Type(name=\"Image\", code=\"image\", target_entity_type=\"File\")\n\n    file1 = File(\n        name=\"Artwork 1\",\n        full_path=\"/mnt/M/JOBs/TEST_PROJECT\",\n        filename=\"a.jpg\",\n        type=file_type_1,\n    )\n\n    file2 = File(\n        name=\"Artwork 2\",\n        full_path=\"/mnt/M/JOBs/TEST_PROJECT\",\n        filename=\"b.jbg\",\n        type=file_type_1,\n    )\n\n    references = [file1, file2]\n\n    data[\"kwargs\"][\"code\"] = \"SH12314\"\n    data[\"kwargs\"][\"references\"] = references\n\n    new_shot = Shot(**data[\"kwargs\"])\n\n    assert new_shot.references == references\n\n\ndef test_TaskMixin_initialization(setup_shot_db_tests):\n    \"\"\"TaskMixin part is initialized correctly.\"\"\"\n    data = setup_shot_db_tests\n    project_status_list = StatusList.query.filter(\n        StatusList.target_entity_type == \"Project\"\n    ).first()\n\n    project_type = Type(name=\"Commercial\", code=\"comm\", target_entity_type=\"Project\")\n\n    new_project = Project(\n        name=\"Commercial1\",\n        code=\"comm1\",\n        status_list=project_status_list,\n        type=project_type,\n        repository=data[\"test_repository\"],\n    )\n    DBSession.add(new_project)\n    DBSession.commit()\n\n    data[\"kwargs\"][\"project\"] = new_project\n    data[\"kwargs\"][\"code\"] = \"SH12314\"\n\n    new_shot = Shot(**data[\"kwargs\"])\n\n    task1 = Task(\n        name=\"Modeling\",\n        status=0,\n        project=new_project,\n        parent=new_shot,\n    )\n\n    task2 = Task(\n        name=\"Lighting\",\n        status=0,\n        project=new_project,\n        parent=new_shot,\n    )\n\n    tasks = [task1, task2]\n\n    assert sorted(new_shot.tasks, key=lambda x: x.name) == sorted(\n        tasks, key=lambda x: x.name\n    )\n\n\ndef test__repr__(setup_shot_db_tests):\n    \"\"\"representation of Shot.\"\"\"\n    data = setup_shot_db_tests\n    assert data[\"test_shot\"].__repr__() == \"<Shot ({}, {})>\".format(\n        data[\"test_shot\"].code,\n        data[\"test_shot\"].code,\n    )\n\n\ndef test_plural_class_name(setup_shot_db_tests):\n    \"\"\"plural name of Shot class.\"\"\"\n    data = setup_shot_db_tests\n    assert data[\"test_shot\"].plural_class_name == \"Shots\"\n\n\ndef test___strictly_typed___is_false():\n    \"\"\"__strictly_typed__ class attribute is False for Shot class.\"\"\"\n    assert Shot.__strictly_typed__ is False\n\n\ndef test_cut_duration_initialization_bug_with_cut_in(setup_shot_db_tests):\n    \"\"\"_cut_duration attribute is initialized correctly for a Shot restored from DB.\"\"\"\n    data = setup_shot_db_tests\n    # retrieve the shot back from DB\n    test_shot_db = Shot.query.filter_by(name=data[\"kwargs\"][\"name\"]).first()\n    # trying to change the cut_in and cut_out values should not raise any\n    # errors\n    test_shot_db.cut_in = 1\n    DBSession.add(test_shot_db)\n    DBSession.commit()\n\n\ndef test_cut_duration_initialization_bug_with_cut_out(setup_shot_db_tests):\n    \"\"\"_cut_duration attribute is initialized correctly for a Shot restored from DB.\"\"\"\n    data = setup_shot_db_tests\n    # reconnect to the database\n    # retrieve the shot back from DB\n    test_shot_db = Shot.query.filter_by(name=data[\"kwargs\"][\"name\"]).first()\n    # trying to change the cut_in and cut_out values should not raise any\n    # errors\n    test_shot_db.cut_out = 100\n    DBSession.add(test_shot_db)\n    DBSession.commit()\n\n\ndef test_cut_values_are_set_correctly(setup_shot_db_tests):\n    \"\"\"cut_in attribute is set correctly in db.\"\"\"\n    data = setup_shot_db_tests\n    data[\"test_shot\"].cut_in = 100\n    assert data[\"test_shot\"].cut_in == 100\n\n    data[\"test_shot\"].cut_out = 153\n    assert data[\"test_shot\"].cut_in == 100\n    assert data[\"test_shot\"].cut_out == 153\n\n    DBSession.add(data[\"test_shot\"])\n    DBSession.commit()\n\n    # retrieve the shot back from DB\n    test_shot_db = Shot.query.filter_by(name=data[\"kwargs\"][\"name\"]).first()\n\n    assert test_shot_db.cut_in == 100\n    assert test_shot_db.cut_out == 153\n\n\ndef test_fps_argument_is_skipped(setup_shot_db_tests):\n    \"\"\"default value used if fps is skipped.\"\"\"\n    data = setup_shot_db_tests\n    if \"fps\" in data[\"kwargs\"]:\n        data[\"kwargs\"].pop(\"fps\")\n\n    data[\"kwargs\"][\"code\"] = \"SHnew\"\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.fps == data[\"test_project1\"].fps\n\n\ndef test_fps_attribute_is_set_to_None(setup_shot_db_tests):\n    \"\"\"Project.fps used if the fps argument is None.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"fps\"] = None\n    data[\"kwargs\"][\"code\"] = \"SHnew\"\n    new_shot = Shot(**data[\"kwargs\"])\n    assert new_shot.fps == data[\"test_project1\"].fps\n\n\n@pytest.mark.parametrize(\"test_value\", [[\"a\", \"list\"], {\"a\": \"list\"}])\ndef test_fps_argument_is_given_as_non_float_or_integer(test_value, setup_shot_db_tests):\n    \"\"\"TypeError raised if the fps arg not float or int.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"fps\"] = test_value\n    data[\"kwargs\"][\"code\"] = \"SH%i\"\n    with pytest.raises(TypeError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Shot.fps should be a positive float or int, not {}: '{}'\".format(\n            test_value.__class__.__name__, test_value\n        )\n    )\n\n\n@pytest.mark.parametrize(\"test_value\", [[\"a\", \"list\"], {\"a\": \"list\"}])\ndef test_fps_attribute_is_given_as_non_float_or_integer(\n    test_value, setup_shot_db_tests\n):\n    \"\"\"TypeError raised if the fps attr is not a float or int.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_shot\"].fps = test_value\n\n    assert str(cm.value) == (\n        \"Shot.fps should be a positive float or int, not {}: '{}'\".format(\n            test_value.__class__.__name__, test_value\n        )\n    )\n\n\ndef test_fps_attribute_float_conversion(setup_shot_db_tests):\n    \"\"\"fps attr is converted to float if the fps argument is given as an int.\"\"\"\n    data = setup_shot_db_tests\n    test_value = 1\n    data[\"kwargs\"][\"fps\"] = test_value\n    data[\"kwargs\"][\"code\"] = \"SHnew\"\n    new_shot = Shot(**data[\"kwargs\"])\n    assert isinstance(new_shot.fps, float)\n    assert new_shot.fps == float(test_value)\n\n\ndef test_fps_attribute_float_conversion_2(setup_shot_db_tests):\n    \"\"\"fps attribute is converted to float if it is set to an int value.\"\"\"\n    data = setup_shot_db_tests\n    test_value = 1\n    data[\"test_shot\"].fps = test_value\n    assert isinstance(data[\"test_shot\"].fps, float)\n    assert data[\"test_shot\"].fps == float(test_value)\n\n\ndef test_fps_argument_is_zero(setup_shot_db_tests):\n    \"\"\"ValueError raised if the fps is 0.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"fps\"] = 0\n    data[\"kwargs\"][\"code\"] = \"SHnew\"\n    with pytest.raises(ValueError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Shot.fps should be a positive float or int, not 0.0\"\n\n\ndef test_fps_attribute_is_set_to_zero(setup_shot_db_tests):\n    \"\"\"value error raised if the fps attribute is set to zero.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_shot\"].fps = 0\n    assert str(cm.value) == \"Shot.fps should be a positive float or int, not 0.0\"\n\n\ndef test_fps_argument_is_negative(setup_shot_db_tests):\n    \"\"\"ValueError raised if the fps argument is negative.\"\"\"\n    data = setup_shot_db_tests\n    data[\"kwargs\"][\"fps\"] = -1.0\n    data[\"kwargs\"][\"code\"] = \"SHrandom\"\n    with pytest.raises(ValueError) as cm:\n        Shot(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Shot.fps should be a positive float or int, not -1.0\"\n\n\ndef test_fps_attribute_is_negative(setup_shot_db_tests):\n    \"\"\"ValueError raised if the fps attribute is set to a negative value.\"\"\"\n    data = setup_shot_db_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_shot\"].fps = -1\n\n    assert str(cm.value) == \"Shot.fps should be a positive float or int, not -1.0\"\n\n\ndef test_fps_changes_with_project(setup_shot_db_tests):\n    \"\"\"fps reflects the project.fps unless it is set to a value.\"\"\"\n    data = setup_shot_db_tests\n    new_shot = Shot(name=\"New Shot\", code=\"ns\", project=data[\"test_project1\"])\n    assert new_shot.fps == data[\"test_project1\"].fps\n    data[\"test_project1\"].fps = 335\n    assert new_shot.fps == 335\n    new_shot.fps = 12\n    assert new_shot.fps == 12\n    data[\"test_project1\"].fps = 24\n    assert new_shot.fps == 12\n\n\ndef test__check_code_availability_code_is_none(setup_shot_db_tests):\n    \"\"\"__check_code_availability() returns True if the code is None.\"\"\"\n    data = setup_shot_db_tests\n    assert isinstance(data[\"test_shot\"], Shot)\n    result = data[\"test_shot\"]._check_code_availability(None, data[\"test_project1\"])\n    assert result is True\n\n\ndef test__check_code_availability_code_is_not_str(setup_shot_db_tests):\n    \"\"\"__check_code_availability() raises TypeError if code is not a str.\"\"\"\n    data = setup_shot_db_tests\n    assert isinstance(data[\"test_shot\"], Shot)\n    with pytest.raises(TypeError) as cm:\n        _ = data[\"test_shot\"]._check_code_availability(1234, data[\"test_project1\"])\n\n    assert str(cm.value) == (\n        \"code should be a string containing a shot code, not int: '1234'\"\n    )\n\n\ndef test__check_code_availability_project_is_none(setup_shot_db_tests):\n    \"\"\"__check_code_availability() returns True if project is None\"\"\"\n    data = setup_shot_db_tests\n    assert isinstance(data[\"test_shot\"], Shot)\n    result = data[\"test_shot\"]._check_code_availability(\"SH123\", None)\n    assert result is True\n\n\ndef test__check_code_availability_project_is_not_a_project_instance(\n    setup_shot_db_tests,\n):\n    \"\"\"__check_code_availability() raises TypeError if the Project is not a Project instance.\"\"\"\n    data = setup_shot_db_tests\n    assert isinstance(data[\"test_shot\"], Shot)\n    with pytest.raises(TypeError) as cm:\n        _ = data[\"test_shot\"]._check_code_availability(\"SH123\", 1234)\n\n    assert str(cm.value) == (\"project should be a Project instance, not int: '1234'\")\n\n\ndef test_check_code_availability_fallbacks_to_python_if_db_is_not_available():\n    \"\"\"__check_code_availability() fallbacks to Python if DB is not available.\"\"\"\n    data = dict()\n    # statuses\n    rts = Status(name=\"Read To Start\", code=\"RTS\")\n    wip = Status(name=\"Work In Progress\", code=\"WIP\")\n    cmpl = Status(name=\"Completed\", code=\"CMPL\")\n    project_status_list = StatusList(\n        name=\"Project Statuses\", statuses=[rts, wip, cmpl], target_entity_type=\"Project\"\n    )\n    shot_status_list = StatusList(\n        name=\"Shot Status List\", statuses=[rts, wip, cmpl], target_entity_type=\"Shot\"\n    )\n\n    # types\n    data[\"test_commercial_project_type\"] = Type(\n        name=\"Commercial Project\",\n        code=\"comm\",\n        target_entity_type=\"Project\",\n    )\n\n    data[\"test_repository_type\"] = Type(\n        name=\"Test Repository Type\", code=\"test\", target_entity_type=\"Repository\"\n    )\n\n    # repository\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"test_repository_type\"],\n    )\n\n    # image format\n    data[\"test_image_format1\"] = ImageFormat(\n        name=\"Test Image Format 1\", width=1920, height=1080, pixel_aspect=1.0\n    )\n\n    # project and sequences\n    data[\"test_project1\"] = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=data[\"test_commercial_project_type\"],\n        repository=data[\"test_repository\"],\n        image_format=data[\"test_image_format1\"],\n        status_list=project_status_list,\n    )\n\n    data[\"kwargs\"] = dict(\n        name=\"SH123\",\n        code=\"SH123\",\n        description=\"This is a test Shot\",\n        project=data[\"test_project1\"],\n        status=0,\n        status_list=shot_status_list,\n    )\n\n    # create a mock shot object\n    data[\"test_shot\"] = Shot(**data[\"kwargs\"])\n\n    assert Shot._check_code_availability(\"SH123\", data[\"test_project1\"]) is False\n\n\ndef test__init_on_load__works_as_expected(setup_shot_db_tests):\n    \"\"\"__init_on_load__() works as expected.\"\"\"\n    data = setup_shot_db_tests\n    assert data[\"test_shot\"] in DBSession\n    DBSession.commit()\n    DBSession.flush()\n    connection = DBSession.connection()\n    connection.close()\n    del data[\"test_shot\"]\n\n    from stalker.db.setup import setup\n\n    setup(data[\"database_settings\"][\"config\"])\n\n    # the following should call Shot.__init_on_load__()\n    shot = Shot.query.filter(Shot.name == \"SH123\").first()\n    assert isinstance(shot, Shot)\n\n\ndef test_template_variables_include_scene_for_shots(setup_shot_db_tests):\n    \"\"\"_template_variables include scene for shots.\"\"\"\n    data = setup_shot_db_tests\n    assert isinstance(data[\"test_shot\"], Shot)\n    template_variables = data[\"test_shot\"]._template_variables()\n    assert \"scene\" in template_variables\n    assert data[\"test_shot\"].scene is not None\n    assert template_variables[\"scene\"] == data[\"test_shot\"].scene\n\n\ndef test_template_variables_include_sequence_for_shots(setup_shot_db_tests):\n    \"\"\"_template_variables include sequence for shots.\"\"\"\n    data = setup_shot_db_tests\n    assert isinstance(data[\"test_shot\"], Shot)\n    template_variables = data[\"test_shot\"]._template_variables()\n    assert \"sequence\" in template_variables\n    assert data[\"test_shot\"].sequence is not None\n    assert template_variables[\"sequence\"] == data[\"test_shot\"].sequence\n\n\ndef test_shots_can_use_task_status_list():\n    \"\"\"It is possible to use TaskStatus lists with Shots.\"\"\"\n    # users\n    test_user1 = User(\n        name=\"User1\", login=\"user1\", password=\"12345\", email=\"user1@user1.com\"\n    )\n    # statuses\n    status_wip = Status(code=\"WIP\", name=\"Work In Progress\")\n    status_cmpl = Status(code=\"CMPL\", name=\"Complete\")\n\n    # Just create a StatusList for Tasks\n    task_status_list = StatusList(\n        statuses=[status_wip, status_cmpl], target_entity_type=\"Task\"\n    )\n    project_status_list = StatusList(\n        statuses=[status_wip, status_cmpl], target_entity_type=\"Project\"\n    )\n\n    # types\n    commercial_project_type = Type(\n        name=\"Commercial Project\",\n        code=\"commproj\",\n        target_entity_type=\"Project\",\n    )\n    # project\n    project1 = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=commercial_project_type,\n        status_list=project_status_list,\n    )\n    # shots\n    test_shot1 = Shot(\n        code=\"TestSH001\",\n        project=project1,\n        status_list=task_status_list,\n        responsible=[test_user1],\n    )\n    assert test_shot1.status_list == task_status_list\n"
  },
  {
    "path": "tests/models/test_simple_entity.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the SimpleEntity class.\"\"\"\nimport json\nimport datetime\nimport sys\n\nimport pytest\n\nimport pytz\n\nimport stalker\n\nfrom stalker import (\n    Department,\n    File,\n    Project,\n    Repository,\n    SimpleEntity,\n    Structure,\n    Type,\n    User,\n)\nfrom stalker.db.session import DBSession\n\n\n# create a new class deriving from the SimpleEntity\nclass NewClass(SimpleEntity):\n    __strictly_typed__ = True\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_simple_entity_tests():\n    \"\"\"Set up some proper values for SimpleEntity tests.\"\"\"\n    # create a user\n    data = dict()\n    data[\"test_user\"] = User(\n        name=\"Test User\",\n        login=\"testuser\",\n        email=\"test@user.com\",\n        password=\"test\",\n        generic_text=json.dumps({\"Phone number\": \"123\"}, sort_keys=True),\n    )\n\n    data[\"date_created\"] = datetime.datetime(2010, 10, 21, 3, 8, 0, tzinfo=pytz.utc)\n    data[\"date_updated\"] = data[\"date_created\"]\n\n    data[\"kwargs\"] = {\n        \"name\": \"Test Entity\",\n        \"code\": \"TstEnt\",\n        \"description\": \"This is a test entity, and this is a proper description for it\",\n        \"created_by\": data[\"test_user\"],\n        \"updated_by\": data[\"test_user\"],\n        \"date_created\": data[\"date_created\"],\n        \"date_updated\": data[\"date_updated\"],\n        \"generic_text\": json.dumps({\"Phone number\": \"123\"}, sort_keys=True),\n    }\n\n    # create a proper SimpleEntity to use it later in the tests\n    data[\"test_simple_entity\"] = SimpleEntity(**data[\"kwargs\"])\n\n    data[\"test_type\"] = Type(\n        name=\"Test Type\", code=\"test\", target_entity_type=\"SimpleEntity\"\n    )\n\n    return data\n\n\ndef test___auto_name__attr_is_true():\n    \"\"\"__auto_name__ class attr is set to True.\"\"\"\n    assert SimpleEntity.__auto_name__ is True\n\n\ndef test_name_arg_is_none(setup_simple_entity_tests):\n    \"\"\"name attr automatically generated if the name arg is None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"name\"] = None\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    assert new_simple_entity.name is not None\n\n\ndef test_name_attr_is_set_to_none(setup_simple_entity_tests):\n    \"\"\"name attr set to an automatic value if it is set to None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"test_simple_entity\"].name = \"\"\n    assert data[\"test_simple_entity\"].name is not None\n\n\ndef test_name_attr_is_set_to_none_2(setup_simple_entity_tests):\n    \"\"\"name attr set to an automatic value if it is set to None.\"\"\"\n    data = setup_simple_entity_tests\n    assert data[\"test_simple_entity\"].name != \"\"\n\n\ndef test_name_arg_is_empty_string(setup_simple_entity_tests):\n    \"\"\"name attr set to an automatic value if the name arg is an empty string.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"name\"] = \"\"\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    assert new_simple_entity.name != \"\"\n\n\ndef test_name_attr_is_set_to_empty_string(setup_simple_entity_tests):\n    \"\"\"name attr set to an automatic value if it is set to an automatic value.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"test_simple_entity\"].name = \"\"\n    assert data[\"test_simple_entity\"].name != \"\"\n\n\n@pytest.mark.parametrize(\"test_value\", [12132, [1, \"name\"], {\"a\": \"name\"}])\ndef test_name_arg_is_not_a_string_instance_or_none(\n    test_value, setup_simple_entity_tests\n):\n    \"\"\"TypeError raised if the name arg is not a string or None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"name\"] = test_value\n    with pytest.raises(TypeError) as _:\n        SimpleEntity(**data[\"kwargs\"])\n\n\n@pytest.mark.parametrize(\"test_value\", [12132, [1, \"name\"], {\"a\": \"name\"}])\ndef test_name_attr_is_not_string_or_none(test_value, setup_simple_entity_tests):\n    \"\"\"TypeError raised if the name attr is not a string or None.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(TypeError) as _:\n        data[\"test_simple_entity\"].name = test_value\n\n\n@pytest.mark.parametrize(\n    \"test_value\",\n    [\n        (\"testName\", \"testName\"),\n        (\"test-Name\", \"test-Name\"),\n        (\"1testName\", \"1testName\"),\n        (\"_testName\", \"_testName\"),\n        (\"2423$+^^+^'%+%%&_testName\", \"2423$+^^+^'%+%%&_testName\"),\n        (\"2423$+^^+^'%+%%&_testName_35\", \"2423$+^^+^'%+%%&_testName_35\"),\n        (\"2423$ +^^+^ '%+%%&_ testName_ 35\", \"2423$ +^^+^ '%+%%&_ testName_ 35\"),\n        (\"SH001\", \"SH001\"),\n        (\"46-BJ-3A\", \"46-BJ-3A\"),\n        (\"304-sb-0403-0040\", \"304-sb-0403-0040\"),\n        (\"Ozgur    Yilmaz\\n\\n\\n\", \"Ozgur Yilmaz\"),\n        (\"     Ozgur Yilmaz    \", \"Ozgur Yilmaz\"),\n    ],\n)\ndef test_name_attr_is_formatted_correctly(test_value, setup_simple_entity_tests):\n    \"\"\"name is formatted correctly\"\"\"\n    data = setup_simple_entity_tests\n    # set the new name\n    data[\"test_simple_entity\"].name = test_value[0]\n    assert data[\"test_simple_entity\"].name == test_value[1]\n\n\n@pytest.mark.parametrize(\n    \"test_value\",\n    [\n        (\"testName\", \"testName\"),\n        (\"1testName\", \"1testName\"),\n        (\"_testName\", \"testName\"),\n        (\"2423$+^^+^'%+%%&_testName\", \"2423_testName\"),\n        (\"2423$+^^+^'%+%%&_testName_35\", \"2423_testName_35\"),\n        (\"2423$ +^^+^ '%+%%&_ testName_ 35\", \"2423_testName_35\"),\n        (\"SH001\", \"SH001\"),\n        (\"My name is Ozgur\", \"My_name_is_Ozgur\"),\n        (\" this is another name for an asset\", \"this_is_another_name_for_an_asset\"),\n        (\"Ozgur    Yilmaz\\n\\n\\n\", \"Ozgur_Yilmaz\"),\n    ],\n)\ndef test_nice_name_attr_is_formatted_correctly(test_value, setup_simple_entity_tests):\n    \"\"\"nice name attr is formatted correctly.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"test_simple_entity\"].name = test_value[0]\n    assert data[\"test_simple_entity\"].nice_name == test_value[1]\n\n\ndef test_nice_name_attr_is_read_only(setup_simple_entity_tests):\n    \"\"\"nice name attr is read-only.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_simple_entity\"].nice_name = \"a text\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'nice_name'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'nice_name' of 'SimpleEntity' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_description_arg_none(setup_simple_entity_tests):\n    \"\"\"description property converted to an empty string if description arg is None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"description\"] = None\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    assert new_simple_entity.description == \"\"\n\n\ndef test_description_attr_none(setup_simple_entity_tests):\n    \"\"\"description attr converted to an empty string if it is set to None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"test_simple_entity\"].description = None\n    assert data[\"test_simple_entity\"].description == \"\"\n\n\ndef test_description_arg_is_not_a_string(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the description arg value is not a string.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"description\"] = {\"a\": \"description\"}\n    with pytest.raises(TypeError) as cm:\n        SimpleEntity(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"SimpleEntity.description should be a string, not dict: '{'a': 'description'}'\"\n    )\n\n\ndef test_description_attr_is_not_a_string(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the description attr value is not a str.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].description = [\"a description\"]\n    assert str(cm.value) == (\n        \"SimpleEntity.description should be a string, not list: '['a description']'\"\n    )\n\n\ndef test_generic_text_arg_none(setup_simple_entity_tests):\n    \"\"\"generic_text value converted to an empty string if generic_text arg is None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"generic_text\"] = None\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    assert new_simple_entity.generic_text == \"\"\n\n\ndef test_generic_text_attr_none(setup_simple_entity_tests):\n    \"\"\"generic_text attr converted to an empty string if it is set to None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"test_simple_entity\"].generic_text = None\n    assert data[\"test_simple_entity\"].generic_text == \"\"\n\n\ndef test_generic_text_arg_is_not_a_string(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the generic_text arg value is not a string.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"generic_text\"] = {\"a\": \"generic_text\"}\n    with pytest.raises(TypeError) as cm:\n        SimpleEntity(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"SimpleEntity.generic_text should be a string, \"\n        \"not dict: '{'a': 'generic_text'}'\"\n    )\n\n\ndef test_generic_text_attr_is_not_a_string(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the generic_text attr is set not to a str.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(TypeError) as _:\n        data[\"test_simple_entity\"].generic_text = [\"a generic_text\"]\n\n\ndef test_equality(setup_simple_entity_tests):\n    \"\"\"equality of two simple entities.\"\"\"\n    # create two simple entities with same parameters and check for equality\n    data = setup_simple_entity_tests\n    se1 = SimpleEntity(**data[\"kwargs\"])\n    se2 = SimpleEntity(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"name\"] = \"a different simple entity\"\n    data[\"kwargs\"][\"description\"] = \"no description\"\n    se3 = SimpleEntity(**data[\"kwargs\"])\n    assert se1 == se2\n    assert not se1 == se3\n\n\ndef test_inequality(setup_simple_entity_tests):\n    \"\"\"inequality of two simple entities.\"\"\"\n    data = setup_simple_entity_tests\n    # create two simple entities with same parameters and check for\n    # equality\n    se1 = SimpleEntity(**data[\"kwargs\"])\n    se2 = SimpleEntity(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"name\"] = \"a different simple entity\"\n    data[\"kwargs\"][\"description\"] = \"no description\"\n    se3 = SimpleEntity(**data[\"kwargs\"])\n\n    assert not se1 != se2\n    assert se1 != se3\n\n\ndef test_created_by_arg_is_not_a_user_instance(setup_simple_entity_tests):\n    \"\"\"TypeError is raised if created_by arg is not a User instance.\"\"\"\n    data = setup_simple_entity_tests\n    # the created_by arg should be an instance of User class, in any\n    # other case it should raise a TypeError\n    test_value = \"A User Name\"\n\n    # be sure that the test value is not an instance of User\n    assert not isinstance(test_value, User)\n\n    # check the value\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].created_by = test_value\n\n    assert str(cm.value) == (\n        \"SimpleEntity.created_by should be a stalker.models.auth.User instance, \"\n        \"not str: 'A User Name'\"\n    )\n\n\ndef test_created_by_attr_instance_of_user(setup_simple_entity_tests):\n    \"\"\"TypeError is raised if created_by attr is set to a value other than a User.\"\"\"\n    data = setup_simple_entity_tests\n    # the created_by attr should be an instance of User class, in any\n    # other case it should raise a TypeError\n    test_value = \"A User Name\"\n\n    # be sure that the test value is not an instance of User\n    assert not isinstance(test_value, User)\n\n    # check the value\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].created_by = test_value\n\n    assert str(cm.value) == (\n        \"SimpleEntity.created_by should be a stalker.models.auth.User instance, \"\n        \"not str: 'A User Name'\"\n    )\n\n\ndef test_updated_by_arg_instance_of_user(setup_simple_entity_tests):\n    \"\"\"TypeError is raised if updated_by arg is not a User instance.\"\"\"\n    data = setup_simple_entity_tests\n    # the updated_by arg should be an instance of User class, in any\n    # other case it should raise a TypeError\n    test_value = \"A User Name\"\n\n    # be sure that the test value is not an instance of User\n    assert not isinstance(test_value, User)\n\n    # check the value\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].updated_by = test_value\n\n    assert str(cm.value) == (\n        \"SimpleEntity.updated_by should be a stalker.models.auth.User instance, \"\n        \"not str: 'A User Name'\"\n    )\n\n\ndef test_updated_by_attr_instance_of_user(setup_simple_entity_tests):\n    \"\"\"TypeError is raised if update_by attr set to a value other than a User.\"\"\"\n    data = setup_simple_entity_tests\n    # the updated_by attr should be an instance of User class, in any\n    # other case it should raise a TypeError\n    test_value = \"A User Name\"\n\n    # be sure that the test value is not an instance of User\n    assert not isinstance(test_value, User)\n\n    # check the value\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].updated_by = test_value\n\n    assert str(cm.value) == (\n        \"SimpleEntity.updated_by should be a stalker.models.auth.User instance, \"\n        \"not str: 'A User Name'\"\n    )\n\n\ndef test_updated_by_arg_empty(setup_simple_entity_tests):\n    \"\"\"updated_by arg is None it is equal to created_by arg.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"updated_by\"] = None\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    # now check if they are same\n    assert new_simple_entity.created_by == new_simple_entity.updated_by\n\n\ndef test_date_created_arg_accepts_datetime_only(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the date_created arg is not a datetime instance.\"\"\"\n    data = setup_simple_entity_tests\n    # the date_created arg should be an instance of datetime.datetime\n    # try to set something else and expect a TypeError\n    test_value = \"a string date time 2010-10-26 etc.\"\n\n    # be sure that the test_value is not an instance of datetime.datetime\n    assert not isinstance(test_value, datetime.datetime)\n\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].date_created = test_value\n\n    assert str(cm.value) == (\n        \"SimpleEntity.date_created should be a datetime.datetime instance, \"\n        \"not str: 'a string date time 2010-10-26 etc.'\"\n    )\n\n\ndef test_date_created_attr_accepts_datetime_only(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the date_created attr is not a datetime instance.\"\"\"\n    data = setup_simple_entity_tests\n    # the date_created attr should be an instance of datetime.datetime\n    # try to set something else and expect a TypeError\n    test_value = \"a string date time 2010-10-26 etc.\"\n    # be sure that the test_value is not an instance of datetime.datetime\n    assert not isinstance(test_value, datetime.datetime)\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].date_created = test_value\n\n    assert str(cm.value) == (\n        \"SimpleEntity.date_created should be a datetime.datetime instance, \"\n        \"not str: 'a string date time 2010-10-26 etc.'\"\n    )\n\n\ndef test_date_created_attr_being_empty(setup_simple_entity_tests):\n    \"\"\"TypeError is raised if the date_created attr is set to None.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].date_created = None\n\n    assert str(cm.value) == \"SimpleEntity.date_created cannot be None\"\n\n\ndef test_date_updated_arg_accepts_datetime_only(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the date_updated arg is not a datetime instance.\"\"\"\n    data = setup_simple_entity_tests\n    # try to set it to something else and expect a TypeError\n    test_value = \"a string date time 2010-10-26 etc.\"\n\n    # be sure that the test_value is not an instance of datetime.datetime\n    assert not isinstance(test_value, datetime.datetime)\n\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].date_updated = test_value\n\n    assert str(cm.value) == (\n        \"SimpleEntity.date_updated should be a datetime.datetime instance, \"\n        \"not str: 'a string date time 2010-10-26 etc.'\"\n    )\n\n\ndef test_date_updated_attr_is_set_to_none(setup_simple_entity_tests):\n    \"\"\"TypeError is raised if the date_updated attr is set to None.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].date_updated = None\n\n    assert str(cm.value) == \"SimpleEntity.date_updated cannot be None\"\n\n\ndef test_date_updated_attr_is_not_datetime(setup_simple_entity_tests):\n    \"\"\"TypeError is raised if the date_updated attr is set to None.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].date_updated = \"this is not datetime\"\n\n    assert str(cm.value) == (\n        \"SimpleEntity.date_updated should be a datetime.datetime instance, \"\n        \"not str: 'this is not datetime'\"\n    )\n\n\ndef test_date_updated_attr_is_working_as_expected(setup_simple_entity_tests):\n    \"\"\"date_updated attr is working as expected.\"\"\"\n    data = setup_simple_entity_tests\n    test_value = datetime.datetime.now(pytz.utc)\n    data[\"test_simple_entity\"].date_updated = test_value\n    assert data[\"test_simple_entity\"].date_updated == test_value\n\n\ndef test_date_created_is_before_date_updated(setup_simple_entity_tests):\n    \"\"\"ValueError raised if date_updated is set to a time before date_created.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"date_created\"] = datetime.datetime(\n        2000, 1, 1, 1, 1, 1, tzinfo=pytz.utc\n    )\n    data[\"kwargs\"][\"date_updated\"] = datetime.datetime(\n        1990, 1, 1, 1, 1, 1, tzinfo=pytz.utc\n    )\n\n    # create a new entity with these dates\n    # and expect a ValueError\n    with pytest.raises(ValueError) as cm:\n        SimpleEntity(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"SimpleEntity.date_updated could not be set to a date before \"\n        \"SimpleEntity.date_created, try setting the ``date_created`` first.\"\n    )\n\n\ndef test___repr__(setup_simple_entity_tests):\n    \"\"\"__repr__ works as expected.\"\"\"\n    data = setup_simple_entity_tests\n    assert data[\"test_simple_entity\"].__repr__() == \"<{} ({})>\".format(\n        data[\"test_simple_entity\"].name,\n        data[\"test_simple_entity\"].entity_type,\n    )\n\n\ndef test_type_arg_is_none(setup_simple_entity_tests):\n    \"\"\"nothing will happen the type arg is None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"type\"] = None\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    assert isinstance(new_simple_entity, SimpleEntity)\n\n\ndef test_type_attr_is_set_to_none(setup_simple_entity_tests):\n    \"\"\"nothing happened if the type attr is set to None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"test_simple_entity\"].type = None\n\n\n@pytest.mark.parametrize(\"test_value\", [1, 1.2, \"a type\"])\ndef test_type_arg_accepts_only_type_instances(test_value, setup_simple_entity_tests):\n    \"\"\"TypeError raised if type attr is not a Type instance.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"type\"] = test_value\n    with pytest.raises(TypeError):\n        SimpleEntity(**data[\"kwargs\"])\n\n\ndef test_type_arg_accepts_type_instances(setup_simple_entity_tests):\n    \"\"\"no error raised if the type arg is a Type instance.\"\"\"\n    data = setup_simple_entity_tests\n    # test with a proper Type\n    data[\"kwargs\"][\"type\"] = data[\"test_type\"]\n    # no error is expected\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    assert isinstance(new_simple_entity, SimpleEntity)\n\n\n@pytest.mark.parametrize(\"test_value\", [1, 1.2, \"a type\"])\ndef test_type_attr_accepts_only_type_instances(test_value, setup_simple_entity_tests):\n    \"\"\"TypeError raised type attr is not Type instance.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(TypeError):\n        data[\"test_simple_entity\"].type = test_value\n\n\ndef test_type_attr_accepts_type_instances(setup_simple_entity_tests):\n    \"\"\"no error raised if the type attr is set to Type instance.\"\"\"\n    data = setup_simple_entity_tests\n    # test with a proper Type\n    data[\"test_simple_entity\"].type = data[\"test_type\"]\n\n\ndef test___strictly_typed___class_attr_is_init_as_false(setup_simple_entity_tests):\n    \"\"\"__strictly_typed__ class attr is initialized as False.\"\"\"\n    data = setup_simple_entity_tests\n    assert data[\"test_simple_entity\"].__strictly_typed__ is False\n\n\ndef test___strictly_typed___attr_set_to_true_and_no_type_arg(setup_simple_entity_tests):\n    \"\"\"TypeError raised if __strictly_typed__ is True but no Type arg given.\"\"\"\n    data = setup_simple_entity_tests\n    # create a new class deriving from the SimpleEntity\n    assert NewClass.__strictly_typed__ is True\n\n    # create a new instance and skip the Type attr and expect a\n    # TypeError\n    if \"type\" in data[\"kwargs\"]:\n        data[\"kwargs\"].pop(\"type\")\n\n    with pytest.raises(TypeError) as cm:\n        NewClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"NewClass.type must be a stalker.models.type.Type instance, \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test___strictly_typed___attr_set_to_true_and_type_arg_is_none(\n    setup_simple_entity_tests,\n):\n    \"\"\"TypeError raised if __strictly_typed__ attr is True but Type arg is None.\"\"\"\n    data = setup_simple_entity_tests\n    # set it to None and expect a TypeError\n    data[\"kwargs\"][\"type\"] = None\n    with pytest.raises(TypeError) as cm:\n        NewClass(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"NewClass.type must be a stalker.models.type.Type instance, \"\n        \"not NoneType: 'None'\"\n    )\n\n\n@pytest.mark.parametrize(\"test_value\", [1, 1.2, [\"a\", \"list\"], {\"a\": \"dict\"}])\ndef test___strictly_typed___attr_set_to_true_and_type_arg_is_not_type(\n    test_value,\n    setup_simple_entity_tests,\n):\n    \"\"\"TypeError raised __strictly_typed__ is True but the type arg is not a str.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"type\"] = test_value\n    with pytest.raises(TypeError):\n        NewClass(**data[\"kwargs\"])\n\n\ndef test_stalker_version_attr_is_automatically_set_to_the_current_stalker_version(\n    setup_simple_entity_tests,\n):\n    \"\"\"stalker_version is automatically set for the newly created SimpleEntities.\"\"\"\n    data = setup_simple_entity_tests\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    assert new_simple_entity.stalker_version == stalker.__version__\n\n    # update stalker.__version__ to a test value\n    current_version = stalker.__version__\n\n    test_version = \"test_version\"\n    stalker.__version__ = test_version\n\n    # test if it is updated\n    assert stalker.__version__ == test_version\n\n    # create a new SimpleEntity and check if it is following the version\n    new_simple_entity2 = SimpleEntity(**data[\"kwargs\"])\n    assert new_simple_entity2.stalker_version == test_version\n\n    # restore the stalker.__version__\n    stalker.__version__ = current_version\n\n\ndef test_thumbnail_arg_is_skipped(setup_simple_entity_tests):\n    \"\"\"thumbnail attr None if the thumbnail arg is skipped.\"\"\"\n    data = setup_simple_entity_tests\n    try:\n        data[\"kwargs\"].pop(\"thumbnail\")\n    except KeyError:\n        pass\n\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    assert new_simple_entity.thumbnail is None\n\n\ndef test_thumbnail_arg_is_none(setup_simple_entity_tests):\n    \"\"\"thumbnail arg can be None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"thumbnail\"] = None\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    assert new_simple_entity.thumbnail is None\n\n\ndef test_thumbnail_attr_is_none(setup_simple_entity_tests):\n    \"\"\"thumbnail attr can be set to None.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"test_simple_entity\"].thumbnail = None\n    assert data[\"test_simple_entity\"].thumbnail is None\n\n\ndef test_thumbnail_arg_is_not_a_file_instance(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the thumbnail arg is not a File instance.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"thumbnail\"] = \"not a File\"\n    with pytest.raises(TypeError) as cm:\n        SimpleEntity(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"SimpleEntity.thumbnail should be a stalker.models.file.File instance, \"\n        \"not str: 'not a File'\"\n    )\n\n\ndef test_thumbnail_attr_is_not_a_file_instance(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the thumbnail is not a File instance.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].thumbnail = \"not a File\"\n\n    assert str(cm.value) == (\n        \"SimpleEntity.thumbnail should be a stalker.models.file.File instance, \"\n        \"not str: 'not a File'\"\n    )\n\n\ndef test_thumbnail_arg_is_working_as_expected(setup_simple_entity_tests):\n    \"\"\"thumbnail arg value is passed to the thumbnail attr correctly.\"\"\"\n    data = setup_simple_entity_tests\n    thumb = File(full_path=\"some path\")\n    data[\"kwargs\"][\"thumbnail\"] = thumb\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    assert new_simple_entity.thumbnail == thumb\n\n\ndef test_thumbnail_attr_is_working_as_expected(setup_simple_entity_tests):\n    \"\"\"thumbnail attr is working as expected.\"\"\"\n    data = setup_simple_entity_tests\n    thumb = File(full_path=\"some path\")\n    assert not data[\"test_simple_entity\"].thumbnail == thumb\n    data[\"test_simple_entity\"].thumbnail = thumb\n    assert data[\"test_simple_entity\"].thumbnail == thumb\n\n\ndef test_html_style_arg_is_skipped(setup_simple_entity_tests):\n    \"\"\"html_style arg is skipped the html_style attr an empty string.\"\"\"\n    data = setup_simple_entity_tests\n    if \"html_style\" in data[\"kwargs\"]:\n        data[\"kwargs\"].pop(\"html_style\")\n    se = SimpleEntity(**data[\"kwargs\"])\n    assert se.html_style == \"\"\n\n\ndef test_html_style_arg_is_none(setup_simple_entity_tests):\n    \"\"\"html_style arg is set to None the html_style attr an empty string.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"html_style\"] = None\n    se = SimpleEntity(**data[\"kwargs\"])\n    assert se.html_style == \"\"\n\n\ndef test_html_style_attr_is_set_to_none(setup_simple_entity_tests):\n    \"\"\"html_style attr is set to None it an empty string.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"test_simple_entity\"].html_style = None\n    assert data[\"test_simple_entity\"].html_style == \"\"\n\n\ndef test_html_style_arg_is_not_a_string(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the html_style arg is not a string.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"html_style\"] = 123\n    with pytest.raises(TypeError) as cm:\n        SimpleEntity(**data[\"kwargs\"])\n    assert str(cm.value) == (\"SimpleEntity.html_style should be a str, not int: '123'\")\n\n\ndef test_html_style_attr_is_not_set_to_a_string(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the html_style attr is not set to a string.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].html_style = 34324\n    assert str(cm.value) == (\n        \"SimpleEntity.html_style should be a str, not int: '34324'\"\n    )\n\n\ndef test_html_style_arg_is_working_as_expected(setup_simple_entity_tests):\n    \"\"\"html_style arg value is correctly passed to the html_style attr.\"\"\"\n    data = setup_simple_entity_tests\n    test_value = \"width: 100px; color: purple; background-color: black\"\n    data[\"kwargs\"][\"html_style\"] = test_value\n    se = SimpleEntity(**data[\"kwargs\"])\n    assert se.html_style == test_value\n\n\ndef test_html_style_attr_is_working_as_expected(setup_simple_entity_tests):\n    \"\"\"html_style attr is working as expected.\"\"\"\n    data = setup_simple_entity_tests\n    test_value = \"width: 100px; color: purple; background-color: black\"\n    data[\"test_simple_entity\"].html_style = test_value\n    assert data[\"test_simple_entity\"].html_style == test_value\n\n\ndef test_html_class_arg_is_skipped(setup_simple_entity_tests):\n    \"\"\"html_class arg is skipped the html_class attr an empty string.\"\"\"\n    data = setup_simple_entity_tests\n    if \"html_class\" in data[\"kwargs\"]:\n        data[\"kwargs\"].pop(\"html_class\")\n    se = SimpleEntity(**data[\"kwargs\"])\n    assert se.html_class == \"\"\n\n\ndef test_html_class_arg_is_none(setup_simple_entity_tests):\n    \"\"\"html_class arg is set to None the html_class attr an empty string.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"html_class\"] = None\n    se = SimpleEntity(**data[\"kwargs\"])\n    assert se.html_class == \"\"\n\n\ndef test_html_class_attr_is_set_to_none(setup_simple_entity_tests):\n    \"\"\"html_class attr is set to None it an empty string.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"test_simple_entity\"].html_class = None\n    assert data[\"test_simple_entity\"].html_class == \"\"\n\n\ndef test_html_class_arg_is_not_a_string(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the html_class arg is not a string.\"\"\"\n    data = setup_simple_entity_tests\n    data[\"kwargs\"][\"html_class\"] = 123\n    with pytest.raises(TypeError) as cm:\n        SimpleEntity(**data[\"kwargs\"])\n    assert str(cm.value) == (\"SimpleEntity.html_class should be a str, not int: '123'\")\n\n\ndef test_html_class_attr_is_not_set_to_a_string(setup_simple_entity_tests):\n    \"\"\"TypeError raised if the html_class attr is not set to a string.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_simple_entity\"].html_class = 34324\n    assert str(cm.value) == (\n        \"SimpleEntity.html_class should be a str, not int: '34324'\"\n    )\n\n\ndef test_html_class_arg_is_working_as_expected(setup_simple_entity_tests):\n    \"\"\"html_class arg value is correctly passed to the html_class attr.\"\"\"\n    data = setup_simple_entity_tests\n    test_value = \"purple\"\n    data[\"kwargs\"][\"html_class\"] = test_value\n    se = SimpleEntity(**data[\"kwargs\"])\n    assert se.html_class == test_value\n\n\ndef test_html_class_attr_is_working_as_expected(setup_simple_entity_tests):\n    \"\"\"html_class attr is working as expected.\"\"\"\n    data = setup_simple_entity_tests\n    test_value = \"purple\"\n    data[\"test_simple_entity\"].html_class = test_value\n    assert data[\"test_simple_entity\"].html_class == test_value\n\n\ndef test_to_tjp_will_raise_a_not_implemented_error(setup_simple_entity_tests):\n    \"\"\"calling to_tjp() method will raise a NotImplementedError.\"\"\"\n    data = setup_simple_entity_tests\n    with pytest.raises(NotImplementedError):\n        data[\"test_simple_entity\"].to_tjp()\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_simple_entity_db_tests(setup_postgresql_db):\n    \"\"\"set up the SimpleEntity tests wit a db.\"\"\"\n    data = dict()\n    data[\"test_user\"] = User(\n        name=\"Test User\",\n        login=\"testuser\",\n        email=\"test@user.com\",\n        password=\"test\",\n        generic_text=json.dumps({\"Phone number\": \"123\"}, sort_keys=True),\n    )\n    DBSession.add(data[\"test_user\"])\n    DBSession.commit()\n\n    data[\"date_created\"] = datetime.datetime(2010, 10, 21, 3, 8, 0, tzinfo=pytz.utc)\n    data[\"date_updated\"] = data[\"date_created\"]\n\n    data[\"kwargs\"] = {\n        \"name\": \"Test Entity\",\n        \"code\": \"TstEnt\",\n        \"description\": \"This is a test entity, and this is a proper \\\n        description for it\",\n        \"created_by\": data[\"test_user\"],\n        \"updated_by\": data[\"test_user\"],\n        \"date_created\": data[\"date_created\"],\n        \"date_updated\": data[\"date_updated\"],\n        \"generic_text\": json.dumps({\"Phone number\": \"123\"}, sort_keys=True),\n    }\n    return data\n\n\ndef test_generic_data_attr_can_hold_a_wide_variety_of_object_types(\n    setup_simple_entity_db_tests,\n):\n    \"\"\"generic_data attr can hold any kind of object as a list.\"\"\"\n    data = setup_simple_entity_db_tests\n    new_simple_entity = SimpleEntity(**data[\"kwargs\"])\n    DBSession.add(new_simple_entity)\n    test_user = User(\n        name=\"email\",\n        login=\"email\",\n        email=\"email@email.com\",\n        password=\"email\",\n    )\n\n    test_department = Department(name=\"department1\")\n    DBSession.add(test_department)\n\n    test_repo = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n    )\n    DBSession.add(test_repo)\n\n    test_struct = Structure(name=\"Test Project Structure\")\n    DBSession.add(test_struct)\n\n    test_proj = Project(\n        name=\"Test Project 1\",\n        code=\"tp1\",\n        repository=test_repo,\n        structure=test_struct,\n    )\n    DBSession.add(test_proj)\n\n    new_simple_entity.generic_data.extend(\n        [test_proj, test_struct, test_repo, test_department, test_user]\n    )\n\n    # now check if it is added to the database correctly\n    del new_simple_entity\n\n    new_simple_entity_db = SimpleEntity.query.filter_by(\n        name=data[\"kwargs\"][\"name\"]\n    ).first()\n\n    assert test_proj in new_simple_entity_db.generic_data\n    assert test_struct in new_simple_entity_db.generic_data\n    assert test_repo in new_simple_entity_db.generic_data\n    assert test_department in new_simple_entity_db.generic_data\n    assert test_user in new_simple_entity_db.generic_data\n"
  },
  {
    "path": "tests/models/test_status.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Status class.\"\"\"\n\nimport pytest\n\nfrom stalker import Entity, Status\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_status_tests():\n    \"\"\"Set up tests for the stalker.models.status.Status class.\"\"\"\n    data = dict()\n    data[\"kwargs\"] = {\n        \"name\": \"Complete\",\n        \"description\": \"use this if the object is complete\",\n        \"code\": \"CMPLT\",\n    }\n\n    # create an entity object with same kwargs for __eq__ and __ne__ tests\n    # (it should return False for __eq__ and True for __ne__ for same\n    # kwargs)\n    data[\"entity1\"] = Entity(**data[\"kwargs\"])\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Status class.\"\"\"\n    assert Status.__auto_name__ is False\n\n\ndef test_equality(setup_status_tests):\n    \"\"\"equality of two statuses.\"\"\"\n    data = setup_status_tests\n    status1 = Status(**data[\"kwargs\"])\n    status2 = Status(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"name\"] = \"Work In Progress\"\n    data[\"kwargs\"][\"description\"] = \"use this if the object is still in progress\"\n    data[\"kwargs\"][\"code\"] = \"WIP\"\n\n    status3 = Status(**data[\"kwargs\"])\n\n    assert status1 == status2\n    assert not status1 == status3\n    assert not status1 == data[\"entity1\"]\n\n\ndef test_status_and_string_equality_in_status_name(setup_status_tests):\n    \"\"\"status can be compared with a string matching the Status.name.\"\"\"\n    data = setup_status_tests\n    a_status = Status(**data[\"kwargs\"])\n    assert a_status == data[\"kwargs\"][\"name\"]\n    assert a_status == data[\"kwargs\"][\"name\"].lower()\n    assert a_status == data[\"kwargs\"][\"name\"].upper()\n    assert a_status != \"another name\"\n\n\ndef test_status_and_string_equality_in_status_code(setup_status_tests):\n    \"\"\"status can be compared with a string matching the Status.code.\"\"\"\n    data = setup_status_tests\n    a_status = Status(**data[\"kwargs\"])\n    assert a_status == data[\"kwargs\"][\"code\"]\n    assert a_status == data[\"kwargs\"][\"code\"].lower()\n    assert a_status == data[\"kwargs\"][\"code\"].upper()\n\n\ndef test_inequality(setup_status_tests):\n    \"\"\"inequality of two statuses.\"\"\"\n    data = setup_status_tests\n    status1 = Status(**data[\"kwargs\"])\n    status2 = Status(**data[\"kwargs\"])\n    data[\"kwargs\"][\"name\"] = \"Work In Progress\"\n    data[\"kwargs\"][\"description\"] = \"use this if the object is still in progress\"\n    data[\"kwargs\"][\"code\"] = \"WIP\"\n\n    status3 = Status(**data[\"kwargs\"])\n\n    assert not status1 != status2\n    assert status1 != status3\n    assert status1 != data[\"entity1\"]\n\n\ndef test_status_and_string_inequality_in_status_name(setup_status_tests):\n    \"\"\"status can be compared with a string.\"\"\"\n    data = setup_status_tests\n    a_status = Status(**data[\"kwargs\"])\n    assert not a_status != data[\"kwargs\"][\"name\"]\n    assert not a_status != data[\"kwargs\"][\"name\"].lower()\n    assert not a_status != data[\"kwargs\"][\"name\"].upper()\n    assert a_status != \"another name\"\n\n\ndef test_status_and_string_inequality_in_status_code(setup_status_tests):\n    \"\"\"status can be compared with a string.\"\"\"\n    data = setup_status_tests\n    a_status = Status(**data[\"kwargs\"])\n    assert not a_status != data[\"kwargs\"][\"code\"]\n    assert not a_status != data[\"kwargs\"][\"code\"].lower()\n    assert not a_status != data[\"kwargs\"][\"code\"].upper()\n\n\ndef test__hash__is_working_as_expected(setup_status_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_status_tests\n    data[\"test_status\"] = Status(**data[\"kwargs\"])\n    result = hash(data[\"test_status\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_status\"].__hash__()\n"
  },
  {
    "path": "tests/models/test_status_list.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the StatusList class.\"\"\"\n\nimport pytest\n\nfrom stalker import Status, StatusList\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_status_list_tests():\n    \"\"\"Set up tests for the StatusList class.\"\"\"\n    data = dict()\n    data[\"kwargs\"] = {\n        \"name\": \"a status list\",\n        \"description\": \"this is a status list for testing purposes\",\n        \"statuses\": [\n            Status(name=\"Waiting For Dependency\", code=\"WFD\"),\n            Status(name=\"Ready To Start\", code=\"RTS\"),\n            Status(name=\"Work In Progress\", code=\"WIP\"),\n            Status(name=\"Pending Review\", code=\"PREV\"),\n            Status(name=\"Has Revision\", code=\"HREV\"),\n            Status(name=\"Completed\", code=\"CMPL\"),\n            Status(name=\"On Hold\", code=\"OH\"),\n        ],\n        \"target_entity_type\": \"Project\",\n    }\n\n    data[\"test_status_list\"] = StatusList(**data[\"kwargs\"])\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_true():\n    \"\"\"__auto_name__ class attribute is set to True for StatusList class.\"\"\"\n    assert StatusList.__auto_name__ is True\n\n\ndef test_statuses_argument_accepts_statuses_only(setup_status_list_tests):\n    \"\"\"statuses list argument accepts list of statuses only.\"\"\"\n    data = setup_status_list_tests\n    # the statuses argument should be a list of statuses\n    # can be empty?\n    test_value = \"a str\"\n    # it should only accept lists of statuses\n    data[\"kwargs\"][\"statuses\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        StatusList(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_statuses_attribute_accepting_only_statuses(setup_status_list_tests):\n    \"\"\"status_list attribute accepting Status objects only.\"\"\"\n    data = setup_status_list_tests\n    test_value = \"1\"\n    # check the attribute\n    with pytest.raises(TypeError) as cm:\n        data[\"test_status_list\"].statuses = test_value\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_statuses_argument_elements_being_status_objects(setup_status_list_tests):\n    \"\"\"status_list elements against not being derived from Status class.\"\"\"\n    data = setup_status_list_tests\n    # every element should be an object derived from Status\n    a_fake_status_list = [1, 2, \"a string\", 4.5]\n    data[\"kwargs\"][\"statuses\"] = a_fake_status_list\n    with pytest.raises(TypeError) as cm:\n        StatusList(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"All of the elements in StatusList.statuses must be an instance \"\n        \"of stalker.models.status.Status, not int: '1'\"\n    )\n\n\ndef test_statuses_attribute_works_as_expected(setup_status_list_tests):\n    \"\"\"status_list attribute is working as expected.\"\"\"\n    data = setup_status_list_tests\n    new_list_of_statutes = [Status(name=\"New Status\", code=\"NSTS\")]\n    data[\"test_status_list\"].statuses = new_list_of_statutes\n    assert data[\"test_status_list\"].statuses == new_list_of_statutes\n\n\ndef test_statuses_attributes_elements_changed_to_none_status_objects(\n    setup_status_list_tests,\n):\n    \"\"\"TypeError raised if an item is set to a none Status instance statuses list.\"\"\"\n    data = setup_status_list_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_status_list\"].statuses[0] = 0\n    assert str(cm.value) == (\n        \"All of the elements in StatusList.statuses must be an instance \"\n        \"of stalker.models.status.Status, not int: '0'\"\n    )\n\n\ndef test_equality_operator(setup_status_list_tests):\n    \"\"\"equality of two status list object.\"\"\"\n    data = setup_status_list_tests\n    status_list1 = StatusList(**data[\"kwargs\"])\n    status_list2 = StatusList(**data[\"kwargs\"])\n    data[\"kwargs\"][\"target_entity_type\"] = \"SomeOtherClass\"\n    status_list3 = StatusList(**data[\"kwargs\"])\n    data[\"kwargs\"][\"statuses\"] = [\n        Status(name=\"Started\", code=\"STRT\"),\n        Status(name=\"Waiting For Approve\", code=\"WAPPR\"),\n        Status(name=\"Approved\", code=\"APPR\"),\n        Status(name=\"Finished\", code=\"FNSH\"),\n    ]\n    status_list4 = StatusList(**data[\"kwargs\"])\n    assert status_list1 == status_list2\n    assert not status_list1 == status_list3\n    assert not status_list1 == status_list4\n\n\ndef test_inequality_operator(setup_status_list_tests):\n    \"\"\"equality of two status list object.\"\"\"\n    data = setup_status_list_tests\n    status_list1 = StatusList(**data[\"kwargs\"])\n    status_list2 = StatusList(**data[\"kwargs\"])\n    data[\"kwargs\"][\"target_entity_type\"] = \"SomeOtherClass\"\n    status_list3 = StatusList(**data[\"kwargs\"])\n    data[\"kwargs\"][\"statuses\"] = [\n        Status(name=\"Started\", code=\"STRT\"),\n        Status(name=\"Waiting For Approve\", code=\"WAPPR\"),\n        Status(name=\"Approved\", code=\"APPR\"),\n        Status(name=\"Finished\", code=\"FNSH\"),\n    ]\n    status_list4 = StatusList(**data[\"kwargs\"])\n    assert not status_list1 != status_list2\n    assert status_list1 != status_list3\n    assert status_list1 != status_list4\n\n\ndef test_indexing_get(setup_status_list_tests):\n    \"\"\"indexing of statuses in the statusList, get.\"\"\"\n    data = setup_status_list_tests\n    # first try indexing\n    # this shouldn't raise a TypeError\n    status1 = data[\"test_status_list\"][0]\n    # check the equality\n    assert data[\"test_status_list\"].statuses[0] == status1\n\n\ndef test_indexing_get_string_indexes(setup_status_list_tests):\n    \"\"\"indexing of statuses in the statusList, get with string.\"\"\"\n    data = setup_status_list_tests\n    status1 = Status(name=\"Complete\", code=\"CMPLT\")\n    status2 = Status(name=\"Work in Progress\", code=\"WIP\")\n    status3 = Status(name=\"Pending Review\", code=\"PRev\")\n    a_status_list = StatusList(\n        name=\"Asset Status List\",\n        statuses=[status1, status2, status3],\n        target_entity_type=\"Asset\",\n    )\n\n    assert a_status_list[0] == a_status_list[\"complete\"]\n    assert a_status_list[1] == a_status_list[\"wip\"]\n\n\ndef test_indexing_setitem_validates_the_given_value(setup_status_list_tests):\n    \"\"\"indexing of statuses in the statusList, set.\"\"\"\n    data = setup_status_list_tests\n    # first try indexing\n    # this shouldn't raise a TypeError\n    with pytest.raises(TypeError) as cm:\n        data[\"test_status_list\"][0] = \"PRev\"\n\n    assert str(cm.value) == (\n        \"All of the elements in StatusList.statuses must be an instance of \"\n        \"stalker.models.status.Status, not str: 'PRev'\"\n    )\n\n\ndef test_indexing_setitem(setup_status_list_tests):\n    \"\"\"indexing of statuses in the statusList, set.\"\"\"\n    data = setup_status_list_tests\n    # first try indexing\n    # this shouldn't raise a TypeError\n    status = Status(name=\"Pending Review\", code=\"PRev\")\n    data[\"test_status_list\"][0] = status\n    # check the equality\n    assert data[\"test_status_list\"].statuses[0] == status\n\n\ndef test_indexing_delitem(setup_status_list_tests):\n    \"\"\"indexing of statuses in the statusList, del.\"\"\"\n    data = setup_status_list_tests\n    # first get the length\n    len_statuses = len(data[\"test_status_list\"].statuses)\n    del data[\"test_status_list\"][-1]\n    assert len(data[\"test_status_list\"].statuses) == len_statuses - 1\n\n\ndef test_indexing_len(setup_status_list_tests):\n    \"\"\"indexing of statuses in the statusList, len.\"\"\"\n    data = setup_status_list_tests\n    # get the len and compare it wiht len(statuses)\n    assert len(data[\"test_status_list\"].statuses) == len(data[\"test_status_list\"])\n\n\ndef test__hash__is_working_as_expected(setup_status_list_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_status_list_tests\n    result = hash(data[\"test_status_list\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_status_list\"].__hash__()\n"
  },
  {
    "path": "tests/models/test_structure.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Structure class.\"\"\"\n\nimport pytest\n\nfrom stalker import FilenameTemplate, Structure, Type\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_structure_tests():\n    \"\"\"stalker.models.structure.Structure class.\"\"\"\n    data = dict()\n    vers_type = Type(name=\"Version\", code=\"vers\", target_entity_type=\"FilenameTemplate\")\n    ref_type = Type(name=\"Reference\", code=\"ref\", target_entity_type=\"FilenameTemplate\")\n    # type templates\n    data[\"asset_template\"] = FilenameTemplate(\n        name=\"Test Asset Template\", target_entity_type=\"Asset\", type=vers_type\n    )\n    data[\"shot_template\"] = FilenameTemplate(\n        name=\"Test Shot Template\", target_entity_type=\"Shot\", type=vers_type\n    )\n    data[\"reference_template\"] = FilenameTemplate(\n        name=\"Test Reference Template\", target_entity_type=\"File\", type=ref_type\n    )\n    data[\"test_templates\"] = [\n        data[\"asset_template\"],\n        data[\"shot_template\"],\n        data[\"reference_template\"],\n    ]\n    data[\"test_templates2\"] = [data[\"asset_template\"]]\n    data[\"custom_template\"] = \"a custom template\"\n    data[\"test_type\"] = Type(\n        name=\"Commercial Structure\",\n        code=\"comm\",\n        target_entity_type=\"Structure\",\n    )\n    # keyword arguments\n    data[\"kwargs\"] = {\n        \"name\": \"Test Structure\",\n        \"description\": \"This is a test structure\",\n        \"templates\": data[\"test_templates\"],\n        \"custom_template\": data[\"custom_template\"],\n        \"type\": data[\"test_type\"],\n    }\n    data[\"test_structure\"] = Structure(**data[\"kwargs\"])\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Structure class.\"\"\"\n    assert Structure.__auto_name__ is False\n\n\ndef test_custom_template_argument_can_be_skipped(setup_structure_tests):\n    \"\"\"custom_template argument can be skipped.\"\"\"\n    data = setup_structure_tests\n    data[\"kwargs\"].pop(\"custom_template\")\n    new_structure = Structure(**data[\"kwargs\"])\n    assert new_structure.custom_template == \"\"\n\n\ndef test_custom_template_argument_is_none(setup_structure_tests):\n    \"\"\"no error raised if the custom_template argument is None.\"\"\"\n    data = setup_structure_tests\n    data[\"kwargs\"][\"custom_template\"] = None\n    new_structure = Structure(**data[\"kwargs\"])\n    assert new_structure.custom_template == \"\"\n\n\ndef test_custom_template_argument_is_empty_string(setup_structure_tests):\n    \"\"\"no error raised if the custom_template argument is an empty string.\"\"\"\n    data = setup_structure_tests\n    data[\"kwargs\"][\"custom_template\"] = \"\"\n    new_structure = Structure(**data[\"kwargs\"])\n    assert new_structure.custom_template == \"\"\n\n\ndef test_custom_template_argument_is_not_a_string(setup_structure_tests):\n    \"\"\"TypeError raised if the custom_template argument is not a string.\"\"\"\n    data = setup_structure_tests\n    data[\"kwargs\"][\"custom_template\"] = [\"this is not a string\"]\n    with pytest.raises(TypeError) as cm:\n        Structure(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Structure.custom_template should be a string, \"\n        \"not list: '['this is not a string']'\"\n    )\n\n\ndef test_custom_template_attribute_is_not_a_string(setup_structure_tests):\n    \"\"\"TypeError raised if the custom_template attribute is not a string.\"\"\"\n    data = setup_structure_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_structure\"].custom_template = [\"this is not a string\"]\n    assert str(cm.value) == (\n        \"Structure.custom_template should be a string, \"\n        \"not list: '['this is not a string']'\"\n    )\n\n\ndef test_templates_argument_can_be_skipped(setup_structure_tests):\n    \"\"\"no error raised if the templates argument is skipped.\"\"\"\n    data = setup_structure_tests\n    data[\"kwargs\"].pop(\"templates\")\n    new_structure = Structure(**data[\"kwargs\"])\n    assert isinstance(new_structure, Structure)\n\n\ndef test_templates_argument_can_be_none(setup_structure_tests):\n    \"\"\"no error raised if the templates argument is None.\"\"\"\n    data = setup_structure_tests\n    data[\"kwargs\"][\"templates\"] = None\n    new_structure = Structure(**data[\"kwargs\"])\n    assert isinstance(new_structure, Structure)\n\n\ndef test_templates_attribute_cannot_be_set_to_none(setup_structure_tests):\n    \"\"\"TypeError raised if the templates attribute is set to None.\"\"\"\n    data = setup_structure_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_structure\"].templates = None\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_templates_argument_only_accepts_list(setup_structure_tests):\n    \"\"\"TypeError raised if the given templates argument is not a list.\"\"\"\n    data = setup_structure_tests\n    data[\"kwargs\"][\"templates\"] = 1\n    with pytest.raises(TypeError) as cm:\n        Structure(**data[\"kwargs\"])\n    assert str(cm.value) == \"Incompatible collection type: int is not list-like\"\n\n\ndef test_templates_attribute_only_accepts_list_1(setup_structure_tests):\n    \"\"\"TypeError raised if the templates attr is set to none list.\"\"\"\n    data = setup_structure_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_structure\"].templates = 1.121\n    assert str(cm.value) == \"Incompatible collection type: float is not list-like\"\n\n\ndef test_templates_attribute_is_working_as_expected(setup_structure_tests):\n    \"\"\"templates attribute is working as expected.\"\"\"\n    data = setup_structure_tests\n    # test the correct value\n    data[\"test_structure\"].templates = data[\"test_templates\"]\n    assert data[\"test_structure\"].templates == data[\"test_templates\"]\n\n\ndef test_templates_argument_accepts_only_list_of_filename_template_instances(\n    setup_structure_tests,\n):\n    \"\"\"TypeError raised if the templates arg is a list of none FilenameTemplate.\"\"\"\n    data = setup_structure_tests\n    test_value = [1, 1.2, \"a string\"]\n    data[\"kwargs\"][\"templates\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        Structure(**data[\"kwargs\"])\n    assert str(cm.value) == (\n        \"Structure.templates should only contain instances of \"\n        \"stalker.models.template.FilenameTemplate, not int: '1'\"\n    )\n\n\ndef test_templates_argument_is_working_as_expected(setup_structure_tests):\n    \"\"\"templates argument value is correctly passed to the templates attribute.\"\"\"\n    data = setup_structure_tests\n    # test the correct value\n    data[\"kwargs\"][\"templates\"] = data[\"test_templates\"]\n    new_structure = Structure(**data[\"kwargs\"])\n    assert new_structure.templates == data[\"test_templates\"]\n\n\ndef test_templates_attribute_accpets_only_list_of_filename_template_instances(\n    setup_structure_tests,\n):\n    \"\"\"TypeError raised if the templates attr is a list of none FilenameTemplate.\"\"\"\n    data = setup_structure_tests\n    test_value = [1, 1.2, \"a string\"]\n    with pytest.raises(TypeError) as cm:\n        data[\"test_structure\"].templates = test_value\n\n    assert str(cm.value) == (\n        \"Structure.templates should only contain instances of \"\n        \"stalker.models.template.FilenameTemplate, not int: '1'\"\n    )\n\n\ndef test___strictly_typed___is_false():\n    \"\"\"__strictly_typed__ is False.\"\"\"\n    assert Structure.__strictly_typed__ is False\n\n\ndef test_equality_operator(setup_structure_tests):\n    \"\"\"equality of two Structure objects.\"\"\"\n    data = setup_structure_tests\n    new_structure2 = Structure(**data[\"kwargs\"])\n    data[\"kwargs\"][\"custom_template\"] = \"a test custom template\"\n    new_structure3 = Structure(**data[\"kwargs\"])\n    data[\"kwargs\"][\"custom_template\"] = data[\"test_structure\"].custom_template\n    data[\"kwargs\"][\"templates\"] = data[\"test_templates2\"]\n    new_structure4 = Structure(**data[\"kwargs\"])\n    assert data[\"test_structure\"] == new_structure2\n    assert not data[\"test_structure\"] == new_structure3\n    assert not data[\"test_structure\"] == new_structure4\n\n\ndef test_inequality_operator(setup_structure_tests):\n    \"\"\"inequality of two Structure objects.\"\"\"\n    data = setup_structure_tests\n    new_structure2 = Structure(**data[\"kwargs\"])\n    data[\"kwargs\"][\"custom_template\"] = \"a test custom template\"\n    new_structure3 = Structure(**data[\"kwargs\"])\n    data[\"kwargs\"][\"custom_template\"] = data[\"test_structure\"].custom_template\n    data[\"kwargs\"][\"templates\"] = data[\"test_templates2\"]\n    new_structure4 = Structure(**data[\"kwargs\"])\n    assert not data[\"test_structure\"] != new_structure2\n    assert data[\"test_structure\"] != new_structure3\n    assert data[\"test_structure\"] != new_structure4\n\n\ndef test_plural_class_name(setup_structure_tests):\n    \"\"\"plural name of Structure class.\"\"\"\n    data = setup_structure_tests\n    assert data[\"test_structure\"].plural_class_name == \"Structures\"\n\n\ndef test__hash__is_working_as_expected(setup_structure_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_structure_tests\n    result = hash(data[\"test_structure\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_structure\"].__hash__()\n"
  },
  {
    "path": "tests/models/test_studio.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Studio class.\"\"\"\n\nimport datetime\nimport sys\n\nfrom jinja2 import Template\n\nimport pytest\n\nimport pytz\n\nfrom stalker import (\n    Asset,\n    Department,\n    Project,\n    Repository,\n    SchedulerBase,\n    Shot,\n    Status,\n    Studio,\n    Task,\n    TaskJugglerScheduler,\n    Type,\n    User,\n    Vacation,\n    WorkingHours,\n    defaults,\n)\nfrom stalker.db.session import DBSession\nfrom stalker.models.enum import TimeUnit\nfrom stalker.models.enum import DependencyTarget\n\n\nclass DummyScheduler(SchedulerBase):\n    \"\"\"This is a dummy scheduler to be used in tests.\"\"\"\n\n    def __init__(self, studio=None, callback=None):\n        SchedulerBase.__init__(self, studio)\n        self.callback = callback\n\n    def schedule(self):\n        \"\"\"Call the callback function before finishing.\"\"\"\n        if self.callback:\n            self.callback()\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_studio_db_tests(setup_postgresql_db):\n    \"\"\"Set up the test for stalker.models.studio.Studio class.\"\"\"\n    data = dict()\n\n    data[\"status_rts\"] = Status.query.filter_by(code=\"RTS\").first()\n    data[\"status_wip\"] = Status.query.filter_by(code=\"WIP\").first()\n\n    data[\"test_user1\"] = User(\n        name=\"User 1\", login=\"user1\", email=\"user1@users.com\", password=\"password\"\n    )\n    DBSession.add(data[\"test_user1\"])\n\n    data[\"test_user2\"] = User(\n        name=\"User 2\", login=\"user2\", email=\"user2@users.com\", password=\"password\"\n    )\n    DBSession.add(data[\"test_user2\"])\n\n    data[\"test_user3\"] = User(\n        name=\"User 3\", login=\"user3\", email=\"user3@users.com\", password=\"password\"\n    )\n    DBSession.add(data[\"test_user3\"])\n\n    data[\"test_department1\"] = Department(name=\"Test Department 1\")\n    DBSession.add(data[\"test_department1\"])\n\n    data[\"test_department2\"] = Department(name=\"Test Department 2\")\n    DBSession.add(data[\"test_department2\"])\n\n    data[\"test_repo\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        windows_path=\"T:/\",\n        linux_path=\"/mnt/T/\",\n        macos_path=\"/Volumes/T/\",\n    )\n    DBSession.add(data[\"test_repo\"])\n\n    # create a couple of projects\n    data[\"test_project1\"] = Project(\n        name=\"Test Project 1\", code=\"TP1\", repository=data[\"test_repo\"]\n    )\n    data[\"test_project1\"].status = data[\"status_wip\"]\n    DBSession.add(data[\"test_project1\"])\n\n    data[\"test_project2\"] = Project(\n        name=\"Test Project 2\", code=\"TP2\", repository=data[\"test_repo\"]\n    )\n    data[\"test_project2\"].status = data[\"status_wip\"]\n    DBSession.add(data[\"test_project2\"])\n\n    # an inactive project\n    data[\"test_project3\"] = Project(\n        name=\"Test Project 3\", code=\"TP3\", repository=data[\"test_repo\"]\n    )\n    data[\"test_project3\"].status = data[\"status_rts\"]\n    DBSession.save(data[\"test_project3\"])\n\n    # create assets and shots\n    data[\"test_asset_type\"] = Type(\n        name=\"Character\", code=\"Char\", target_entity_type=\"Asset\"\n    )\n    DBSession.add(data[\"test_asset_type\"])\n\n    data[\"test_asset1\"] = Asset(\n        name=\"Test Asset 1\",\n        code=\"TA1\",\n        project=data[\"test_project1\"],\n        type=data[\"test_asset_type\"],\n    )\n    DBSession.add(data[\"test_asset1\"])\n\n    data[\"test_asset2\"] = Asset(\n        name=\"Test Asset 2\",\n        code=\"TA2\",\n        project=data[\"test_project2\"],\n        type=data[\"test_asset_type\"],\n    )\n    DBSession.add(data[\"test_asset2\"])\n\n    # shots\n    # for project 1\n    data[\"test_shot1\"] = Shot(\n        code=\"shot1\",\n        project=data[\"test_project1\"],\n    )\n    DBSession.add(data[\"test_shot1\"])\n\n    data[\"test_shot2\"] = Shot(\n        code=\"shot2\",\n        project=data[\"test_project1\"],\n    )\n    DBSession.add(data[\"test_shot2\"])\n\n    # for project 2\n    data[\"test_shot3\"] = Shot(\n        code=\"shot3\",\n        project=data[\"test_project2\"],\n    )\n    DBSession.add(data[\"test_shot3\"])\n\n    data[\"test_shot4\"] = Shot(\n        code=\"shot4\",\n        project=data[\"test_project2\"],\n    )\n    DBSession.add(data[\"test_shot4\"])\n\n    # for project 3\n    data[\"test_shot5\"] = Shot(\n        code=\"shot5\",\n        project=data[\"test_project3\"],\n    )\n    DBSession.add(data[\"test_shot5\"])\n\n    #########################################################\n    # tasks for projects\n    data[\"test_task1\"] = Task(\n        name=\"Project Planing\",\n        project=data[\"test_project1\"],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task1\"])\n\n    data[\"test_task2\"] = Task(\n        name=\"Project Planing\",\n        project=data[\"test_project2\"],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task2\"])\n\n    data[\"test_task3\"] = Task(\n        name=\"Project Planing\",\n        project=data[\"test_project3\"],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        schedule_timing=5,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task3\"])\n\n    # for shots\n\n    # Shot 1\n    data[\"test_task4\"] = Task(\n        name=\"Match Move\",\n        parent=data[\"test_shot1\"],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task4\"])\n\n    data[\"test_task5\"] = Task(\n        name=\"FX\",\n        parent=data[\"test_shot1\"],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task4\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task5\"])\n\n    data[\"test_task6\"] = Task(\n        name=\"Lighting\",\n        parent=data[\"test_shot1\"],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task4\"], data[\"test_task5\"]],\n        schedule_timing=3,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task6\"])\n\n    data[\"test_task7\"] = Task(\n        name=\"Comp\",\n        parent=data[\"test_shot1\"],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task6\"]],\n        schedule_timing=3,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task7\"])\n\n    # Shot 2\n    data[\"test_task8\"] = Task(\n        name=\"Match Move\",\n        parent=data[\"test_shot2\"],\n        resources=[data[\"test_user3\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task8\"])\n\n    data[\"test_task9\"] = Task(\n        name=\"FX\",\n        parent=data[\"test_shot2\"],\n        resources=[data[\"test_user3\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        depends_on=[data[\"test_task8\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task9\"])\n\n    data[\"test_task10\"] = Task(\n        name=\"Lighting\",\n        parent=data[\"test_shot2\"],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task8\"], data[\"test_task9\"]],\n        schedule_timing=3,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task10\"])\n\n    data[\"test_task11\"] = Task(\n        name=\"Comp\",\n        parent=data[\"test_shot2\"],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task10\"]],\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task11\"])\n\n    # Shot 3\n    data[\"test_task12\"] = Task(\n        name=\"Match Move\",\n        parent=data[\"test_shot3\"],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task12\"])\n\n    data[\"test_task13\"] = Task(\n        name=\"FX\",\n        parent=data[\"test_shot3\"],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task12\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task13\"])\n\n    data[\"test_task14\"] = Task(\n        name=\"Lighting\",\n        parent=data[\"test_shot3\"],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task12\"], data[\"test_task13\"]],\n        schedule_timing=3,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task14\"])\n\n    data[\"test_task15\"] = Task(\n        name=\"Comp\",\n        parent=data[\"test_shot3\"],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task14\"]],\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task15\"])\n\n    # Shot 4\n    data[\"test_task16\"] = Task(\n        name=\"Match Move\",\n        parent=data[\"test_shot4\"],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task16\"])\n\n    data[\"test_task17\"] = Task(\n        name=\"FX\",\n        parent=data[\"test_shot4\"],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task16\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task17\"])\n\n    data[\"test_task18\"] = Task(\n        name=\"Lighting\",\n        parent=data[\"test_shot4\"],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task16\"], data[\"test_task17\"]],\n        schedule_timing=3,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task18\"])\n\n    data[\"test_task19\"] = Task(\n        name=\"Comp\",\n        parent=data[\"test_shot4\"],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        depends_on=[data[\"test_task18\"]],\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task19\"])\n\n    # Shot 5\n    data[\"test_task20\"] = Task(\n        name=\"Match Move\",\n        parent=data[\"test_shot5\"],\n        resources=[data[\"test_user3\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task20\"])\n\n    data[\"test_task21\"] = Task(\n        name=\"FX\",\n        parent=data[\"test_shot5\"],\n        resources=[data[\"test_user3\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        depends_on=[data[\"test_task20\"]],\n        schedule_timing=2,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task21\"])\n\n    data[\"test_task22\"] = Task(\n        name=\"Lighting\",\n        parent=data[\"test_shot5\"],\n        resources=[data[\"test_user3\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        depends_on=[data[\"test_task20\"], data[\"test_task21\"]],\n        schedule_timing=3,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task22\"])\n\n    data[\"test_task23\"] = Task(\n        name=\"Comp\",\n        parent=data[\"test_shot5\"],\n        resources=[data[\"test_user3\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        depends_on=[data[\"test_task22\"]],\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task23\"])\n\n    ####################################################\n    # For Assets\n\n    # Asset 1\n    data[\"test_task24\"] = Task(\n        name=\"Design\",\n        parent=data[\"test_asset1\"],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task24\"])\n\n    data[\"test_task25\"] = Task(\n        name=\"Model\",\n        parent=data[\"test_asset1\"],\n        depends_on=[data[\"test_task24\"]],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        schedule_timing=15,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task25\"])\n\n    data[\"test_task26\"] = Task(\n        name=\"LookDev\",\n        parent=data[\"test_asset1\"],\n        depends_on=[data[\"test_task25\"]],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task26\"])\n\n    data[\"test_task27\"] = Task(\n        name=\"Rig\",\n        parent=data[\"test_asset1\"],\n        depends_on=[data[\"test_task25\"]],\n        resources=[data[\"test_user1\"]],\n        alternative_resources=[data[\"test_user2\"], data[\"test_user3\"]],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task27\"])\n\n    # Asset 2\n    data[\"test_task28\"] = Task(\n        name=\"Design\",\n        parent=data[\"test_asset2\"],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task28\"])\n\n    data[\"test_task29\"] = Task(\n        name=\"Model\",\n        parent=data[\"test_asset2\"],\n        depends_on=[data[\"test_task28\"]],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        schedule_timing=15,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task29\"])\n\n    data[\"test_task30\"] = Task(\n        name=\"LookDev\",\n        parent=data[\"test_asset2\"],\n        depends_on=[data[\"test_task29\"]],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task30\"])\n\n    data[\"test_task31\"] = Task(\n        name=\"Rig\",\n        parent=data[\"test_asset2\"],\n        depends_on=[data[\"test_task29\"]],\n        resources=[data[\"test_user2\"]],\n        alternative_resources=[data[\"test_user1\"], data[\"test_user3\"]],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n    )\n    DBSession.add(data[\"test_task31\"])\n\n    # TODO: Add Milestones\n    data[\"kwargs\"] = dict(\n        name=\"Studio\",\n        daily_working_hours=8,\n        timing_resolution=datetime.timedelta(hours=1),\n    )\n\n    data[\"test_studio\"] = Studio(**data[\"kwargs\"])\n    DBSession.add(data[\"test_studio\"])\n    DBSession.commit()\n    return data\n\n\ndef test_working_hours_arg_is_skipped(setup_studio_db_tests):\n    \"\"\"default working hours is used if the working_hours arg is skipped.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"name\"] = \"New Studio\"\n    try:\n        data[\"kwargs\"].pop(\"working_hours\")  # pop if there are any\n    except KeyError:\n        pass\n\n    new_studio = Studio(**data[\"kwargs\"])\n    assert new_studio.working_hours == WorkingHours()\n\n\ndef test_working_hours_arg_is_none(setup_studio_db_tests):\n    \"\"\"WorkingHour with default settings is used if working_hours arg is skipped.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"name\"] = \"New Studio\"\n    data[\"kwargs\"][\"working_hours\"] = None\n    new_studio = Studio(**data[\"kwargs\"])\n    assert new_studio.working_hours == WorkingHours()\n\n\ndef test_working_hours_attribute_is_none(setup_studio_db_tests):\n    \"\"\"WorkingHour with default values is used if working_hours attr is set to None.\"\"\"\n    data = setup_studio_db_tests\n    data[\"test_studio\"].working_horus = None\n    assert data[\"test_studio\"].working_hours == WorkingHours()\n\n\ndef test_working_hours_arg_is_not_a_working_hours_instance(setup_studio_db_tests):\n    \"\"\"TypeError is raised if the working_hours arg is not a WorkingHours instance.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"working_hours\"] = \"not a working hours instance\"\n    data[\"kwargs\"][\"name\"] = \"New Studio\"\n    with pytest.raises(TypeError) as cm:\n        Studio(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Studio.working_hours should be a stalker.models.studio.WorkingHours instance, \"\n        \"not str: 'not a working hours instance'\"\n    )\n\n\ndef test_working_hours_attribute_is_not_a_working_hours_instance(setup_studio_db_tests):\n    \"\"\"TypeError is raised if working_hours attr is not a WorkingHours instance.\"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_studio\"].working_hours = \"not a working hours instance\"\n\n    assert str(cm.value) == (\n        \"Studio.working_hours should be a stalker.models.studio.WorkingHours instance, \"\n        \"not str: 'not a working hours instance'\"\n    )\n\n\ndef test_working_hours_arg_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"working_hours arg is passed to the working_hours attr without any problem.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"name\"] = \"New Studio\"\n    wh = WorkingHours(working_hours={\"mon\": [[60, 900]]})\n\n    data[\"kwargs\"][\"working_hours\"] = wh\n    new_studio = Studio(**data[\"kwargs\"])\n    assert new_studio.working_hours == wh\n\n\ndef test_working_hours_attribute_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"working_hours attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    new_working_hours = WorkingHours(\n        working_hours={\n            \"mon\": [[60, 1200]]  # they were doing all the jobs in\n            # Monday :))\n        }\n    )\n    assert data[\"test_studio\"].working_hours != new_working_hours\n    data[\"test_studio\"].working_hours = new_working_hours\n    assert data[\"test_studio\"].working_hours == new_working_hours\n\n\ndef test_tjp_id_attribute_returns_a_plausible_id(setup_studio_db_tests):\n    \"\"\"tjp_id is returning something meaningful.\"\"\"\n    data = setup_studio_db_tests\n    data[\"test_studio\"].id = 432\n    assert data[\"test_studio\"].tjp_id == \"Studio_432\"\n\n\ndef test_projects_attribute_is_read_only(setup_studio_db_tests):\n    \"\"\"project attribute is a read only attribute.\"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_studio\"].projects = [data[\"test_project1\"]]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'projects'\",\n    }.get(\n        sys.version_info.minor, \"property 'projects' of 'Studio' object has no setter\"\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_projects_attribute_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"projects attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    assert sorted(data[\"test_studio\"].projects, key=lambda x: x.name) == sorted(\n        [data[\"test_project1\"], data[\"test_project2\"], data[\"test_project3\"]],\n        key=lambda x: x.name,\n    )\n\n\ndef test_active_projects_attribute_is_read_only(setup_studio_db_tests):\n    \"\"\"active_projects attribute is a read only attribute.\"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_studio\"].active_projects = [data[\"test_project1\"]]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'active_projects'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'active_projects' of 'Studio' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_active_projects_attribute_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"active_projects attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    assert sorted(data[\"test_studio\"].active_projects, key=lambda x: x.name) == sorted(\n        [data[\"test_project1\"], data[\"test_project2\"]], key=lambda x: x.name\n    )\n\n\ndef test_inactive_projects_attribute_is_read_only(setup_studio_db_tests):\n    \"\"\"inactive_projects attribute is a read only attribute.\"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_studio\"].inactive_projects = [data[\"test_project1\"]]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'inactive_projects'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'inactive_projects' of 'Studio' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_inactive_projects_attribute_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"inactive_projects attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    assert sorted(\n        data[\"test_studio\"].inactive_projects, key=lambda x: x.name\n    ) == sorted([data[\"test_project3\"]], key=lambda x: x.name)\n\n\ndef test_departments_attribute_is_read_only(setup_studio_db_tests):\n    \"\"\"departments attribute is a read only attribute.\"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_studio\"].departments = [data[\"test_project1\"]]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'departments'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'departments' of 'Studio' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_departments_attribute_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"departments attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    admins_dep = Department.query.filter(Department.name == \"admins\").first()\n    assert admins_dep is not None\n    assert sorted(data[\"test_studio\"].departments, key=lambda x: x.name) == sorted(\n        [data[\"test_department1\"], data[\"test_department2\"], admins_dep],\n        key=lambda x: x.name,\n    )\n\n\ndef test_users_attribute_is_read_only(setup_studio_db_tests):\n    \"\"\"users attribute is a read only attribute.\"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_studio\"].users = [data[\"test_project1\"]]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'users'\",\n    }.get(sys.version_info.minor, \"property 'users' of 'Studio' object has no setter\")\n\n    assert str(cm.value) == error_message\n\n\ndef test_users_attribute_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"users attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    # don't forget the admin\n    admin = User.query.filter_by(name=\"admin\").first()\n    assert admin is not None\n    assert sorted(data[\"test_studio\"].users, key=lambda x: x.name) == sorted(\n        [admin, data[\"test_user1\"], data[\"test_user2\"], data[\"test_user3\"]],\n        key=lambda x: x.name,\n    )\n\n\ndef test_to_tjp_attribute_is_read_only(setup_studio_db_tests):\n    \"\"\"to_tjp attribute is a read only attribute.\"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_studio\"].to_tjp = \"some text\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'to_tjp'\",\n    }.get(sys.version_info.minor, \"property 'to_tjp' of 'Studio' object has no setter\")\n\n    assert str(cm.value) == error_message\n\n\ndef test_now_arg_is_skipped(setup_studio_db_tests):\n    \"\"\"now attr uses rounded datetime.now(pytz.utc) value if the now arg is skipped.\"\"\"\n    data = setup_studio_db_tests\n    try:\n        data[\"kwargs\"].pop(\"now\")\n    except KeyError:\n        pass\n\n    new_studio = Studio(**data[\"kwargs\"])\n    assert new_studio.now == new_studio.round_time(datetime.datetime.now(pytz.utc))\n\n\ndef test_now_arg_is_None(setup_studio_db_tests):\n    \"\"\"now attr uses rounded datetime.now(pytz.utc) value if the now arg is None.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"now\"] = None\n    new_studio = Studio(**data[\"kwargs\"])\n    assert new_studio.now == new_studio.round_time(datetime.datetime.now(pytz.utc))\n\n\ndef test_now_attribute_is_none(setup_studio_db_tests):\n    \"\"\"now attr equals rounded value of datetime.now(pytz.utc) if it is set to None.\"\"\"\n    data = setup_studio_db_tests\n    data[\"test_studio\"].now = None\n    assert data[\"test_studio\"].now == data[\"test_studio\"].round_time(\n        datetime.datetime.now(pytz.utc)\n    )\n\n\ndef test_now_arg_is_not_a_datetime_instance(setup_studio_db_tests):\n    \"\"\"TypeError is raised if the now arg is not a datetime.datetime instance.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"now\"] = \"not a datetime instance\"\n    with pytest.raises(TypeError) as cm:\n        Studio(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Studio.now attribute should be an instance of datetime.datetime, \"\n        \"not str: 'not a datetime instance'\"\n    )\n\n\ndef test_now_attribute_is_set_to_a_value_other_than_datetime_instance(\n    setup_studio_db_tests,\n):\n    \"\"\"TypeError is raised if the now attribute is set\n    to a value other than a datetime.datetime instance\n    \"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_studio\"].now = \"not a datetime instance\"\n\n    assert (\n        str(cm.value) == \"Studio.now attribute should be an instance of \"\n        \"datetime.datetime, not str: 'not a datetime instance'\"\n    )\n\n\ndef test_now_arg_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"now arg value is passed to the now attribute.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"now\"] = datetime.datetime(2013, 4, 15, 21, 9, tzinfo=pytz.utc)\n    expected_now = datetime.datetime(2013, 4, 15, 21, 0, tzinfo=pytz.utc)\n    new_studio = Studio(**data[\"kwargs\"])\n    assert new_studio.now == expected_now\n\n\ndef test_now_attribute_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"now attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    data[\"test_studio\"].now = datetime.datetime(2013, 4, 15, 21, 11, tzinfo=pytz.utc)\n    expected_now = datetime.datetime(2013, 4, 15, 21, 0, tzinfo=pytz.utc)\n    assert data[\"test_studio\"].now == expected_now\n\n\ndef test_now_attribute_is_working_as_expected_case2(setup_studio_db_tests):\n    \"\"\"now attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    data[\"test_studio\"]._now = None\n    expected_now = Studio.round_time(datetime.datetime.now(pytz.utc))\n    assert data[\"test_studio\"].now == expected_now\n\n\ndef test_to_tjp_attribute_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"to_tjp attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    data[\"test_studio\"].start = datetime.datetime(2013, 4, 15, 17, 40, tzinfo=pytz.utc)\n    data[\"test_studio\"].end = datetime.datetime(2013, 6, 30, 17, 40, tzinfo=pytz.utc)\n    data[\"test_studio\"].working_hours[0] = [[540, 1080]]\n    data[\"test_studio\"].working_hours[1] = [[540, 1080]]\n    data[\"test_studio\"].working_hours[2] = [[540, 1080]]\n    data[\"test_studio\"].working_hours[3] = [[540, 1080]]\n    data[\"test_studio\"].working_hours[4] = [[540, 1080]]\n    data[\"test_studio\"].working_hours[5] = [[540, 720]]\n    data[\"test_studio\"].working_hours[6] = []\n\n    expected_tjp_template = Template(\n        \"\"\"project Studio_{{studio.id}} \"Studio_{{studio.id}}\" 2013-04-15 - 2013-06-30 {\n    timingresolution 60min\n    now {{ studio.now.strftime('%Y-%m-%d-%H:%M') }}\n    dailyworkinghours 8\n    weekstartsmonday\n    workinghours mon 09:00 - 18:00\n    workinghours tue 09:00 - 18:00\n    workinghours wed 09:00 - 18:00\n    workinghours thu 09:00 - 18:00\n    workinghours fri 09:00 - 18:00\n    workinghours sat 09:00 - 12:00\n    workinghours sun off\n    timeformat \"%Y-%m-%d\"\n    scenario plan \"Plan\"\n    trackingscenario plan\n}\n\"\"\"\n    )\n\n    expected_tjp = expected_tjp_template.render({\"studio\": data[\"test_studio\"]})\n    # print('-----------------------------------')\n    # print(expected_tjp)\n    # print('-----------------------------------')\n    # print(data[\"test_studio\"].to_tjp)\n    # print('-----------------------------------')\n    assert data[\"test_studio\"].to_tjp == expected_tjp\n\n\ndef test_scheduler_attribute_can_be_set_to_none(setup_studio_db_tests):\n    \"\"\"scheduler attribute can be set to None.\"\"\"\n    data = setup_studio_db_tests\n    data[\"test_studio\"].scheduler = None\n\n\ndef test_scheduler_attribute_accepts_scheduler_instances_only(setup_studio_db_tests):\n    \"\"\"TypeError raised if scheduler attr is set to a value which is not a Scheduler.\"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_studio\"].scheduler = \"not a Scheduler instance\"\n\n    assert (\n        str(cm.value) == \"Studio.scheduler should be an instance of \"\n        \"stalker.models.scheduler.SchedulerBase, not str: 'not a Scheduler instance'\"\n    )\n\n\ndef test_scheduler_attribute_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"scheduler attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    tj_s = TaskJugglerScheduler()\n    data[\"test_studio\"].scheduler = tj_s\n    assert data[\"test_studio\"].scheduler == tj_s\n\n\ndef test_schedule_will_not_work_without_a_scheduler(setup_studio_db_tests):\n    \"\"\"RuntimeError is raised if the scheduler\n    attribute is not set to a Scheduler instance and schedule is called\n    \"\"\"\n    data = setup_studio_db_tests\n    data[\"test_studio\"].scheduler = None\n    with pytest.raises(RuntimeError) as cm:\n        data[\"test_studio\"].schedule()\n\n    assert (\n        str(cm.value) == \"There is no scheduler for this Studio, please assign a \"\n        \"scheduler to the Studio.scheduler attribute, before calling \"\n        \"Studio.schedule()\"\n    )\n\n\ndef test_schedule_will_schedule_the_tasks_with_the_given_scheduler(\n    setup_studio_db_tests,\n):\n    \"\"\"schedule method will schedule the tasks with the given scheduler.\"\"\"\n    data = setup_studio_db_tests\n    tj_scheduler = TaskJugglerScheduler(compute_resources=True)\n    data[\"test_studio\"].now = datetime.datetime(2013, 4, 15, 22, 56, tzinfo=pytz.utc)\n    data[\"test_studio\"].start = datetime.datetime(2013, 4, 15, 22, 56, tzinfo=pytz.utc)\n    data[\"test_studio\"].end = datetime.datetime(2013, 7, 30, 0, 0, tzinfo=pytz.utc)\n\n    # just to be sure that it is not creating any issue on schedule\n    data[\"test_task25\"].task_depends_on[0].dependency_target = DependencyTarget.OnStart\n    data[\"test_task25\"].resources = [data[\"test_user2\"]]\n\n    data[\"test_studio\"].scheduler = tj_scheduler\n    data[\"test_studio\"].schedule()\n    DBSession.commit()\n\n    # now check the timings of the tasks are all adjusted\n    # Projects\n    # data[\"test_project\"]\n    assert data[\"test_project1\"].computed_start == datetime.datetime(\n        2013, 4, 16, 9, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_project1\"].computed_end == datetime.datetime(\n        2013, 6, 24, 16, 0, tzinfo=pytz.utc\n    )\n\n    # data[\"test_asset1\"]\n    assert data[\"test_asset1\"].computed_start == datetime.datetime(\n        2013, 4, 16, 9, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_asset1\"].computed_end == datetime.datetime(\n        2013, 5, 17, 18, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_asset1\"].computed_resources == []\n\n    # data[\"test_task24\"]\n    assert data[\"test_task24\"].computed_start == datetime.datetime(\n        2013, 4, 16, 9, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task24\"].computed_end == datetime.datetime(\n        2013, 4, 26, 17, 0, tzinfo=pytz.utc\n    )\n\n    possible_resources = [data[\"test_user1\"], data[\"test_user2\"], data[\"test_user3\"]]\n    assert len(data[\"test_task24\"].computed_resources) == 1\n    assert data[\"test_task24\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task25\"]\n    assert data[\"test_task25\"].computed_start == datetime.datetime(\n        2013, 4, 16, 9, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task25\"].computed_end == datetime.datetime(\n        2013, 5, 3, 12, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task25\"].computed_resources) == 1\n    assert data[\"test_task25\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task26\"]\n    assert data[\"test_task26\"].computed_start == datetime.datetime(\n        2013, 5, 6, 11, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task26\"].computed_end == datetime.datetime(\n        2013, 5, 17, 10, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task26\"].computed_resources) == 1\n    assert data[\"test_task26\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task27\"]\n    assert data[\"test_task27\"].computed_start == datetime.datetime(\n        2013, 5, 7, 10, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task27\"].computed_end == datetime.datetime(\n        2013, 5, 17, 18, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task27\"].computed_resources) == 1\n    assert data[\"test_task27\"].computed_resources[0] in possible_resources\n\n    # data[\"test_shot2\"]\n    assert data[\"test_shot2\"].computed_start == datetime.datetime(\n        2013, 4, 26, 17, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_shot2\"].computed_end == datetime.datetime(\n        2013, 6, 20, 10, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_shot2\"].computed_resources == []\n\n    # data[\"test_task8\"]\n    assert data[\"test_task8\"].computed_start == datetime.datetime(\n        2013, 4, 26, 17, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task8\"].computed_end == datetime.datetime(\n        2013, 4, 30, 15, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task8\"].computed_resources) == 1\n    assert data[\"test_task8\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task9\"]\n    assert data[\"test_task9\"].computed_start == datetime.datetime(\n        2013, 5, 30, 17, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task9\"].computed_end == datetime.datetime(\n        2013, 6, 3, 15, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task9\"].computed_resources) == 1\n    assert data[\"test_task9\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task10\"]\n    assert data[\"test_task10\"].computed_start == datetime.datetime(\n        2013, 6, 5, 13, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task10\"].computed_end == datetime.datetime(\n        2013, 6, 10, 10, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task10\"].computed_resources) == 1\n    assert data[\"test_task10\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task11\"]\n    assert data[\"test_task11\"].computed_start == datetime.datetime(\n        2013, 6, 14, 14, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task11\"].computed_end == datetime.datetime(\n        2013, 6, 20, 10, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task11\"].computed_resources) == 1\n    assert data[\"test_task11\"].computed_resources[0] in possible_resources\n\n    # data[\"test_shot1\"]\n    assert data[\"test_shot1\"].computed_start == datetime.datetime(\n        2013, 5, 16, 11, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_shot1\"].computed_end == datetime.datetime(\n        2013, 6, 24, 16, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_shot1\"].computed_resources == []\n\n    # data[\"test_task4\"]\n    assert data[\"test_task4\"].computed_start == datetime.datetime(\n        2013, 5, 16, 11, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task4\"].computed_end == datetime.datetime(\n        2013, 5, 17, 18, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task4\"].computed_resources) == 1\n    assert data[\"test_task4\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task5\"]\n    assert data[\"test_task5\"].computed_start == datetime.datetime(\n        2013, 6, 5, 13, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task5\"].computed_end == datetime.datetime(\n        2013, 6, 7, 11, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task5\"].computed_resources) == 1\n    assert data[\"test_task5\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task6\"]\n    assert data[\"test_task6\"].computed_start == datetime.datetime(\n        2013, 6, 11, 17, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task6\"].computed_end == datetime.datetime(\n        2013, 6, 14, 14, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task6\"].computed_resources) == 1\n    assert data[\"test_task6\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task7\"]\n    assert data[\"test_task7\"].computed_start == datetime.datetime(\n        2013, 6, 20, 10, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task7\"].computed_end == datetime.datetime(\n        2013, 6, 24, 16, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task7\"].computed_resources) == 1\n    assert data[\"test_task7\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task1\"]\n    assert data[\"test_task1\"].computed_start == datetime.datetime(\n        2013, 5, 17, 10, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task1\"].computed_end == datetime.datetime(\n        2013, 5, 29, 18, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task1\"].computed_resources) == 1\n    assert data[\"test_task1\"].computed_resources[0] in possible_resources\n\n    # data[\"test_project2\"]\n    # assert data[\"test_project2\"].computed_start == \\\n    #     datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n    #\n    # assert data[\"test_project2\"].computed_end == \\\n    #     datetime.datetime(2013, 6, 18, 12, 0, tzinfo=pytz.utc)\n    #\n    # assert data[\"test_project2\"].computed_resources == []\n\n    # data[\"test_asset2\"]\n    assert data[\"test_asset2\"].computed_start == datetime.datetime(\n        2013, 4, 16, 9, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_asset2\"].computed_end == datetime.datetime(\n        2013, 5, 30, 17, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_asset2\"].computed_resources == []\n\n    # data[\"test_task28\"]\n    assert data[\"test_task28\"].computed_start == datetime.datetime(\n        2013, 4, 16, 9, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task28\"].computed_end == datetime.datetime(\n        2013, 4, 26, 17, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task28\"].computed_resources) == 1\n    assert data[\"test_task28\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task29\"]\n    assert data[\"test_task29\"].computed_start == datetime.datetime(\n        2013, 4, 26, 17, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task29\"].computed_end == datetime.datetime(\n        2013, 5, 16, 11, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task29\"].computed_resources) == 1\n    assert data[\"test_task29\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task30\"]\n    assert data[\"test_task30\"].computed_start == datetime.datetime(\n        2013, 5, 20, 9, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task30\"].computed_end == datetime.datetime(\n        2013, 5, 30, 17, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task30\"].computed_resources) == 1\n    assert data[\"test_task30\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task31\"]\n    assert data[\"test_task31\"].computed_start == datetime.datetime(\n        2013, 5, 20, 9, 0, tzinfo=pytz.utc\n    )\n\n    assert data[\"test_task31\"].computed_end == datetime.datetime(\n        2013, 5, 30, 17, 0, tzinfo=pytz.utc\n    )\n\n    assert len(data[\"test_task31\"].computed_resources) == 1\n    assert data[\"test_task31\"].computed_resources[0] in possible_resources\n\n    # data[\"test_shot3\"]\n    assert data[\"test_shot3\"].computed_start == datetime.datetime(\n        2013, 4, 30, 15, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_shot3\"].computed_end == datetime.datetime(\n        2013, 6, 20, 10, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_shot3\"].computed_resources == []\n\n    # data[\"test_task12\"]\n    assert data[\"test_task12\"].computed_start == datetime.datetime(\n        2013, 4, 30, 15, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_task12\"].computed_end == datetime.datetime(\n        2013, 5, 2, 13, 0, tzinfo=pytz.utc\n    )\n    assert len(data[\"test_task12\"].computed_resources) == 1\n    assert data[\"test_task12\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task13\"]\n    assert data[\"test_task13\"].computed_start == datetime.datetime(\n        2013, 5, 30, 17, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_task13\"].computed_end == datetime.datetime(\n        2013, 6, 3, 15, 0, tzinfo=pytz.utc\n    )\n    assert len(data[\"test_task13\"].computed_resources) == 1\n    assert data[\"test_task13\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task14\"]\n    assert data[\"test_task14\"].computed_start == datetime.datetime(\n        2013, 6, 7, 11, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_task14\"].computed_end == datetime.datetime(\n        2013, 6, 11, 17, 0, tzinfo=pytz.utc\n    )\n    assert len(data[\"test_task14\"].computed_resources) == 1\n    assert data[\"test_task14\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task15\"]\n    assert data[\"test_task15\"].computed_start == datetime.datetime(\n        2013, 6, 14, 14, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_task15\"].computed_end == datetime.datetime(\n        2013, 6, 20, 10, 0, tzinfo=pytz.utc\n    )\n    assert len(data[\"test_task15\"].computed_resources) == 1\n    assert data[\"test_task15\"].computed_resources[0] in possible_resources\n\n    # data[\"test_shot4\"]\n    assert data[\"test_shot4\"].computed_start == datetime.datetime(\n        2013, 5, 2, 13, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_shot4\"].computed_end == datetime.datetime(\n        2013, 6, 24, 16, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_shot4\"].computed_resources == []\n\n    # data[\"test_task16\"]\n    assert data[\"test_task16\"].computed_start == datetime.datetime(\n        2013, 5, 2, 13, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_task16\"].computed_end == datetime.datetime(\n        2013, 5, 6, 11, 0, tzinfo=pytz.utc\n    )\n    assert len(data[\"test_task16\"].computed_resources) == 1\n    assert data[\"test_task16\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task17\"]\n    assert data[\"test_task17\"].computed_start == datetime.datetime(\n        2013, 6, 3, 15, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_task17\"].computed_end == datetime.datetime(\n        2013, 6, 5, 13, 0, tzinfo=pytz.utc\n    )\n    assert len(data[\"test_task17\"].computed_resources) == 1\n    assert data[\"test_task17\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task18\"]\n    assert data[\"test_task18\"].computed_start == datetime.datetime(\n        2013, 6, 10, 10, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_task18\"].computed_end == datetime.datetime(\n        2013, 6, 12, 16, 0, tzinfo=pytz.utc\n    )\n    assert len(data[\"test_task18\"].computed_resources) == 1\n    assert data[\"test_task18\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task19\"]\n    assert data[\"test_task19\"].computed_start == datetime.datetime(\n        2013, 6, 19, 11, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_task19\"].computed_end == datetime.datetime(\n        2013, 6, 24, 16, 0, tzinfo=pytz.utc\n    )\n    assert len(data[\"test_task19\"].computed_resources) == 1\n    assert data[\"test_task19\"].computed_resources[0] in possible_resources\n\n    # data[\"test_task2\"]\n    assert data[\"test_task2\"].computed_start == datetime.datetime(\n        2013, 5, 30, 9, 0, tzinfo=pytz.utc\n    )\n    assert data[\"test_task2\"].computed_end == datetime.datetime(\n        2013, 6, 11, 17, 0, tzinfo=pytz.utc\n    )\n    assert len(data[\"test_task2\"].computed_resources) == 1\n    assert data[\"test_task2\"].computed_resources[0] in possible_resources\n\n\ndef test_schedule_schedules_only_tasks_of_the_given_projects_with_the_given_scheduler(\n    setup_studio_db_tests,\n):\n    \"\"\"schedule method schedules the tasks of the projects with the Scheduler.\"\"\"\n    data = setup_studio_db_tests\n    # create a dummy Project to schedule\n    dummy_project = Project(\n        name=\"Dummy Project\", code=\"DP\", repository=data[\"test_repo\"]\n    )\n\n    dt1 = Task(\n        name=\"Dummy Task 1\",\n        project=dummy_project,\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Hour,\n        resources=[data[\"test_user1\"]],\n    )\n\n    dt2 = Task(\n        name=\"Dummy Task 2\",\n        project=dummy_project,\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Hour,\n        resources=[data[\"test_user2\"]],\n    )\n    DBSession.add_all([dummy_project, dt1, dt2])\n    DBSession.commit()\n\n    tj_scheduler = TaskJugglerScheduler(\n        compute_resources=True, projects=[dummy_project]\n    )\n\n    data[\"test_studio\"].now = datetime.datetime(2013, 4, 15, 22, 56, tzinfo=pytz.utc)\n    data[\"test_studio\"].start = datetime.datetime(2013, 4, 15, 22, 56, tzinfo=pytz.utc)\n    data[\"test_studio\"].end = datetime.datetime(2013, 7, 30, 0, 0, tzinfo=pytz.utc)\n\n    data[\"test_studio\"].scheduler = tj_scheduler\n    data[\"test_studio\"].schedule()\n    DBSession.commit()\n\n    # now check the timings of the tasks are all adjusted\n    assert dt1.computed_start == datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n    assert dt1.computed_end == datetime.datetime(2013, 4, 16, 13, 0, tzinfo=pytz.utc)\n    assert dt2.computed_start == datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n    assert dt2.computed_end == datetime.datetime(2013, 4, 16, 13, 0, tzinfo=pytz.utc)\n\n    # data[\"test_project\"]\n    assert data[\"test_project1\"].computed_start is None\n    assert data[\"test_project1\"].computed_end is None\n\n    # data[\"test_asset1\"]\n    assert data[\"test_asset1\"].computed_start is None\n    assert data[\"test_asset1\"].computed_end is None\n    assert data[\"test_asset1\"].computed_resources == data[\"test_asset1\"].resources\n\n    # data[\"test_task24\"]\n    assert data[\"test_task24\"].computed_start is None\n    assert data[\"test_task24\"].computed_end is None\n    assert data[\"test_task24\"].computed_resources == data[\"test_task24\"].resources\n\n    # data[\"test_task25\"]\n    assert data[\"test_task25\"].computed_start is None\n    assert data[\"test_task25\"].computed_end is None\n    assert data[\"test_task25\"].computed_resources == data[\"test_task25\"].resources\n\n    # data[\"test_task26\"]\n    assert data[\"test_task26\"].computed_start is None\n    assert data[\"test_task26\"].computed_end is None\n    assert data[\"test_task26\"].computed_resources == data[\"test_task26\"].resources\n\n    # data[\"test_task27\"]\n    assert data[\"test_task27\"].computed_start is None\n    assert data[\"test_task27\"].computed_end is None\n    assert data[\"test_task27\"].computed_resources == data[\"test_task27\"].resources\n\n    # data[\"test_shot2\"]\n    assert data[\"test_shot2\"].computed_start is None\n    assert data[\"test_shot2\"].computed_end is None\n    assert data[\"test_shot2\"].computed_resources == data[\"test_shot2\"].resources\n\n    # data[\"test_task8\"]\n    assert data[\"test_task8\"].computed_start is None\n    assert data[\"test_task8\"].computed_end is None\n    assert data[\"test_task8\"].computed_resources == data[\"test_task8\"].resources\n\n    # data[\"test_task9\"]\n    assert data[\"test_task9\"].computed_start is None\n    assert data[\"test_task9\"].computed_end is None\n    assert data[\"test_task9\"].computed_resources == data[\"test_task9\"].resources\n\n    # data[\"test_task10\"]\n    assert data[\"test_task10\"].computed_start is None\n    assert data[\"test_task10\"].computed_end is None\n    assert data[\"test_task10\"].computed_resources == data[\"test_task10\"].resources\n\n    # data[\"test_task11\"]\n    assert data[\"test_task11\"].computed_start is None\n    assert data[\"test_task11\"].computed_end is None\n    assert data[\"test_task11\"].computed_resources == data[\"test_task11\"].resources\n\n    # data[\"test_shot1\"]\n    assert data[\"test_shot1\"].computed_start is None\n    assert data[\"test_shot1\"].computed_end is None\n    assert data[\"test_shot1\"].computed_resources == data[\"test_shot1\"].resources\n\n    # data[\"test_task4\"]\n    assert data[\"test_task4\"].computed_start is None\n    assert data[\"test_task4\"].computed_end is None\n    assert data[\"test_task4\"].computed_resources == data[\"test_task4\"].resources\n\n    # data[\"test_task5\"]\n    assert data[\"test_task5\"].computed_start is None\n    assert data[\"test_task5\"].computed_end is None\n    assert data[\"test_task5\"].computed_resources == data[\"test_task5\"].resources\n\n    # data[\"test_task6\"]\n    assert data[\"test_task6\"].computed_start is None\n    assert data[\"test_task6\"].computed_end is None\n    assert data[\"test_task6\"].computed_resources == data[\"test_task6\"].resources\n\n    # data[\"test_task7\"]\n    assert data[\"test_task7\"].computed_start is None\n    assert data[\"test_task7\"].computed_end is None\n    assert data[\"test_task7\"].computed_resources == data[\"test_task7\"].resources\n\n    # data[\"test_task1\"]\n    assert data[\"test_task1\"].computed_start is None\n    assert data[\"test_task1\"].computed_end is None\n    assert data[\"test_task1\"].computed_resources == data[\"test_task1\"].resources\n\n    # data[\"test_asset2\"]\n    assert data[\"test_asset2\"].computed_start is None\n    assert data[\"test_asset2\"].computed_end is None\n    assert data[\"test_asset2\"].computed_resources == data[\"test_asset2\"].resources\n\n    # data[\"test_task28\"]\n    assert data[\"test_task28\"].computed_start is None\n    assert data[\"test_task28\"].computed_end is None\n    assert data[\"test_task28\"].computed_resources == data[\"test_task28\"].resources\n\n    # data[\"test_task29\"]\n    assert data[\"test_task29\"].computed_start is None\n    assert data[\"test_task29\"].computed_end is None\n    assert data[\"test_task29\"].computed_resources == data[\"test_task29\"].resources\n\n    # data[\"test_task30\"]\n    assert data[\"test_task30\"].computed_start is None\n    assert data[\"test_task30\"].computed_end is None\n    assert data[\"test_task30\"].computed_resources == data[\"test_task30\"].resources\n\n    # data[\"test_task31\"]\n    assert data[\"test_task31\"].computed_start is None\n    assert data[\"test_task31\"].computed_end is None\n    assert data[\"test_task31\"].computed_resources == data[\"test_task31\"].resources\n\n    # data[\"test_shot3\"]\n    assert data[\"test_shot3\"].computed_start is None\n    assert data[\"test_shot3\"].computed_end is None\n    assert data[\"test_shot3\"].computed_resources == data[\"test_shot3\"].resources\n\n    # data[\"test_task12\"]\n    assert data[\"test_task12\"].computed_start is None\n    assert data[\"test_task12\"].computed_end is None\n    assert data[\"test_task12\"].computed_resources == data[\"test_task12\"].resources\n\n    # data[\"test_task13\"]\n    assert data[\"test_task13\"].computed_start is None\n    assert data[\"test_task13\"].computed_end is None\n    assert data[\"test_task13\"].computed_resources == data[\"test_task13\"].resources\n\n    # data[\"test_task14\"]\n    assert data[\"test_task14\"].computed_start is None\n    assert data[\"test_task14\"].computed_end is None\n    assert data[\"test_task14\"].computed_resources == data[\"test_task14\"].resources\n\n    # data[\"test_task15\"]\n    assert data[\"test_task15\"].computed_start is None\n    assert data[\"test_task15\"].computed_end is None\n    assert data[\"test_task15\"].computed_resources == data[\"test_task15\"].resources\n\n    # data[\"test_shot4\"]\n    assert data[\"test_shot4\"].computed_start is None\n    assert data[\"test_shot4\"].computed_end is None\n    assert data[\"test_shot4\"].computed_resources == data[\"test_shot4\"].resources\n\n    # data[\"test_task16\"]\n    assert data[\"test_task16\"].computed_start is None\n    assert data[\"test_task16\"].computed_end is None\n    assert data[\"test_task16\"].computed_resources == data[\"test_task16\"].resources\n\n    # data[\"test_task17\"]\n    assert data[\"test_task17\"].computed_start is None\n    assert data[\"test_task17\"].computed_end is None\n    assert data[\"test_task17\"].computed_resources == data[\"test_task17\"].resources\n\n    # data[\"test_task18\"]\n    assert data[\"test_task18\"].computed_start is None\n    assert data[\"test_task18\"].computed_end is None\n    assert data[\"test_task18\"].computed_resources == data[\"test_task18\"].resources\n\n    # data[\"test_task19\"]\n    assert data[\"test_task19\"].computed_start is None\n    assert data[\"test_task19\"].computed_end is None\n    assert data[\"test_task19\"].computed_resources == data[\"test_task19\"].resources\n\n    # data[\"test_task2\"]\n    assert data[\"test_task2\"].computed_start is None\n    assert data[\"test_task2\"].computed_end is None\n    assert data[\"test_task2\"].computed_resources == data[\"test_task2\"].resources\n\n\ndef test_is_scheduling_will_be_false_after_scheduling_is_done(setup_studio_db_tests):\n    \"\"\"is_scheduling attribute is back to False if the scheduling is finished.\"\"\"\n    data = setup_studio_db_tests\n    # use a dummy scheduler\n    data[\"test_studio\"].now = datetime.datetime(2013, 4, 15, 22, 56, tzinfo=pytz.utc)\n    data[\"test_studio\"].start = datetime.datetime(2013, 4, 15, 22, 56, tzinfo=pytz.utc)\n    data[\"test_studio\"].end = datetime.datetime(2013, 7, 30, 0, 0, tzinfo=pytz.utc)\n\n    def callback():\n        assert data[\"test_studio\"].is_scheduling is True\n\n    dummy_scheduler = DummyScheduler(callback=callback)\n\n    data[\"test_studio\"].scheduler = dummy_scheduler\n    assert data[\"test_studio\"].is_scheduling is False\n\n    # with v0.2.6.9 it is now the users duty to set is_scheduling to True\n    data[\"test_studio\"].is_scheduling = True\n\n    data[\"test_studio\"].schedule()\n    assert data[\"test_studio\"].is_scheduling is False\n\n\ndef test_schedule_will_store_schedule_info_in_database(setup_studio_db_tests):\n    \"\"\"schedule method will store the schedule info in database.\"\"\"\n    data = setup_studio_db_tests\n    tj_scheduler = TaskJugglerScheduler()\n    data[\"test_studio\"].now = datetime.datetime(2013, 4, 15, 22, 56, tzinfo=pytz.utc)\n    data[\"test_studio\"].start = datetime.datetime(2013, 4, 15, 22, 56, tzinfo=pytz.utc)\n    data[\"test_studio\"].end = datetime.datetime(2013, 7, 30, 0, 0, tzinfo=pytz.utc)\n\n    data[\"test_studio\"].scheduler = tj_scheduler\n    data[\"test_studio\"].schedule(scheduled_by=data[\"test_user1\"])\n\n    assert data[\"test_studio\"].last_scheduled_by == data[\"test_user1\"]\n\n    last_schedule_message = data[\"test_studio\"].last_schedule_message\n    last_scheduled_at = data[\"test_studio\"].last_scheduled_at\n    last_scheduled_by = data[\"test_studio\"].last_scheduled_by\n\n    assert last_schedule_message is not None\n    assert last_scheduled_at is not None\n    assert last_scheduled_by is not None\n\n    DBSession.add(data[\"test_studio\"])\n    DBSession.commit()\n\n    # delete the studio instance and retrieve it back and check if it has\n    # the info\n    del data[\"test_studio\"]\n\n    studio = Studio.query.first()\n\n    assert studio.is_scheduling is False\n    assert datetime.datetime.now(\n        pytz.utc\n    ) - studio.scheduling_started_at < datetime.timedelta(minutes=1)\n    assert studio.last_schedule_message == last_schedule_message\n    assert studio.last_scheduled_at == last_scheduled_at\n    assert studio.last_scheduled_by == last_scheduled_by\n\n    assert studio.last_scheduled_by_id == data[\"test_user1\"].id\n    assert studio.last_scheduled_by == data[\"test_user1\"]\n\n\ndef test_vacation_attribute_is_read_only(setup_studio_db_tests):\n    \"\"\"vacation attribute is a read-only attribute.\"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_studio\"].vacations = \"some random value\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'vacations'\",\n    }.get(\n        sys.version_info.minor, \"property 'vacations' of 'Studio' object has no setter\"\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_vacation_attribute_returns_studio_vacation_instances(setup_studio_db_tests):\n    \"\"\"vacation attribute is returning the Vacation instances with no user set.\"\"\"\n    data = setup_studio_db_tests\n    vacation1 = Vacation(\n        start=datetime.datetime(2013, 8, 2, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 8, 10, tzinfo=pytz.utc),\n    )\n    vacation2 = Vacation(\n        start=datetime.datetime(2013, 8, 11, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 8, 20, tzinfo=pytz.utc),\n    )\n    vacation3 = Vacation(\n        user=data[\"test_user1\"],\n        start=datetime.datetime(2013, 8, 11, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 8, 20, tzinfo=pytz.utc),\n    )\n    DBSession.add_all([vacation1, vacation2, vacation3])\n    DBSession.commit()\n\n    assert sorted(data[\"test_studio\"].vacations, key=lambda x: x.name) == sorted(\n        [vacation1, vacation2], key=lambda x: x.name\n    )\n\n\ndef test_timing_resolution_arg_skipped(setup_studio_db_tests):\n    \"\"\"timing_resolution attr is set to default if timing_resolution arg is skipped.\"\"\"\n    data = setup_studio_db_tests\n    try:\n        data[\"kwargs\"].pop(\"timing_resolution\")\n    except KeyError:\n        pass\n\n    studio = Studio(**data[\"kwargs\"])\n    assert studio.timing_resolution == defaults.timing_resolution\n\n\ndef test_timing_resolution_arg_is_none(setup_studio_db_tests):\n    \"\"\"timing_resolution attr is set to default if timing_resolution arg is None.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"timing_resolution\"] = None\n    studio = Studio(**data[\"kwargs\"])\n    assert studio.timing_resolution == defaults.timing_resolution\n\n\ndef test_timing_resolution_attribute_is_set_to_none(setup_studio_db_tests):\n    \"\"\"timing_resolution attr is set to the default if it is set to None.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"timing_resolution\"] = datetime.timedelta(minutes=5)\n    studio = Studio(**data[\"kwargs\"])\n    # check start conditions\n    assert studio.timing_resolution == data[\"kwargs\"][\"timing_resolution\"]\n    studio.timing_resolution = None\n    assert studio.timing_resolution == defaults.timing_resolution\n\n\ndef test_timing_resolution_arg_is_not_a_timedelta_instance(setup_studio_db_tests):\n    \"\"\"TypeError is raised if timing_resolution arg is not datetime.timedelta.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"timing_resolution\"] = \"not a timedelta instance\"\n    with pytest.raises(TypeError) as cm:\n        Studio(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Studio.timing_resolution should be an instance of datetime.timedelta, \"\n        \"not str: 'not a timedelta instance'\"\n    )\n\n\ndef test_timing_resolution_attribute_is_not_a_timedelta_instance(setup_studio_db_tests):\n    \"\"\"TypeError raised if timing_resolution attr is not a datetime.timedelta.\"\"\"\n    data = setup_studio_db_tests\n    new_foo_obj = Studio(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_foo_obj.timing_resolution = \"not a timedelta instance\"\n\n    assert str(cm.value) == (\n        \"Studio.timing_resolution should be an instance of datetime.timedelta, \"\n        \"not str: 'not a timedelta instance'\"\n    )\n\n\ndef test_timing_resolution_arg_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"timing_resolution arg value is passed to timing_resolution attr correctly.\"\"\"\n    data = setup_studio_db_tests\n    data[\"kwargs\"][\"timing_resolution\"] = datetime.timedelta(minutes=5)\n    studio = Studio(**data[\"kwargs\"])\n    assert studio.timing_resolution == data[\"kwargs\"][\"timing_resolution\"]\n\n\ndef test_timing_resolution_attribute_is_working_as_expected(setup_studio_db_tests):\n    \"\"\"timing_resolution attribute is working as expected.\"\"\"\n    data = setup_studio_db_tests\n    studio = Studio(**data[\"kwargs\"])\n    res = studio\n    new_res = datetime.timedelta(hours=1, minutes=30)\n    assert res != new_res\n    studio.timing_resolution = new_res\n    assert studio.timing_resolution == new_res\n\n\ndef test_to_unit_is_not_implemented_yet(setup_studio_db_tests):\n    \"\"\"to_unit() is not implemented yet.\"\"\"\n    data = setup_studio_db_tests\n    with pytest.raises(NotImplementedError) as cm:\n        _ = data[\"test_studio\"].to_unit(1, \"h\", \"min\")\n\n    assert str(cm.value) == \"this is not implemented yet\"\n"
  },
  {
    "path": "tests/models/test_tag.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Tag class.\"\"\"\n\nfrom stalker import Tag, SimpleEntity\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Tag class.\"\"\"\n    assert Tag.__auto_name__ is False\n\n\ndef test_tag_init():\n    \"\"\"tag inits as expected.\"\"\"\n    # this should work without any error\n    tag = Tag(name=\"a test tag\", description=\"this is a test tag\")\n    assert isinstance(tag, Tag)\n\n\ndef test_equality():\n    \"\"\"equality of two Tags.\"\"\"\n    kwargs = dict(name=\"a test tag\", description=\"this is a test tag\")\n\n    simple_entity = SimpleEntity(**kwargs)\n\n    a_tag_object1 = Tag(**kwargs)\n    a_tag_object2 = Tag(**kwargs)\n\n    kwargs[\"name\"] = \"a new test Tag\"\n    kwargs[\"description\"] = \"this is a new test Tag\"\n\n    a_tag_object3 = Tag(**kwargs)\n\n    assert a_tag_object1 == a_tag_object2\n    assert not a_tag_object1 == a_tag_object3\n    assert not a_tag_object1 == simple_entity\n\n\ndef test_inequality():\n    \"\"\"inequality of two Tags.\"\"\"\n    kwargs = dict(name=\"a test tag\", description=\"this is a test tag\")\n\n    simple_entity = SimpleEntity(**kwargs)\n\n    a_tag_object1 = Tag(**kwargs)\n    a_tag_object2 = Tag(**kwargs)\n\n    kwargs[\"name\"] = \"a new test Tag\"\n    kwargs[\"description\"] = \"this is a new test Tag\"\n\n    a_tag_object3 = Tag(**kwargs)\n\n    assert not a_tag_object1 != a_tag_object2\n    assert a_tag_object1 != a_tag_object3\n    assert a_tag_object1 != simple_entity\n\n\ndef test_plural_class_name():\n    \"\"\"plural name of Tag class.\"\"\"\n    kwargs = dict(name=\"a test tag\", description=\"this is a test tag\")\n    test_tag = Tag(**kwargs)\n    assert test_tag.plural_class_name == \"Tags\"\n\n\ndef test__hash__is_working_as_expected():\n    \"\"\"__hash__ is working as expected.\"\"\"\n    kwargs = dict(name=\"a test tag\", description=\"this is a test tag\")\n    test_tag = Tag(**kwargs)\n    result = hash(test_tag)\n    assert isinstance(result, int)\n    assert result == test_tag.__hash__()\n"
  },
  {
    "path": "tests/models/test_task.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Task class.\"\"\"\n\nimport copy\nimport datetime\nimport os\nimport sys\nimport warnings\n\nimport pytest\n\nimport pytz\n\nimport stalker\nimport stalker.db.setup\nfrom stalker import (\n    Entity,\n    FilenameTemplate,\n    Good,\n    Project,\n    Repository,\n    Status,\n    StatusList,\n    Structure,\n    Studio,\n    Task,\n    Ticket,\n    TimeLog,\n    Type,\n    User,\n    defaults,\n)\nfrom stalker.db.session import DBSession\nfrom stalker.exceptions import CircularDependencyError\nfrom stalker.models.enum import (\n    DependencyTarget,\n    ScheduleConstraint,\n    ScheduleModel,\n    TimeUnit,\n)\nfrom stalker.models.mixins import (\n    DateRangeMixin,\n)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_task_tests():\n    \"\"\"tests that doesn't require a database.\"\"\"\n    data = dict()\n    defaults.config_values = defaults.default_config_values.copy()\n    defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n    assert defaults.daily_working_hours == 9\n    assert defaults.weekly_working_days == 5\n    assert defaults.yearly_working_days == 261\n\n    data[\"status_wfd\"] = Status(name=\"Waiting For Dependency\", code=\"WFD\")\n    data[\"status_rts\"] = Status(name=\"Ready To Start\", code=\"RTS\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_prev\"] = Status(name=\"Pending Review\", code=\"PREV\")\n    data[\"status_hrev\"] = Status(name=\"Has Revision\", code=\"HREV\")\n    data[\"status_drev\"] = Status(name=\"Dependency Has Revision\", code=\"DREV\")\n    data[\"status_oh\"] = Status(name=\"On Hold\", code=\"OH\")\n    data[\"status_stop\"] = Status(name=\"Stopped\", code=\"STOP\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n\n    data[\"task_status_list\"] = StatusList(\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_oh\"],\n            data[\"status_stop\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n\n    data[\"test_project_status_list\"] = StatusList(\n        name=\"Project Statuses\",\n        statuses=[data[\"status_wip\"], data[\"status_prev\"], data[\"status_cmpl\"]],\n        target_entity_type=\"Project\",\n    )\n\n    data[\"test_movie_project_type\"] = Type(\n        name=\"Movie Project\",\n        code=\"movie\",\n        target_entity_type=\"Project\",\n    )\n\n    data[\"test_repository_type\"] = Type(\n        name=\"Test Repository Type\",\n        code=\"test\",\n        target_entity_type=\"Repository\",\n    )\n\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"test_repository_type\"],\n        linux_path=\"/mnt/T/\",\n        windows_path=\"T:/\",\n        macos_path=\"/Volumes/T/\",\n    )\n\n    data[\"test_user1\"] = User(\n        name=\"User1\", login=\"user1\", email=\"user1@user1.com\", password=\"1234\"\n    )\n\n    data[\"test_user2\"] = User(\n        name=\"User2\", login=\"user2\", email=\"user2@user2.com\", password=\"1234\"\n    )\n\n    data[\"test_user3\"] = User(\n        name=\"User3\", login=\"user3\", email=\"user3@user3.com\", password=\"1234\"\n    )\n\n    data[\"test_user4\"] = User(\n        name=\"User4\", login=\"user4\", email=\"user4@user4.com\", password=\"1234\"\n    )\n\n    data[\"test_user5\"] = User(\n        name=\"User5\", login=\"user5\", email=\"user5@user5.com\", password=\"1234\"\n    )\n\n    data[\"test_project1\"] = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=data[\"test_movie_project_type\"],\n        status_list=data[\"test_project_status_list\"],\n        repositories=[data[\"test_repository\"]],\n    )\n\n    data[\"test_dependent_task1\"] = Task(\n        name=\"Dependent Task1\",\n        project=data[\"test_project1\"],\n        status_list=data[\"task_status_list\"],\n        responsible=[data[\"test_user1\"]],\n    )\n\n    data[\"test_dependent_task2\"] = Task(\n        name=\"Dependent Task2\",\n        project=data[\"test_project1\"],\n        status_list=data[\"task_status_list\"],\n        responsible=[data[\"test_user1\"]],\n    )\n\n    data[\"kwargs\"] = {\n        \"name\": \"Modeling\",\n        \"description\": \"A Modeling Task\",\n        \"project\": data[\"test_project1\"],\n        \"priority\": 500,\n        \"responsible\": [data[\"test_user1\"]],\n        \"resources\": [data[\"test_user1\"], data[\"test_user2\"]],\n        \"alternative_resources\": [\n            data[\"test_user3\"],\n            data[\"test_user4\"],\n            data[\"test_user5\"],\n        ],\n        \"allocation_strategy\": \"minloaded\",\n        \"persistent_allocation\": True,\n        \"watchers\": [data[\"test_user3\"]],\n        \"bid_timing\": 4,\n        \"bid_unit\": TimeUnit.Day,\n        \"schedule_timing\": 1,\n        \"schedule_unit\": TimeUnit.Day,\n        \"start\": datetime.datetime(2013, 4, 8, 13, 0, tzinfo=pytz.utc),\n        \"end\": datetime.datetime(2013, 4, 8, 18, 0, tzinfo=pytz.utc),\n        \"depends_on\": [data[\"test_dependent_task1\"], data[\"test_dependent_task2\"]],\n        \"time_logs\": [],\n        \"versions\": [],\n        \"is_milestone\": False,\n        \"status\": 0,\n        \"status_list\": data[\"task_status_list\"],\n    }\n    yield data\n    defaults.config_values = copy.deepcopy(defaults.default_config_values)\n    defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Task class.\"\"\"\n    assert Task.__auto_name__ is False\n\n\ndef test_priority_arg_is_skipped_defaults_to_task_priority(setup_task_tests):\n    \"\"\"priority arg skipped priority attr defaults to task_priority.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"priority\")\n    new_task = Task(**kwargs)\n    assert new_task.priority == defaults.task_priority\n\n\ndef test_priority_arg_is_given_as_none_defaults_to_task_priority(\n    setup_task_tests,\n):\n    \"\"\"priority arg is None defaults the priority attr to task_priority.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"priority\"] = None\n    new_task = Task(**kwargs)\n    assert new_task.priority == defaults.task_priority\n\n\ndef test_priority_attribute_is_given_as_none_defaults_to_task_priority(\n    setup_task_tests,\n):\n    \"\"\"priority attr is None defaults the priority attr to task_priority.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    new_task.priority = None\n    assert new_task.priority == defaults.task_priority\n\n\ndef test_priority_arg_any_given_other_value_then_int_defaults_to_task_priority(\n    setup_task_tests,\n):\n    \"\"\"TypeError raised if the priority arg value is not an int.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"priority\"] = \"a324\"\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.priority should be an integer value between 0 and 1000, not str: 'a324'\"\n    )\n\n\ndef test_priority_attribute_is_not_an_int(setup_task_tests):\n    \"\"\"TypeError raised if priority attr not a number.\"\"\"\n    data = setup_task_tests\n    test_value = \"test_value_324\"\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.priority = test_value\n\n    assert str(cm.value) == (\n        \"Task.priority should be an integer value between 0 and 1000, \"\n        \"not str: 'test_value_324'\"\n    )\n\n\ndef test_priority_arg_is_negative(setup_task_tests):\n    \"\"\"priority arg is negative value sets the priority attribute to zero.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"priority\"] = -1\n    new_task = Task(**kwargs)\n    assert new_task.priority == 0\n\n\ndef test_priority_attr_is_negative(setup_task_tests):\n    \"\"\"priority attr is given as a negative value sets the priority attr to zero.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    new_task.priority = -1\n    assert new_task.priority == 0\n\n\ndef test_priority_arg_is_too_big(setup_task_tests):\n    \"\"\"priority arg is bigger than 1000 clamps the priority attr value to 1000.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"priority\"] = 1001\n    new_task = Task(**kwargs)\n    assert new_task.priority == 1000\n\n\ndef test_priority_attr_is_too_big(setup_task_tests):\n    \"\"\"priority attr is set to a value bigger than 1000 clamps the value to 1000.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    new_task.priority = 1001\n    assert new_task.priority == 1000\n\n\n@pytest.mark.parametrize(\"test_value\", [500.1, 334.23])\ndef test_priority_arg_is_float(test_value, setup_task_tests):\n    \"\"\"float numbers for priority arg is converted to int.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"priority\"] = test_value\n    new_task = Task(**kwargs)\n    assert new_task.priority == int(test_value)\n\n\n@pytest.mark.parametrize(\"test_value\", [500.1, 334.23])\ndef test_priority_attr_is_float(test_value, setup_task_tests):\n    \"\"\"float numbers for priority attr is converted to int.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    new_task.priority = test_value\n    assert new_task.priority == int(test_value)\n\n\ndef test_priority_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"priority attr is working as expected.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    test_value = 234\n    new_task.priority = test_value\n    assert new_task.priority == test_value\n\n\ndef test_resources_arg_is_skipped(setup_task_tests):\n    \"\"\"resources attr is an empty list if the resources arg is skipped.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"resources\")\n    new_task = Task(**kwargs)\n    assert new_task.resources == []\n\n\ndef test_resources_arg_is_none(setup_task_tests):\n    \"\"\"resources attr is an empty list if the resources arg is None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resources\"] = None\n    new_task = Task(**kwargs)\n    assert new_task.resources == []\n\n\ndef test_resources_attr_is_none(setup_task_tests):\n    \"\"\"TypeError raised whe the resources attr is set to None.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.resources = None\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_resources_arg_is_not_list(setup_task_tests):\n    \"\"\"TypeError raised if the resources arg is not a list.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resources\"] = \"a resource\"\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_resources_attr_is_not_list(setup_task_tests):\n    \"\"\"TypeError raised if the resources attr is set to any other value then a list.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.resources = \"a resource\"\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_resources_arg_is_set_to_a_list_of_other_values_then_user(\n    setup_task_tests,\n):\n    \"\"\"TypeError raised if the resources arg is set to a list non User.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resources\"] = [\"a\", \"list\", \"of\", \"resources\", data[\"test_user1\"]]\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.resources should only contain instances of \"\n        \"stalker.models.auth.User, not str: 'a'\"\n    )\n\n\ndef test_resources_attr_is_set_to_a_list_of_other_values_then_user(\n    setup_task_tests,\n):\n    \"\"\"TypeError raised if the resources attr is set to a list of non User.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.resources = [\"a\", \"list\", \"of\", \"resources\", data[\"test_user1\"]]\n\n    assert str(cm.value) == (\n        \"Task.resources should only contain instances of \"\n        \"stalker.models.auth.User, not str: 'a'\"\n    )\n\n\ndef test_resources_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"resources attr is working as expected.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    test_value = [data[\"test_user1\"]]\n    new_task.resources = test_value\n    assert new_task.resources == test_value\n\n\ndef test_resources_arg_back_references_to_user(setup_task_tests):\n    \"\"\"User in the resources arg has the current task in their \"User.tasks\" attr.\"\"\"\n    data = setup_task_tests\n    # create a couple of new users\n    new_user1 = User(\n        name=\"test1\", login=\"test1\", email=\"test1@test.com\", password=\"test1\"\n    )\n    new_user2 = User(\n        name=\"test2\", login=\"test2\", email=\"test2@test.com\", password=\"test2\"\n    )\n\n    # assign it to a newly created task\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resources\"] = [new_user1]\n    new_task = Task(**kwargs)\n\n    # now check if the user has the task in its tasks list\n    assert new_task in new_user1.tasks\n\n    # now change the resources list\n    new_task.resources = [new_user2]\n    assert new_task in new_user2.tasks\n    assert new_task not in new_user1.tasks\n\n    # now append the new resource\n    new_task.resources.append(new_user1)\n    assert new_task in new_user1.tasks\n\n    # clean up test\n    new_task.resources = []\n\n\ndef test_resources_attr_back_references_to_user(setup_task_tests):\n    \"\"\"User in the resources arg has the current task in their \"User.tasks\" attr.\"\"\"\n    data = setup_task_tests\n    # create a new user\n    new_user = User(\n        name=\"Test User\",\n        login=\"test_user\",\n        email=\"testuser@test.com\",\n        password=\"test_pass\",\n    )\n    # assign it to a newly created task\n    # data[\"kwargs\"][\"resources\"] = [new_user]\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    new_task.resources = [new_user]\n    # now check if the user has the task in its tasks list\n    assert new_task in new_user.tasks\n\n\ndef test_resources_attr_clears_itself_from_the_previous_users(\n    setup_task_tests,\n):\n    \"\"\"resources attr is update clears itself from the current resources tasks attr.\"\"\"\n    data = setup_task_tests\n    # create a couple of new users\n    new_user1 = User(\n        name=\"Test User1\",\n        login=\"test_user1\",\n        email=\"testuser1@test.com\",\n        password=\"test_pass\",\n    )\n    new_user2 = User(\n        name=\"Test User2\",\n        login=\"test_user2\",\n        email=\"testuser2@test.com\",\n        password=\"test_pass\",\n    )\n    new_user3 = User(\n        name=\"Test User3\",\n        login=\"test_user3\",\n        email=\"testuser3@test.com\",\n        password=\"test_pass\",\n    )\n    new_user4 = User(\n        name=\"Test User4\",\n        login=\"test_user4\",\n        email=\"testuser4@test.com\",\n        password=\"test_pass\",\n    )\n\n    # now add the 1 and 2 to the resources with the resources arg\n    # assign it to a newly created task\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resources\"] = [new_user1, new_user2]\n    new_task = Task(**kwargs)\n\n    # now check if the user has the task in its tasks list\n    assert new_task in new_user1.tasks\n    assert new_task in new_user2.tasks\n\n    # now update the resources list\n    new_task.resources = [new_user3, new_user4]\n\n    # now check if the new resources has the task in their tasks attr\n    assert new_task in new_user3.tasks\n    assert new_task in new_user4.tasks\n\n    # and if it is not in the previous users tasks\n    assert new_task not in new_user1.tasks\n    assert new_task not in new_user2.tasks\n\n\ndef test_watchers_arg_is_skipped(setup_task_tests):\n    \"\"\"watchers attr is an empty list if the watchers arg is skipped.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"watchers\")\n    new_task = Task(**kwargs)\n    assert new_task.watchers == []\n\n\ndef test_watchers_arg_is_none(setup_task_tests):\n    \"\"\"watchers attr is an empty list if the watchers arg is None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"watchers\"] = None\n    new_task = Task(**kwargs)\n    assert new_task.watchers == []\n\n\ndef test_watchers_attr_is_none(setup_task_tests):\n    \"\"\"TypeError raised whe the watchers attr is set to None.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.watchers = None\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_watchers_arg_is_not_list(setup_task_tests):\n    \"\"\"TypeError raised if the watchers arg is not a list.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"watchers\"] = \"a resource\"\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_watchers_attr_is_not_list(setup_task_tests):\n    \"\"\"TypeError raised if the watchers attr is set to any other value then a list.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.watchers = \"a resource\"\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_watchers_arg_is_set_to_a_list_of_other_values_then_user(setup_task_tests):\n    \"\"\"TypeError raised if the watchers arg is not a list of User instances.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"watchers\"] = [\"a\", \"list\", \"of\", \"watchers\", data[\"test_user1\"]]\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.watchers should only contain instances of \"\n        \"stalker.models.auth.User, not str: 'a'\"\n    )\n\n\ndef test_watchers_attr_is_set_to_a_list_of_other_values_then_user(\n    setup_task_tests,\n):\n    \"\"\"TypeError raised if the watchers attr is set to a list of non User objects.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    test_values = [\"a\", \"list\", \"of\", \"watchers\", data[\"test_user1\"]]\n    with pytest.raises(TypeError):\n        new_task.watchers = test_values\n\n\ndef test_watchers_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"watchers attr is working as expected.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    test_value = [data[\"test_user1\"]]\n    new_task.watchers = test_value\n    assert new_task.watchers == test_value\n\n\ndef test_watchers_arg_back_references_to_user(setup_task_tests):\n    \"\"\"User in the watchers arg has the current task in their \"User.watching\" attr.\"\"\"\n    data = setup_task_tests\n    # create a couple of new users\n    new_user1 = User(\n        name=\"new_user1\",\n        login=\"new_user1\",\n        email=\"new_user1@test.com\",\n        password=\"new_user1\",\n    )\n    new_user2 = User(\n        name=\"new_user2\",\n        login=\"new_user2\",\n        email=\"new_user2@test.com\",\n        password=\"new_user2\",\n    )\n    # assign it to a newly created task\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"watchers\"] = [new_user1]\n    new_task = Task(**kwargs)\n\n    # now check if the user has the task in its tasks list\n    assert new_task in new_user1.watching\n\n    # now change the watchers list\n    new_task.watchers = [new_user2]\n    assert new_task in new_user2.watching\n    assert new_task not in new_user1.watching\n\n    # now append the new user\n    new_task.watchers.append(new_user1)\n    assert new_task in new_user1.watching\n\n\ndef test_watchers_attr_back_references_to_user(setup_task_tests):\n    \"\"\"User in the watchers arg has the current task in their \"User.watching\" attr.\"\"\"\n    data = setup_task_tests\n    # create a new user\n    new_user = User(\n        name=\"new_user\",\n        login=\"new_user\",\n        email=\"new_user@test.com\",\n        password=\"new_user\",\n    )\n\n    # assign it to a newly created task\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    new_task.watchers = [new_user]\n\n    # now check if the user has the task in its watching list\n    assert new_task in new_user.watching\n\n\ndef test_watchers_attr_clears_itself_from_the_previous_users(setup_task_tests):\n    \"\"\"watchers attr is updated clears itself from the watchers watching attr.\"\"\"\n    data = setup_task_tests\n    # create a couple of new users\n    new_user1 = User(\n        name=\"new_user1\",\n        login=\"new_user1\",\n        email=\"new_user1@test.com\",\n        password=\"new_user1\",\n    )\n    new_user2 = User(\n        name=\"new_user2\",\n        login=\"new_user2\",\n        email=\"new_user2@test.com\",\n        password=\"new_user2\",\n    )\n    new_user3 = User(\n        name=\"new_user3\",\n        login=\"new_user3\",\n        email=\"new_user3@test.com\",\n        password=\"new_user3\",\n    )\n    new_user4 = User(\n        name=\"new_user4\",\n        login=\"new_user4\",\n        email=\"new_user4@test.com\",\n        password=\"new_user4\",\n    )\n    # now add the 1 and 2 to the watchers with the watchers arg\n    # assign it to a newly created task\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"watchers\"] = [new_user1, new_user2]\n    new_task = Task(**kwargs)\n\n    # now check if the user has the task in its watching list\n    assert new_task in new_user1.watching\n    assert new_task in new_user2.watching\n\n    # now update the watchers list\n    new_task.watchers = [new_user3, new_user4]\n\n    # now check if the new watchers has the task in their watching\n    # attr\n    assert new_task in new_user3.watching\n    assert new_task in new_user4.watching\n\n    # and if it is not in the previous users watching list\n    assert new_task not in new_user1.watching\n    assert new_task not in new_user2.watching\n\n\ndef test_depends_arg_is_skipped_depends_attr_is_empty_list(setup_task_tests):\n    \"\"\" \"depends_on\" attr is an empty list if the \"depends_on\" arg is skipped.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"depends_on\")\n    new_task = Task(**kwargs)\n    assert new_task.depends_on == []\n\n\ndef test_depends_arg_is_none_depends_attr_is_empty_list(setup_task_tests):\n    \"\"\" \"depends_on\" attr is an empty list if the \"depends_on\" arg is None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = None\n    new_task = Task(**kwargs)\n    assert new_task.depends_on == []\n\n\ndef test_depends_arg_is_not_a_list(setup_task_tests):\n    \"\"\"TypeError raised if the \"depends_on\" arg is not a list.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = data[\"test_dependent_task1\"]\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n    assert str(cm.value) == \"'Task' object is not iterable\"\n\n\ndef test_depends_attr_is_not_a_list(setup_task_tests):\n    \"\"\"TypeError raised if the \"depends_on\" attr is set to something else then a list.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    with pytest.raises(TypeError) as cm:\n        new_task.depends_on = data[\"test_dependent_task1\"]\n    assert str(cm.value) == \"'Task' object is not iterable\"\n\n\ndef test_depends_arg_is_a_list_of_other_objects_than_a_task(setup_task_tests):\n    \"\"\"AttributeError raised if the \"depends_on\" arg is a list of non Task objects.\"\"\"\n    data = setup_task_tests\n    test_value = [\"a\", \"dependent\", \"task\", 1, 1.2]\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"TaskDependency.depends_on should be and instance of \"\n        \"stalker.models.task.Task, not str: 'a'\"\n    )\n\n\ndef test_depends_attr_is_a_list_of_other_objects_than_a_task(setup_task_tests):\n    \"\"\"AttributeError raised if the \"depends_on\" attr is set to a list of non Task.\"\"\"\n    data = setup_task_tests\n    test_value = [\"a\", \"dependent\", \"task\", 1, 1.2]\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    with pytest.raises(TypeError) as cm:\n        new_task.depends_on = test_value\n\n    assert str(cm.value) == (\n        \"TaskDependency.depends_on should be and instance of \"\n        \"stalker.models.task.Task, not str: 'a'\"\n    )\n\n\ndef test_depends_attr_does_not_allow_simple_cyclic_dependencies(setup_task_tests):\n    \"\"\"CircularDependencyError raised if \"depends_on\" in circular dependency.\"\"\"\n    data = setup_task_tests\n    # create two new tasks A, B\n    # make B dependent to A\n    # and make A dependent to B\n    # and expect a CircularDependencyError\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = None\n\n    task_a = Task(**kwargs)\n    task_b = Task(**kwargs)\n\n    task_b.depends_on = [task_a]\n\n    with pytest.raises(CircularDependencyError) as cm:\n        task_a.depends_on = [task_b]\n\n    assert (\n        str(cm.value) == \"<Modeling (Task)> (Task) and <Modeling (Task)> (Task) are in \"\n        'a circular dependency in their \"depends_on\" attribute'\n    )\n\n\ndef test_depends_attr_does_not_allow_cyclic_dependencies(setup_task_tests):\n    \"\"\"CircularDependencyError raised if \"depends_on\" attr has a circular dependency.\"\"\"\n    data = setup_task_tests\n    # create three new tasks A, B, C\n    # make B dependent to A\n    # make C dependent to B\n    # and make A dependent to C\n    # and expect a CircularDependencyError\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = None\n\n    kwargs[\"name\"] = \"taskA\"\n    task_a = Task(**kwargs)\n\n    kwargs[\"name\"] = \"taskB\"\n    task_b = Task(**kwargs)\n\n    kwargs[\"name\"] = \"taskC\"\n    task_c = Task(**kwargs)\n\n    task_b.depends_on = [task_a]\n    task_c.depends_on = [task_b]\n\n    with pytest.raises(CircularDependencyError) as cm:\n        task_a.depends_on = [task_c]\n\n    assert (\n        str(cm.value) == \"<taskC (Task)> (Task) and <taskA (Task)> (Task) are in a \"\n        'circular dependency in their \"depends_on\" attribute'\n    )\n\n\ndef test_depends_attr_does_not_allow_more_deeper_cyclic_dependencies(\n    setup_task_tests,\n):\n    \"\"\"CircularDependencyError raised if depends_on attr has deeper circular dependency.\"\"\"\n    data = setup_task_tests\n    # create new tasks A, B, C, D\n    # make B dependent to A\n    # make C dependent to B\n    # make D dependent to C\n    # and make A dependent to D\n    # and expect a CircularDependencyError\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = None\n\n    kwargs[\"name\"] = \"taskA\"\n    task_a = Task(**kwargs)\n\n    kwargs[\"name\"] = \"taskB\"\n    task_b = Task(**kwargs)\n\n    kwargs[\"name\"] = \"taskC\"\n    task_c = Task(**kwargs)\n\n    kwargs[\"name\"] = \"taskD\"\n    task_d = Task(**kwargs)\n\n    task_b.depends_on = [task_a]\n    task_c.depends_on = [task_b]\n    task_d.depends_on = [task_c]\n\n    with pytest.raises(CircularDependencyError) as cm:\n        task_a.depends_on = [task_d]\n\n    assert (\n        str(cm.value) == \"<taskD (Task)> (Task) and <taskA (Task)> (Task) are in a \"\n        'circular dependency in their \"depends_on\" attribute'\n    )\n\n\ndef test_depends_arg_cyclic_dependency_bug_2(setup_task_tests):\n    \"\"\"CircularDependencyError raised in the following\n\n    case:\n      T1 is parent of T2\n      T3 depends on T1\n      T2 depends on T3\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = None\n    kwargs[\"name\"] = \"T1\"\n    t1 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"T3\"\n    t3 = Task(**kwargs)\n\n    t3.depends_on.append(t1)\n\n    kwargs[\"name\"] = \"T2\"\n    kwargs[\"parent\"] = t1\n    kwargs[\"depends_on\"] = [t3]\n\n    # the following should generate the CircularDependencyError\n    with pytest.raises(CircularDependencyError) as cm:\n        Task(**kwargs)\n\n    assert (\n        str(cm.value) == \"One of the parents of <T2 (Task)> is depending on <T3 (Task)>\"\n    )\n\n\ndef test_depends_arg_does_not_allow_one_of_the_parents_of_the_task(setup_task_tests):\n    \"\"\"CircularDependencyError raised if \"depends_on\" attr has one of the parents.\"\"\"\n    data = setup_task_tests\n    # create two new tasks A, B\n    # make A parent to B\n    # and make B dependent to A\n    # and expect a CircularDependencyError\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = None\n\n    task_a = Task(**kwargs)\n    task_b = Task(**kwargs)\n    task_c = Task(**kwargs)\n\n    task_b.parent = task_a\n    task_a.parent = task_c\n\n    assert task_b in task_a.children\n    assert task_a in task_c.children\n\n    with pytest.raises(CircularDependencyError) as cm:\n        task_b.depends_on = [task_a]\n\n    assert (\n        str(cm.value) == \"<Modeling (Task)> (Task) and <Modeling (Task)> (Task) are in \"\n        'a circular dependency in their \"children\" attribute'\n    )\n\n    with pytest.raises(CircularDependencyError) as cm:\n        task_b.depends_on = [task_c]\n\n    assert (\n        str(cm.value) == \"<Modeling (Task)> (Task) and <Modeling (Task)> (Task) are in \"\n        'a circular dependency in their \"children\" attribute'\n    )\n\n\ndef test_depends_arg_is_working_as_expected(setup_task_tests):\n    \"\"\"depends_on arg is working as expected.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = None\n    task_a = Task(**kwargs)\n    task_b = Task(**kwargs)\n\n    kwargs[\"depends_on\"] = [task_a, task_b]\n    task_c = Task(**kwargs)\n\n    assert task_a in task_c.depends_on\n    assert task_b in task_c.depends_on\n    assert len(task_c.depends_on) == 2\n\n\ndef test_depends_attr_is_working_as_expected(setup_task_tests):\n    \"\"\" \"depends_on\" attr is working as expected.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = None\n    task_a = Task(**kwargs)\n    task_b = Task(**kwargs)\n    task_c = Task(**kwargs)\n    task_a.depends_on = [task_b]\n    task_a.depends_on.append(task_c)\n    assert task_b in task_a.depends_on\n    assert task_c in task_a.depends_on\n\n\ndef test_percent_complete_attr_is_read_only(setup_task_tests):\n    \"\"\"percent_complete attr is a read-only attr.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(AttributeError) as cm:\n        new_task.percent_complete = 32\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'percent_complete'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'percent_complete' of 'Task' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_percent_complete_attr_is_working_as_expected_for_a_duration_based_leaf_task_1(\n    setup_task_tests,\n):\n    \"\"\"percent_complete attr is working as expected for a duration based leaf task.\n\n    #########\n               ^\n               |\n              now\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    kwargs[\"schedule_model\"] = ScheduleModel.Duration\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    new_task = Task(**kwargs)\n\n    new_task.computed_start = now - td(days=2)\n    new_task.computed_end = now - td(days=1)\n\n    assert new_task.percent_complete == 100\n\n\ndef test_percent_complete_attr_is_working_as_expected_for_a_duration_based_leaf_task_2(\n    setup_task_tests,\n):\n    \"\"\"percent_complete attr is working as expected for a duration based leaf task.\n\n    #########\n            ^\n            |\n           now\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    kwargs[\"schedule_model\"] = ScheduleModel.Duration\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    new_task = Task(**kwargs)\n    new_task.start = now - td(days=1, hours=1)\n    new_task.end = now - td(hours=1)\n\n    assert new_task.percent_complete == 100\n\n\ndef test_percent_complete_attr_is_working_as_expected_for_a_duration_based_leaf_task_3(\n    setup_task_tests,\n):\n    \"\"\"percent_complete attr is working as expected for a duration based leaf task.\n\n    #########\n        ^\n        |\n       now\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    kwargs[\"schedule_model\"] = ScheduleModel.Duration\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    new_task = Task(**kwargs)\n    new_task.start = now - td(hours=12)\n    new_task.end = now + td(hours=12)\n\n    # it should be somewhere around 50%\n    # due to the timing resolution we cannot know it exactly\n    # and I don't want to patch datetime.datetime.now(pytz.utc)\n    # this is a very simple test\n    assert abs(new_task.percent_complete - 50 < 5)\n\n\ndef test_percent_complete_attr_is_working_as_expected_for_a_duration_based_leaf_task_4(\n    setup_task_tests,\n):\n    \"\"\"percent_complete attr is working as expected for a duration based leaf task.\n\n     #########\n     ^\n     |\n    now\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    kwargs[\"schedule_model\"] = ScheduleModel.Duration\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    new_task = Task(**kwargs)\n    new_task.computed_start = now\n    new_task.computed_end = now + td(days=1)\n\n    assert new_task.percent_complete < 5\n\n\ndef test_percent_complete_attr_is_working_as_expected_for_a_duration_based_leaf_task_5(\n    setup_task_tests,\n):\n    \"\"\"percent_complete attr is working as expected for a duration based leaf task.\n\n       #########\n     ^\n     |\n    now\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    kwargs[\"schedule_model\"] = ScheduleModel.Duration\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    new_task = Task(**kwargs)\n    new_task.computed_start = now + td(days=1)\n    new_task.computed_end = now + td(days=2)\n\n    assert new_task.percent_complete == 0\n\n\ndef test_is_milestone_arg_is_skipped(setup_task_tests):\n    \"\"\"is_milestone attr is False if the is_milestone arg is skipped.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"is_milestone\")\n    new_task = Task(**kwargs)\n    assert new_task.is_milestone is False\n\n\ndef test_is_milestone_arg_is_none(setup_task_tests):\n    \"\"\"is_milestone attr is set to False if the is_milestone arg is given as None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"is_milestone\"] = None\n    new_task = Task(**kwargs)\n    assert new_task.is_milestone is False\n\n\ndef test_is_milestone_attr_is_none(setup_task_tests):\n    \"\"\"is_milestone attr is False if set to None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    new_task.is_milestone = None\n    assert new_task.is_milestone is False\n\n\ndef test_is_milestone_arg_is_not_a_bool(setup_task_tests):\n    \"\"\"TypeError raised if the is_milestone arg is anything other than a bool.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"name\"] = \"test\" + str(0)\n    kwargs[\"is_milestone\"] = \"A string\"\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.is_milestone should be a bool value (True or False), not str: 'A string'\"\n    )\n\n\ndef test_is_milestone_attr_is_not_a_bool(setup_task_tests):\n    \"\"\"TypeError raised if the is_milestone attr is set not to a bool value.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    test_value = \"A string\"\n    with pytest.raises(TypeError) as cm:\n        new_task.is_milestone = test_value\n\n    assert str(cm.value) == (\n        \"Task.is_milestone should be a bool value (True or False), not str: 'A string'\"\n    )\n\n\ndef test_is_milestone_arg_makes_the_resources_list_an_empty_list(setup_task_tests):\n    \"\"\"resources is an empty list if the is_milestone arg is given as True.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"is_milestone\"] = True\n    kwargs[\"resources\"] = [data[\"test_user1\"], data[\"test_user2\"]]\n    new_task = Task(**kwargs)\n    assert new_task.resources == []\n\n\ndef test_is_milestone_attr_makes_the_resource_list_an_empty_list(setup_task_tests):\n    \"\"\"resources is an empty list if the is_milestone attr is given as True.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    new_task.resources = [data[\"test_user1\"], data[\"test_user2\"]]\n    new_task.is_milestone = True\n    assert new_task.resources == []\n\n\ndef test_time_logs_attr_is_none(setup_task_tests):\n    \"\"\"TypeError raised if the time_logs attr is set to None.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.time_logs = None\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_time_logs_attr_is_not_a_list(setup_task_tests):\n    \"\"\"TypeError raised if the time_logs attr is not set to a list.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.time_logs = 123\n\n    assert str(cm.value) == \"Incompatible collection type: int is not list-like\"\n\n\ndef test_time_logs_attr_is_not_a_list_of_timelog_instances(setup_task_tests):\n    \"\"\"TypeError raised if the time_logs attr is not a list of TimeLog instances.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.time_logs = [1, \"1\", 1.2, \"a time_log\"]\n\n    assert str(cm.value) == (\n        \"Task.time_logs should only contain instances of \"\n        \"stalker.models.task.TimeLog, not int: '1'\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"schedule_timing, schedule_unit, schedule_seconds\",\n    [\n        [10, \"h\", 10 * 3600],\n        [23, \"d\", 23 * 9 * 3600],\n        [2, \"w\", 2 * 45 * 3600],\n        [2.5, \"m\", 2.5 * 4 * 45 * 3600],\n        [10, TimeUnit.Hour, 10 * 3600],\n        [23, TimeUnit.Day, 23 * 9 * 3600],\n        [2, TimeUnit.Week, 2 * 45 * 3600],\n        [2.5, TimeUnit.Month, 2.5 * 4 * 45 * 3600],\n        # [\n        #     3.1,\n        #     \"y\",\n        #     3.1 * defaults.yearly_working_days * defaults.daily_working_hours * 3600,\n        # ],\n    ],\n)\ndef test_schedule_seconds_is_working_as_expected_for_an_effort_based_task_no_studio(\n    setup_task_tests, schedule_timing, schedule_unit, schedule_seconds\n):\n    \"\"\"schedule_seconds attr is working okay for an effort based task when no studio.\"\"\"\n    data = setup_task_tests\n    # no studio, using defaults\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"schedule_timing\"] = schedule_timing\n    kwargs[\"schedule_unit\"] = schedule_unit\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == schedule_seconds\n\n\n@pytest.mark.parametrize(\n    \"schedule_timing, schedule_unit, schedule_seconds\",\n    [\n        [10, \"h\", 10 * 3600],\n        [23, \"d\", 23 * 8 * 3600],\n        [2, \"w\", 2 * 40 * 3600],\n        [2.5, \"m\", 2.5 * 4 * 40 * 3600],\n        [10, TimeUnit.Hour, 10 * 3600],\n        [23, TimeUnit.Day, 23 * 8 * 3600],\n        [2, TimeUnit.Week, 2 * 40 * 3600],\n        [2.5, TimeUnit.Month, 2.5 * 4 * 40 * 3600],\n        # [\n        #     3.1,\n        #     \"y\",\n        #     3.1 * studio.yearly_working_days * studio.daily_working_hours * 3600,\n        # ],\n    ],\n)\ndef test_schedule_seconds_is_working_as_expected_for_an_effort_based_task_with_studio(\n    setup_task_tests, schedule_timing, schedule_unit, schedule_seconds\n):\n    \"\"\"schedule_seconds attr is working okay for an effort based task when no studio.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    # no studio, using defaults\n    defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n    _ = Studio(\n        name=\"Test Studio\",\n        daily_working_hours=8,\n        timing_resolution=datetime.timedelta(hours=1),\n    )\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"schedule_timing\"] = schedule_timing\n    kwargs[\"schedule_unit\"] = schedule_unit\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == schedule_seconds\n\n\ndef test_schedule_seconds_is_working_as_expected_for_a_container_task(setup_task_tests):\n    \"\"\"schedule_seconds attr is working as expected for a container task.\"\"\"\n    assert defaults.daily_working_hours == 9\n    assert defaults.weekly_working_days == 5\n    assert defaults.yearly_working_days == 261\n    data = setup_task_tests\n    # no studio, using defaults\n    kwargs = copy.copy(data[\"kwargs\"])\n    parent_task = Task(**kwargs)\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"schedule_timing\"] = 10\n    kwargs[\"schedule_unit\"] = TimeUnit.Hour\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == 10 * 3600\n    new_task.parent = parent_task\n    assert parent_task.schedule_seconds == 10 * 3600\n    kwargs[\"schedule_timing\"] = 23\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == 23 * defaults.daily_working_hours * 3600\n    new_task.parent = parent_task\n    assert (\n        parent_task.schedule_seconds\n        == 10 * 3600 + 23 * defaults.daily_working_hours * 3600\n    )\n    kwargs[\"schedule_timing\"] = 2\n    kwargs[\"schedule_unit\"] = TimeUnit.Week\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == 2 * defaults.weekly_working_hours * 3600\n    new_task.parent = parent_task\n    assert (\n        parent_task.schedule_seconds\n        == 10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 2 * defaults.weekly_working_hours * 3600\n    )\n\n    kwargs[\"schedule_timing\"] = 2.5\n    kwargs[\"schedule_unit\"] = TimeUnit.Month\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == 2.5 * 4 * defaults.weekly_working_hours * 3600\n    new_task.parent = parent_task\n    assert (\n        parent_task.schedule_seconds\n        == 10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 2 * defaults.weekly_working_hours * 3600\n        + 2.5 * 4 * defaults.weekly_working_hours * 3600\n    )\n\n    kwargs[\"schedule_timing\"] = 3.1\n    kwargs[\"schedule_unit\"] = TimeUnit.Year\n    new_task = Task(**kwargs)\n\n    assert new_task.schedule_seconds == pytest.approx(\n        3.1 * defaults.yearly_working_days * defaults.daily_working_hours * 3600\n    )\n\n    new_task.parent = parent_task\n    assert parent_task.schedule_seconds == pytest.approx(\n        10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 2 * defaults.weekly_working_hours * 3600\n        + 2.5 * 4 * defaults.weekly_working_hours * 3600\n        + 3.1 * defaults.yearly_working_days * defaults.daily_working_hours * 3600\n    )\n\n\ndef test_schedule_seconds_is_working_okay_for_a_container_task_if_the_child_is_updated(\n    setup_task_tests,\n):\n    \"\"\"schedule_seconds attr is working as expected for a container task.\"\"\"\n    assert defaults.daily_working_hours == 9\n    assert defaults.weekly_working_days == 5\n    assert defaults.yearly_working_days == 261\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    # no studio, using defaults\n    parent_task = Task(**kwargs)\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"schedule_timing\"] = 10\n    kwargs[\"schedule_unit\"] = TimeUnit.Hour\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == 10 * 3600\n    new_task.parent = parent_task\n    assert parent_task.schedule_seconds == 10 * 3600\n    # update the schedule_timing of the child\n    new_task.schedule_timing = 5\n    assert new_task.schedule_seconds == 5 * 3600\n    new_task.parent = parent_task\n    assert parent_task.schedule_seconds == 5 * 3600\n    # update it back to 10 hours\n    new_task.schedule_timing = 10\n    assert new_task.schedule_seconds == 10 * 3600\n    new_task.parent = parent_task\n    assert parent_task.schedule_seconds == 10 * 3600\n    kwargs[\"schedule_timing\"] = 23\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == 23 * defaults.daily_working_hours * 3600\n    new_task.parent = parent_task\n    assert (\n        parent_task.schedule_seconds\n        == 10 * 3600 + 23 * defaults.daily_working_hours * 3600\n    )\n\n    kwargs[\"schedule_timing\"] = 2\n    kwargs[\"schedule_unit\"] = TimeUnit.Week\n    new_task = Task(**kwargs)\n\n    assert new_task.schedule_seconds == 2 * defaults.weekly_working_hours * 3600\n\n    new_task.parent = parent_task\n    assert (\n        parent_task.schedule_seconds\n        == 10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 2 * defaults.weekly_working_hours * 3600\n    )\n\n    kwargs[\"schedule_timing\"] = 2.5\n    kwargs[\"schedule_unit\"] = TimeUnit.Month\n    new_task = Task(**kwargs)\n\n    assert new_task.schedule_seconds == 2.5 * 4 * defaults.weekly_working_hours * 3600\n\n    new_task.parent = parent_task\n    assert (\n        parent_task.schedule_seconds\n        == 10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 2 * defaults.weekly_working_hours * 3600\n        + 2.5 * 4 * defaults.weekly_working_hours * 3600\n    )\n\n    kwargs[\"schedule_timing\"] = 3.1\n    kwargs[\"schedule_unit\"] = TimeUnit.Year\n    new_task = Task(**kwargs)\n\n    assert new_task.schedule_seconds == pytest.approx(\n        3.1 * defaults.yearly_working_days * defaults.daily_working_hours * 3600\n    )\n\n    new_task.parent = parent_task\n    assert parent_task.schedule_seconds == pytest.approx(\n        10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 2 * defaults.weekly_working_hours * 3600\n        + 2.5 * 4 * defaults.weekly_working_hours * 3600\n        + 3.1 * defaults.yearly_working_days * defaults.daily_working_hours * 3600\n    )\n\n\ndef test_schedule_seconds_is_working_okay_for_a_task_if_the_child_is_updated_deeper(\n    setup_task_tests,\n):\n    \"\"\"schedule_seconds attr is working as expected for a container task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n    defaults[\"daily_working_hours\"] = 9\n    # no studio, using defaults\n    parent_task1 = Task(**kwargs)\n    assert parent_task1.schedule_seconds == 9 * 3600\n    parent_task2 = Task(**kwargs)\n    assert parent_task2.schedule_seconds == 9 * 3600\n    parent_task2.schedule_timing = 5\n    assert parent_task2.schedule_seconds == 5 * 9 * 3600\n    parent_task2.schedule_unit = TimeUnit.Hour\n    assert parent_task2.schedule_seconds == 5 * 3600\n    parent_task1.parent = parent_task2\n    assert parent_task2.schedule_seconds == 9 * 3600\n    # create another child task for parent_task2\n    child_task = Task(**kwargs)\n    child_task.schedule_timing = 10\n    child_task.schedule_unit = TimeUnit.Hour\n    assert child_task.schedule_seconds == 10 * 3600\n    parent_task2.children.append(child_task)\n    assert parent_task2.schedule_seconds, 10 * 3600 + 9 * 3600\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"schedule_timing\"] = 10\n    kwargs[\"schedule_unit\"] = TimeUnit.Hour\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == 10 * 3600\n    new_task.parent = parent_task1\n    assert parent_task1.schedule_seconds == 10 * 3600\n    assert parent_task2.schedule_seconds == 10 * 3600 + 10 * 3600\n    # update the schedule_timing of the child\n    new_task.schedule_timing = 5\n    assert new_task.schedule_seconds == 5 * 3600\n    new_task.parent = parent_task1\n    assert parent_task1.schedule_seconds == 5 * 3600\n    assert parent_task2.schedule_seconds == 5 * 3600 + 10 * 3600\n    # update it back to 10 hours\n    new_task.schedule_timing = 10\n    assert new_task.schedule_seconds == 10 * 3600\n    new_task.parent = parent_task1\n    assert parent_task1.schedule_seconds == 10 * 3600\n    assert parent_task2.schedule_seconds == 10 * 3600 + 10 * 3600\n    kwargs[\"schedule_timing\"] = 23\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == 23 * defaults.daily_working_hours * 3600\n    new_task.parent = parent_task1\n    assert (\n        parent_task1.schedule_seconds\n        == 10 * 3600 + 23 * defaults.daily_working_hours * 3600\n    )\n\n    assert (\n        parent_task2.schedule_seconds\n        == 10 * 3600 + 23 * defaults.daily_working_hours * 3600 + 10 * 3600\n    )\n\n    kwargs[\"schedule_timing\"] = 2\n    kwargs[\"schedule_unit\"] = TimeUnit.Week\n    new_task = Task(**kwargs)\n    assert new_task.schedule_seconds == 2 * defaults.weekly_working_hours * 3600\n    new_task.parent = parent_task1\n    assert (\n        parent_task1.schedule_seconds\n        == 10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 2 * defaults.weekly_working_hours * 3600\n    )\n\n    # update it to 1 week\n    new_task.schedule_timing = 1\n    assert (\n        parent_task1.schedule_seconds\n        == 10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 1 * defaults.weekly_working_hours * 3600\n    )\n\n    assert (\n        parent_task2.schedule_seconds\n        == 10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 1 * defaults.weekly_working_hours * 3600\n        + 10 * 3600\n    )\n\n    kwargs[\"schedule_timing\"] = 2.5\n    kwargs[\"schedule_unit\"] = TimeUnit.Month\n    new_task = Task(**kwargs)\n\n    assert new_task.schedule_seconds == 2.5 * 4 * defaults.weekly_working_hours * 3600\n\n    new_task.parent = parent_task1\n    assert (\n        parent_task1.schedule_seconds\n        == 10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 1 * defaults.weekly_working_hours * 3600\n        + 2.5 * 4 * defaults.weekly_working_hours * 3600\n    )\n\n    assert (\n        parent_task2.schedule_seconds\n        == 10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 1 * defaults.weekly_working_hours * 3600\n        + 2.5 * 4 * defaults.weekly_working_hours * 3600\n        + 10 * 3600\n    )\n\n    kwargs[\"schedule_timing\"] = 3.1\n    kwargs[\"schedule_unit\"] = TimeUnit.Year\n    new_task = Task(**kwargs)\n\n    assert new_task.schedule_seconds == pytest.approx(\n        3.1 * defaults.yearly_working_days * defaults.daily_working_hours * 3600\n    )\n\n    new_task.parent = parent_task1\n    assert parent_task1.schedule_seconds == pytest.approx(\n        10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 1 * defaults.weekly_working_hours * 3600\n        + 2.5 * 4 * defaults.weekly_working_hours * 3600\n        + 3.1 * defaults.yearly_working_days * defaults.daily_working_hours * 3600\n    )\n\n    assert parent_task2.schedule_seconds == pytest.approx(\n        10 * 3600\n        + 23 * defaults.daily_working_hours * 3600\n        + 1 * defaults.weekly_working_hours * 3600\n        + 2.5 * 4 * defaults.weekly_working_hours * 3600\n        + 3.1 * defaults.yearly_working_days * defaults.daily_working_hours * 3600\n        + 10 * 3600\n    )\n\n\ndef test_remaining_seconds_attr_is_a_read_only_attr(setup_task_tests):\n    \"\"\"remaining hours is a read only attr.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(AttributeError) as cm:\n        setattr(new_task, \"remaining_seconds\", 2342)\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'remaining_seconds'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'remaining_seconds' of 'Task' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_versions_attr_is_none(setup_task_tests):\n    \"\"\"TypeError raised if the versions attr is set to None.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.versions = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_versions_attr_is_not_a_list(setup_task_tests):\n    \"\"\"TypeError raised if the versions attr is set to a value other than a list.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.versions = 1\n\n    assert str(cm.value) == \"Incompatible collection type: int is not list-like\"\n\n\ndef test_versions_attr_is_not_a_list_of_version_instances(setup_task_tests):\n    \"\"\"TypeError raised if the versions attr is set to a list of non Version objects.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.versions = [1, 1.2, \"a version\"]\n\n    assert str(cm.value) == (\n        \"Task.versions should only contain instances of \"\n        \"stalker.models.version.Version, and not int: '1'\"\n    )\n\n\ndef test_equality(setup_task_tests):\n    \"\"\"equality operator.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    entity1 = Entity(**kwargs)\n    task0 = Task(**kwargs)\n    task1 = Task(**kwargs)\n    task2 = Task(**kwargs)\n    task3 = Task(**kwargs)\n    task4 = Task(**kwargs)\n    task5 = Task(**kwargs)\n    task6 = Task(**kwargs)\n\n    task1.depends_on = [task2]\n    task2.parent = task3\n    task3.parent = task4\n    task5.children = [task6]\n    task6.depends_on = [task2]\n\n    assert not new_task == entity1\n    assert new_task == task0\n    assert not new_task == task1\n    assert not new_task == task5\n\n    assert not task1 == task2\n    assert not task1 == task3\n    assert not task1 == task4\n\n    assert not task2 == task3\n    assert not task2 == task4\n    assert not task3 == task4\n\n    assert not task5 == task6\n\n    # check task with same names but different projects\n\n\ndef test_inequality(setup_task_tests):\n    \"\"\"inequality operator.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    _ = Entity(**kwargs)\n    _ = Task(**kwargs)\n\n    entity1 = Entity(**kwargs)\n    task0 = Task(**kwargs)\n    task1 = Task(**kwargs)\n    task2 = Task(**kwargs)\n    task3 = Task(**kwargs)\n    task4 = Task(**kwargs)\n    task5 = Task(**kwargs)\n    task6 = Task(**kwargs)\n\n    task1.depends_on = [task2]\n    task2.parent = task3\n    task3.parent = task4\n    task5.children = [task6]\n\n    assert new_task != entity1\n    assert not new_task != task0\n    assert new_task != task1\n    assert new_task != task5\n\n    assert task1 != task2\n    assert task1 != task3\n    assert task1 != task4\n\n    assert task2 != task3\n    assert task2 != task4\n\n    assert task3 != task4\n\n    assert task5 != task6\n\n\ndef test_parent_arg_is_skipped_there_is_a_project_arg(setup_task_tests):\n    \"\"\"Task is created okay without a parent if a Project is supplied in project arg.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    try:\n        kwargs.pop(\"parent\")\n    except KeyError:\n        pass\n\n    kwargs[\"project\"] = data[\"test_project1\"]\n    new_task = Task(**kwargs)\n    assert new_task.project == data[\"test_project1\"]\n\n\n# parent arg there but project skipped already tested\n# both skipped already tested\n\n\ndef test_parent_arg_is_none_but_there_is_a_project_arg(setup_task_tests):\n    \"\"\"task is created okay without a parent if a Project is given in project arg.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = None\n    kwargs[\"project\"] = data[\"test_project1\"]\n    new_task = Task(**kwargs)\n    assert new_task.project == data[\"test_project1\"]\n\n\ndef test_parent_attr_is_set_to_none(setup_task_tests):\n    \"\"\"parent of a task can be set to None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task1 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task1\n    new_task2 = Task(**kwargs)\n    assert new_task2.parent == new_task1\n    # DBSession.add_all([new_task1, new_task2])\n    # DBSession.commit()\n\n    # store the id to be used later\n    # id_ = new_task2.id\n    # assert id_ is not None\n\n    new_task2.parent = None\n    assert new_task2.parent is None\n    # DBSession.commit()\n\n    # we still should have this task\n    # t = DBSession.get(Task, id_)\n    # assert t is not None\n    # assert t.name == kwargs['name']\n\n\ndef test_parent_arg_is_not_a_task_instance(setup_task_tests):\n    \"\"\"TypeError raised if the parent arg is not a Task instance.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = \"not a task\"\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.parent should be an instance of stalker.models.task.Task, \"\n        \"not str: 'not a task'\"\n    )\n\n\ndef test_parent_attr_is_not_a_task_instance(setup_task_tests):\n    \"\"\"TypeError raised if the parent attr is not a Task instance.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(TypeError) as cm:\n        new_task.parent = \"not a task\"\n\n    assert str(cm.value) == (\n        \"Task.parent should be an instance of stalker.models.task.Task, \"\n        \"not str: 'not a task'\"\n    )\n\n    # there is no way to generate a CycleError by using the parent arg\n    # cause the Task is just created, it is not in relationship with other\n\n    # Tasks, there is no parent nor child.\n\n\ndef test_parent_attr_creates_a_cycle(setup_task_tests):\n    \"\"\"CycleError raised if a child is tried to be set as the parent.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task1 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"New Task\"\n    kwargs[\"parent\"] = new_task1\n    new_task2 = Task(**kwargs)\n\n    with pytest.raises(CircularDependencyError) as cm:\n        new_task1.parent = new_task2\n\n    assert (\n        str(cm.value) == \"<Modeling (Task)> (Task) and <New Task (Task)> (Task) are in \"\n        'a circular dependency in their \"children\" attribute'\n    )\n\n    # deeper test\n    kwargs[\"parent\"] = new_task2\n    new_task3 = Task(**kwargs)\n\n    with pytest.raises(CircularDependencyError) as cm:\n        new_task1.parent = new_task3\n\n    assert (\n        str(cm.value) == \"<Modeling (Task)> (Task) and <New Task (Task)> (Task) are in \"\n        'a circular dependency in their \"children\" attribute'\n    )\n\n\ndef test_parent_arg_is_working_as_expected(setup_task_tests):\n    \"\"\"parent arg is working as expected.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task1 = Task(**kwargs)\n    kwargs[\"parent\"] = new_task1\n    new_task2 = Task(**kwargs)\n    assert new_task2.parent == new_task1\n\n\ndef test_parent_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"parent attr is working as expected.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task1 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task1\n    kwargs[\"name\"] = \"New Task\"\n    new_task = Task(**kwargs)\n\n    kwargs[\"name\"] = \"New Task 2\"\n    new_task2 = Task(**kwargs)\n\n    assert new_task.parent != new_task2\n\n    new_task.parent = new_task2\n    assert new_task.parent == new_task2\n\n\ndef test_parent_arg_do_not_allow_a_dependent_task_to_be_parent(setup_task_tests):\n    \"\"\"CircularDependencyError raised if one of the dependencies assigned as parent.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n\n    kwargs[\"depends_on\"] = None\n    task_a = Task(**kwargs)\n    task_b = Task(**kwargs)\n    task_c = Task(**kwargs)\n\n    kwargs[\"depends_on\"] = [task_a, task_b, task_c]\n    kwargs[\"parent\"] = task_a\n    with pytest.raises(CircularDependencyError) as cm:\n        Task(**kwargs)\n\n    assert (\n        str(cm.value) == \"<Modeling (Task)> (Task) and <Modeling (Task)> (Task) are in \"\n        'a circular dependency in their \"children\" attribute'\n    )\n\n\ndef test_parent_attr_do_not_allow_a_dependent_task_to_be_parent(\n    setup_task_tests,\n):\n    \"\"\"CircularDependencyError raised if one of the dependent tasks is set as parent.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = None\n    task_a = Task(**kwargs)\n    task_b = Task(**kwargs)\n    task_c = Task(**kwargs)\n    task_d = Task(**kwargs)\n\n    task_d.depends_on = [task_a, task_b, task_c]\n\n    with pytest.raises(CircularDependencyError) as cm:\n        task_d.parent = task_a\n\n    assert (\n        str(cm.value) == \"<Modeling (Task)> (Task) and <Modeling (Task)> (Task) are in \"\n        'a circular dependency in their \"depends_on\" attribute'\n    )\n\n\ndef test_children_attr_is_empty_list_by_default(setup_task_tests):\n    \"\"\"children attr is an empty list by default.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    assert new_task.children == []\n\n\ndef test_children_attr_is_set_to_none(setup_task_tests):\n    \"\"\"TypeError raised if the children attr is set to None.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.children = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_children_attr_accepts_tasks_only(setup_task_tests):\n    \"\"\"TypeError raised if children attr is set to a non list.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.children = \"no task\"\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_children_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"children attr is working as expected.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task\n    kwargs[\"name\"] = \"Task 1\"\n    task1 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Task 2\"\n    task2 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Task 3\"\n    task3 = Task(**kwargs)\n\n    assert task2 not in task1.children\n    assert task3 not in task1.children\n\n    task1.children.append(task2)\n    assert task2 in task1.children\n\n    task3.parent = task1\n    assert task3 in task1.children\n\n\ndef test_is_leaf_attr_is_read_only(setup_task_tests):\n    \"\"\"is_leaf attr is a read only attr.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    with pytest.raises(AttributeError) as cm:\n        new_task.is_leaf = True\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'is_leaf'\",\n    }.get(sys.version_info.minor, \"property 'is_leaf' of 'Task' object has no setter\")\n\n    assert str(cm.value) == error_message\n\n\ndef test_is_leaf_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"is_leaf attr is True for a Task without a child Task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task\n    kwargs[\"name\"] = \"Task 1\"\n    task1 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Task 2\"\n    task2 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Task 3\"\n    task3 = Task(**kwargs)\n\n    task2.parent = task1\n    task3.parent = task1\n\n    assert task2.is_leaf\n    assert task3.is_leaf\n    assert not task1.is_leaf\n\n\ndef test_is_root_attr_is_read_only(setup_task_tests):\n    \"\"\"is_root attr is a read only attr.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(AttributeError) as cm:\n        new_task.is_root = True\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'is_root'\",\n    }.get(sys.version_info.minor, \"property 'is_root' of 'Task' object has no setter\")\n\n    assert str(cm.value) == error_message\n\n\ndef test_is_root_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"is_root attr is True for a Task without a parent Task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task\n    kwargs[\"name\"] = \"Task 1\"\n    task1 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Task 2\"\n    task2 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Task 3\"\n    task3 = Task(**kwargs)\n\n    task2.parent = task1\n    task3.parent = task1\n\n    assert not task2.is_root\n    assert not task3.is_root\n    assert not task1.is_root\n    assert new_task.is_root\n\n\ndef test_is_container_attr_is_read_only(setup_task_tests):\n    \"\"\"is_container attr is a read only attr.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(AttributeError) as cm:\n        new_task.is_container = False\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'is_container'\",\n    }.get(\n        sys.version_info.minor, \"property 'is_container' of 'Task' object has no setter\"\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_is_container_attr_working_as_expected(setup_task_tests):\n    \"\"\"is_container attr is True for a Task with at least one child Task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task\n    kwargs[\"name\"] = \"Task 1\"\n    task1 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Task 2\"\n    task2 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Task 3\"\n    task3 = Task(**kwargs)\n\n    task2.parent = task1\n    task3.parent = task1\n\n    assert not task2.is_container\n    assert not task3.is_container\n    assert task1.is_container\n\n\ndef test_project_and_parent_args_are_skipped(setup_task_tests):\n    \"\"\"TypeError raised if there is no project nor a\n    parent task is given with the project and parent args respectively\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"project\")\n\n    try:\n        kwargs.pop(\"parent\")\n    except KeyError:\n        pass\n\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert (\n        str(cm.value) == \"Task.project should be an instance of \"\n        \"stalker.models.project.Project, not NoneType: 'None'.\\n\\nOr please supply \"\n        \"a stalker.models.task.Task with the parent argument, so \"\n        \"Stalker can use the project of the supplied parent task\"\n    )\n\n\ndef test_project_arg_is_skipped_but_there_is_a_parent_arg(setup_task_tests):\n    \"\"\"Task created okay without a Project if there is a Task given in parent arg.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs.pop(\"project\")\n    kwargs[\"parent\"] = new_task\n\n    new_task2 = Task(**kwargs)\n    assert new_task2.project == data[\"test_project1\"]\n\n\ndef test_project_arg_is_not_a_project_instance(setup_task_tests):\n    \"\"\"TypeError raised if the given value for the\n    project arg is not a stalker.models.project.Project instance\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"name\"] = \"New Task 1\"\n    kwargs[\"project\"] = \"Not a Project instance\"\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.project should be an instance of stalker.models.project.Project, \"\n        \"not str: 'Not a Project instance'\"\n    )\n\n\ndef test_project_attr_is_a_read_only_attr(setup_task_tests):\n    \"\"\"project attr is a read only attr.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    with pytest.raises(AttributeError) as cm:\n        new_task.project = data[\"test_project1\"]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'Task' object has no setter\",\n        12: \"property of 'Task' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_project_getter' of 'Task' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_project_arg_is_not_matching_the_given_parent_arg(setup_task_tests):\n    \"\"\"RuntimeWarning raised if project and parent is not matching.\n\n    The project of the given parent is different from the supplied Project with the\n    project arg.\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"name\"] = \"New Task\"\n    kwargs[\"parent\"] = new_task\n    kwargs[\"project\"] = Project(\n        name=\"Some Other Project\",\n        code=\"SOP\",\n        status_list=data[\"test_project_status_list\"],\n        repository=data[\"test_repository\"],\n    )\n    # catching warnings are different from catching exceptions\n    # pytest.raises(RuntimeWarning, Task, **data[\"kwargs\"])\n    warnings.simplefilter(\"always\")\n\n    with warnings.catch_warnings(record=True) as w:\n        Task(**kwargs)\n        assert issubclass(w[-1].category, RuntimeWarning)\n\n    assert str(w[0].message) == (\n        \"The supplied parent and the project is not matching in <New Task (Task)>, \"\n        \"Stalker will use the parent's project (<Test Project1 (Project)>) as the \"\n        \"parent of this Task\"\n    )\n\n\ndef test_project_arg_is_not_matching_the_given_parent_arg_new_task_uses_parents_project(\n    setup_task_tests,\n):\n    \"\"\"task uses the parent's project if project is not matching parent's project.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"name\"] = \"New Task\"\n    kwargs[\"parent\"] = new_task\n    kwargs[\"project\"] = Project(\n        name=\"Some Other Project\",\n        code=\"SOP\",\n        status_list=data[\"test_project_status_list\"],\n        repository=data[\"test_repository\"],\n    )\n    new_task2 = Task(**kwargs)\n    assert new_task2.project == new_task.project\n\n\ndef test_start_and_end_attr_values_of_a_container_task_are_defined_by_its_child_tasks(\n    setup_task_tests,\n):\n    \"\"\"start and end attr values is defined by the\n    earliest start and the latest end date values of the children Tasks for\n    a container Task\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n\n    # remove effort and duration. Why?\n    kwargs.pop(\"schedule_timing\")\n    kwargs.pop(\"schedule_unit\")\n    kwargs[\"schedule_constraint\"] = ScheduleConstraint.Both\n\n    now = datetime.datetime(2013, 3, 22, 15, 0, tzinfo=pytz.utc)\n    dt = datetime.timedelta\n\n    # task1\n    kwargs[\"name\"] = \"Task1\"\n    kwargs[\"start\"] = now\n    kwargs[\"end\"] = now + dt(3)\n    task1 = Task(**kwargs)\n\n    # task2\n    kwargs[\"name\"] = \"Task2\"\n    kwargs[\"start\"] = now + dt(1)\n    kwargs[\"end\"] = now + dt(5)\n    task2 = Task(**kwargs)\n\n    # task3\n    kwargs[\"name\"] = \"Task3\"\n    kwargs[\"start\"] = now + dt(3)\n    kwargs[\"end\"] = now + dt(8)\n    task3 = Task(**kwargs)\n\n    # check start conditions\n    assert task1.start == now\n    assert task1.end == now + dt(3)\n\n    # now parent the task2 and task3 to task1\n    task2.parent = task1\n    task1.children.append(task3)\n\n    # check if the start is not `now` anymore\n    assert task1.start != now\n    assert task1.end != now + dt(3)\n\n    # but\n    assert task1.start == now + dt(1)\n    assert task1.end == now + dt(8)\n\n    kwargs[\"name\"] = \"Task4\"\n    kwargs[\"start\"] = now + dt(15)\n    kwargs[\"end\"] = now + dt(16)\n    task4 = Task(**kwargs)\n    task3.parent = task4\n    assert task4.start == task3.start\n    assert task4.end == task3.end\n    assert task1.start == task2.start\n    assert task1.end == task2.end\n    # TODO: with SQLAlchemy 0.9 please also check if removing the last\n    #       child from a parent will update the parents start and end date\n    #       values\n\n\ndef test_end_value_is_calculated_with_the_schedule_timing_and_schedule_unit(\n    setup_task_tests,\n):\n    \"\"\"end attr is calculated using schedule_timing and schedule_unit for new task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n\n    kwargs[\"start\"] = datetime.datetime(2013, 4, 17, 0, 0, tzinfo=pytz.utc)\n    kwargs.pop(\"end\")\n    kwargs[\"schedule_timing\"] = 10\n    kwargs[\"schedule_unit\"] = TimeUnit.Hour\n\n    new_task = Task(**kwargs)\n    assert new_task.end == datetime.datetime(2013, 4, 17, 10, 0, tzinfo=pytz.utc)\n\n    kwargs[\"schedule_timing\"] = 5\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n    new_task = Task(**kwargs)\n    print(new_task.end)\n    print(type(new_task.end))\n    assert new_task.end == datetime.datetime(2013, 4, 22, 0, 0, tzinfo=pytz.utc)\n\n\ndef test_start_calc_with_schedule_timing_and_schedule_unit_if_schedule_constraint_is_end(\n    setup_task_tests,\n):\n    \"\"\"start_date is calc.\n\n    from schedule_timing and schedule_unit if schedule_constraint is \"end\".\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n\n    kwargs[\"start\"] = datetime.datetime(2013, 4, 17, 0, 0, tzinfo=pytz.utc)\n    kwargs[\"end\"] = datetime.datetime(2013, 4, 18, 0, 0, tzinfo=pytz.utc)\n    kwargs[\"schedule_constraint\"] = ScheduleConstraint.End\n    kwargs[\"schedule_timing\"] = 10\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n\n    new_task = Task(**kwargs)\n    assert new_task.end == datetime.datetime(2013, 4, 18, 0, 0, tzinfo=pytz.utc)\n    assert new_task.start == datetime.datetime(2013, 4, 8, 0, 0, tzinfo=pytz.utc)\n\n\ndef test_start_and_end_values_are_not_touched_if_the_schedule_constraint_is_set_to_both(\n    setup_task_tests,\n):\n    \"\"\"start and end date are not touched if schedule constraint is set to \"both\".\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    _ = Task(**kwargs)\n\n    kwargs[\"start\"] = datetime.datetime(2013, 4, 17, 0, 0, tzinfo=pytz.utc)\n    kwargs[\"end\"] = datetime.datetime(2013, 4, 27, 0, 0, tzinfo=pytz.utc)\n    kwargs[\"schedule_constraint\"] = ScheduleConstraint.Both\n    kwargs[\"schedule_timing\"] = 100\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n\n    new_task = Task(**kwargs)\n    assert new_task.start == datetime.datetime(2013, 4, 17, 0, 0, tzinfo=pytz.utc)\n    assert new_task.end == datetime.datetime(2013, 4, 27, 0, 0, tzinfo=pytz.utc)\n\n\ndef test_level_attr_is_a_read_only_property(setup_task_tests):\n    \"\"\"level attr is a read only property.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(AttributeError) as cm:\n        new_task.level = 0\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'level'\",\n    }.get(sys.version_info.minor, \"property 'level' of 'Task' object has no setter\")\n\n    assert str(cm.value) == error_message\n\n\ndef test_level_attr_returns_the_hierarchical_level_of_this_task(setup_task_tests):\n    \"\"\"level attr returns the hierarchical level of this task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    _ = Task(**kwargs)\n\n    kwargs[\"name\"] = \"T1\"\n    test_task1 = Task(**kwargs)\n    assert test_task1.level == 1\n\n    kwargs[\"name\"] = \"T2\"\n    test_task2 = Task(**kwargs)\n    test_task2.parent = test_task1\n    assert test_task2.level == 2\n\n    kwargs[\"name\"] = \"T3\"\n    test_task3 = Task(**kwargs)\n    test_task3.parent = test_task2\n    assert test_task3.level == 3\n\n\ndef test__check_circular_dependency_causes_recursion(setup_task_tests):\n    \"\"\"Bug ID: None\n\n    Try to create one parent and three child tasks, second and third child\n    are dependent to the first child. This was causing a recursion.\n    \"\"\"\n    data = setup_task_tests\n    task1 = Task(\n        project=data[\"test_project1\"],\n        name=\"Set Day\",\n        start=datetime.datetime(2013, 4, 1, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 5, 6, tzinfo=pytz.utc),\n        status_list=data[\"task_status_list\"],\n        responsible=[data[\"test_user1\"]],\n    )\n\n    task2 = Task(\n        parent=task1,\n        name=\"Supervising Shootings Part1\",\n        start=datetime.datetime(2013, 4, 1, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 4, 11, tzinfo=pytz.utc),\n        status_list=data[\"task_status_list\"],\n    )\n\n    task3 = Task(\n        parent=task1,\n        name=\"Supervising Shootings Part2\",\n        depends_on=[task2],\n        start=datetime.datetime(2013, 4, 12, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 4, 16, tzinfo=pytz.utc),\n        status_list=data[\"task_status_list\"],\n    )\n\n    task4 = Task(\n        parent=task1,\n        name=\"Supervising Shootings Part3\",\n        depends_on=[task3],\n        start=datetime.datetime(2013, 4, 12, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 4, 17, tzinfo=pytz.utc),\n        status_list=data[\"task_status_list\"],\n    )\n\n    # move task4 dependency to task2\n    task4.depends_on = [task2]\n\n\ndef test_parent_attr_checks_cycle_on_self(setup_task_tests):\n    \"\"\"Bug ID: None\n\n    Check if a CircularDependency Error raised if the parent attr is pointing itself.\"\"\"\n    data = setup_task_tests\n    task1 = Task(\n        project=data[\"test_project1\"],\n        name=\"Set Day\",\n        start=datetime.datetime(2013, 4, 1, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 5, 6, tzinfo=pytz.utc),\n        status_list=data[\"task_status_list\"],\n        responsible=[data[\"test_user1\"]],\n    )\n\n    with pytest.raises(CircularDependencyError) as cm:\n        task1.parent = task1\n\n    assert (\n        str(cm.value) == \"<Set Day (Task)> (Task) and <Set Day (Task)> (Task) \"\n        'are in a circular dependency in their \"children\" attribute'\n    )\n\n\ndef test_bid_timing_arg_is_skipped(setup_task_tests):\n    \"\"\"bid_timing is equal to schedule_timing if bid_timing arg is skipped.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"schedule_timing\"] = 155\n    kwargs.pop(\"bid_timing\")\n    new_task = Task(**kwargs)\n    assert new_task.schedule_timing == kwargs[\"schedule_timing\"]\n    assert new_task.bid_timing == new_task.schedule_timing\n\n\ndef test_bid_timing_arg_is_none(setup_task_tests):\n    \"\"\"bid_timing attr value is equal to\n    schedule_timing attr value if the bid_timing arg is None\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"bid_timing\"] = None\n    kwargs[\"schedule_timing\"] = 1342\n    new_task = Task(**kwargs)\n    assert new_task.schedule_timing == kwargs[\"schedule_timing\"]\n    assert new_task.bid_timing == new_task.schedule_timing\n\n\ndef test_bid_timing_attr_is_set_to_none(setup_task_tests):\n    \"\"\"bid_timing attr can be set to None.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    new_task.bid_timing = None\n    assert new_task.bid_timing is None\n\n\ndef test_bid_timing_arg_is_not_an_int_or_float(setup_task_tests):\n    \"\"\"TypeError raised if the bid_timing arg is not an int or float.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"bid_timing\"] = \"10d\"\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.bid_timing should be an integer or float showing the value of the \"\n        \"initial bid for this Task, not str: '10d'\"\n    )\n\n\ndef test_bid_timing_attr_is_not_an_int_or_float(setup_task_tests):\n    \"\"\"TypeError raised if the bid_timing attr\n    is set to a value which is not an int or float\n    \"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.bid_timing = \"10d\"\n\n    assert str(cm.value) == (\n        \"Task.bid_timing should be an integer or float showing the value of the \"\n        \"initial bid for this Task, not str: '10d'\"\n    )\n\n\ndef test_bid_timing_arg_is_working_as_expected(setup_task_tests):\n    \"\"\"bid_timing arg is working as expected.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"bid_timing\"] = 23\n    new_task = Task(**kwargs)\n    assert new_task.bid_timing == kwargs[\"bid_timing\"]\n\n\ndef test_bid_timing_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"bid_timing attr is working as expected.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    test_value = 23\n    new_task.bid_timing = test_value\n    assert new_task.bid_timing == test_value\n\n\ndef test_bid_unit_arg_is_skipped(setup_task_tests):\n    \"\"\"bid_unit attr value is equal to\n    schedule_unit attr value if the bid_unit arg is skipped\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n    kwargs.pop(\"bid_unit\")\n    new_task = Task(**kwargs)\n    assert new_task.schedule_unit == kwargs[\"schedule_unit\"]\n    assert new_task.bid_unit == new_task.schedule_unit\n\n\ndef test_bid_unit_arg_is_none(setup_task_tests):\n    \"\"\"bid_unit attr value is equal to\n    schedule_unit attr value if the bid_unit arg is None\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"bid_unit\"] = None\n    kwargs[\"schedule_unit\"] = TimeUnit.Minute\n    new_task = Task(**kwargs)\n    assert new_task.schedule_unit == kwargs[\"schedule_unit\"]\n    assert new_task.bid_unit == new_task.schedule_unit\n\n\ndef test_bid_unit_attr_is_set_to_none(setup_task_tests):\n    \"\"\"bid_unit attr can be set to default value of 'h'.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    new_task.bid_unit = None\n    assert new_task.bid_unit == TimeUnit.Hour\n\n\ndef test_bid_unit_arg_is_not_a_str(setup_task_tests):\n    \"\"\"TypeError raised if the bid_hour arg is not a str.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"bid_unit\"] = 10\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not int: '10'\"\n    )\n\n\ndef test_bid_unit_attr_is_not_a_str(setup_task_tests):\n    \"\"\"TypeError raised if the bid_unit attr is set to a value which is not an int.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.bid_unit = 10\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not int: '10'\"\n    )\n\n\ndef test_bid_unit_arg_is_working_as_expected(setup_task_tests):\n    \"\"\"bid_unit arg is working as expected.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"bid_unit\"] = TimeUnit.Hour\n    new_task = Task(**kwargs)\n    assert new_task.bid_unit == kwargs[\"bid_unit\"]\n\n\ndef test_bid_unit_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"bid_unit attr is working as expected.\"\"\"\n    data = setup_task_tests\n    test_value = TimeUnit.Hour\n    new_task = Task(**data[\"kwargs\"])\n    new_task.bid_unit = test_value\n    assert new_task.bid_unit == test_value\n\n\ndef test_bid_unit_arg_value_not_in_defaults_datetime_units(setup_task_tests):\n    \"\"\"ValueError raised if the given unit value is\n    not in the stalker.config.Config.datetime_units\n    \"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"bid_unit\"] = \"os\"\n    with pytest.raises(ValueError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not 'os'\"\n    )\n\n\ndef test_bid_unit_attr_value_not_in_defaults_datetime_units(setup_task_tests):\n    \"\"\"ValueError raised if the bid_unit value is\n    set to a value which is not in stalker.config.Config.datetime_units.\n    \"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(ValueError) as cm:\n        new_task.bid_unit = \"sys\"\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not 'sys'\"\n    )\n\n\ndef test_tjp_id_is_a_read_only_attr(setup_task_tests):\n    \"\"\"tjp_id attr is a read only attr.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(AttributeError):\n        new_task.tjp_id = \"some value\"\n\n\ndef test_tjp_abs_id_is_a_read_only_attr(setup_task_tests):\n    \"\"\"tjp_abs_id attr is a read only attr.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(AttributeError) as cm:\n        new_task.tjp_abs_id = \"some_value\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'tjp_abs_id'\",\n    }.get(\n        sys.version_info.minor, \"property 'tjp_abs_id' of 'Task' object has no setter\"\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_tjp_id_attr_is_working_as_expected_for_a_root_task(setup_task_tests):\n    \"\"\"tjp_id is working as expected for a root task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = None\n    new_task = Task(**kwargs)\n    assert new_task.tjp_id == f\"Task_{new_task.id}\"\n\n\ndef test_tjp_id_attr_is_working_as_expected_for_a_leaf_task(setup_task_tests):\n    \"\"\"tjp_id is working as expected for a leaf task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task1 = Task(**kwargs)\n    kwargs[\"parent\"] = new_task1\n    kwargs[\"depends_on\"] = None\n    new_task2 = Task(**kwargs)\n    assert new_task2.tjp_id == f\"Task_{new_task2.id}\"\n\n\ndef test_tjp_abs_id_attr_is_working_as_expected_for_a_root_task(setup_task_tests):\n    \"\"\"tjp_abs_id is working as expected for a root task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = None\n    new_task = Task(**kwargs)\n    assert new_task.tjp_abs_id == \"Project_{}.Task_{}\".format(\n        kwargs[\"project\"].id,\n        new_task.id,\n    )\n\n\ndef test_tjp_abs_id_attr_is_working_as_expected_for_a_leaf_task(setup_task_tests):\n    \"\"\"tjp_abs_id is working as expected for a leaf task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = None\n\n    t1 = Task(**kwargs)\n    t2 = Task(**kwargs)\n    t3 = Task(**kwargs)\n\n    t2.parent = t1\n    t3.parent = t2\n\n    assert t3.tjp_abs_id == \"Project_{}.Task_{}.Task_{}.Task_{}\".format(\n        kwargs[\"project\"].id,\n        t1.id,\n        t2.id,\n        t3.id,\n    )\n\n\ndef test_to_tjp_attr_is_working_as_expected_for_a_root_task(setup_task_tests):\n    \"\"\"to_tjp attr is working as expected for a root task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = None\n    kwargs[\"schedule_timing\"] = 10\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"depends_on\"] = []\n    kwargs[\"resources\"] = [data[\"test_user1\"], data[\"test_user2\"]]\n\n    dep_t1 = Task(**kwargs)\n    dep_t2 = Task(**kwargs)\n\n    kwargs[\"depends_on\"] = [dep_t1, dep_t2]\n    kwargs[\"name\"] = \"Modeling\"\n\n    t1 = Task(**kwargs)\n\n    data[\"test_project1\"].id = 120\n    t1.id = 121\n    dep_t1.id = 122\n    dep_t2.id = 123\n    data[\"test_user1\"].id = 124\n    data[\"test_user2\"].id = 125\n    data[\"test_user3\"].id = 126\n    data[\"test_user4\"].id = 127\n    data[\"test_user5\"].id = 128\n\n    expected_tjp = \"\"\"task Task_{t1_id} \"Task_{t1_id}\" {{\n    depends Project_{project1_id}.Task_{dep_t1_id} {{onend}}, Project_{project1_id}.Task_{dep_t2_id} {{onend}}\n    effort 10d\n    allocate User_{user1_id} {{\n        alternative\n        User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n        persistent\n    }}, User_{user2_id} {{\n        alternative\n        User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n        persistent\n    }}\n}}\"\"\".format(\n        project1_id=data[\"test_project1\"].id,\n        t1_id=t1.id,\n        dep_t1_id=dep_t1.id,\n        dep_t2_id=dep_t2.id,\n        user1_id=data[\"test_user1\"].id,\n        user2_id=data[\"test_user2\"].id,\n        user3_id=data[\"test_user3\"].id,\n        user4_id=data[\"test_user4\"].id,\n        user5_id=data[\"test_user5\"].id,\n    )\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print('---------------------------------')\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(t1.to_tjp)\n    assert t1.to_tjp == expected_tjp\n\n\ndef test_to_tjp_attr_is_working_as_expected_for_a_leaf_task(setup_task_tests):\n    \"\"\"to_tjp attr is working as expected for a leaf task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task\n    kwargs[\"depends_on\"] = []\n\n    dep_task1 = Task(**kwargs)\n    dep_task2 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Modeling\"\n    kwargs[\"schedule_timing\"] = 1003\n    kwargs[\"schedule_unit\"] = TimeUnit.Hour\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"depends_on\"] = [dep_task1, dep_task2]\n\n    kwargs[\"resources\"] = [data[\"test_user1\"], data[\"test_user2\"]]\n\n    new_task2 = Task(**kwargs)\n\n    # create some random ids\n    data[\"test_project1\"].id = 120\n    new_task.id = 121\n    new_task2.id = 122\n    dep_task1.id = 123\n    dep_task2.id = 124\n    data[\"test_user1\"].id = 125\n    data[\"test_user2\"].id = 126\n    data[\"test_user3\"].id = 127\n    data[\"test_user4\"].id = 128\n    data[\"test_user5\"].id = 129\n\n    # data[\"maxDiff\"] = None\n    expected_tjp = \"\"\"    task Task_{new_task2_id} \"Task_{new_task2_id}\" {{\n        depends Project_{project1_id}.Task_{new_task_id}.Task_{dep_task1_id} {{onend}}, Project_{project1_id}.Task_{new_task_id}.Task_{dep_task2_id} {{onend}}\n        effort 1003h\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\"\"\".format(\n        project1_id=data[\"test_project1\"].id,\n        new_task_id=new_task.id,\n        new_task2_id=new_task2.id,\n        dep_task1_id=dep_task1.id,\n        dep_task2_id=dep_task2.id,\n        user1_id=data[\"test_user1\"].id,\n        user2_id=data[\"test_user2\"].id,\n        user3_id=data[\"test_user3\"].id,\n        user4_id=data[\"test_user4\"].id,\n        user5_id=data[\"test_user5\"].id,\n    )\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print('---------------------------------')\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(new_task2.to_tjp)\n    assert new_task2.to_tjp == expected_tjp\n\n\ndef test_to_tjp_attr_is_working_as_expected_for_a_leaf_task_with_timelogs(\n    setup_task_tests,\n):\n    \"\"\"to_tjp attr is working as expected for a leaf task with timelogs.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task\n    kwargs[\"depends_on\"] = []\n\n    dep_task1 = Task(**kwargs)\n    dep_task2 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Modeling\"\n    kwargs[\"schedule_timing\"] = 1003\n    kwargs[\"schedule_unit\"] = TimeUnit.Hour\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"resources\"] = [data[\"test_user1\"], data[\"test_user2\"]]\n\n    new_task2 = Task(**kwargs)\n\n    # create some random ids\n    data[\"test_project1\"].id = 120\n    new_task.id = 121\n    new_task2.id = 122\n    dep_task1.id = 123\n    dep_task2.id = 124\n    data[\"test_user1\"].id = 125\n    data[\"test_user2\"].id = 126\n    data[\"test_user3\"].id = 127\n    data[\"test_user4\"].id = 128\n    data[\"test_user5\"].id = 129\n\n    # add some timelogs\n    start = datetime.datetime(2024, 11, 13, 12, 0, tzinfo=pytz.utc)\n    end = start + datetime.timedelta(hours=2)\n    new_task2.create_time_log(data[\"test_user1\"], start, end)\n\n    # data[\"maxDiff\"] = None\n    expected_tjp = \"\"\"    task Task_{new_task2_id} \"Task_{new_task2_id}\" {{\n        effort 1003h\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n        booking User_125 2024-11-13-12:00:00 - 2024-11-13-14:00:00 {{ overtime 2 }}\n    }}\"\"\".format(\n        project1_id=data[\"test_project1\"].id,\n        new_task_id=new_task.id,\n        new_task2_id=new_task2.id,\n        dep_task1_id=dep_task1.id,\n        dep_task2_id=dep_task2.id,\n        user1_id=data[\"test_user1\"].id,\n        user2_id=data[\"test_user2\"].id,\n        user3_id=data[\"test_user3\"].id,\n        user4_id=data[\"test_user4\"].id,\n        user5_id=data[\"test_user5\"].id,\n    )\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print('---------------------------------')\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(new_task2.to_tjp)\n    assert new_task2.to_tjp == expected_tjp\n\n\ndef test_to_tjp_attr_is_working_as_expected_for_a_leaf_task_with_dependency_details(\n    setup_task_tests,\n):\n    \"\"\"to_tjp attr is working as expected for a leaf task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    kwargs[\"parent\"] = new_task\n    kwargs[\"depends_on\"] = []\n\n    dep_task1 = Task(**kwargs)\n    dep_task2 = Task(**kwargs)\n    kwargs[\"name\"] = \"Modeling\"\n    kwargs[\"schedule_timing\"] = 1003\n    kwargs[\"schedule_unit\"] = TimeUnit.Hour\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"depends_on\"] = [dep_task1, dep_task2]\n    kwargs[\"resources\"] = [data[\"test_user1\"], data[\"test_user2\"]]\n\n    new_task2 = Task(**kwargs)\n\n    # modify dependency attributes\n    tdep1 = new_task2.task_depends_on[0]\n    tdep1.dependency_target = DependencyTarget.OnStart\n    tdep1.gap_timing = 2\n    tdep1.gap_unit = TimeUnit.Day\n    tdep1.gap_model = ScheduleModel.Length\n\n    tdep2 = new_task2.task_depends_on[1]\n    tdep1.dependency_target = DependencyTarget.OnStart\n    tdep2.gap_timing = 4\n    tdep2.gap_unit = TimeUnit.Day\n    tdep2.gap_model = ScheduleModel.Duration\n\n    # create some random ids\n    data[\"test_project1\"].id = 120\n    new_task.id = 121\n    new_task2.id = 122\n    dep_task1.id = 123\n    dep_task2.id = 124\n    data[\"test_user1\"].id = 125\n    data[\"test_user2\"].id = 126\n    data[\"test_user3\"].id = 127\n    data[\"test_user4\"].id = 128\n    data[\"test_user5\"].id = 129\n\n    expected_tjp = \"\"\"    task Task_{new_task2_id} \"Task_{new_task2_id}\" {{\n        depends Project_{project1_id}.Task_{new_task_id}.Task_{dep_task1_id} {{onstart gaplength 2d}}, Project_{project1_id}.Task_{new_task_id}.Task_{dep_task2_id} {{onend gapduration 4d}}\n        effort 1003h\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\"\"\".format(\n        project1_id=data[\"test_project1\"].id,\n        new_task_id=new_task.id,\n        new_task2_id=new_task2.id,\n        dep_task1_id=dep_task1.id,\n        dep_task2_id=dep_task2.id,\n        user1_id=data[\"test_user1\"].id,\n        user2_id=data[\"test_user2\"].id,\n        user3_id=data[\"test_user3\"].id,\n        user4_id=data[\"test_user4\"].id,\n        user5_id=data[\"test_user5\"].id,\n    )\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print('---------------------------------')\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(new_task2.to_tjp)\n    assert new_task2.to_tjp == expected_tjp\n\n\ndef test_to_tjp_attr_is_working_okay_for_a_leaf_task_with_custom_allocation_strategy(\n    setup_task_tests,\n):\n    \"\"\"to_tjp attr is working okay for a leaf task with custom allocation_strategy.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task1 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task1\n    kwargs[\"depends_on\"] = []\n\n    dep_task1 = Task(**kwargs)\n    dep_task2 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Modeling\"\n    kwargs[\"schedule_timing\"] = 1003\n    kwargs[\"schedule_unit\"] = TimeUnit.Hour\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"depends_on\"] = [dep_task1, dep_task2]\n    kwargs[\"resources\"] = [data[\"test_user1\"], data[\"test_user2\"]]\n    kwargs[\"alternative_resources\"] = [data[\"test_user3\"]]\n    kwargs[\"allocation_strategy\"] = \"minloaded\"\n\n    new_task2 = Task(**kwargs)\n\n    # modify dependency attributes\n    tdep1 = new_task2.task_depends_on[0]\n    tdep1.dependency_target = DependencyTarget.OnStart\n    tdep1.gap_timing = 2\n    tdep1.gap_unit = TimeUnit.Day\n    tdep1.gap_model = ScheduleModel.Length\n\n    tdep2 = new_task2.task_depends_on[1]\n    tdep1.dependency_target = DependencyTarget.OnStart\n    tdep2.gap_timing = 4\n    tdep2.gap_unit = TimeUnit.Day\n    tdep2.gap_model = ScheduleModel.Duration\n\n    # create some random id\n    data[\"test_project1\"].id = 120\n    new_task1.id = 121\n    new_task2.id = 122\n    dep_task1.id = 123\n    dep_task2.id = 124\n    data[\"test_user1\"].id = 125\n    data[\"test_user2\"].id = 126\n    data[\"test_user3\"].id = 127\n    data[\"test_user4\"].id = 128\n    data[\"test_user5\"].id = 129\n\n    expected_tjp = \"\"\"    task Task_{new_task2_id} \"Task_{new_task2_id}\" {{\n        depends Project_{project1_id}.Task_{new_task1_id}.Task_{dep_task1_id} {{onstart gaplength 2d}}, Project_{project1_id}.Task_{new_task1_id}.Task_{dep_task2_id} {{onend gapduration 4d}}\n        effort 1003h\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id} select minloaded\n            persistent\n        }}\n    }}\"\"\".format(\n        project1_id=data[\"test_project1\"].id,\n        new_task1_id=new_task1.id,\n        new_task2_id=new_task2.id,\n        dep_task1_id=dep_task1.id,\n        dep_task2_id=dep_task2.id,\n        user1_id=data[\"test_user1\"].id,\n        user2_id=data[\"test_user2\"].id,\n        user3_id=data[\"test_user3\"].id,\n        user4_id=data[\"test_user4\"].id,\n        user5_id=data[\"test_user5\"].id,\n    )\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print('---------------------------------')\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(new_task2.to_tjp)\n    assert new_task2.to_tjp == expected_tjp\n\n\ndef test_to_tjp_attr_is_working_as_expected_for_a_container_task(setup_task_tests):\n    \"\"\"to_tjp attr is working as expected for a container task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = None\n    kwargs[\"depends_on\"] = []\n\n    t1 = Task(**kwargs)\n    kwargs[\"parent\"] = t1\n\n    dep_task1 = Task(**kwargs)\n    dep_task2 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Modeling\"\n    kwargs[\"schedule_timing\"] = 1\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"depends_on\"] = [dep_task1, dep_task2]\n\n    kwargs[\"resources\"] = [data[\"test_user1\"], data[\"test_user2\"]]\n\n    t2 = Task(**kwargs)\n    # set some random ids\n    data[\"test_user1\"].id = 123\n    data[\"test_user2\"].id = 124\n    data[\"test_user3\"].id = 125\n    data[\"test_user4\"].id = 126\n    data[\"test_user5\"].id = 127\n\n    data[\"test_project1\"].id = 128\n\n    t1.id = 129\n    t2.id = 130\n    dep_task1.id = 131\n    dep_task2.id = 132\n\n    expected_tjp = \"\"\"task Task_{t1_id} \"Task_{t1_id}\" {{\n    task Task_{dep_task1_id} \"Task_{dep_task1_id}\" {{\n        effort 1d\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\n    task Task_{dep_task2_id} \"Task_{dep_task2_id}\" {{\n        effort 1d\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\n    task Task_{t2_id} \"Task_{t2_id}\" {{\n        depends Project_{project1_id}.Task_{t1_id}.Task_{dep_task1_id} {{onend}}, Project_{project1_id}.Task_{t1_id}.Task_{dep_task2_id} {{onend}}\n        effort 1d\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\n}}\"\"\".format(\n        user1_id=data[\"test_user1\"].id,\n        user2_id=data[\"test_user2\"].id,\n        user3_id=data[\"test_user3\"].id,\n        user4_id=data[\"test_user4\"].id,\n        user5_id=data[\"test_user5\"].id,\n        project1_id=data[\"test_project1\"].id,\n        t1_id=t1.id,\n        t2_id=t2.id,\n        dep_task1_id=dep_task1.id,\n        dep_task2_id=dep_task2.id,\n    )\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print('---------------------------------')\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(t1.to_tjp)\n    assert t1.to_tjp == expected_tjp\n\n\ndef test_to_tjp_attr_is_working_as_expected_for_a_container_task_with_dependency(\n    setup_task_tests,\n):\n    \"\"\"to_tjp attr is working as expected for a container task which has dependency.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n\n    # kwargs['project'].id = 87987\n    kwargs[\"parent\"] = None\n    kwargs[\"depends_on\"] = []\n    kwargs[\"name\"] = \"Random Task Name 1\"\n\n    t0 = Task(**kwargs)\n\n    kwargs[\"depends_on\"] = [t0]\n    kwargs[\"name\"] = \"Modeling\"\n\n    t1 = Task(**kwargs)\n    t1.priority = 888\n\n    kwargs[\"parent\"] = t1\n    kwargs[\"depends_on\"] = []\n\n    dep_task1 = Task(**kwargs)\n    dep_task1.depends_on = []\n\n    dep_task2 = Task(**kwargs)\n    dep_task1.depends_on = []\n\n    kwargs[\"name\"] = \"Modeling\"\n    kwargs[\"schedule_timing\"] = 1\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"depends_on\"] = [dep_task1, dep_task2]\n\n    data[\"test_user1\"].name = \"Test User 1\"\n    data[\"test_user1\"].login = \"test_user1\"\n    # data[\"test_user1\"].id = 1231\n\n    data[\"test_user2\"].name = \"Test User 2\"\n    data[\"test_user2\"].login = \"test_user2\"\n    # data[\"test_user2\"].id = 1232\n\n    kwargs[\"resources\"] = [data[\"test_user1\"], data[\"test_user2\"]]\n\n    t2 = Task(**kwargs)\n\n    # generate random ids\n    data[\"test_user1\"].id = 123\n    data[\"test_user2\"].id = 124\n    data[\"test_user3\"].id = 125\n    data[\"test_user4\"].id = 126\n    data[\"test_user5\"].id = 127\n\n    data[\"test_project1\"].id = 128\n\n    t0.id = 129\n    t1.id = 130\n    t2.id = 131\n    dep_task1.id = 132\n    dep_task2.id = 133\n\n    expected_tjp = \"\"\"task Task_{t1_id} \"Task_{t1_id}\" {{\n    priority 888\n    depends Project_{project1_id}.Task_{t0_id} {{onend}}\n    task Task_{dep_task1_id} \"Task_{dep_task1_id}\" {{\n        effort 1d\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\n    task Task_{dep_task2_id} \"Task_{dep_task2_id}\" {{\n        effort 1d\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\n    task Task_{t2_id} \"Task_{t2_id}\" {{\n        depends Project_{project1_id}.Task_{t1_id}.Task_{dep_task1_id} {{onend}}, Project_{project1_id}.Task_{t1_id}.Task_{dep_task2_id} {{onend}}\n        effort 1d\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\n}}\"\"\".format(\n        user1_id=data[\"test_user1\"].id,\n        user2_id=data[\"test_user2\"].id,\n        user3_id=data[\"test_user3\"].id,\n        user4_id=data[\"test_user4\"].id,\n        user5_id=data[\"test_user5\"].id,\n        project1_id=data[\"test_project1\"].id,\n        t0_id=t0.id,\n        t1_id=t1.id,\n        t2_id=t2.id,\n        dep_task1_id=dep_task1.id,\n        dep_task2_id=dep_task2.id,\n    )\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print('---------------------------------')\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(t1.to_tjp)\n    assert t1.to_tjp == expected_tjp\n\n\ndef test_to_tjp_schedule_constraint_is_reflected_in_tjp_file(setup_task_tests):\n    \"\"\"schedule_constraint is reflected in the tjp file.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n\n    # kwargs['project'].id = 87987\n    kwargs[\"parent\"] = None\n    kwargs[\"depends_on\"] = []\n\n    t1 = Task(**kwargs)\n    kwargs[\"parent\"] = t1\n    dep_task1 = Task(**kwargs)\n    dep_task2 = Task(**kwargs)\n\n    kwargs[\"name\"] = \"Modeling\"\n    kwargs[\"schedule_timing\"] = 1\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n    kwargs[\"depends_on\"] = [dep_task1, dep_task2]\n    kwargs[\"schedule_constraint\"] = 3\n    kwargs[\"start\"] = datetime.datetime(2013, 5, 3, 14, 0, tzinfo=pytz.utc)\n    kwargs[\"end\"] = datetime.datetime(2013, 5, 4, 14, 0, tzinfo=pytz.utc)\n\n    data[\"test_user1\"].name = \"Test User 1\"\n    data[\"test_user1\"].login = \"test_user1\"\n    # data[\"test_user1\"].id = 1231\n\n    data[\"test_user2\"].name = \"Test User 2\"\n    data[\"test_user2\"].login = \"test_user2\"\n    # data[\"test_user2\"].id = 1232\n\n    kwargs[\"resources\"] = [data[\"test_user1\"], data[\"test_user2\"]]\n\n    t2 = Task(**kwargs)\n\n    # create some random ids\n    data[\"test_user1\"].id = 120\n    data[\"test_user2\"].id = 121\n    data[\"test_user3\"].id = 122\n    data[\"test_user4\"].id = 123\n    data[\"test_user5\"].id = 124\n    data[\"test_project1\"].id = 125\n    t1.id = 126\n    t2.id = 127\n    dep_task1.id = 128\n    dep_task2.id = 129\n\n    expected_tjp = \"\"\"task Task_{t1_id} \"Task_{t1_id}\" {{\n    task Task_{dep_task1_id} \"Task_{dep_task1_id}\" {{\n        effort 1d\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\n    task Task_{dep_task2_id} \"Task_{dep_task2_id}\" {{\n        effort 1d\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\n    task Task_{t2_id} \"Task_{t2_id}\" {{\n        depends Project_{project1_id}.Task_{t1_id}.Task_{dep_task1_id} {{onend}}, Project_{project1_id}.Task_{t1_id}.Task_{dep_task2_id} {{onend}}\n        start 2013-05-03-14:00\n        end 2013-05-04-14:00\n        effort 1d\n        allocate User_{user1_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}, User_{user2_id} {{\n            alternative\n            User_{user3_id}, User_{user4_id}, User_{user5_id} select minloaded\n            persistent\n        }}\n    }}\n}}\"\"\".format(\n        user1_id=data[\"test_user1\"].id,\n        user2_id=data[\"test_user2\"].id,\n        user3_id=data[\"test_user3\"].id,\n        user4_id=data[\"test_user4\"].id,\n        user5_id=data[\"test_user5\"].id,\n        project1_id=data[\"test_project1\"].id,\n        t1_id=t1.id,\n        t2_id=t2.id,\n        dep_task1_id=dep_task1.id,\n        dep_task2_id=dep_task2.id,\n    )\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print('---------------------------------')\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(t1.to_tjp)\n    data[\"maxDiff\"] = None\n    assert t1.to_tjp == expected_tjp\n\n\ndef test_is_scheduled_is_a_read_only_attr(setup_task_tests):\n    \"\"\"is_scheduled is a read-only attr.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(AttributeError) as cm:\n        new_task.is_scheduled = True\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'is_scheduled'\",\n    }.get(\n        sys.version_info.minor, \"property 'is_scheduled' of 'Task' object has no setter\"\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_is_scheduled_is_true_if_the_computed_start_and_computed_end_is_not_none(\n    setup_task_tests,\n):\n    \"\"\"is_scheduled attr is True if computed_start and computed_end are both valid.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    new_task.computed_start = datetime.datetime.now(pytz.utc)\n    new_task.computed_end = datetime.datetime.now(pytz.utc) + datetime.timedelta(10)\n    assert new_task.is_scheduled is True\n\n\ndef test_is_scheduled_is_false_if_one_of_computed_start_and_computed_end_is_none(\n    setup_task_tests,\n):\n    \"\"\"is_scheduled attr is False if one of computed_start or computed_end is None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    new_task.computed_start = None\n    new_task.computed_end = datetime.datetime.now(pytz.utc)\n    assert new_task.is_scheduled is False\n    new_task.computed_start = datetime.datetime.now(pytz.utc)\n    new_task.computed_end = None\n    assert new_task.is_scheduled is False\n\n\ndef test_parents_attr_is_read_only(setup_task_tests):\n    \"\"\"parents attr is read only.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    with pytest.raises(AttributeError) as cm:\n        new_task.parents = data[\"test_dependent_task1\"]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'parents'\",\n    }.get(sys.version_info.minor, \"property 'parents' of 'Task' object has no setter\")\n\n    assert str(cm.value) == error_message\n\n\ndef test_parents_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"parents attr is working as expected.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"parent\"] = None\n    t1 = Task(**kwargs)\n    t2 = Task(**kwargs)\n    t3 = Task(**kwargs)\n    t2.parent = t1\n    t3.parent = t2\n    assert t3.parents == [t1, t2]\n\n\ndef test_responsible_arg_is_skipped_for_a_root_task(setup_task_tests):\n    \"\"\"responsible list is an empty list if a root task have no responsible set.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"responsible\")\n    new_task = Task(**kwargs)\n    assert new_task.responsible == []\n\n\ndef test_responsible_arg_is_skipped_for_a_non_root_task(setup_task_tests):\n    \"\"\"parent task's responsible is used if the responsible arg is skipped.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"name\"] = \"Root Task\"\n    root_task = Task(**kwargs)\n    assert root_task.responsible == [data[\"test_user1\"]]\n\n    kwargs.pop(\"responsible\")\n    kwargs[\"parent\"] = root_task\n    kwargs[\"name\"] = \"Child Task\"\n    new_task = Task(**kwargs)\n    assert new_task.responsible == root_task.responsible\n\n\ndef test_responsible_arg_is_none_for_a_root_task(setup_task_tests):\n    \"\"\"RuntimeError raised if the responsible arg is None for a root task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"responsible\"] = None\n    new_task = Task(**kwargs)\n    assert new_task.responsible == []\n\n\ndef test_responsible_arg_is_none_for_a_non_root_task(setup_task_tests):\n    \"\"\"parent tasks responsible is used if responsible arg is None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"name\"] = \"Root Task\"\n    root_task = Task(**kwargs)\n    assert root_task.responsible == [data[\"test_user1\"]]\n\n    kwargs[\"responsible\"] = None\n    kwargs[\"parent\"] = root_task\n    kwargs[\"name\"] = \"Child Task\"\n    new_task = Task(**kwargs)\n    assert new_task.responsible == root_task.responsible\n\n\ndef test_responsible_arg_not_a_list_instance(setup_task_tests):\n    \"\"\"TypeError raised if the responsible arg is not a List instance.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"responsible\"] = \"not a list\"\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_responsible_attr_not_a_list_instance(setup_task_tests):\n    \"\"\"TypeError raised if the responsible attr is not a List of User instances.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(TypeError) as cm:\n        new_task.responsible = \"not a list of users\"\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_responsible_arg_is_not_a_list_of_user_instance(setup_task_tests):\n    \"\"\"TypeError raised if the responsible arg value is not a List of User instance.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"responsible\"] = [\"not a user instance\"]\n\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.responsible should only contain instances of \"\n        \"stalker.models.auth.User, not str: 'not a user instance'\"\n    )\n\n\ndef test_responsible_attr_is_set_to_something_other_than_a_list_of_user_instance(\n    setup_task_tests,\n):\n    \"\"\"TypeError raised if the responsible attr is not list of Users.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(TypeError) as cm:\n        new_task.responsible = [\"not a user instance\"]\n\n    assert str(cm.value) == (\n        \"Task.responsible should only contain instances of \"\n        \"stalker.models.auth.User, not str: 'not a user instance'\"\n    )\n\n\ndef test_responsible_arg_is_none_or_skipped_responsible_attr_comes_from_parents(\n    setup_task_tests,\n):\n    \"\"\"responsible arg is None or skipped, responsible attr value comes from parents.\"\"\"\n    data = setup_task_tests\n    # create two new tasks\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"responsible\"] = None\n\n    kwargs[\"parent\"] = new_task\n    new_task1 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task1\n    new_task2 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task2\n    new_task3 = Task(**kwargs)\n\n    assert new_task1.responsible == [data[\"test_user1\"]]\n    assert new_task2.responsible == [data[\"test_user1\"]]\n    assert new_task3.responsible == [data[\"test_user1\"]]\n\n    new_task2.responsible = [data[\"test_user2\"]]\n    assert new_task1.responsible == [data[\"test_user1\"]]\n    assert new_task2.responsible == [data[\"test_user2\"]]\n    assert new_task3.responsible == [data[\"test_user1\"]]\n\n\ndef test_responsible_arg_is_none_or_skipped_responsible_attr_comes_from_the_first_parent_with_responsible(\n    setup_task_tests,\n):\n    \"\"\"responsible arg is None or skipped, responsible attr value comes from the first parent with responsible.\"\"\"\n    data = setup_task_tests\n    # create two new tasks\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"responsible\"] = None\n\n    kwargs[\"parent\"] = new_task\n    new_task1 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task1\n    new_task2 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task2\n    new_task3 = Task(**kwargs)\n\n    new_task2.responsible = [data[\"test_user2\"]]\n    assert new_task1.responsible == [data[\"test_user1\"]]\n    assert new_task2.responsible == [data[\"test_user2\"]]\n    assert new_task3.responsible == [data[\"test_user2\"]]\n\n\ndef test_responsible_attr_is_set_to_none_responsible_attr_comes_from_parents(\n    setup_task_tests,\n):\n    \"\"\"responsible attr is None or skipped then its value comes from parents.\"\"\"\n    data = setup_task_tests\n    # create two new tasks\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task\n    new_task1 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task1\n    new_task2 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task2\n    new_task3 = Task(**kwargs)\n\n    new_task1.responsible = []\n    new_task2.responsible = []\n    new_task3.responsible = []\n    new_task.responsible = [data[\"test_user2\"]]\n\n    assert new_task1.responsible == [data[\"test_user2\"]]\n    assert new_task2.responsible == [data[\"test_user2\"]]\n    assert new_task3.responsible == [data[\"test_user2\"]]\n\n    new_task2.responsible = [data[\"test_user1\"]]\n    assert new_task1.responsible == [data[\"test_user2\"]]\n    assert new_task2.responsible == [data[\"test_user1\"]]\n    assert new_task3.responsible == [data[\"test_user2\"]]\n\n\ndef test_responsible_attr_is_set_to_none_responsible_attr_comes_from_parents_immutable(\n    setup_task_tests,\n):\n    \"\"\"responsible attr is None or skipped then its value comes from parents.\"\"\"\n    data = setup_task_tests\n    # create two new tasks\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task\n    new_task1 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task1\n    new_task2 = Task(**kwargs)\n\n    kwargs[\"parent\"] = new_task2\n    new_task3 = Task(**kwargs)\n\n    new_task1.responsible = []\n    new_task2.responsible = []\n    new_task3.responsible = []\n    new_task.responsible = [data[\"test_user2\"]]\n\n    # set the attr now and expect the parent and the current tasks\n    # responsible are divergent\n    new_task1.responsible.append(data[\"test_user3\"])\n    assert data[\"test_user3\"] in new_task1.responsible\n    assert data[\"test_user2\"] in new_task1.responsible\n    assert data[\"test_user3\"] not in new_task.responsible\n    assert data[\"test_user2\"] in new_task.responsible\n\n\ndef test_computed_start_also_sets_start(setup_task_tests):\n    \"\"\"computed_start also sets the start value of the task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task1 = Task(**kwargs)\n    test_value = datetime.datetime(2013, 8, 2, 13, 0, tzinfo=pytz.utc)\n    assert new_task1.start != test_value\n    new_task1.computed_start = test_value\n    assert new_task1.computed_start == test_value\n    assert new_task1.start == test_value\n\n\ndef test_computed_end_also_sets_end(setup_task_tests):\n    \"\"\"computed_end also sets the end value of the task.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    _ = Task(**kwargs)\n\n    new_task1 = Task(**kwargs)\n    test_value = datetime.datetime(2013, 8, 2, 13, 0, tzinfo=pytz.utc)\n    assert new_task1.end != test_value\n    new_task1.computed_end = test_value\n    assert new_task1.computed_end == test_value\n    assert new_task1.end == test_value\n\n\n# TODO: please add tests for _total_logged_seconds for leaf tasks\n\n\ndef test_tickets_attr_is_a_read_only_property(setup_task_tests):\n    \"\"\"tickets attr is a read-only property.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(AttributeError) as cm:\n        new_task.tickets = \"some value\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'tickets'\",\n    }.get(sys.version_info.minor, \"property 'tickets' of 'Task' object has no setter\")\n\n    assert str(cm.value) == error_message\n\n\ndef test_open_tickets_attr_is_a_read_only_property(setup_task_tests):\n    \"\"\"open_tickets attr is a read-only property.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(AttributeError) as cm:\n        new_task.open_tickets = \"some value\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'open_tickets'\",\n    }.get(\n        sys.version_info.minor, \"property 'open_tickets' of 'Task' object has no setter\"\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_reviews_attr_is_an_empty_list_by_default(setup_task_tests):\n    \"\"\"reviews attr is an empty list by default.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    assert new_task.reviews == []\n\n\ndef test_reviews_is_not_a_list_of_review_instances(setup_task_tests):\n    \"\"\"reviews attr is not a List[Review] raises TypeError.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    test_value = [1234, \"test value\"]\n    with pytest.raises(TypeError) as cm:\n        new_task.reviews = test_value\n\n    assert str(cm.value) == (\n        \"Task.reviews should only contain instances of \"\n        \"stalker.models.review.Review, not int: '1234'\"\n    )\n\n\ndef test_reviews_attr_is_validated_as_expected(setup_task_db_tests):\n    \"\"\"reviews attr is validated as expected.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    from stalker import Review\n\n    assert new_task.reviews == []\n    new_review = Review(\n        task=new_task,\n        reviewer=data[\"test_user1\"],\n    )\n    assert new_task.reviews == [new_review]\n\n\ndef test_status_is_wfd_for_a_newly_created_task_with_dependencies(setup_task_tests):\n    \"\"\"status for a newly created task is WFD by default if there are dependencies.\"\"\"\n    data = setup_task_tests\n    # try to trick it\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"status\"] = data[\"status_cmpl\"]  # this is ignored\n    new_task = Task(**kwargs)\n    assert new_task.status == data[\"status_wfd\"]\n\n\ndef test_status_is_rts_for_a_newly_created_task_without_dependency(setup_task_tests):\n    \"\"\"status for a newly created task is RTS if there are no dependencies.\"\"\"\n    data = setup_task_tests\n    # try to trick it\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"status\"] = data[\"status_cmpl\"]\n    kwargs.pop(\"depends_on\")\n    new_task = Task(**kwargs)\n    assert new_task.status == data[\"status_rts\"]\n\n\ndef test_review_number_attr_is_read_only(setup_task_tests):\n    \"\"\"review_number attr is read-only.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    with pytest.raises(AttributeError) as cm:\n        new_task.review_number = 12\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'Task' object has no setter\",\n        12: \"property of 'Task' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_review_number_getter' of 'Task' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_review_number_attr_initializes_with_0(setup_task_tests):\n    \"\"\"review_number attr initializes to 0.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    assert new_task.review_number == 0\n\n\ndef test_task_dependency_auto_generates_task_dependency_object(setup_task_tests):\n    \"\"\"TaskDependency instance is automatically created if association proxy is used.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    new_task = Task(**kwargs)\n    new_task.depends_on.append(data[\"test_dependent_task1\"])\n    task_depends = new_task.task_depends_on[0]\n    assert task_depends.task == new_task\n    assert task_depends.depends_on == data[\"test_dependent_task1\"]\n\n\ndef test_task_depends_on_is_an_empty_list(setup_task_tests):\n    \"\"\"task_depends_on is an empty list.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    new_task.task_depends_on = []\n\n\ndef test_task_depends_on_is_not_a_task_dependency_object(setup_task_tests):\n    \"\"\"task_depends_on is not a TaskDependency object raises TypeError.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    with pytest.raises(TypeError) as cm:\n        new_task.task_depends_on.append(\"not a TaskDependency object.\")\n    assert str(cm.value) == (\n        \"Task.task_depends_on should only contain instances of TaskDependency, \"\n        \"not str: 'not a TaskDependency object.'\"\n    )\n\n\ndef test_alternative_resources_arg_is_skipped(setup_task_tests):\n    \"\"\"alternative_resources attr is an empty list if it is skipped.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"alternative_resources\")\n    new_task = Task(**kwargs)\n    assert new_task.alternative_resources == []\n\n\ndef test_alternative_resources_arg_is_none(setup_task_tests):\n    \"\"\"alternative_resources attr is empty list if alternative_resources arg is None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"alternative_resources\"] = None\n    new_task = Task(**kwargs)\n    assert new_task.alternative_resources == []\n\n\ndef test_alternative_resources_attr_is_set_to_none(setup_task_tests):\n    \"\"\"TypeError raised if the alternative_resources attr is set to None.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.alternative_resources = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_alternative_resources_arg_is_not_a_list(setup_task_tests):\n    \"\"\"TypeError raised if the alternative_resources arg value is not a list.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"alternative_resources\"] = data[\"test_user3\"]\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == \"Incompatible collection type: User is not list-like\"\n\n\ndef test_alternative_resources_attr_is_not_a_list(setup_task_tests):\n    \"\"\"TypeError raised if the alternative_resources attr is not a list.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.alternative_resources = data[\"test_user3\"]\n\n    assert str(cm.value) == \"Incompatible collection type: User is not list-like\"\n\n\ndef test_alternative_resources_arg_elements_are_not_user_instances(\n    setup_task_tests,\n):\n    \"\"\"TypeError raised if items in the alternative_resources arg are not all User.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"alternative_resources\"] = [\"not\", 1, \"user\"]\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.alternative_resources should only contain instances of \"\n        \"stalker.models.auth.User, not str: 'not'\"\n    )\n\n\ndef test_alternative_resources_attr_elements_are_not_all_user_instances(\n    setup_task_tests,\n):\n    \"\"\"TypeError raised if the items in the alternative_resources attr not all User.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.alternative_resources = [\"not\", 1, \"user\"]\n\n    assert str(cm.value) == (\n        \"Task.alternative_resources should only contain instances of \"\n        \"stalker.models.auth.User, not str: 'not'\"\n    )\n\n\ndef test_alternative_resources_arg_is_working_as_expected(setup_task_tests):\n    \"\"\"alternative_resources arg is passed okay to the alternative_resources attr.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    assert sorted(\n        [data[\"test_user3\"], data[\"test_user4\"], data[\"test_user5\"]],\n        key=lambda x: x.name,\n    ) == sorted(new_task.alternative_resources, key=lambda x: x.name)\n\n\ndef test_alternative_resources_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"alternative_resources attr value can be correctly set.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    assert sorted(new_task.alternative_resources, key=lambda x: x.name) == sorted(\n        [data[\"test_user3\"], data[\"test_user4\"], data[\"test_user5\"]],\n        key=lambda x: x.name,\n    )\n    alternative_resources = [data[\"test_user4\"], data[\"test_user5\"]]\n    new_task.alternative_resources = alternative_resources\n    assert sorted(alternative_resources, key=lambda x: x.name) == sorted(\n        new_task.alternative_resources, key=lambda x: x.name\n    )\n\n\ndef test_allocation_strategy_arg_is_skipped(setup_task_tests):\n    \"\"\"default value is used for allocation_strategy attr if arg is skipped.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"allocation_strategy\")\n    new_task = Task(**kwargs)\n    assert new_task.allocation_strategy == defaults.allocation_strategy[0]\n\n\ndef test_allocation_strategy_arg_is_none(setup_task_tests):\n    \"\"\"default value is used for allocation_strategy attr if arg is None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"allocation_strategy\"] = None\n    new_task = Task(**kwargs)\n    assert new_task.allocation_strategy == defaults.allocation_strategy[0]\n\n\ndef test_allocation_strategy_attr_is_set_to_none(setup_task_tests):\n    \"\"\"default value is used for the allocation_strategy if it is set to None.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    new_task.allocation_strategy = None\n    assert new_task.allocation_strategy == defaults.allocation_strategy[0]\n\n\ndef test_allocation_strategy_arg_is_not_a_str(setup_task_tests):\n    \"\"\"TypeError raised if the allocation_strategy arg value is not a str.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"allocation_strategy\"] = 234\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.allocation_strategy should be one of ['minallocated', \"\n        \"'maxloaded', 'minloaded', 'order', 'random'], not int: '234'\"\n    )\n\n\ndef test_allocation_strategy_attr_is_set_to_a_value_other_than_str(\n    setup_task_tests,\n):\n    \"\"\"TypeError is raised if the allocation_strategy attr is not a str.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.allocation_strategy = 234\n\n    assert (\n        str(cm.value) == \"Task.allocation_strategy should be one of ['minallocated', \"\n        \"'maxloaded', 'minloaded', 'order', 'random'], not int: '234'\"\n    )\n\n\ndef test_allocation_strategy_arg_value_is_not_correct(setup_task_tests):\n    \"\"\"ValueError raised if the allocation_strategy arg value is not valid.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"allocation_strategy\"] = \"not in the list\"\n    with pytest.raises(ValueError) as cm:\n        Task(**kwargs)\n\n    assert (\n        str(cm.value) == \"Task.allocation_strategy should be one of ['minallocated', \"\n        \"'maxloaded', 'minloaded', 'order', 'random'], not 'not in the list'\"\n    )\n\n\ndef test_allocation_strategy_attr_value_is_not_correct(setup_task_tests):\n    \"\"\"ValueError raised if the allocation_strategy attr is set to an invalid value.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(ValueError) as cm:\n        new_task.allocation_strategy = \"not in the list\"\n\n    assert (\n        str(cm.value) == \"Task.allocation_strategy should be one of ['minallocated', \"\n        \"'maxloaded', 'minloaded', 'order', 'random'], not 'not in the list'\"\n    )\n\n\ndef test_allocation_strategy_arg_is_working_as_expected(setup_task_tests):\n    \"\"\"allocation_strategy arg value is passed to the allocation_strategy attr.\"\"\"\n    data = setup_task_tests\n    test_value = defaults.allocation_strategy[1]\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"allocation_strategy\"] = test_value\n    new_task = Task(**kwargs)\n    assert test_value == new_task.allocation_strategy\n\n\ndef test_allocation_strategy_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"allocation_strategy attr value can be correctly set.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n\n    test_value = defaults.allocation_strategy[1]\n    assert new_task.allocation_strategy != test_value\n\n    new_task.allocation_strategy = test_value\n    assert new_task.allocation_strategy == test_value\n\n\ndef test_computed_resources_attr_value_is_equal_to_the_resources_attr_for_a_new_task(\n    setup_task_tests,\n):\n    \"\"\"computed_resources attr is equal to the resources attr if a task initialized.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    # DBSession.commit()\n\n    assert new_task.is_scheduled is False\n    assert new_task.resources == new_task.computed_resources\n\n\ndef test_computed_resources_attr_updates_with_resources_if_is_scheduled_is_false_append(\n    setup_task_tests,\n):\n    \"\"\"computed_resources attr updated with the resources if is_scheduled is False.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    assert new_task.is_scheduled is False\n    test_value = [data[\"test_user3\"], data[\"test_user5\"]]\n    assert new_task.resources != test_value\n    assert new_task.computed_resources != test_value\n    new_task.resources = test_value\n    assert sorted(new_task.computed_resources, key=lambda x: x.name) == sorted(\n        test_value, key=lambda x: x.name\n    )\n\n\ndef test_computed_resources_attr_updates_with_resources_if_is_scheduled_is_false_remove(\n    setup_task_tests,\n):\n    \"\"\"computed_resources attr updated with the resources if is_scheduled is False.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    assert new_task.is_scheduled is False\n    test_value = [data[\"test_user3\"], data[\"test_user5\"]]\n    assert new_task.resources != test_value\n    assert new_task.computed_resources != test_value\n    new_task.resources = test_value\n    assert sorted(new_task.computed_resources, key=lambda x: x.name) == sorted(\n        test_value, key=lambda x: x.name\n    )\n\n\ndef test_computed_resources_attr_dont_update_with_resources_if_is_scheduled_is_true(\n    setup_task_tests,\n):\n    \"\"\"computed_resources attr not updated with resources if is_scheduled is True.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    assert new_task.is_scheduled is False\n    test_value = [data[\"test_user3\"], data[\"test_user5\"]]\n    assert new_task.resources != test_value\n    assert new_task.computed_resources != test_value\n\n    # now set computed_start and computed_end to emulate a computation has\n    # been done\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    assert new_task.is_scheduled is False\n    new_task.computed_start = now\n    new_task.computed_end = now + td(hours=1)\n\n    assert new_task.is_scheduled is True\n\n    new_task.resources = test_value\n    assert new_task.computed_resources != test_value\n\n\ndef test_computed_resources_is_not_a_user_instance(setup_task_tests):\n    \"\"\"computed_resource is not a User instance raises TypeError.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    with pytest.raises(TypeError) as cm:\n        new_task.computed_resources.append(\"not a user\")\n\n    assert str(cm.value) == (\n        \"Task.computed_resources should only contain instances of \"\n        \"stalker.models.auth.User, not str: 'not a user'\"\n    )\n\n\ndef test_persistent_allocation_arg_is_skipped(setup_task_tests):\n    \"\"\"persistent_allocation defaults if the persistent_allocation arg is skipped.\"\"\"\n    data = setup_task_tests\n    data[\"kwargs\"].pop(\"persistent_allocation\")\n    new_task = Task(**data[\"kwargs\"])\n    assert new_task.persistent_allocation is True\n\n\ndef test_persistent_allocation_arg_is_none(setup_task_tests):\n    \"\"\"persistent_allocation defaults if the persistent_allocation arg is None.\"\"\"\n    data = setup_task_tests\n    data[\"kwargs\"][\"persistent_allocation\"] = None\n    new_task = Task(**data[\"kwargs\"])\n    assert new_task.persistent_allocation is True\n\n\ndef test_persistent_allocation_attr_is_set_to_none(setup_task_tests):\n    \"\"\"persistent_allocation defaults if it is set to None.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    new_task.persistent_allocation = None\n    assert new_task.persistent_allocation is True\n\n\ndef test_persistent_allocation_arg_is_not_bool(setup_task_tests):\n    \"\"\"persistent_allocation is converted to bool if arg is not a bool value.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n\n    test_value = \"not a bool\"\n    kwargs[\"persistent_allocation\"] = test_value\n    new_task1 = Task(**kwargs)\n    assert bool(test_value) == new_task1.persistent_allocation\n\n    test_value = 0\n    kwargs[\"persistent_allocation\"] = test_value\n    new_task2 = Task(**kwargs)\n    assert bool(test_value) == new_task2.persistent_allocation\n\n\ndef test_persistent_allocation_attr_is_not_bool(setup_task_tests):\n    \"\"\"persistent_allocation attr is converted to a bool if is not set to a bool.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n\n    test_value = \"not a bool\"\n    new_task.persistent_allocation = test_value\n    assert bool(test_value) == new_task.persistent_allocation\n\n    test_value = 0\n    new_task.persistent_allocation = test_value\n    assert bool(test_value) == new_task.persistent_allocation\n\n\ndef test_persistent_allocation_arg_is_working_as_expected(setup_task_tests):\n    \"\"\"persistent_allocation arg is passed to the persistent_allocation attr.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"persistent_allocation\"] = False\n    new_task = Task(**kwargs)\n    assert new_task.persistent_allocation is False\n\n\ndef test_persistent_allocation_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"persistent_allocation attr value can be correctly set.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n\n    new_task.persistent_allocation = False\n    assert new_task.persistent_allocation is False\n\n\ndef test_path_attr_is_read_only(setup_task_tests):\n    \"\"\"path attr is read only.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(AttributeError) as cm:\n        new_task.path = \"some_path\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'path'\",\n    }.get(sys.version_info.minor, \"property 'path' of 'Task' object has no setter\")\n\n    assert str(cm.value) == error_message\n\n\ndef test_path_attr_raises_a_runtime_error_if_no_filename_template_found(\n    setup_task_tests,\n):\n    \"\"\"path attr raises RuntimeError if no FilenameTemplate w/ matching entity_type.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(RuntimeError) as cm:\n        _ = new_task.path\n\n    assert (\n        str(cm.value)\n        == \"There are no suitable FilenameTemplate (target_entity_type == \"\n        \"'Task') defined in the Structure of the related Project \"\n        \"instance, please create a new \"\n        \"stalker.models.template.FilenameTemplate instance with its \"\n        \"'target_entity_type' attribute is set to 'Task' and assign it \"\n        \"to the `templates` attribute of the structure of the project\"\n    )\n\n\ndef test_path_attr_raises_a_runtime_error_if_no_matching_filename_template_found(\n    setup_task_tests,\n):\n    \"\"\"path attr raises RuntimeError if no FilenameTemplate w/ matching entity_type.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    ft = FilenameTemplate(\n        name=\"Asset Filename Template\",\n        target_entity_type=\"Asset\",\n        path=\"{{project.code}}/{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/{%- endfor -%}\",\n        filename=\"{{task.nice_name}}\"\n        '_v{{\"%03d\"|format(version.version_number)}}{{extension}}',\n    )\n    structure = Structure(name=\"Movie Project Structure\", templates=[ft])\n    data[\"test_project1\"].structure = structure\n    with pytest.raises(RuntimeError) as cm:\n        _ = new_task.path\n\n    assert (\n        str(cm.value)\n        == \"There are no suitable FilenameTemplate (target_entity_type == \"\n        \"'Task') defined in the Structure of the related Project \"\n        \"instance, please create a new \"\n        \"stalker.models.template.FilenameTemplate instance with its \"\n        \"'target_entity_type' attribute is set to 'Task' and assign it \"\n        \"to the `templates` attribute of the structure of the project\"\n    )\n\n\ndef test_path_attr_is_the_rendered_vers_of_the_related_filename_template_in_the_project(\n    setup_task_tests,\n):\n    \"\"\"path attr is the rendered from the FilenameTemplate with matching entity_type.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n\n    ft = FilenameTemplate(\n        name=\"Task Filename Template\",\n        target_entity_type=\"Task\",\n        path=\"{{project.code}}/{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/{%- endfor -%}\",\n        filename=\"{{task.nice_name}}\"\n        '_v{{\"%03d\"|format(version.version_number)}}{{extension}}',\n    )\n\n    structure = Structure(name=\"Movie Project Structure\", templates=[ft])\n\n    data[\"test_project1\"].structure = structure\n\n    assert new_task.path == \"tp1/Modeling\"\n    data[\"test_project1\"].structure = None\n\n\ndef test_absolute_path_attr_is_read_only(setup_task_tests):\n    \"\"\"absolute_path is read only.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(AttributeError) as cm:\n        new_task.absolute_path = \"some_path\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'absolute_path'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'absolute_path' of 'Task' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_absolute_path_attr_raises_a_runtime_error_if_no_filename_template_found(\n    setup_task_tests,\n):\n    \"\"\"absolute_path attr raises RuntimeError.\n\n    if there are no FilenameTemplate with matching entity_type.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(RuntimeError) as cm:\n        _ = new_task.absolute_path\n\n    assert (\n        str(cm.value)\n        == \"There are no suitable FilenameTemplate (target_entity_type == \"\n        \"'Task') defined in the Structure of the related Project \"\n        \"instance, please create a new \"\n        \"stalker.models.template.FilenameTemplate instance with its \"\n        \"'target_entity_type' attribute is set to 'Task' and assign it \"\n        \"to the `templates` attribute of the structure of the project\"\n    )\n\n\ndef test_absolute_path_attr_raises_a_runtime_error_if_no_matching_filename_template(\n    setup_task_tests,\n):\n    \"\"\"absolute_path attr raises RuntimeError.\n\n    if there is no FilenameTemplate with matching entity_type.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n\n    ft = FilenameTemplate(\n        name=\"Asset Filename Template\",\n        target_entity_type=\"Asset\",\n        path=\"{{project.code}}/{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/{%- endfor -%}\",\n        filename=\"{{task.nice_name}}\"\n        '_v{{\"%03d\"|format(version.version_number)}}{{extension}}',\n    )\n\n    structure = Structure(name=\"Movie Project Structure\", templates=[ft])\n\n    data[\"test_project1\"].structure = structure\n    with pytest.raises(RuntimeError) as cm:\n        _ = new_task.path\n\n    assert (\n        str(cm.value)\n        == \"There are no suitable FilenameTemplate (target_entity_type == \"\n        \"'Task') defined in the Structure of the related Project \"\n        \"instance, please create a new \"\n        \"stalker.models.template.FilenameTemplate instance with its \"\n        \"'target_entity_type' attribute is set to 'Task' and assign it \"\n        \"to the `templates` attribute of the structure of the project\"\n    )\n\n\ndef test_absolute_path_attr_is_rendered_version_of_related_filename_template_in_project(\n    setup_task_tests,\n):\n    \"\"\"absolute_path attr is rendered vers. of FilenameTemplate matching entity_type.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n\n    ft = FilenameTemplate(\n        name=\"Task Filename Template\",\n        target_entity_type=\"Task\",\n        path=\"{{project.repository.path}}/{{project.code}}/\"\n        \"{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/\"\n        \"{%- endfor -%}\",\n        filename=\"{{task.nice_name}}\"\n        '_v{{\"%03d\"|format(version.version_number)}}{{extension}}',\n    )\n\n    structure = Structure(name=\"Movie Project Structure\", templates=[ft])\n    data[\"test_project1\"].structure = structure\n\n    assert (\n        os.path.normpath(\n            \"{}/tp1/Modeling\".format(data[\"test_project1\"].repositories[0].path)\n        ).replace(\"\\\\\", \"/\")\n        == new_task.absolute_path\n    )\n\n\ndef test_good_arg_is_skipped(setup_task_tests):\n    \"\"\"good attr is None if good arg is skipped.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    try:\n        kwargs.pop(\"good\")\n    except KeyError:\n        pass\n\n    new_task = Task(**kwargs)\n    # DBSession.add(new_task)\n    # DBSession.commit()\n    assert new_task.good is None\n\n\ndef test_good_arg_is_none(setup_task_tests):\n    \"\"\"good attr is None if good arg is None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"good\"] = None\n    new_task = Task(**kwargs)\n    # DBSession.add(new_task)\n    # DBSession.commit()\n    assert new_task.good is None\n\n\ndef test_good_attr_is_none(setup_task_tests):\n    \"\"\"it is possible to set the good attr to None.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"good\"] = Good(name=\"Some Good\")\n    new_task = Task(**kwargs)\n    # DBSession.add(new_task)\n    # DBSession.commit()\n    assert new_task.good is not None\n    new_task.good = None\n    assert new_task.good is None\n\n\ndef test_good_arg_is_not_a_good_instance(setup_task_tests):\n    \"\"\"TypeError raised if the good arg value is not a Good instance.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"good\"] = \"not a good instance\"\n    with pytest.raises(TypeError) as cm:\n        Task(**kwargs)\n\n    assert str(cm.value) == (\n        \"Task.good should be a stalker.models.budget.Good instance, \"\n        \"not str: 'not a good instance'\"\n    )\n\n\ndef test_good_attr_is_not_a_good_instance(setup_task_tests):\n    \"\"\"TypeError raised if the good attr is not set to a Good instance.\"\"\"\n    data = setup_task_tests\n    new_task = Task(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_task.good = \"not a good instance\"\n\n    assert str(cm.value) == (\n        \"Task.good should be a stalker.models.budget.Good instance, \"\n        \"not str: 'not a good instance'\"\n    )\n\n\ndef test_good_arg_is_working_as_expected(setup_task_tests):\n    \"\"\"good arg value is passed to the good attr.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_good = Good(name=\"Some Good\")\n    kwargs[\"good\"] = new_good\n    new_task = Task(**kwargs)\n    assert new_task.good == new_good\n\n\ndef test_good_attr_is_working_as_expected(setup_task_tests):\n    \"\"\"good attr value can be correctly set.\"\"\"\n    data = setup_task_tests\n    new_good = Good(name=\"Some Good\")\n    new_task = Task(**data[\"kwargs\"])\n    assert new_task.good != new_good\n    new_task.good = new_good\n    assert new_task.good == new_good\n\n\n@pytest.mark.parametrize(\"schedule_unit\", [\"d\", TimeUnit.Day])\ndef test_reschedule_on_a_container_task(setup_task_tests, schedule_unit):\n    \"\"\"_reschedule on a container task will return immediately.\"\"\"\n    data = setup_task_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = None\n\n    task_a = Task(**kwargs)\n    task_b = Task(**kwargs)\n    task_c = Task(**kwargs)\n\n    task_b.parent = task_a\n    task_a.parent = task_c\n\n    start = task_a.start\n    end = task_a.end\n    assert task_a._reschedule(10, schedule_unit) is None\n    assert task_a.start == start\n    assert task_a.end == end\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_task_db_tests(setup_postgresql_db):\n    \"\"\"stalker.models.task.Task class with a DB.\"\"\"\n    data = dict()\n    data[\"status_wfd\"] = Status.query.filter_by(code=\"WFD\").first()\n    data[\"status_rts\"] = Status.query.filter_by(code=\"RTS\").first()\n    data[\"status_wip\"] = Status.query.filter_by(code=\"WIP\").first()\n    data[\"status_prev\"] = Status.query.filter_by(code=\"PREV\").first()\n    data[\"status_hrev\"] = Status.query.filter_by(code=\"HREV\").first()\n    data[\"status_drev\"] = Status.query.filter_by(code=\"DREV\").first()\n    data[\"status_oh\"] = Status.query.filter_by(code=\"OH\").first()\n    data[\"status_stop\"] = Status.query.filter_by(code=\"STOP\").first()\n    data[\"status_cmpl\"] = Status.query.filter_by(code=\"CMPL\").first()\n    data[\"task_status_list\"] = StatusList.query.filter_by(\n        target_entity_type=\"Task\"\n    ).first()\n    data[\"test_movie_project_type\"] = Type(\n        name=\"Movie Project\",\n        code=\"movie\",\n        target_entity_type=\"Project\",\n    )\n    data[\"test_repository_type\"] = Type(\n        name=\"Test Repository Type\",\n        code=\"test\",\n        target_entity_type=\"Repository\",\n    )\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"test_repository_type\"],\n        linux_path=\"/mnt/T/\",\n        windows_path=\"T:/\",\n        macos_path=\"/Volumes/T/\",\n    )\n    data[\"test_user1\"] = User(\n        name=\"User1\",\n        login=\"user1\",\n        email=\"user1@user1.com\",\n        password=\"1234\",\n    )\n    data[\"test_user2\"] = User(\n        name=\"User2\",\n        login=\"user2\",\n        email=\"user2@user2.com\",\n        password=\"1234\",\n    )\n    data[\"test_user3\"] = User(\n        name=\"User3\",\n        login=\"user3\",\n        email=\"user3@user3.com\",\n        password=\"1234\",\n    )\n    data[\"test_user4\"] = User(\n        name=\"User4\",\n        login=\"user4\",\n        email=\"user4@user4.com\",\n        password=\"1234\",\n    )\n    data[\"test_user5\"] = User(\n        name=\"User5\",\n        login=\"user5\",\n        email=\"user5@user5.com\",\n        password=\"1234\",\n    )\n    data[\"test_project1\"] = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=data[\"test_movie_project_type\"],\n        repositories=[data[\"test_repository\"]],\n    )\n    data[\"test_dependent_task1\"] = Task(\n        name=\"Dependent Task1\",\n        project=data[\"test_project1\"],\n        status_list=data[\"task_status_list\"],\n        responsible=[data[\"test_user1\"]],\n    )\n    data[\"test_dependent_task2\"] = Task(\n        name=\"Dependent Task2\",\n        project=data[\"test_project1\"],\n        status_list=data[\"task_status_list\"],\n        responsible=[data[\"test_user1\"]],\n    )\n    data[\"kwargs\"] = {\n        \"name\": \"Modeling\",\n        \"description\": \"A Modeling Task\",\n        \"project\": data[\"test_project1\"],\n        \"priority\": 500,\n        \"responsible\": [data[\"test_user1\"]],\n        \"resources\": [data[\"test_user1\"], data[\"test_user2\"]],\n        \"alternative_resources\": [\n            data[\"test_user3\"],\n            data[\"test_user4\"],\n            data[\"test_user5\"],\n        ],\n        \"allocation_strategy\": \"minloaded\",\n        \"persistent_allocation\": True,\n        \"watchers\": [data[\"test_user3\"]],\n        \"bid_timing\": 4,\n        \"bid_unit\": TimeUnit.Day,\n        \"schedule_timing\": 1,\n        \"schedule_unit\": TimeUnit.Day,\n        \"start\": datetime.datetime(2013, 4, 8, 13, 0, tzinfo=pytz.utc),\n        \"end\": datetime.datetime(2013, 4, 8, 18, 0, tzinfo=pytz.utc),\n        \"depends_on\": [data[\"test_dependent_task1\"], data[\"test_dependent_task2\"]],\n        \"time_logs\": [],\n        \"versions\": [],\n        \"is_milestone\": False,\n        \"status\": 0,\n        \"status_list\": data[\"task_status_list\"],\n    }\n    # create a test Task\n    DBSession.add_all(\n        [\n            data[\"test_movie_project_type\"],\n            data[\"test_repository_type\"],\n            data[\"test_repository\"],\n            data[\"test_user1\"],\n            data[\"test_user2\"],\n            data[\"test_user3\"],\n            data[\"test_user4\"],\n            data[\"test_user5\"],\n            data[\"test_project1\"],\n            data[\"test_dependent_task1\"],\n            data[\"test_dependent_task2\"],\n        ]\n    )\n    DBSession.commit()\n    return data\n\n\ndef test_open_tickets_attr_is_working_as_expected(setup_task_db_tests):\n    \"\"\"open_tickets attr is working as expected.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    DBSession.add(new_task)\n    DBSession.commit()\n\n    # create ticket statuses\n    stalker.db.setup.init()\n\n    new_ticket1 = Ticket(project=new_task.project, links=[new_task])\n    DBSession.add(new_ticket1)\n    DBSession.commit()\n\n    new_ticket2 = Ticket(project=new_task.project, links=[new_task])\n    DBSession.add(new_ticket2)\n    DBSession.commit()\n\n    # close this ticket\n    new_ticket2.resolve(None, \"fixed\")\n    DBSession.commit()\n\n    # add some other tickets\n    new_ticket3 = Ticket(\n        project=new_task.project,\n        links=[],\n    )\n    DBSession.add(new_ticket3)\n    DBSession.commit()\n\n    assert new_task.open_tickets == [new_ticket1]\n\n\ndef test_tickets_attr_is_working_as_expected(setup_task_db_tests):\n    \"\"\"tickets attr is working as expected.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    new_task = Task(**kwargs)\n    DBSession.add(new_task)\n    DBSession.commit()\n\n    # create ticket statuses\n    stalker.db.setup.init()\n\n    new_ticket1 = Ticket(project=new_task.project, links=[new_task])\n    DBSession.add(new_ticket1)\n    DBSession.commit()\n\n    new_ticket2 = Ticket(project=new_task.project, links=[new_task])\n    DBSession.add(new_ticket2)\n    DBSession.commit()\n\n    # add some other tickets\n    new_ticket3 = Ticket(project=new_task.project, links=[])\n    DBSession.add(new_ticket3)\n    DBSession.commit()\n\n    assert sorted(new_task.tickets, key=lambda x: x.name) == sorted(\n        [new_ticket1, new_ticket2], key=lambda x: x.name\n    )\n\n\ndef test_percent_complete_attr_is_not_using_any_time_logs_for_a_duration_task(\n    setup_task_db_tests,\n):\n    \"\"\"percent_complete attr doesn't use any time log info if task is duration based.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    kwargs[\"schedule_model\"] = ScheduleModel.Duration\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    new_task = Task(**kwargs)\n    new_task.computed_start = now + td(days=1)\n    new_task.computed_end = now + td(days=2)\n\n    resource1 = new_task.resources[0]\n    _ = TimeLog(\n        task=new_task,\n        resource=resource1,\n        start=now + td(days=1),\n        end=now + td(days=2),\n    )\n\n    assert new_task.percent_complete == 0\n\n\ndef test_percent_complete_attr_is_working_as_expected_for_a_container_task(\n    setup_task_db_tests,\n):\n    \"\"\"percent complete attr is working as expected for a container task.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []  # remove dependencies just to make it\n    # easy to create time logs after stalker\n    # v0.2.6.1\n\n    new_task = Task(**kwargs)\n    new_task.status = data[\"status_rts\"]\n    DBSession.add(new_task)\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    defaults[\"timing_resolution\"] = td(hours=1)\n    defaults[\"daily_working_hours\"] = 9\n\n    parent_task = Task(**kwargs)\n    DBSession.add(parent_task)\n\n    new_task.time_logs = []\n    resource1 = new_task.resources[0]\n    resource2 = new_task.resources[1]\n    tlog1 = TimeLog(\n        task=new_task,\n        resource=resource1,\n        start=now - td(hours=4),\n        end=now - td(hours=2),\n    )\n    DBSession.add(tlog1)\n\n    assert tlog1 in new_task.time_logs\n\n    tlog2 = TimeLog(\n        task=new_task,\n        resource=resource2,\n        start=now - td(hours=4),\n        end=now + td(hours=1),\n    )\n    DBSession.add(tlog2)\n    DBSession.commit()\n\n    new_task.parent = parent_task\n    DBSession.commit()\n\n    assert tlog2 in new_task.time_logs\n    assert new_task.total_logged_seconds == 7 * 3600\n    assert new_task.schedule_seconds == 9 * 3600\n    assert new_task.percent_complete == pytest.approx(77.7777778)\n    assert parent_task.percent_complete == pytest.approx(77.7777778)\n\n\ndef test_percent_complete_attr_is_working_as_expected_for_a_container_task_with_no_data_1(\n    setup_task_db_tests,\n):\n    \"\"\"percent complete attr is working as expected for a container task with no data.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []  # remove dependencies just to make it\n    # easy to create time logs after stalker\n    # v0.2.6.1\n\n    new_task = Task(**kwargs)\n    new_task.status = data[\"status_rts\"]\n    DBSession.add(new_task)\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    defaults[\"timing_resolution\"] = td(hours=1)\n    defaults[\"daily_working_hours\"] = 9\n\n    parent_task = Task(**kwargs)\n    DBSession.add(parent_task)\n\n    new_task.time_logs = []\n    resource1 = new_task.resources[0]\n    resource2 = new_task.resources[1]\n    tlog1 = TimeLog(\n        task=new_task,\n        resource=resource1,\n        start=now - td(hours=4),\n        end=now - td(hours=2),\n    )\n    DBSession.add(tlog1)\n    assert tlog1 in new_task.time_logs\n\n    tlog2 = TimeLog(\n        task=new_task,\n        resource=resource2,\n        start=now - td(hours=4),\n        end=now + td(hours=1),\n    )\n    DBSession.add(tlog2)\n    DBSession.commit()\n\n    new_task.parent = parent_task\n    DBSession.commit()\n\n    assert tlog2 in new_task.time_logs\n    assert new_task.total_logged_seconds == 7 * 3600\n    assert new_task.schedule_seconds == 9 * 3600\n    assert new_task.percent_complete == pytest.approx(77.7777778)\n\n    parent_task._total_logged_seconds = None\n    # parent_task._schedule_seconds = None\n    assert parent_task.percent_complete == pytest.approx(77.7777778)\n\n\ndef test_percent_complete_attr_is_working_as_expected_for_a_container_task_with_no_data_2(\n    setup_task_db_tests,\n):\n    \"\"\"percent complete attr is working as expected for a container task with no data.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []  # remove dependencies just to make it\n    # easy to create time logs after stalker\n    # v0.2.6.1\n\n    new_task = Task(**kwargs)\n    new_task.status = data[\"status_rts\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    defaults[\"timing_resolution\"] = td(hours=1)\n    defaults[\"daily_working_hours\"] = 9\n\n    parent_task = Task(**kwargs)\n\n    new_task.time_logs = []\n    resource1 = new_task.resources[0]\n    resource2 = new_task.resources[1]\n    tlog1 = TimeLog(\n        task=new_task,\n        resource=resource1,\n        start=now - td(hours=4),\n        end=now - td(hours=2),\n    )\n    DBSession.add(tlog1)\n\n    assert tlog1 in new_task.time_logs\n\n    tlog2 = TimeLog(\n        task=new_task,\n        resource=resource2,\n        start=now - td(hours=4),\n        end=now + td(hours=1),\n    )\n    DBSession.add(tlog2)\n    DBSession.commit()\n\n    new_task.parent = parent_task\n    DBSession.commit()\n\n    assert tlog2 in new_task.time_logs\n    assert new_task.total_logged_seconds == 7 * 3600\n    assert new_task.schedule_seconds == 9 * 3600\n    assert new_task.percent_complete == pytest.approx(77.7777778)\n\n    # parent_task._total_logged_seconds = None\n    parent_task._schedule_seconds = None\n    assert parent_task.percent_complete == pytest.approx(77.7777778)\n\n\ndef test_percent_complete_attr_working_okay_for_a_task_w_effort_and_duration_children(\n    setup_task_db_tests,\n):\n    \"\"\"percent complete attr is okay with effort and duration based children tasks.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []  # remove dependencies just to make it\n    # easy to create time logs after stalker\n    # v0.2.6.1\n    dt = datetime.datetime\n    td = datetime.timedelta\n\n    defaults[\"timing_resolution\"] = td(hours=1)\n    defaults[\"daily_working_hours\"] = 9\n\n    now = DateRangeMixin.round_time(dt.now(pytz.utc))\n\n    new_task1 = Task(**kwargs)\n    new_task1.status = data[\"status_rts\"]\n    DBSession.add(new_task1)\n\n    parent_task = Task(**kwargs)\n    DBSession.add(parent_task)\n\n    new_task1.time_logs = []\n    tlog1 = TimeLog(\n        task=new_task1,\n        resource=new_task1.resources[0],\n        start=now - td(hours=4),\n        end=now - td(hours=2),\n    )\n    DBSession.add(tlog1)\n    assert tlog1 in new_task1.time_logs\n\n    tlog2 = TimeLog(\n        task=new_task1,\n        resource=new_task1.resources[1],\n        start=now - td(hours=6),\n        end=now - td(hours=1),\n    )\n    DBSession.add(tlog2)\n    DBSession.commit()\n\n    # create a duration based task\n    new_task2 = Task(**kwargs)\n    new_task2.status = data[\"status_rts\"]\n    new_task2.schedule_model = ScheduleModel.Duration\n    new_task2.start = now - td(days=1, hours=1)\n    new_task2.end = now - td(hours=1)\n    DBSession.add(new_task2)\n    DBSession.commit()\n\n    new_task1.parent = parent_task\n    DBSession.commit()\n\n    new_task2.parent = parent_task\n    DBSession.commit()\n\n    assert tlog2 in new_task1.time_logs\n    assert new_task1.total_logged_seconds == 7 * 3600\n    assert new_task1.schedule_seconds == 9 * 3600\n    assert new_task1.percent_complete == pytest.approx(77.7777778)\n    assert (\n        new_task2.total_logged_seconds == 24 * 3600\n    )  # 1 day for a duration task is 24 hours\n    assert (\n        new_task2.schedule_seconds == 24 * 3600\n    )  # 1 day for a duration task is 24 hours\n    assert new_task2.percent_complete == 100\n\n    # as if there are 9 * 3600 seconds of time logs entered to new_task2\n    assert parent_task.percent_complete == pytest.approx(93.939393939)\n\n\ndef test_percent_complete_attr_is_okay_for_a_task_with_effort_and_length_based_children(\n    setup_task_db_tests,\n):\n    \"\"\"percent complete attr is okay with effort and length based children tasks.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []  # remove dependencies just to make it\n    # easy to create time logs after stalker\n    # v0.2.6.1\n    dt = datetime.datetime\n    td = datetime.timedelta\n\n    defaults[\"timing_resolution\"] = td(hours=1)\n    defaults[\"daily_working_hours\"] = 9\n\n    now = DateRangeMixin.round_time(dt.now(pytz.utc))\n\n    new_task1 = Task(**kwargs)\n    new_task1.status = data[\"status_rts\"]\n    DBSession.add(new_task1)\n\n    parent_task = Task(**kwargs)\n    DBSession.add(parent_task)\n\n    new_task1.time_logs = []\n    tlog1 = TimeLog(\n        task=new_task1,\n        resource=new_task1.resources[0],\n        start=now - td(hours=4),\n        end=now - td(hours=2),\n    )\n    DBSession.add(tlog1)\n\n    assert tlog1 in new_task1.time_logs\n\n    tlog2 = TimeLog(\n        task=new_task1,\n        resource=new_task1.resources[1],\n        start=now - td(hours=6),\n        end=now - td(hours=1),\n    )\n    DBSession.add(tlog2)\n    DBSession.commit()\n\n    # create a length based task\n    new_task2 = Task(**kwargs)\n    new_task2.status = data[\"status_rts\"]\n    new_task2.schedule_model = ScheduleModel.Length\n    new_task2.start = now - td(hours=10)\n    new_task2.end = now - td(hours=1)\n    DBSession.add(new_task2)\n    DBSession.commit()\n\n    new_task1.parent = parent_task\n    DBSession.commit()\n\n    new_task2.parent = parent_task\n    DBSession.commit()\n\n    assert tlog2 in new_task1.time_logs\n    assert new_task1.total_logged_seconds == 7 * 3600\n    assert new_task1.schedule_seconds == 9 * 3600\n    assert new_task1.percent_complete == pytest.approx(77.7777778)\n    assert (\n        new_task2.total_logged_seconds == 9 * 3600\n    )  # 1 day for a length task is 9 hours\n    assert new_task2.schedule_seconds == 9 * 3600  # 1 day for a length task is 9 hours\n    assert new_task2.percent_complete == 100\n\n    # as if there are 9 * 3600 seconds of time logs entered to new_task2\n    assert parent_task.percent_complete == pytest.approx(88.8888889)\n\n\ndef test_percent_complete_attr_is_working_as_expected_for_a_leaf_task(\n    setup_task_db_tests,\n):\n    \"\"\"percent_complete attr is working as expected for a leaf task.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n\n    new_task = Task(**kwargs)\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    new_task.time_logs = []\n    # we can't use new_task.resources list directly between commits,\n    # as apparently the order is changing after a TimeLog is created\n    resource1 = new_task.resources[0]\n    resource2 = new_task.resources[1]\n    tlog1 = TimeLog(task=new_task, resource=resource1, start=now, end=now + td(hours=8))\n    DBSession.add(tlog1)\n    DBSession.commit()\n\n    assert tlog1 in new_task.time_logs\n\n    tlog2 = TimeLog(\n        task=new_task, resource=resource2, start=now, end=now + td(hours=12)\n    )\n    DBSession.add(tlog2)\n    DBSession.commit()\n\n    assert tlog2 in new_task.time_logs\n    assert new_task.total_logged_seconds == 20 * 3600\n    assert new_task.percent_complete == 20.0 / 9.0 * 100.0\n\n\ndef test_time_logs_attr_is_working_as_expected(setup_task_db_tests):\n    \"\"\"time_log attr is working as expected.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    new_task1 = Task(**kwargs)\n\n    assert new_task1.depends_on == []\n\n    now = datetime.datetime.now(pytz.utc)\n    dt = datetime.timedelta\n\n    new_time_log1 = TimeLog(\n        task=new_task1,\n        resource=new_task1.resources[0],\n        start=now + dt(100),\n        end=now + dt(101),\n    )\n\n    new_time_log2 = TimeLog(\n        task=new_task1,\n        resource=new_task1.resources[0],\n        start=now + dt(101),\n        end=now + dt(102),\n    )\n\n    # create a new task\n    kwargs[\"name\"] = \"New Task\"\n    new_task2 = Task(**kwargs)\n\n    # create a new TimeLog for that task\n    new_time_log3 = TimeLog(\n        task=new_task2,\n        resource=new_task2.resources[0],\n        start=now + dt(102),\n        end=now + dt(103),\n    )\n    # logger.debug('DBSession.get(Task, 37): {}'.format(DBSession.get(Task, 37)))\n\n    assert new_task2.depends_on == []\n\n    # check if everything is in place\n    assert new_time_log1 in new_task1.time_logs\n    assert new_time_log2 in new_task1.time_logs\n    assert new_time_log3 in new_task2.time_logs\n\n    # now move the time_log to test_task1\n    new_task1.time_logs.append(new_time_log3)\n\n    # check if new_time_log3 is in test_task1\n    assert new_time_log3 in new_task1.time_logs\n\n    # there needs to be a database session commit to remove the time_log\n    # from the previous tasks time_logs attr\n\n    assert new_time_log3 in new_task1.time_logs\n    assert new_time_log3 not in new_task2.time_logs\n\n\ndef test_total_logged_seconds_attr_is_correct_if_the_time_log_of_child_is_changed(\n    setup_task_db_tests,\n):\n    \"\"\"total_logged_seconds attr is correct if time log updated on children.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    _ = Task(**kwargs)\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    parent_task = Task(**kwargs)\n    child_task = Task(**kwargs)\n    parent_task.children.append(child_task)\n\n    tlog1 = TimeLog(\n        task=child_task,\n        resource=child_task.resources[0],\n        start=now,\n        end=now + td(hours=8),\n    )\n\n    assert parent_task.total_logged_seconds == 8 * 60 * 60\n\n    # now update the time log\n    tlog1.end = now + td(hours=16)\n    assert parent_task.total_logged_seconds == 16 * 60 * 60\n\n\ndef test_total_logged_seconds_is_the_sum_of_all_time_logs(setup_task_db_tests):\n    \"\"\"total_logged_seconds is the sum of all time_logs in hours.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    new_task = Task(**kwargs)\n    new_task.depends_on = []\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    new_task.time_logs = []\n\n    # apparently the new_task.resources order is changing between commits.\n    resource1 = new_task.resources[0]\n    resource2 = new_task.resources[1]\n    tlog1 = TimeLog(task=new_task, resource=resource1, start=now, end=now + td(hours=8))\n    DBSession.add(tlog1)\n    DBSession.commit()\n\n    assert tlog1 in new_task.time_logs\n\n    tlog2 = TimeLog(\n        task=new_task, resource=resource2, start=now, end=now + td(hours=12)\n    )\n    DBSession.add(tlog2)\n    DBSession.commit()\n\n    assert tlog2 in new_task.time_logs\n    assert new_task.total_logged_seconds == 20 * 3600\n\n\ndef test_total_logged_seconds_calls_update_schedule_info(\n    setup_task_db_tests,\n):\n    \"\"\"total_logged_seconds is the sum of all time_logs of the child tasks.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    new_task = Task(**kwargs)\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    kwargs.pop(\"schedule_timing\")\n    kwargs.pop(\"schedule_unit\")\n    parent_task = Task(**kwargs)\n    new_task.parent = parent_task\n    new_task.time_logs = []\n    resource1 = new_task.resources[0]\n    resource2 = new_task.resources[1]\n    tlog1 = TimeLog(task=new_task, resource=resource1, start=now, end=now + td(hours=8))\n    DBSession.add(tlog1)\n    DBSession.commit()\n    assert tlog1 in new_task.time_logs\n    tlog2 = TimeLog(\n        task=new_task, resource=resource2, start=now, end=now + td(hours=12)\n    )\n    DBSession.add(tlog2)\n    DBSession.commit()\n    # set the total_logged_seconds to None\n    # so the getter calls the update_schedule_info\n    parent_task._total_logged_seconds = None\n    assert tlog2 in new_task.time_logs\n    assert new_task.total_logged_seconds == 20 * 3600\n    assert parent_task.total_logged_seconds == 20 * 3600\n\n\ndef test_update_schedule_info_on_a_container_of_containers_task(\n    setup_task_db_tests,\n):\n    \"\"\"total_logged_seconds is the sum of all time_logs of the child tasks.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    new_task = Task(**kwargs)\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    kwargs.pop(\"schedule_timing\")\n    kwargs.pop(\"schedule_unit\")\n    parent_task = Task(**kwargs)\n    root_task = Task(**kwargs)\n    new_task.parent = parent_task\n    parent_task.parent = root_task\n    new_task.time_logs = []\n    # apparently the new_task.resources order is changing between commits.\n    resource1 = new_task.resources[0]\n    resource2 = new_task.resources[1]\n    tlog1 = TimeLog(task=new_task, resource=resource1, start=now, end=now + td(hours=8))\n    DBSession.add(new_task)\n    DBSession.add(parent_task)\n    DBSession.add(root_task)\n    DBSession.add(tlog1)\n    DBSession.commit()\n    assert tlog1 in new_task.time_logs\n    tlog2 = TimeLog(\n        task=new_task, resource=resource2, start=now, end=now + td(hours=12)\n    )\n    DBSession.add(tlog2)\n    DBSession.commit()\n    # set the total_logged_seconds to None\n    # so the getter calls the update_schedule_info\n    root_task.update_schedule_info()\n\n\ndef test_update_schedule_info_with_leaf_tasks(\n    setup_task_db_tests,\n):\n    \"\"\"total_logged_seconds is the sum of all time_logs of the child tasks.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    new_task = Task(**kwargs)\n    new_task.update_schedule_info()\n\n\ndef test_total_logged_seconds_is_the_sum_of_all_time_logs_of_children(\n    setup_task_db_tests,\n):\n    \"\"\"total_logged_seconds is the sum of all time_logs of the child tasks.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n    new_task = Task(**kwargs)\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    kwargs.pop(\"schedule_timing\")\n    kwargs.pop(\"schedule_unit\")\n    parent_task = Task(**kwargs)\n    new_task.parent = parent_task\n    new_task.time_logs = []\n    # apparently the new_task.resources order is changing between commits.\n    resource1 = new_task.resources[0]\n    resource2 = new_task.resources[1]\n    tlog1 = TimeLog(task=new_task, resource=resource1, start=now, end=now + td(hours=8))\n    DBSession.add(tlog1)\n    DBSession.commit()\n    assert tlog1 in new_task.time_logs\n    tlog2 = TimeLog(\n        task=new_task, resource=resource2, start=now, end=now + td(hours=12)\n    )\n    DBSession.add(tlog2)\n    DBSession.commit()\n    assert tlog2 in new_task.time_logs\n    assert new_task.total_logged_seconds == 20 * 3600\n    assert parent_task.total_logged_seconds == 20 * 3600\n\n\ndef test_total_logged_seconds_is_the_sum_of_all_time_logs_of_children_deeper(\n    setup_task_db_tests,\n):\n    \"\"\"total_logged_seconds is the sum of all time_logs of the children (deeper).\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n\n    new_task = Task(**kwargs)\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    kwargs.pop(\"schedule_timing\")\n    kwargs.pop(\"schedule_unit\")\n\n    parent_task1 = Task(**kwargs)\n    assert parent_task1.total_logged_seconds == 0\n\n    parent_task2 = Task(**kwargs)\n    assert parent_task2.total_logged_seconds == 0\n\n    # create some other child\n    child = Task(**kwargs)\n\n    assert child.total_logged_seconds == 0\n    # create a TimeLog for that child\n    tlog1 = TimeLog(\n        task=child,\n        resource=child.resources[0],\n        start=now - td(hours=50),\n        end=now - td(hours=40),\n    )\n    DBSession.add(tlog1)\n    DBSession.commit()\n\n    assert child.total_logged_seconds == 10 * 3600\n    parent_task2.children.append(child)\n    assert parent_task2.total_logged_seconds == 10 * 3600\n\n    # data[\"test_task1\"].parent = parent_task\n    parent_task1.children.append(new_task)\n    assert parent_task1.total_logged_seconds == 0\n\n    parent_task1.parent = parent_task2\n    assert parent_task2.total_logged_seconds == 10 * 3600\n\n    # we can't use new_task.resources list directly between commits,\n    # as apparently the order is changing after a TimeLog is created\n    resource1 = new_task.resources[0]\n    resource2 = new_task.resources[1]\n\n    new_task.time_logs = []\n    tlog2 = TimeLog(task=new_task, resource=resource1, start=now, end=now + td(hours=8))\n    DBSession.add(tlog2)\n    DBSession.commit()\n\n    assert tlog2 in new_task.time_logs\n    assert new_task.total_logged_seconds == 8 * 3600\n    assert parent_task1.total_logged_seconds == 8 * 3600\n    assert parent_task2.total_logged_seconds == 18 * 3600\n\n    tlog3 = TimeLog(\n        task=new_task, resource=resource2, start=now, end=now + td(hours=12)\n    )\n    DBSession.add(tlog3)\n    DBSession.commit()\n\n    assert new_task.total_logged_seconds == 20 * 3600\n    assert parent_task1.total_logged_seconds == 20 * 3600\n    assert parent_task2.total_logged_seconds == 30 * 3600\n\n\ndef test_remaining_seconds_is_working_as_expected(setup_task_db_tests):\n    \"\"\"remaining hours is working as expected.\"\"\"\n    data = setup_task_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"depends_on\"] = []\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt(2013, 4, 19, 10, 0, tzinfo=pytz.utc)\n\n    kwargs[\"schedule_model\"] = ScheduleModel.Effort\n\n    # -------------- HOURS --------------\n    kwargs[\"schedule_timing\"] = 10\n    kwargs[\"schedule_unit\"] = TimeUnit.Hour\n    new_task = Task(**kwargs)\n\n    # create a time_log of 2 hours\n    resource1 = new_task.resources[0]\n    _ = TimeLog(task=new_task, start=now, duration=td(hours=2), resource=resource1)\n    # check\n    assert (\n        new_task.remaining_seconds\n        == new_task.schedule_seconds - new_task.total_logged_seconds\n    )\n\n    # -------------- DAYS --------------\n    kwargs[\"schedule_timing\"] = 23\n    kwargs[\"schedule_unit\"] = TimeUnit.Day\n    new_task = Task(**kwargs)\n\n    # create a time_log of 5 days\n    _ = TimeLog(\n        task=new_task,\n        start=now + td(hours=2),\n        end=now + td(days=5),\n        resource=resource1,\n    )\n    # check\n    assert (\n        new_task.remaining_seconds\n        == new_task.schedule_seconds - new_task.total_logged_seconds\n    )\n\n    # add another 2 hours\n    _ = TimeLog(\n        task=new_task,\n        start=now + td(days=5),\n        duration=td(hours=2),\n        resource=resource1,\n    )\n    assert (\n        new_task.remaining_seconds\n        == new_task.schedule_seconds - new_task.total_logged_seconds\n    )\n\n    # ------------------- WEEKS ------------------\n    kwargs[\"schedule_timing\"] = 2\n    kwargs[\"schedule_unit\"] = TimeUnit.Week\n    new_task = Task(**kwargs)\n\n    # create a time_log of 2 hours\n    tlog4 = TimeLog(\n        task=new_task,\n        start=now + td(days=6),\n        duration=td(hours=2),\n        resource=resource1,\n    )\n    new_task.time_logs.append(tlog4)\n\n    # check\n    assert (\n        new_task.remaining_seconds\n        == new_task.schedule_seconds - new_task.total_logged_seconds\n    )\n\n    # create a time_log of 1 week\n    tlog5 = TimeLog(\n        task=new_task,\n        start=now + td(days=7),\n        duration=td(weeks=1),\n        resource=resource1,\n    )\n    new_task.time_logs.append(tlog5)\n\n    # check\n    assert (\n        new_task.remaining_seconds\n        == new_task.schedule_seconds - new_task.total_logged_seconds\n    )\n\n    # ------------------ MONTH -------------------\n    kwargs[\"schedule_timing\"] = 2.5\n    kwargs[\"schedule_unit\"] = TimeUnit.Month\n    new_task = Task(**kwargs)\n\n    # create a time_log of 1 month or 30 days, remaining_seconds can be\n    # negative\n    tlog6 = TimeLog(\n        task=new_task,\n        start=now + td(days=15),\n        duration=td(days=30),\n        resource=resource1,\n    )\n    new_task.time_logs.append(tlog6)\n\n    # check\n    assert (\n        new_task.remaining_seconds\n        == new_task.schedule_seconds - new_task.total_logged_seconds\n    )\n\n    # ------------------ YEARS ---------------------\n    kwargs[\"schedule_timing\"] = 3.1\n    kwargs[\"schedule_unit\"] = TimeUnit.Year\n    new_task = Task(**kwargs)\n\n    # create a time_log of 1 month or 30 days, remaining_seconds can be\n    # negative\n    tlog8 = TimeLog(\n        task=new_task,\n        start=now + td(days=55),\n        duration=td(days=30),\n        resource=resource1,\n    )\n    new_task.time_logs.append(tlog8)\n    # check\n    assert (\n        new_task.remaining_seconds\n        == new_task.schedule_seconds - new_task.total_logged_seconds\n    )\n\n\ndef test_template_variables_for_non_shot_related_task(setup_task_db_tests):\n    \"\"\"_template_variables() for a non shot related task returns correct data.\"\"\"\n    data = setup_task_db_tests\n    task = Task(**data[\"kwargs\"])\n    assert task._template_variables() == {\n        \"asset\": None,\n        \"parent_tasks\": [task],\n        \"project\": data[\"test_project1\"],\n        \"scene\": None,\n        \"sequence\": None,\n        \"shot\": None,\n        \"task\": task,\n        \"type\": None,\n    }\n"
  },
  {
    "path": "tests/models/test_task_dependency.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests related to the TaskDependency class.\"\"\"\n\nimport pytest\n\nfrom sqlalchemy.exc import IntegrityError, SAWarning\n\nfrom stalker import defaults\nfrom stalker import Project\nfrom stalker import Repository\nfrom stalker import Structure\nfrom stalker import Task\nfrom stalker import TaskDependency\nfrom stalker import User\nfrom stalker.db.session import DBSession\nfrom stalker.models.enum import ScheduleModel, TimeUnit\nfrom stalker.models.enum import DependencyTarget\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_task_dependency_db_test(setup_postgresql_db):\n    \"\"\"set up the test TaskDependency class.\"\"\"\n    data = dict()\n    data[\"test_user1\"] = User(\n        name=\"Test User 1\", login=\"testuser1\", email=\"user1@test.com\", password=\"secret\"\n    )\n    DBSession.add(data[\"test_user1\"])\n\n    data[\"test_user2\"] = User(\n        name=\"Test User 2\", login=\"testuser2\", email=\"user2@test.com\", password=\"secret\"\n    )\n    DBSession.add(data[\"test_user2\"])\n\n    data[\"test_user3\"] = User(\n        name=\"Test User 3\", login=\"testuser3\", email=\"user3@test.com\", password=\"secret\"\n    )\n    DBSession.add(data[\"test_user3\"])\n\n    data[\"test_repo\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n    )\n    DBSession.add(data[\"test_repo\"])\n\n    data[\"test_structure\"] = Structure(name=\"test structure\")\n    DBSession.add(data[\"test_structure\"])\n\n    data[\"test_project1\"] = Project(\n        name=\"Test Project 1\",\n        code=\"TP1\",\n        repository=data[\"test_repo\"],\n        structure=data[\"test_structure\"],\n    )\n    DBSession.add(data[\"test_project1\"])\n    DBSession.commit()\n\n    # create three Tasks\n    data[\"test_task1\"] = Task(name=\"Test Task 1\", project=data[\"test_project1\"])\n    DBSession.add(data[\"test_task1\"])\n\n    data[\"test_task2\"] = Task(name=\"Test Task 2\", project=data[\"test_project1\"])\n    DBSession.add(data[\"test_task2\"])\n\n    data[\"test_task3\"] = Task(name=\"Test Task 3\", project=data[\"test_project1\"])\n    DBSession.add(data[\"test_task3\"])\n    DBSession.commit()\n\n    data[\"kwargs\"] = {\n        \"task\": data[\"test_task1\"],\n        \"depends_on\": data[\"test_task2\"],\n        \"dependency_target\": \"onend\",\n        \"gap_timing\": 0,\n        \"gap_unit\": TimeUnit.Hour,\n        \"gap_model\": ScheduleModel.Length,\n    }\n    return data\n\n\ndef test_task_argument_is_skipped(setup_task_dependency_db_test):\n    \"\"\"no error raised if the task argument is skipped.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"].pop(\"task\")\n    TaskDependency(**data[\"kwargs\"])\n\n\ndef test_task_argument_is_skipped_raises_error_on_commit(setup_task_dependency_db_test):\n    \"\"\"IntegrityError raised if the task arg is skipped and the session is committed.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"].pop(\"task\")\n    new_dependency = TaskDependency(**data[\"kwargs\"])\n    DBSession.add(new_dependency)\n    with pytest.raises(IntegrityError) as cm:\n        with pytest.warns(SAWarning) as _:\n            DBSession.commit()\n\n    assert (\n        '(psycopg2.errors.NotNullViolation) null value in column \"task_id\" of '\n        'relation \"Task_Dependencies\" violates not-null constraint' in str(cm.value)\n    )\n\n\ndef test_task_argument_is_not_a_task_instance(setup_task_dependency_db_test):\n    \"\"\"TypeError raised if the task arg is not a stalker.models.task.Task instance.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"task\"] = \"Not a Task instance\"\n    with pytest.raises(TypeError) as cm:\n        TaskDependency(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"TaskDependency.task should be and instance of \"\n        \"stalker.models.task.Task, not str: 'Not a Task instance'\"\n    )\n\n\ndef test_task_attribute_is_not_a_task_instance(setup_task_dependency_db_test):\n    \"\"\"TypeError raised if the task attr is not a stalker.models.task.Task instance.\"\"\"\n    data = setup_task_dependency_db_test\n    new_dep = TaskDependency(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_dep.task = \"not a task\"\n\n    assert (\n        str(cm.value) == \"TaskDependency.task should be and instance of \"\n        \"stalker.models.task.Task, not str: 'not a task'\"\n    )\n\n\ndef test_task_argument_is_working_as_expected(setup_task_dependency_db_test):\n    \"\"\"task argument value is correctly passed to task attribute.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"test_task1\"].depends_on = []\n    new_dep = TaskDependency(**data[\"kwargs\"])\n    assert new_dep.task == data[\"test_task1\"]\n\n\ndef test_depends_on_argument_is_skipped(setup_task_dependency_db_test):\n    \"\"\"no error raised if the depends_on argument is skipped.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"].pop(\"depends_on\")\n    TaskDependency(**data[\"kwargs\"])\n\n\ndef test_depends_on_argument_is_skipped_raises_error_on_commit(\n    setup_task_dependency_db_test,\n):\n    \"\"\"IntegrityError raised if depends_on arg is skipped and session is committed.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"].pop(\"depends_on\")\n    new_dependency = TaskDependency(**data[\"kwargs\"])\n    DBSession.add(new_dependency)\n    with pytest.raises(IntegrityError) as cm:\n        with pytest.warns(SAWarning) as _:\n            DBSession.commit()\n\n    assert (\n        '(psycopg2.errors.NotNullViolation) null value in column \"depends_on_id\" of '\n        'relation \"Task_Dependencies\" violates not-null constraint' in str(cm.value)\n    )\n\n\ndef test_depends_on_argument_is_not_a_task_instance(setup_task_dependency_db_test):\n    \"\"\"TypeError raised if the depends_on arg is not a Task instance.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"depends_on\"] = \"Not a Task instance\"\n    with pytest.raises(TypeError) as cm:\n        TaskDependency(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"TaskDependency.depends_on should be and instance of \"\n        \"stalker.models.task.Task, not str: 'Not a Task instance'\"\n    )\n\n\ndef test_depends_on_attribute_is_not_a_task_instance(setup_task_dependency_db_test):\n    \"\"\"TypeError raised if depends_on attr is not a Task instance.\"\"\"\n    data = setup_task_dependency_db_test\n    new_dep = TaskDependency(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_dep.depends_on = \"not a task\"\n\n    assert (\n        str(cm.value) == \"TaskDependency.depends_on should be and instance of \"\n        \"stalker.models.task.Task, not str: 'not a task'\"\n    )\n\n\ndef test_depends_on_argument_is_working_as_expected(setup_task_dependency_db_test):\n    \"\"\"depends_on argument value is correctly passed to depends_on attribute.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"test_task1\"].depends_on = []\n    new_dep = TaskDependency(**data[\"kwargs\"])\n    assert new_dep.depends_on == data[\"test_task2\"]\n\n\ndef test_gap_timing_argument_is_skipped(setup_task_dependency_db_test):\n    \"\"\"gap_timing attribute value 0 if the gap_timing argument is skipped.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"].pop(\"gap_timing\")\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.gap_timing == 0\n\n\ndef test_gap_timing_argument_is_none(setup_task_dependency_db_test):\n    \"\"\"gap_timing attribute value 0 if the gap_timing argument value is None.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"gap_timing\"] = None\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.gap_timing == 0\n\n\ndef test_gap_timing_attribute_is_set_to_none(setup_task_dependency_db_test):\n    \"\"\"gap_timing attribute value 0 if it is set to None.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    tdep.gap_timing = None\n    assert tdep.gap_timing == 0\n\n\ndef test_gap_timing_argument_is_not_a_float(setup_task_dependency_db_test):\n    \"\"\"TypeError raised if the gap_timing argument value is not a float value.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"gap_timing\"] = \"not a time delta\"\n    with pytest.raises(TypeError) as cm:\n        TaskDependency(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"TaskDependency.gap_timing should be an integer or float number showing the \"\n        \"value of the gap timing of this TaskDependency, \"\n        \"not str: 'not a time delta'\"\n    )\n\n\ndef test_gap_timing_attribute_is_not_a_float(setup_task_dependency_db_test):\n    \"\"\"TypeError raised if the gap_timing attribute value is not float.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        tdep.gap_timing = \"not float\"\n\n    assert str(cm.value) == (\n        \"TaskDependency.gap_timing should be an integer or float number showing the \"\n        \"value of the gap timing of this TaskDependency, not str: 'not float'\"\n    )\n\n\ndef test_gap_timing_argument_is_working_as_expected(setup_task_dependency_db_test):\n    \"\"\"gap_timing argument value is correctly passed to the gap_timing attribute.\"\"\"\n    data = setup_task_dependency_db_test\n    test_value = 11\n    data[\"kwargs\"][\"gap_timing\"] = test_value\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.gap_timing == test_value\n\n\ndef test_gap_timing_attribute_is_working_as_expected(setup_task_dependency_db_test):\n    \"\"\"gap_timing attribute is working as expected.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    test_value = 11\n    tdep.gap_timing = test_value\n    assert tdep.gap_timing == test_value\n\n\ndef test_gap_unit_argument_is_skipped(setup_task_dependency_db_test):\n    \"\"\"default value used if the gap_unit argument is skipped.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"].pop(\"gap_unit\")\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.gap_unit == TaskDependency.__default_schedule_unit__\n\n\ndef test_gap_unit_argument_is_none(setup_task_dependency_db_test):\n    \"\"\"default value used if the gap_unit argument is None.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"gap_unit\"] = None\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.gap_unit == TaskDependency.__default_schedule_unit__\n\n\ndef test_gap_unit_attribute_is_none(setup_task_dependency_db_test):\n    \"\"\"default value used if the gap_unit attribute is set to None.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    tdep.gap_unit = None\n    assert tdep.gap_unit == TaskDependency.__default_schedule_unit__\n\n\ndef test_gap_unit_argument_is_not_a_str_instance(setup_task_dependency_db_test):\n    \"\"\"TypeError raised if the gap_unit argument is not a str.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"gap_unit\"] = 231\n    with pytest.raises(TypeError) as cm:\n        TaskDependency(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not int: '231'\"\n    )\n\n\ndef test_gap_unit_attribute_is_not_a_str_instance(setup_task_dependency_db_test):\n    \"\"\"TypeError raised if the gap_unit attribute is not a str.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        tdep.gap_unit = 2342\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not int: '2342'\"\n    )\n\n\ndef test_gap_unit_argument_value_is_not_in_the_enum_list(setup_task_dependency_db_test):\n    \"\"\"ValueError raised if the gap_unit arg value is not valid.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"gap_unit\"] = \"not in the list\"\n    with pytest.raises(ValueError) as cm:\n        TaskDependency(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not 'not in the list'\"\n    )\n\n\ndef test_gap_unit_attribute_value_is_not_in_the_enum_list(\n    setup_task_dependency_db_test,\n):\n    \"\"\"ValueError raised if the gap_unit attr is not valid.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    with pytest.raises(ValueError) as cm:\n        tdep.gap_unit = \"not in the list\"\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not 'not in the list'\"\n    )\n\n\n@pytest.mark.parametrize(\"gap_unit\", [\"y\", TimeUnit.Year])\ndef test_gap_unit_argument_is_working_as_expected(\n    setup_task_dependency_db_test, gap_unit\n):\n    \"\"\"gap_unit argument value is correctly passed to the gap_unit attribute on init.\"\"\"\n    data = setup_task_dependency_db_test\n    test_value = gap_unit\n    data[\"kwargs\"][\"gap_unit\"] = test_value\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.gap_unit == TimeUnit.Year\n\n\n@pytest.mark.parametrize(\"gap_unit\", [\"w\", TimeUnit.Week])\ndef test_gap_unit_attribute_is_working_as_expected(\n    setup_task_dependency_db_test, gap_unit\n):\n    \"\"\"gap_unit attribute is working as expected.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    test_value = gap_unit\n    assert tdep.gap_unit != TimeUnit.to_unit(test_value)\n    tdep.gap_unit = test_value\n    assert tdep.gap_unit == TimeUnit.to_unit(test_value)\n\n\ndef test_gap_model_argument_is_skipped(setup_task_dependency_db_test):\n    \"\"\"default value used if the gap_model argument is skipped.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"].pop(\"gap_model\")\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.gap_model == ScheduleModel.to_model(\n        defaults.task_dependency_gap_models[0]\n    )\n\n\ndef test_gap_model_argument_is_none(setup_task_dependency_db_test):\n    \"\"\"default value used if the gap_model argument is None.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"gap_model\"] = None\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.gap_model == ScheduleModel.to_model(\n        defaults.task_dependency_gap_models[0]\n    )\n\n\ndef test_gap_model_attribute_is_none(setup_task_dependency_db_test):\n    \"\"\"default value used if the gap_model attribute is set to None.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    tdep.gap_model = None\n    assert tdep.gap_model == ScheduleModel.to_model(\n        defaults.task_dependency_gap_models[0]\n    )\n\n\ndef test_gap_model_argument_is_not_a_str_instance(setup_task_dependency_db_test):\n    \"\"\"TypeError raised if the gap_model argument is not a str.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"gap_model\"] = 231\n    with pytest.raises(TypeError) as cm:\n        TaskDependency(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], \"\n        \"not int: '231'\"\n    )\n\n\ndef test_gap_model_attribute_is_not_a_str_instance(setup_task_dependency_db_test):\n    \"\"\"TypeError raised if the gap_model attribute is not a str.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        tdep.gap_model = 2342\n\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], \"\n        \"not int: '2342'\"\n    )\n\n\ndef test_gap_model_argument_value_is_not_in_the_enum_list(\n    setup_task_dependency_db_test,\n):\n    \"\"\"ValueError raised if the gap_model arg is not valid.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"gap_model\"] = \"not in the list\"\n    with pytest.raises(ValueError) as cm:\n        TaskDependency(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], \"\n        \"not 'not in the list'\"\n    )\n\n\ndef test_gap_model_attribute_value_is_not_in_the_enum_list(\n    setup_task_dependency_db_test,\n):\n    \"\"\"ValueError raised if the gap_model attr is not valid.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    with pytest.raises(ValueError) as cm:\n        tdep.gap_model = \"not in the list\"\n\n    assert str(cm.value) == (\n        \"model should be a ScheduleModel enum value or one of ['Effort', \"\n        \"'Duration', 'Length', 'effort', 'duration', 'length'], \"\n        \"not 'not in the list'\"\n    )\n\n\n@pytest.mark.parametrize(\"gap_model\", [\"duration\", ScheduleModel.Duration])\ndef test_gap_model_argument_is_working_as_expected(\n    setup_task_dependency_db_test, gap_model\n):\n    \"\"\"gap_model arg is passed okay to the gap_model attr on init.\"\"\"\n    data = setup_task_dependency_db_test\n    test_value = gap_model\n    data[\"kwargs\"][\"gap_model\"] = test_value\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.gap_model == ScheduleModel.to_model(test_value)\n\n\n@pytest.mark.parametrize(\"gap_model\", [\"duration\", ScheduleModel.Duration])\ndef test_gap_model_attribute_is_working_as_expected(\n    setup_task_dependency_db_test, gap_model\n):\n    \"\"\"gap_model attribute is working as expected.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    test_value = gap_model\n    assert tdep.gap_model != ScheduleModel.to_model(test_value)\n    tdep.gap_model = test_value\n    assert tdep.gap_model == ScheduleModel.to_model(test_value)\n\n\ndef test_dependency_target_argument_is_skipped(setup_task_dependency_db_test):\n    \"\"\"default value used if the dependency_target argument is skipped.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"].pop(\"dependency_target\")\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.dependency_target == DependencyTarget.to_target(\n        defaults.task_dependency_targets[0]\n    )\n\n\ndef test_dependency_target_argument_is_none(setup_task_dependency_db_test):\n    \"\"\"default value used if the dependency_target argument is None.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"dependency_target\"] = None\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.dependency_target == DependencyTarget.to_target(\n        defaults.task_dependency_targets[0]\n    )\n\n\ndef test_dependency_target_attribute_is_none(setup_task_dependency_db_test):\n    \"\"\"default value used if the dependency_target attribute is set to None.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    tdep.dependency_target = None\n    assert tdep.dependency_target == DependencyTarget.to_target(\n        defaults.task_dependency_targets[0]\n    )\n\n\ndef test_dependency_target_argument_is_not_a_str_instance(\n    setup_task_dependency_db_test,\n):\n    \"\"\"TypeError raised if the dependency_target argument is not a str.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"dependency_target\"] = 0\n    with pytest.raises(TypeError) as cm:\n        TaskDependency(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"target should be a DependencyTarget enum value or one of ['OnStart', \"\n        \"'OnEnd', 'onstart', 'onend'], not int: '0'\"\n    )\n\n\ndef test_dependency_target_attribute_is_not_a_str_instance(\n    setup_task_dependency_db_test,\n):\n    \"\"\"TypeError raised if the dependency_target attribute is not a str.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        tdep.dependency_target = 0\n\n    assert str(cm.value) == (\n        \"target should be a DependencyTarget enum value or one of ['OnStart', \"\n        \"'OnEnd', 'onstart', 'onend'], not int: '0'\"\n    )\n\n\ndef test_dependency_target_argument_value_is_not_in_the_enum_list(\n    setup_task_dependency_db_test,\n):\n    \"\"\"ValueError raised if dependency_target arg is not valid.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"dependency_target\"] = \"not in the list\"\n    with pytest.raises(ValueError) as cm:\n        TaskDependency(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"target should be a DependencyTarget enum value or one of ['OnStart', \"\n        \"'OnEnd', 'onstart', 'onend'], not 'not in the list'\"\n    )\n\n\ndef test_dependency_target_attribute_value_is_not_in_the_enum_list(\n    setup_task_dependency_db_test,\n):\n    \"\"\"ValueError raised if the dependency_target attr is not valid.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    with pytest.raises(ValueError) as cm:\n        tdep.dependency_target = \"not in the list\"\n\n    assert str(cm.value) == (\n        \"target should be a DependencyTarget enum value or one of ['OnStart', \"\n        \"'OnEnd', 'onstart', 'onend'], not 'not in the list'\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"target\",\n    [\n        \"onstart\",\n        \"onend\",\n        DependencyTarget.OnStart,\n        DependencyTarget.OnEnd,\n    ],\n)\ndef test_dependency_target_argument_is_working_as_expected(\n    setup_task_dependency_db_test, target\n):\n    \"\"\"dependency_target arg is passed okay to the dependency_target attr on init.\"\"\"\n    data = setup_task_dependency_db_test\n    data[\"kwargs\"][\"dependency_target\"] = target\n    tdep = TaskDependency(**data[\"kwargs\"])\n    assert tdep.dependency_target == DependencyTarget.to_target(target)\n\n\n@pytest.mark.parametrize(\n    \"target\",\n    [\n        \"onstart\",\n        \"onend\",\n        DependencyTarget.OnStart,\n        DependencyTarget.OnEnd,\n    ],\n)\ndef test_dependency_target_attribute_is_working_as_expected(\n    setup_task_dependency_db_test, target\n):\n    \"\"\"dependency_target attribute is working as expected.\"\"\"\n    data = setup_task_dependency_db_test\n    tdep = TaskDependency(**data[\"kwargs\"])\n    onstart = target\n    tdep.dependency_target = onstart\n    assert tdep.dependency_target == DependencyTarget.to_target(onstart)\n"
  },
  {
    "path": "tests/models/test_task_juggler_scheduler.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the stalker.models.scheduler.TaskJugglerScheduler class.\"\"\"\n\nimport datetime\nimport os\nimport tempfile\nimport sys\n\nimport jinja2\n\nimport pytest\n\nimport pytz\n\nimport stalker\nfrom stalker import TaskJugglerScheduler\nfrom stalker import Department\nfrom stalker import User\nfrom stalker import Repository\nfrom stalker import Status\nfrom stalker import Studio\nfrom stalker import Project\nfrom stalker import Task\nfrom stalker import TimeLog\nfrom stalker.db.session import DBSession\nfrom stalker.models.enum import TimeUnit\nfrom stalker.models.enum import ScheduleModel\n\n\n@pytest.fixture(scope=\"function\")\ndef monkeypatch_tj3():\n    \"\"\"patch tj3 command with a python script that returns an error message.\"\"\"\n    default_tj3_command_path = stalker.defaults.tj_command\n    patched_tj3_command_path = tempfile.mktemp(\"patched_tj3_command\")\n    # create the script\n    with open(patched_tj3_command_path, \"w\") as f:\n        f.write(\n            f\"#!{sys.executable}\\n\"\n            \"# -*- coding: utf-8 -*-\\n\"\n            \"import sys\\n\"\n            'sys.exit(\"some random exit message\")\\n'\n        )\n    # make it executable\n    os.chmod(patched_tj3_command_path, 0o777)\n    stalker.defaults[\"tj_command\"] = patched_tj3_command_path\n    yield\n    stalker.defaults[\"tj_command\"] = default_tj3_command_path\n    # and clean the temp file\n    os.remove(patched_tj3_command_path)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_tsk_juggler_scheduler_db_tests(setup_postgresql_db):\n    \"\"\"Set up tests for the  TaskJugglerScheduler class.\"\"\"\n    data = dict()\n\n    # create departments\n    data[\"test_dep1\"] = Department(name=\"Dep1\")\n    data[\"test_dep2\"] = Department(name=\"Dep2\")\n\n    # create resources\n    data[\"test_user1\"] = User(\n        login=\"user1\",\n        name=\"User1\",\n        email=\"user1@users.com\",\n        password=\"1234\",\n        departments=[data[\"test_dep1\"]],\n    )\n    DBSession.add(data[\"test_user1\"])\n\n    data[\"test_user2\"] = User(\n        login=\"user2\",\n        name=\"User2\",\n        email=\"user2@users.com\",\n        password=\"1234\",\n        departments=[data[\"test_dep1\"]],\n    )\n    DBSession.add(data[\"test_user2\"])\n\n    data[\"test_user3\"] = User(\n        login=\"user3\",\n        name=\"User3\",\n        email=\"user3@users.com\",\n        password=\"1234\",\n        departments=[data[\"test_dep2\"]],\n    )\n    DBSession.add(data[\"test_user3\"])\n\n    data[\"test_user4\"] = User(\n        login=\"user4\",\n        name=\"User4\",\n        email=\"user4@users.com\",\n        password=\"1234\",\n        departments=[data[\"test_dep2\"]],\n    )\n    DBSession.add(data[\"test_user4\"])\n\n    # user with two departments\n    data[\"test_user5\"] = User(\n        login=\"user5\",\n        name=\"User5\",\n        email=\"user5@users.com\",\n        password=\"1234\",\n        departments=[data[\"test_dep1\"], data[\"test_dep2\"]],\n    )\n    DBSession.add(data[\"test_user5\"])\n\n    # user with no departments\n    data[\"test_user6\"] = User(\n        login=\"user6\", name=\"User6\", email=\"user6@users.com\", password=\"1234\"\n    )\n    DBSession.add(data[\"test_user6\"])\n\n    # repository\n    data[\"test_repo\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        linux_path=\"/mnt/T/\",\n        windows_path=\"T:/\",\n        macos_path=\"/Volumes/T/\",\n    )\n    DBSession.add(data[\"test_repo\"])\n\n    # statuses\n    data[\"test_status1\"] = Status(name=\"Status 1\", code=\"STS1\")\n    data[\"test_status2\"] = Status(name=\"Status 2\", code=\"STS2\")\n    data[\"test_status3\"] = Status(name=\"Status 3\", code=\"STS3\")\n    data[\"test_status4\"] = Status(name=\"Status 4\", code=\"STS4\")\n    data[\"test_status5\"] = Status(name=\"Status 5\", code=\"STS5\")\n    DBSession.add_all(\n        [\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ]\n    )\n\n    # create one project\n    data[\"test_proj1\"] = Project(\n        name=\"Test Project 1\",\n        code=\"TP1\",\n        repository=data[\"test_repo\"],\n        start=datetime.datetime(2013, 4, 4, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 5, 4, tzinfo=pytz.utc),\n    )\n    DBSession.add(data[\"test_proj1\"])\n    data[\"test_proj1\"].now = datetime.datetime(2013, 4, 4, tzinfo=pytz.utc)\n\n    # create two tasks with the same resources\n    data[\"test_task1\"] = Task(\n        name=\"Task1\",\n        project=data[\"test_proj1\"],\n        resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        alternative_resources=[\n            data[\"test_user3\"],\n            data[\"test_user4\"],\n            data[\"test_user5\"],\n        ],\n        schedule_model=ScheduleModel.Effort,\n        schedule_timing=50,\n        schedule_unit=TimeUnit.Hour,\n    )\n    DBSession.add(data[\"test_task1\"])\n\n    data[\"test_task2\"] = Task(\n        name=\"Task2\",\n        project=data[\"test_proj1\"],\n        resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        alternative_resources=[\n            data[\"test_user3\"],\n            data[\"test_user4\"],\n            data[\"test_user5\"],\n        ],\n        depends_on=[data[\"test_task1\"]],\n        schedule_model=ScheduleModel.Effort,\n        schedule_timing=60,\n        schedule_unit=TimeUnit.Hour,\n        priority=800,\n    )\n    DBSession.save(data[\"test_task2\"])\n    return data\n\n\ndef test_tjp_file_is_created(setup_tsk_juggler_scheduler_db_tests):\n    \"\"\"tjp file is correctly created.\"\"\"\n    data = setup_tsk_juggler_scheduler_db_tests\n    # create the scheduler\n    tjp_sched = TaskJugglerScheduler()\n    tjp_sched.projects = [data[\"test_proj1\"]]\n\n    tjp_sched._create_tjp_file()\n    tjp_sched._create_tjp_file_content()\n    tjp_sched._fill_tjp_file()\n\n    # check\n    assert os.path.exists(tjp_sched.tjp_file_full_path)\n\n    # clean up the test\n    tjp_sched._clean_up()\n\n\ndef test_tjp_file_content_is_correct(setup_tsk_juggler_scheduler_db_tests):\n    \"\"\"tjp file content is correct.\"\"\"\n    data = setup_tsk_juggler_scheduler_db_tests\n    # enter a couple of time_logs\n    tlog1 = TimeLog(\n        resource=data[\"test_user1\"],\n        task=data[\"test_task1\"],\n        start=datetime.datetime(2013, 4, 16, 6, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc),\n    )\n    DBSession.add(tlog1)\n    DBSession.commit()\n\n    tjp_sched = TaskJugglerScheduler()\n    test_studio = Studio(\n        name=\"Test Studio\", timing_resolution=datetime.timedelta(minutes=30)\n    )\n    test_studio.daily_working_hours = 9\n\n    test_studio.id = 564\n    test_studio.start = datetime.datetime(2013, 4, 16, 0, 7, tzinfo=pytz.utc)\n    test_studio.end = datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc)\n    test_studio.now = datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    tjp_sched.studio = test_studio\n\n    tjp_sched._create_tjp_file()\n    tjp_sched._create_tjp_file_content()\n\n    assert TimeLog.query.all() != []\n\n    expected_tjp_template = jinja2.Template(\n        \"\"\"# Generated By Stalker v{{stalker.__version__}}\nproject Studio_564 \"Studio_564\" 2013-04-16 - 2013-06-30 {\n    timingresolution 30min\n    now 2013-04-16-00:00\n    dailyworkinghours 9\n    weekstartsmonday\n    workinghours mon 09:00 - 18:00\n    workinghours tue 09:00 - 18:00\n    workinghours wed 09:00 - 18:00\n    workinghours thu 09:00 - 18:00\n    workinghours fri 09:00 - 18:00\n    workinghours sat off\n    workinghours sun off\n    timeformat \"%Y-%m-%d\"\n    scenario plan \"Plan\"\n    trackingscenario plan\n}\n\n# resources\nresource resources \"Resources\" {\nresource User_3 \"User_3\" {\n    efficiency 1.0\n}\nresource User_{{user1.id}} \"User_{{user1.id}}\" {\n    efficiency 1.0\n}\nresource User_{{user2.id}} \"User_{{user2.id}}\" {\n    efficiency 1.0\n}\nresource User_{{user3.id}} \"User_{{user3.id}}\" {\n    efficiency 1.0\n}\nresource User_{{user4.id}} \"User_{{user4.id}}\" {\n    efficiency 1.0\n}\nresource User_{{user5.id}} \"User_{{user5.id}}\" {\n    efficiency 1.0\n}\nresource User_{{user6.id}} \"User_{{user6.id}}\" {\n    efficiency 1.0\n}\n}\n\n# tasks\ntask Project_{{proj.id}} \"Project_{{proj.id}}\" {\n  task Task_{{task1.id}} \"Task_{{task1.id}}\" {\n    effort 50.0h\n    allocate User_{{user1.id}} { alternative User_{{user3.id}}, User_{{user4.id}}, User_{{user5.id}} select minallocated persistent }, User_{{user2.id}} { alternative User_{{user3.id}}, User_{{user4.id}}, User_{{user5.id}} select minallocated persistent }\n    booking User_{{user1.id}} 2013-04-16-06:00:00 - 2013-04-16-09:00:00 { overtime 2 }\n  }\n  task Task_{{task2.id}} \"Task_{{task2.id}}\" {\n    priority 800\n    depends Project_{{proj.id}}.Task_{{task1.id}} {onend}\n    effort 60.0h\n    allocate User_{{user1.id}} { alternative User_{{user3.id}}, User_{{user4.id}}, User_{{user5.id}} select minallocated persistent }, User_{{user2.id}} { alternative User_{{user3.id}}, User_{{user4.id}}, User_{{user5.id}} select minallocated persistent }\n  }\n}\n\n# reports\ntaskreport breakdown \"{{csv_path}}\"{\n    formats csv\n    timeformat \"%Y-%m-%d-%H:%M\"\n    columns id, start, end\n}\n\"\"\"\n    )\n    expected_tjp_content = expected_tjp_template.render(\n        {\n            \"stalker\": stalker,\n            \"studio\": test_studio,\n            \"csv_path\": tjp_sched.temp_file_name,\n            \"user1\": data[\"test_user1\"],\n            \"user2\": data[\"test_user2\"],\n            \"user3\": data[\"test_user3\"],\n            \"user4\": data[\"test_user4\"],\n            \"user5\": data[\"test_user5\"],\n            \"user6\": data[\"test_user6\"],\n            \"proj\": data[\"test_proj1\"],\n            \"task1\": data[\"test_task1\"],\n            \"task2\": data[\"test_task2\"],\n        }\n    )\n\n    data[\"maxDiff\"] = None\n    tjp_content = tjp_sched.tjp_content\n    # print tjp_content\n    tjp_sched._clean_up()\n\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp_content)\n    # print('----------------')\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(tjp_content)\n    assert tjp_content == expected_tjp_content\n\n\ndef test_schedule_will_not_work_if_the_studio_attribute_is_None(\n    setup_tsk_juggler_scheduler_db_tests,\n):\n    \"\"\"TypeError raised if the studio attribute is None.\"\"\"\n    tjp_sched = TaskJugglerScheduler()\n    tjp_sched.studio = None\n    with pytest.raises(TypeError) as cm:\n        tjp_sched.schedule()\n\n    assert (\n        str(cm.value) == \"TaskJugglerScheduler.studio should be an instance of \"\n        \"stalker.models.studio.Studio, not NoneType: 'None'\"\n    )\n\n\n@pytest.mark.skipif(sys.platform == \"win32\", reason=\"Runs in Linux/macOS for now!\")\ndef test_schedule_will_raise_tj3_command_errors_as_a_runtime_error(\n    setup_tsk_juggler_scheduler_db_tests, monkeypatch_tj3\n):\n    data = setup_tsk_juggler_scheduler_db_tests\n    # create a dummy Project to schedule\n    dummy_project = Project(\n        name=\"Dummy Project\", code=\"DP\", repository=data[\"test_repo\"]\n    )\n\n    dt1 = Task(\n        name=\"Dummy Task 1\",\n        project=dummy_project,\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Hour,\n        resources=[data[\"test_user1\"]],\n    )\n\n    dt2 = Task(\n        name=\"Dummy Task 2\",\n        project=dummy_project,\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Hour,\n        resources=[data[\"test_user2\"]],\n    )\n    DBSession.add_all([dummy_project, dt1, dt2])\n    DBSession.commit()\n\n    tjp_sched = TaskJugglerScheduler(compute_resources=True, projects=[dummy_project])\n    test_studio = Studio(\n        name=\"Test Studio\", now=datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    )\n    test_studio.start = datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    test_studio.end = datetime.datetime(2013, 4, 30, 0, 0, tzinfo=pytz.utc)\n    test_studio.daily_working_hours = 9\n    DBSession.add(test_studio)\n    DBSession.commit()\n\n    tjp_sched.studio = test_studio\n\n    # update the defaults.tj_command to false so that it returns an error\n\n    with pytest.raises(RuntimeError) as cm:\n        tjp_sched.schedule()\n\n    assert str(cm.value) == \"some random exit message\"\n\n\ndef test_tasks_are_correctly_scheduled(setup_tsk_juggler_scheduler_db_tests):\n    \"\"\"tasks are correctly scheduled.\"\"\"\n    data = setup_tsk_juggler_scheduler_db_tests\n    tjp_sched = TaskJugglerScheduler(compute_resources=True)\n    test_studio = Studio(\n        name=\"Test Studio\", now=datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    )\n    test_studio.start = datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    test_studio.end = datetime.datetime(2013, 4, 30, 0, 0, tzinfo=pytz.utc)\n    test_studio.daily_working_hours = 9\n    DBSession.add(test_studio)\n\n    tjp_sched.studio = test_studio\n    tjp_sched.schedule()\n    DBSession.commit()\n\n    # check if the task and project timings are all adjusted\n    assert (\n        datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n        == data[\"test_proj1\"].computed_start\n    )\n    assert (\n        datetime.datetime(2013, 4, 24, 10, 0, tzinfo=pytz.utc)\n        == data[\"test_proj1\"].computed_end\n    )\n\n    possible_resources = [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n        data[\"test_user3\"],\n        data[\"test_user4\"],\n        data[\"test_user5\"],\n    ]\n    assert (\n        datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n        == data[\"test_task1\"].computed_start\n    )\n    assert (\n        datetime.datetime(2013, 4, 18, 16, 0, tzinfo=pytz.utc)\n        == data[\"test_task1\"].computed_end\n    )\n\n    assert len(data[\"test_task1\"].computed_resources) == 2\n    assert data[\"test_task1\"].computed_resources[0] in possible_resources\n    assert data[\"test_task1\"].computed_resources[1] in possible_resources\n\n    assert (\n        datetime.datetime(2013, 4, 18, 16, 0, tzinfo=pytz.utc)\n        == data[\"test_task2\"].computed_start\n    )\n    assert (\n        datetime.datetime(2013, 4, 24, 10, 0, tzinfo=pytz.utc)\n        == data[\"test_task2\"].computed_end\n    )\n\n    assert len(data[\"test_task2\"].computed_resources) == 2\n    assert data[\"test_task2\"].computed_resources[0] in possible_resources\n    assert data[\"test_task2\"].computed_resources[1] in possible_resources\n\n\ndef test_tasks_are_correctly_scheduled_if_compute_resources_is_False(\n    setup_tsk_juggler_scheduler_db_tests,\n):\n    \"\"\"tasks are correctly scheduled if the compute_resources is False.\"\"\"\n    data = setup_tsk_juggler_scheduler_db_tests\n    tjp_sched = TaskJugglerScheduler(compute_resources=False)\n    test_studio = Studio(\n        name=\"Test Studio\", now=datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    )\n    test_studio.start = datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    test_studio.end = datetime.datetime(2013, 4, 30, 0, 0, tzinfo=pytz.utc)\n    test_studio.daily_working_hours = 9\n    DBSession.add(test_studio)\n\n    tjp_sched.studio = test_studio\n    tjp_sched.schedule()\n    DBSession.commit()\n\n    # check if the task and project timings are all adjusted\n    assert (\n        datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n        == data[\"test_proj1\"].computed_start\n    )\n    assert (\n        datetime.datetime(2013, 4, 24, 10, 0, tzinfo=pytz.utc)\n        == data[\"test_proj1\"].computed_end\n    )\n\n    assert (\n        datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n        == data[\"test_task1\"].computed_start\n    )\n    assert (\n        datetime.datetime(2013, 4, 18, 16, 0, tzinfo=pytz.utc)\n        == data[\"test_task1\"].computed_end\n    )\n    assert len(data[\"test_task1\"].computed_resources) == 2\n    assert data[\"test_task1\"].computed_resources[0] in [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n        data[\"test_user3\"],\n        data[\"test_user4\"],\n        data[\"test_user5\"],\n    ]\n    assert data[\"test_task1\"].computed_resources[1] in [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n        data[\"test_user3\"],\n        data[\"test_user4\"],\n        data[\"test_user5\"],\n    ]\n\n    assert (\n        datetime.datetime(2013, 4, 18, 16, 0, tzinfo=pytz.utc)\n        == data[\"test_task2\"].computed_start\n    )\n    assert (\n        datetime.datetime(2013, 4, 24, 10, 0, tzinfo=pytz.utc)\n        == data[\"test_task2\"].computed_end\n    )\n    assert len(data[\"test_task2\"].computed_resources) == 2\n    assert data[\"test_task2\"].computed_resources[0] in [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n        data[\"test_user3\"],\n        data[\"test_user4\"],\n        data[\"test_user5\"],\n    ]\n    assert data[\"test_task2\"].computed_resources[1] in [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n        data[\"test_user3\"],\n        data[\"test_user4\"],\n        data[\"test_user5\"],\n    ]\n\n\ndef test_tasks_are_correctly_scheduled_if_compute_resources_is_True(\n    setup_tsk_juggler_scheduler_db_tests,\n):\n    \"\"\"tasks are correctly scheduled if the compute_resources is True.\"\"\"\n    data = setup_tsk_juggler_scheduler_db_tests\n    tjp_sched = TaskJugglerScheduler(compute_resources=True)\n    test_studio = Studio(\n        name=\"Test Studio\", now=datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    )\n    test_studio.start = datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    test_studio.end = datetime.datetime(2013, 4, 30, 0, 0, tzinfo=pytz.utc)\n    test_studio.daily_working_hours = 9\n    DBSession.add(test_studio)\n\n    tjp_sched.studio = test_studio\n    tjp_sched.schedule()\n    DBSession.commit()\n\n    possible_resources = [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n        data[\"test_user3\"],\n        data[\"test_user4\"],\n        data[\"test_user5\"],\n    ]\n\n    # check if the task and project timings are all adjusted\n    assert (\n        datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n        == data[\"test_proj1\"].computed_start\n    )\n    assert (\n        datetime.datetime(2013, 4, 24, 10, 0, tzinfo=pytz.utc)\n        == data[\"test_proj1\"].computed_end\n    )\n\n    assert (\n        datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n        == data[\"test_task1\"].computed_start\n    )\n    assert (\n        datetime.datetime(2013, 4, 18, 16, 0, tzinfo=pytz.utc)\n        == data[\"test_task1\"].computed_end\n    )\n    assert len(data[\"test_task1\"].computed_resources) == 2\n    assert data[\"test_task1\"].computed_resources[0] in possible_resources\n    assert data[\"test_task1\"].computed_resources[1] in possible_resources\n\n    assert (\n        datetime.datetime(2013, 4, 18, 16, 0, tzinfo=pytz.utc)\n        == data[\"test_task2\"].computed_start\n    )\n    assert (\n        datetime.datetime(2013, 4, 24, 10, 0, tzinfo=pytz.utc)\n        == data[\"test_task2\"].computed_end\n    )\n    assert data[\"test_task2\"].computed_resources[0] in possible_resources\n    assert data[\"test_task2\"].computed_resources[1] in possible_resources\n\n\ndef test_tasks_of_given_projects_are_correctly_scheduled(\n    setup_tsk_juggler_scheduler_db_tests,\n):\n    \"\"\"tasks of given projects are correctly scheduled.\"\"\"\n    data = setup_tsk_juggler_scheduler_db_tests\n    # create a dummy Project to schedule\n    dummy_project = Project(\n        name=\"Dummy Project\", code=\"DP\", repository=data[\"test_repo\"]\n    )\n\n    dt1 = Task(\n        name=\"Dummy Task 1\",\n        project=dummy_project,\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Hour,\n        resources=[data[\"test_user1\"]],\n    )\n\n    dt2 = Task(\n        name=\"Dummy Task 2\",\n        project=dummy_project,\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Hour,\n        resources=[data[\"test_user2\"]],\n    )\n    DBSession.add_all([dummy_project, dt1, dt2])\n    DBSession.commit()\n\n    tjp_sched = TaskJugglerScheduler(compute_resources=True, projects=[dummy_project])\n    test_studio = Studio(\n        name=\"Test Studio\", now=datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    )\n    test_studio.start = datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    test_studio.end = datetime.datetime(2013, 4, 30, 0, 0, tzinfo=pytz.utc)\n    test_studio.daily_working_hours = 9\n    DBSession.add(test_studio)\n    DBSession.commit()\n\n    tjp_sched.studio = test_studio\n    tjp_sched.schedule()\n    DBSession.commit()\n\n    # check if the task and project timings are all adjusted\n    assert data[\"test_proj1\"].computed_start is None\n    assert data[\"test_proj1\"].computed_end is None\n\n    assert data[\"test_task1\"].computed_start is None\n    assert data[\"test_task1\"].computed_end is None\n    assert sorted(data[\"test_task1\"].computed_resources, key=lambda x: x.id) == sorted(\n        [\n            data[\"test_user1\"],\n            data[\"test_user2\"],\n        ],\n        key=lambda x: x.id,\n    )\n\n    assert data[\"test_task2\"].computed_start is None\n    assert data[\"test_task2\"].computed_end is None\n    assert sorted(data[\"test_task2\"].computed_resources, key=lambda x: x.id) == sorted(\n        [\n            data[\"test_user1\"],\n            data[\"test_user2\"],\n        ],\n        key=lambda x: x.id,\n    )\n\n    assert dt1.computed_start == datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n    assert dt1.computed_end == datetime.datetime(2013, 4, 16, 13, 0, tzinfo=pytz.utc)\n\n    assert dt2.computed_start == datetime.datetime(2013, 4, 16, 9, 0, tzinfo=pytz.utc)\n    assert dt2.computed_end == datetime.datetime(2013, 4, 16, 13, 0, tzinfo=pytz.utc)\n\n\ndef test_csv_file_does_not_exist_returns_without_scheduling(\n    setup_tsk_juggler_scheduler_db_tests, monkeypatch\n):\n    \"\"\"csv_file_full_path doesn't exist will return without schedule data parsed.\"\"\"\n    data = setup_tsk_juggler_scheduler_db_tests\n    # create a dummy Project to schedule\n    dummy_project = Project(\n        name=\"Dummy Project\", code=\"DP\", repository=data[\"test_repo\"]\n    )\n\n    dt1 = Task(\n        name=\"Dummy Task 1\",\n        project=dummy_project,\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Hour,\n        resources=[data[\"test_user1\"]],\n    )\n\n    dt2 = Task(\n        name=\"Dummy Task 2\",\n        project=dummy_project,\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Hour,\n        resources=[data[\"test_user2\"]],\n    )\n    DBSession.add_all([dummy_project, dt1, dt2])\n    DBSession.commit()\n\n    tjp_sched = TaskJugglerScheduler(compute_resources=True, projects=[dummy_project])\n    test_studio = Studio(\n        name=\"Test Studio\", now=datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    )\n    test_studio.start = datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    test_studio.end = datetime.datetime(2013, 4, 30, 0, 0, tzinfo=pytz.utc)\n    test_studio.daily_working_hours = 9\n    DBSession.add(test_studio)\n    DBSession.commit()\n\n    tjp_sched.studio = test_studio\n\n    # trick _arse_csv_file() to think that the csv file doesn't exist\n    import os\n\n    called = []\n\n    def patched_exists(path):\n        if path == tjp_sched.csv_file_full_path:\n            called.append(path)\n            return False\n        return os.path.exists(path)\n\n    monkeypatch.setattr(\"stalker.models.schedulers.os.path.exists\", patched_exists)\n    assert len(called) == 0\n    # should run without any errors\n    tjp_sched.schedule()\n    assert len(called) > 0\n    assert tjp_sched.csv_file_full_path in called\n\n\ndef test_projects_argument_is_skipped(setup_tsk_juggler_scheduler_db_tests):\n    \"\"\"projects attribute an empty list if the projects argument is skipped.\"\"\"\n    tjp_sched = TaskJugglerScheduler(compute_resources=True)\n    assert tjp_sched.projects == []\n\n\ndef test_projects_argument_is_None(setup_tsk_juggler_scheduler_db_tests):\n    \"\"\"projects attribute an empty list if the projects argument is None.\"\"\"\n    tjp_sched = TaskJugglerScheduler(compute_resources=True, projects=None)\n    assert tjp_sched.projects == []\n\n\ndef test_projects_attribute_is_set_to_None(setup_tsk_juggler_scheduler_db_tests):\n    \"\"\"projects attribute an empty list if it is set to None.\"\"\"\n    tjp_sched = TaskJugglerScheduler(compute_resources=True)\n    tjp_sched.projects = None\n    assert tjp_sched.projects == []\n\n\ndef test_projects_argument_is_not_a_list(setup_tsk_juggler_scheduler_db_tests):\n    \"\"\"TypeError raised if the projects argument value is not a list.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        TaskJugglerScheduler(compute_resources=True, projects=\"not a list of projects\")\n\n    assert str(cm.value) == (\n        \"TaskJugglerScheduler.projects should only contain instances of \"\n        \"stalker.models.project.Project, not str: 'not a list of projects'\"\n    )\n\n\ndef test_projects_attribute_is_not_a_list(setup_tsk_juggler_scheduler_db_tests):\n    \"\"\"TypeError raised if the projects attribute not a list.\"\"\"\n    tjp = TaskJugglerScheduler(compute_resources=True)\n    with pytest.raises(TypeError) as cm:\n        tjp.projects = \"not a list of projects\"\n\n    assert str(cm.value) == (\n        \"TaskJugglerScheduler.projects should only contain instances of \"\n        \"stalker.models.project.Project, not str: 'not a list of projects'\"\n    )\n\n\ndef test_projects_argument_is_not_a_list_of_all_projects():\n    \"\"\"TypeError raised if the items in the projects arg are not all Projects.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        TaskJugglerScheduler(\n            compute_resources=True, projects=[\"not\", 1, [], \"of\", \"projects\"]\n        )\n\n    assert str(cm.value) == (\n        \"TaskJugglerScheduler.projects should only contain instances of \"\n        \"stalker.models.project.Project, not str: 'not'\"\n    )\n\n\ndef test_projects_attribute_is_not_list_of_all_projects():\n    \"\"\"TypeError raised if the items in the projects attr is not all Projects.\"\"\"\n    tjp = TaskJugglerScheduler(compute_resources=True)\n    with pytest.raises(TypeError) as cm:\n        tjp.projects = [\"not\", 1, [], \"of\", \"projects\"]\n\n    assert str(cm.value) == (\n        \"TaskJugglerScheduler.projects should only contain instances of \"\n        \"stalker.models.project.Project, not str: 'not'\"\n    )\n\n\ndef test_projects_argument_is_working_as_expected(setup_tsk_juggler_scheduler_db_tests):\n    \"\"\"projects argument value is correctly passed to the projects attribute.\"\"\"\n    data = setup_tsk_juggler_scheduler_db_tests\n    dp1 = Project(name=\"Dummy Project\", code=\"DP\", repository=data[\"test_repo\"])\n    dp2 = Project(name=\"Dummy Project\", code=\"DP\", repository=data[\"test_repo\"])\n    tjp = TaskJugglerScheduler(compute_resources=True, projects=[dp1, dp2])\n    assert tjp.projects == [dp1, dp2]\n\n\ndef test_projects_attribute_is_working_as_expected(\n    setup_tsk_juggler_scheduler_db_tests,\n):\n    \"\"\"projects attribute is working as expected.\"\"\"\n    data = setup_tsk_juggler_scheduler_db_tests\n    dp1 = Project(name=\"Dummy Project\", code=\"DP\", repository=data[\"test_repo\"])\n    dp2 = Project(name=\"Dummy Project\", code=\"DP\", repository=data[\"test_repo\"])\n    tjp = TaskJugglerScheduler(compute_resources=True)\n    tjp.projects = [dp1, dp2]\n    assert tjp.projects == [dp1, dp2]\n\n\ndef test_tjp_file_content_is_correct_2(setup_tsk_juggler_scheduler_db_tests):\n    \"\"\"tjp file content is correct.\"\"\"\n    data = setup_tsk_juggler_scheduler_db_tests\n    tjp_sched = TaskJugglerScheduler()\n    test_studio = Studio(\n        name=\"Test Studio\", timing_resolution=datetime.timedelta(minutes=30)\n    )\n    test_studio.daily_working_hours = 9\n    test_studio.id = 564\n    test_studio.start = datetime.datetime(2013, 4, 16, 0, 7, tzinfo=pytz.utc)\n    test_studio.end = datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc)\n    test_studio.now = datetime.datetime(2013, 4, 16, 0, 0, tzinfo=pytz.utc)\n    tjp_sched.studio = test_studio\n\n    tjp_sched._create_tjp_file()\n    tjp_sched._create_tjp_file_content()\n\n    expected_tjp_template = jinja2.Template(\n        \"\"\"# Generated By Stalker v{{stalker.__version__}}\nproject Studio_564 \"Studio_564\" 2013-04-16 - 2013-06-30 {\n    timingresolution 30min\n    now 2013-04-16-00:00\n    dailyworkinghours 9\n    weekstartsmonday\n    workinghours mon 09:00 - 18:00\n    workinghours tue 09:00 - 18:00\n    workinghours wed 09:00 - 18:00\n    workinghours thu 09:00 - 18:00\n    workinghours fri 09:00 - 18:00\n    workinghours sat off\n    workinghours sun off\n    timeformat \"%Y-%m-%d\"\n    scenario plan \"Plan\"\n    trackingscenario plan\n}\n\n# resources\nresource resources \"Resources\" {\nresource User_3 \"User_3\" {\n    efficiency 1.0\n}\nresource User_{{user1.id}} \"User_{{user1.id}}\" {\n    efficiency 1.0\n}\nresource User_{{user2.id}} \"User_{{user2.id}}\" {\n    efficiency 1.0\n}\nresource User_{{user3.id}} \"User_{{user3.id}}\" {\n    efficiency 1.0\n}\nresource User_{{user4.id}} \"User_{{user4.id}}\" {\n    efficiency 1.0\n}\nresource User_{{user5.id}} \"User_{{user5.id}}\" {\n    efficiency 1.0\n}\nresource User_{{user6.id}} \"User_{{user6.id}}\" {\n    efficiency 1.0\n}\n}\n\n# tasks\ntask Project_{{proj1.id}} \"Project_{{proj1.id}}\" {\n  task Task_{{task1.id}} \"Task_{{task1.id}}\" {\n    effort 50.0h\n    allocate User_{{user1.id}} { alternative User_{{user3.id}}, User_{{user4.id}}, User_{{user5.id}} select minallocated persistent }, User_{{user2.id}} { alternative User_{{user3.id}}, User_{{user4.id}}, User_{{user5.id}} select minallocated persistent }\n  }\n  task Task_{{task2.id}} \"Task_{{task2.id}}\" {\n    priority 800\n    depends Project_{{proj1.id}}.Task_{{task1.id}} {onend}\n    effort 60.0h\n    allocate User_{{user1.id}} { alternative User_{{user3.id}}, User_{{user4.id}}, User_{{user5.id}} select minallocated persistent }, User_{{user2.id}} { alternative User_{{user3.id}}, User_{{user4.id}}, User_{{user5.id}} select minallocated persistent }\n  }\n}\n\n# reports\ntaskreport breakdown \"{{csv_path}}\"{\n    formats csv\n    timeformat \"%Y-%m-%d-%H:%M\"\n    columns id, start, end\n}\"\"\"\n    )\n    expected_tjp_content = expected_tjp_template.render(\n        {\n            \"stalker\": stalker,\n            \"studio\": test_studio,\n            \"csv_path\": tjp_sched.temp_file_name,\n            \"user1\": data[\"test_user1\"],\n            \"user2\": data[\"test_user2\"],\n            \"user3\": data[\"test_user3\"],\n            \"user4\": data[\"test_user4\"],\n            \"user5\": data[\"test_user5\"],\n            \"user6\": data[\"test_user6\"],\n            \"proj1\": data[\"test_proj1\"],\n            \"task1\": data[\"test_task1\"],\n            \"task2\": data[\"test_task2\"],\n        }\n    )\n\n    data[\"maxDiff\"] = None\n    tjp_content = tjp_sched.tjp_content\n    # print tjp_content\n    tjp_sched._clean_up()\n    assert tjp_content == expected_tjp_content\n"
  },
  {
    "path": "tests/models/test_task_status_workflow.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Task status workflow.\"\"\"\n\nimport datetime\nimport pytest\nimport pytz\n\nfrom stalker import (\n    Asset,\n    Project,\n    Repository,\n    Review,\n    Status,\n    StatusList,\n    Task,\n    TaskDependency,\n    TimeLog,\n    Type,\n    User,\n    Version,\n)\nfrom stalker.db.session import DBSession\nfrom stalker.exceptions import StatusError\nfrom stalker.models.enum import ScheduleModel, TimeUnit\nfrom stalker.models.enum import DependencyTarget\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_task_status_workflow_tests():\n    \"\"\"Set up tests for the Task Status Workflow.\"\"\"\n    data = dict()\n    # test users\n    data[\"test_user1\"] = User(\n        name=\"Test User 1\",\n        login=\"tuser1\",\n        email=\"tuser1@test.com\",\n        password=\"secret\",\n    )\n    data[\"test_user2\"] = User(\n        name=\"Test User 2\",\n        login=\"tuser2\",\n        email=\"tuser2@test.com\",\n        password=\"secret\",\n    )\n    # create a couple of tasks\n    data[\"status_new\"] = Status(name=\"New\", code=\"NEW\")\n    data[\"status_wfd\"] = Status(name=\"Waiting For Dependency\", code=\"WFD\")\n    data[\"status_rts\"] = Status(name=\"Ready To Start\", code=\"RTS\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_prev\"] = Status(name=\"Pending Review\", code=\"PREV\")\n    data[\"status_hrev\"] = Status(name=\"Has Revision\", code=\"HREV\")\n    data[\"status_drev\"] = Status(name=\"Dependency Has Revision\", code=\"DREV\")\n    data[\"status_oh\"] = Status(name=\"On Hold\", code=\"OH\")\n    data[\"status_stop\"] = Status(name=\"Stopped\", code=\"STOP\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n    data[\"status_rrev\"] = Status(name=\"Requested Revision\", code=\"RREV\")\n    data[\"status_app\"] = Status(name=\"Approved\", code=\"APP\")\n\n    data[\"test_project_status_list\"] = StatusList(\n        name=\"Project Statuses\",\n        target_entity_type=\"Project\",\n        statuses=[data[\"status_wfd\"], data[\"status_wip\"], data[\"status_cmpl\"]],\n    )\n\n    data[\"test_task_status_list\"] = StatusList(\n        name=\"Task Statuses\",\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_oh\"],\n            data[\"status_stop\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n\n    # repository\n    data[\"test_repo\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        linux_path=\"/mnt/T/\",\n        windows_path=\"T:/\",\n        macos_path=\"/Volumes/T\",\n    )\n\n    # proj1\n    data[\"test_project1\"] = Project(\n        name=\"Test Project 1\",\n        code=\"TProj1\",\n        status_list=data[\"test_project_status_list\"],\n        repository=data[\"test_repo\"],\n        start=datetime.datetime(2013, 6, 20, 0, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, 0, tzinfo=pytz.utc),\n    )\n\n    # root tasks\n    data[\"test_task1\"] = Task(\n        name=\"Test Task 1\",\n        project=data[\"test_project1\"],\n        responsible=[data[\"test_user1\"]],\n        status_list=data[\"test_task_status_list\"],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n\n    data[\"test_task2\"] = Task(\n        name=\"Test Task 2\",\n        project=data[\"test_project1\"],\n        responsible=[data[\"test_user1\"]],\n        status_list=data[\"test_task_status_list\"],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n\n    data[\"test_task3\"] = Task(\n        name=\"Test Task 3\",\n        project=data[\"test_project1\"],\n        status_list=data[\"test_task_status_list\"],\n        resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        responsible=[data[\"test_user1\"], data[\"test_user2\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n\n    # children tasks\n    # children of data[\"test_task1\"]\n    data[\"test_task4\"] = Task(\n        name=\"Test Task 4\",\n        parent=data[\"test_task1\"],\n        status=data[\"status_wfd\"],\n        status_list=data[\"test_task_status_list\"],\n        resources=[data[\"test_user1\"]],\n        depends_on=[data[\"test_task3\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n\n    data[\"test_task5\"] = Task(\n        name=\"Test Task 5\",\n        parent=data[\"test_task1\"],\n        status_list=data[\"test_task_status_list\"],\n        resources=[data[\"test_user1\"]],\n        depends_on=[data[\"test_task4\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n\n    data[\"test_task6\"] = Task(\n        name=\"Test Task 6\",\n        parent=data[\"test_task1\"],\n        status_list=data[\"test_task_status_list\"],\n        resources=[data[\"test_user1\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n\n    # children of data[\"test_task2\"]\n    data[\"test_task7\"] = Task(\n        name=\"Test Task 7\",\n        parent=data[\"test_task2\"],\n        status_list=data[\"test_task_status_list\"],\n        resources=[data[\"test_user2\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n\n    data[\"test_task8\"] = Task(\n        name=\"Test Task 8\",\n        parent=data[\"test_task2\"],\n        status_list=data[\"test_task_status_list\"],\n        resources=[data[\"test_user2\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n\n    data[\"test_asset_status_list\"] = StatusList(\n        name=\"Asset Statuses\",\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_oh\"],\n            data[\"status_stop\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Asset\",\n    )\n\n    # create an asset in between\n    data[\"test_asset1\"] = Asset(\n        name=\"Test Asset 1\",\n        code=\"TA1\",\n        parent=data[\"test_task7\"],\n        type=Type(\n            name=\"Character\",\n            code=\"Char\",\n            target_entity_type=\"Asset\",\n        ),\n        status_list=data[\"test_asset_status_list\"],\n    )\n\n    # new task under asset\n    data[\"test_task9\"] = Task(\n        name=\"Test Task 9\",\n        parent=data[\"test_asset1\"],\n        status_list=data[\"test_task_status_list\"],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        resources=[data[\"test_user2\"]],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n\n    # --------------\n    # Task Hierarchy\n    # --------------\n    #\n    # +-> Test Task 1\n    # |   |\n    # |   +-> Test Task 4\n    # |   |\n    # |   +-> Test Task 5\n    # |   |\n    # |   +-> Test Task 6\n    # |\n    # +-> Test Task 2\n    # |   |\n    # |   +-> Test Task 7\n    # |   |   |\n    # |   |   +-> Test Asset 1\n    # |   |       |\n    # |   |       +-> Test Task 9\n    # |   |\n    # |   +-> Test Task 8\n    # |\n    # +-> Test Task 3\n\n    # no children for data[\"test_task3\"]\n    data[\"all_tasks\"] = [\n        data[\"test_task1\"],\n        data[\"test_task2\"],\n        data[\"test_task3\"],\n        data[\"test_task4\"],\n        data[\"test_task5\"],\n        data[\"test_task6\"],\n        data[\"test_task7\"],\n        data[\"test_task8\"],\n        data[\"test_task9\"],\n        data[\"test_asset1\"],\n    ]\n    return data\n\n\ndef test_walk_hierarchy_is_working_as_expected(setup_task_status_workflow_tests):\n    \"\"\"walk_hierarchy is working as expected.\"\"\"\n    data = setup_task_status_workflow_tests\n    # this test should not be placed here\n    visited_tasks = []\n    expected_result = [\n        data[\"test_task2\"],\n        data[\"test_task7\"],\n        data[\"test_task8\"],\n        data[\"test_asset1\"],\n        data[\"test_task9\"],\n    ]\n\n    for task in data[\"test_task2\"].walk_hierarchy(method=1):\n        visited_tasks.append(task)\n\n    assert expected_result == visited_tasks\n\n\ndef test_walk_dependencies_is_working_as_expected(setup_task_status_workflow_tests):\n    \"\"\"walk_dependencies is working as expected.\"\"\"\n    data = setup_task_status_workflow_tests\n    # this test should not be placed here\n    visited_tasks = []\n    expected_result = [\n        data[\"test_task9\"],\n        data[\"test_task6\"],\n        data[\"test_task4\"],\n        data[\"test_task5\"],\n        data[\"test_task8\"],\n        data[\"test_task3\"],\n        data[\"test_task4\"],\n        data[\"test_task8\"],\n        data[\"test_task3\"],\n    ]\n\n    # setup dependencies\n    data[\"test_task9\"].depends_on = [data[\"test_task6\"]]\n    data[\"test_task6\"].depends_on = [data[\"test_task4\"], data[\"test_task5\"]]\n    data[\"test_task5\"].depends_on = [data[\"test_task4\"]]\n    data[\"test_task4\"].depends_on = [data[\"test_task8\"], data[\"test_task3\"]]\n\n    for task in data[\"test_task9\"].walk_dependencies():\n        visited_tasks.append(task)\n\n    assert expected_result == visited_tasks\n\n\n# The following tests will test the status changes in dependency changes\n\n\n# Leaf Tasks - dependency relation changes\n# WFD\ndef test_leaf_wfd_task_updated_to_have_a_dependency_of_wfd_task_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set dependency between a WFD task to another WFD task and the status stay WFD.\"\"\"\n    # create another dependency to make the task3 a WFD task\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task9\"].status = data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task9\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    # make a task with HREV status\n    # create dependency\n    data[\"test_task8\"].status = data[\"status_wfd\"]\n    assert data[\"test_task8\"].status == data[\"status_wfd\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_wfd_task_updated_to_have_a_dependency_of_rts_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set dependency between a WFD task to an RTS task and the status stay WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create another dependency to make the task3 a WFD task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task9\"].status = data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task9\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    # make a task with HREV status\n    # create dependency\n    data[\"test_task8\"].status = data[\"status_rts\"]\n    assert data[\"test_task8\"].status == data[\"status_rts\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_wfd_task_updated_to_have_a_dependency_of_wip_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a WFD task to a WIP task and the status stay WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create another dependency to make the task3 a WFD task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task9\"].status = data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task9\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    # make a task with HREV status\n    # create dependency\n    data[\"test_task8\"].status = data[\"status_wip\"]\n    assert data[\"test_task8\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_wfd_task_updated_to_have_a_dependency_of_prev_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a WFD task to a PREV task and the status stay WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create another dependency to make the task3 a WFD task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task9\"].status = data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task9\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    # make a task with HREV status\n    # create dependency\n    data[\"test_task8\"].status = data[\"status_prev\"]\n    assert data[\"test_task8\"].status == data[\"status_prev\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_wfd_task_updated_to_have_a_dependency_of_hrev_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a WFD task to a HREV task and the status stay WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create another dependency to make the task3 a WFD task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task9\"].status = data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task9\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    # make a task with HREV status\n    # create dependency\n    data[\"test_task8\"].status = data[\"status_hrev\"]\n    assert data[\"test_task8\"].status == data[\"status_hrev\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_wfd_task_updated_to_have_a_dependency_of_oh_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a WFD task to a OH task and the status stay WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create another dependency to make the task3 a WFD task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task9\"].status = data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task9\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    # make a task with HREV status\n    # create dependency\n    data[\"test_task8\"].status = data[\"status_oh\"]\n    assert data[\"test_task8\"].status == data[\"status_oh\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_wfd_task_updated_to_have_a_dependency_of_stop_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a WFD task to a STOP task and the status stay WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create another dependency to make the task3 a WFD task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task9\"].status = data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task9\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    # make a task with HREV status\n    # create dependency\n    data[\"test_task8\"].status = data[\"status_stop\"]\n    assert data[\"test_task8\"].status == data[\"status_stop\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_wfd_task_updated_to_have_a_dependency_of_cmpl_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a WFD task to a CMPL task and the status stay to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create another dependency to make the task3 a WFD task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task9\"].status = data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task9\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    # make a task with HREV status\n    # create dependency\n    data[\"test_task8\"].status = data[\"status_cmpl\"]\n    assert data[\"test_task8\"].status == data[\"status_cmpl\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\n# Leaf Tasks - dependency relation changes\n# RTS\ndef test_leaf_rts_task_updated_to_have_a_dependency_of_wfd_task_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a RTS task to a WFD task the status updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an RTS task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    # make a task with WFD status\n    data[\"test_task8\"].status = data[\"status_wfd\"]\n    assert data[\"test_task8\"].status == data[\"status_wfd\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_rts_task_updated_to_have_a_dependency_of_rts_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a RTS task to a RTS task the status updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an RTS task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    # make a task with RTS status\n    data[\"test_task8\"].status = data[\"status_rts\"]\n    assert data[\"test_task8\"].status == data[\"status_rts\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_rts_task_updated_to_have_a_dependency_of_wip_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a RTS task to a WIP task the status updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an RTS task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    # make a task with WIP status\n    data[\"test_task8\"].status = data[\"status_wip\"]\n    assert data[\"test_task8\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_rts_task_updated_to_have_a_dependency_of_prev_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a RTS task to a PREV task the status updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an RTS task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    # make a task with PREV status\n    data[\"test_task8\"].status = data[\"status_prev\"]\n    assert data[\"test_task8\"].status == data[\"status_prev\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_rts_task_updated_to_have_a_dependency_of_hrev_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a RTS task to a HREV task the status updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an RTS task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    # make a task with HREV status\n    data[\"test_task8\"].status = data[\"status_hrev\"]\n    assert data[\"test_task8\"].status == data[\"status_hrev\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_rts_task_updated_to_have_a_dependency_of_oh_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between a RTS task to a OH task the status updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an RTS task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    # make a task with OH status\n    data[\"test_task8\"].status = data[\"status_oh\"]\n    assert data[\"test_task8\"].status == data[\"status_oh\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_leaf_rts_task_updated_to_have_a_dependency_of_stop_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between an RTS task to a STOP task the status will stay RTS.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an RTS task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    # make a task with STOP status\n    data[\"test_task8\"].status = data[\"status_stop\"]\n    assert data[\"test_task8\"].status == data[\"status_stop\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n\n\ndef test_leaf_rts_task_updated_to_have_a_dependency_of_cmpl_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"set a dependency between an RTS task to a CMPL task the status will stay RTS.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an RTS task\n    assert data[\"test_task3\"].depends_on == []\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    # make a task with CMPL status\n    data[\"test_task8\"].status = data[\"status_cmpl\"]\n    assert data[\"test_task8\"].status == data[\"status_cmpl\"]\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n\n\n# Leaf Tasks - dependency changes\n# WIP - DREV - PREV - HREV - OH - STOP - CMPL\ndef test_leaf_wip_task_dependency_cannot_be_updated(setup_task_status_workflow_tests):\n    \"\"\"it is not possible to update the dependencies of a WIP task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an WIP task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    # create dependency\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n\n    assert (\n        str(cm.value) == \"This is a WIP task and it is not allowed to change the \"\n        \"dependencies of a WIP task\"\n    )\n\n\ndef test_leaf_prev_task_dependency_cannot_be_updated(setup_task_status_workflow_tests):\n    \"\"\"it is not possible to update the dependencies of a PREV task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an PREV task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_prev\"]\n    assert data[\"test_task3\"].status == data[\"status_prev\"]\n    # create dependency\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n\n    assert (\n        str(cm.value) == \"This is a PREV task and it is not allowed to change the \"\n        \"dependencies of a PREV task\"\n    )\n\n\ndef test_leaf_hrev_task_dependency_cannot_be_updated(setup_task_status_workflow_tests):\n    \"\"\"it is not possible to update the dependencies of a HREV task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an HREV task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_hrev\"]\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    # create dependency\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n\n    assert (\n        str(cm.value) == \"This is a HREV task and it is not allowed to change the \"\n        \"dependencies of a HREV task\"\n    )\n\n\ndef test_leaf_drev_task_dependency_cannot_be_updated(setup_task_status_workflow_tests):\n    \"\"\"it is not possible to update the dependencies of a DREV\n    task\n    \"\"\"\n    data = setup_task_status_workflow_tests\n    # find an DREV task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_drev\"]\n    assert data[\"test_task3\"].status == data[\"status_drev\"]\n    # create dependency\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n\n    assert (\n        str(cm.value) == \"This is a DREV task and it is not allowed to change the \"\n        \"dependencies of a DREV task\"\n    )\n\n\ndef test_leaf_oh_task_dependency_cannot_be_updated(setup_task_status_workflow_tests):\n    \"\"\"it is not possible to update the dependencies of a OH task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an OH task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_oh\"]\n    assert data[\"test_task3\"].status == data[\"status_oh\"]\n    # create dependency\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n\n    assert (\n        str(cm.value) == \"This is a OH task and it is not allowed to change the \"\n        \"dependencies of a OH task\"\n    )\n\n\ndef test_leaf_stop_task_dependency_cannot_be_updated(setup_task_status_workflow_tests):\n    \"\"\"it is not possible to update the dependencies of a STOP task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an STOP task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_stop\"]\n    assert data[\"test_task3\"].status == data[\"status_stop\"]\n    # create dependency\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n\n    assert (\n        str(cm.value) == \"This is a STOP task and it is not allowed to change the \"\n        \"dependencies of a STOP task\"\n    )\n\n\ndef test_leaf_cmpl_task_dependency_cannot_be_updated(setup_task_status_workflow_tests):\n    \"\"\"it is not possible to update the dependencies of a CMPL task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an CMPL task\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].status = data[\"status_cmpl\"]\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    # create dependency\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n\n    assert (\n        str(cm.value) == \"This is a CMPL task and it is not allowed to change the \"\n        \"dependencies of a CMPL task\"\n    )\n\n\n# dependencies of containers\n# container Tasks - dependency relation changes\n# RTS\ndef test_container_rts_task_updated_to_have_a_dependency_of_wfd_task_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"dep. between an RTS parent task to a WFD task and status is updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # make a task with WFD status\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task8\"].status = data[\"status_wfd\"]\n    assert data[\"test_task8\"].status == data[\"status_wfd\"]\n    # find a RTS container task\n    data[\"test_task3\"].children.append(data[\"test_task2\"])\n    data[\"test_task2\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_container_rts_task_updated_to_have_a_dependency_of_rts_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"dep. between an RTS parent task to an RTS task and status is updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # make a task with WFD status\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task8\"].status = data[\"status_rts\"]\n    assert data[\"test_task8\"].status == data[\"status_rts\"]\n    # find a RTS container task\n    data[\"test_task3\"].children.append(data[\"test_task2\"])\n    data[\"test_task2\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_container_rts_task_updated_to_have_a_dependency_of_wip_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"dep. between an RTS parent task to a WIP task and status is updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # make a task with WIP status\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task8\"].status = data[\"status_wip\"]\n    assert data[\"test_task8\"].status == data[\"status_wip\"]\n    # find a RTS container task\n    data[\"test_task3\"].children.append(data[\"test_task2\"])\n    data[\"test_task2\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_container_rts_task_updated_to_have_a_dependency_of_prev_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"dep. between an RTS parent task to a PREV task and status is updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # make a task with PREV status\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task8\"].status = data[\"status_prev\"]\n    assert data[\"test_task8\"].status == data[\"status_prev\"]\n    # find a RTS container task\n    data[\"test_task3\"].children.append(data[\"test_task2\"])\n    data[\"test_task2\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_container_rts_task_updated_to_have_a_dependency_of_hrev_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"dep. between an RTS parent task to an HREV task and status is updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # make a task with HREV status\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task8\"].status = data[\"status_hrev\"]\n    assert data[\"test_task8\"].status == data[\"status_hrev\"]\n    # find a RTS container task\n    data[\"test_task3\"].children.append(data[\"test_task2\"])\n    data[\"test_task2\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_container_rts_task_updated_to_have_a_dependency_of_oh_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"dep. between an RTS parent task to an OH task and status is updated to WFD.\"\"\"\n    data = setup_task_status_workflow_tests\n    # make a task with OH status\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task8\"].status = data[\"status_oh\"]\n    assert data[\"test_task8\"].status == data[\"status_oh\"]\n    # find a RTS container task\n    data[\"test_task3\"].children.append(data[\"test_task2\"])\n    data[\"test_task2\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n\n\ndef test_container_rts_task_updated_to_have_a_dependency_of_stop_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"dep. between an RTS parent task to a STOP task and status will stay RTS.\"\"\"\n    data = setup_task_status_workflow_tests\n    # make a task with STOP status\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task8\"].status = data[\"status_stop\"]\n    assert data[\"test_task8\"].status == data[\"status_stop\"]\n    # find a RTS container task\n    data[\"test_task3\"].children.append(data[\"test_task2\"])\n    data[\"test_task2\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    # create dependency\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n\n\n# Container Tasks - dependency relation changes\n# WIP - DREV - PREV - HREV - OH - STOP - CMPL\ndef test_container_wip_task_dependency_cannot_be_updated(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"it is not possible to update the dependencies of a WIP container task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an WIP task\n    data[\"test_task1\"].depends_on = []\n    data[\"test_task1\"].status = data[\"status_wip\"]\n    assert data[\"test_task1\"].status == data[\"status_wip\"]\n    # create dependency\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task1\"].depends_on.append(data[\"test_task8\"])\n\n    assert (\n        str(cm.value) == \"This is a WIP task and it is not allowed to change the \"\n        \"dependencies of a WIP task\"\n    )\n\n\ndef test_container_cmpl_task_dependency_cannot_be_updated(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"it is not possible to update the dependencies of a CMPL container task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # find an CMPL task\n    data[\"test_task1\"].status = data[\"status_cmpl\"]\n    assert data[\"test_task1\"].status == data[\"status_cmpl\"]\n    # create dependency\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task1\"].depends_on.append(data[\"test_task8\"])\n\n    assert (\n        str(cm.value) == \"This is a CMPL task and it is not allowed to change the \"\n        \"dependencies of a CMPL task\"\n    )\n\n\n#\n# Action Tests\n#\n\n\n# create_time_log\n# WFD\ndef test_create_time_log_in_wfd_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if create_time_log action is used in a WFD task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wfd\"]\n    resource = data[\"test_task3\"].resources[0]\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].create_time_log(resource, start, end)\n\n    assert (\n        str(cm.value) == \"Test Task 3 is a WFD task, and it is not allowed to create \"\n        \"TimeLogs for a WFD task, please supply a RTS, WIP, HREV or \"\n        \"DREV task!\"\n    )\n\n\n# RTS: status updated to WIP\ndef test_create_time_log_in_rts_leaf_task_status_updated_to_wip(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"RTS task converted to WIP if create_time_log action is used.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    resource = data[\"test_task9\"].resources[0]\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    data[\"test_task9\"].create_time_log(resource, start, end)\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n\n# RTS -> parent update\ndef test_create_time_log_in_rts_leaf_task_update_parent_status(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"parent of the RTS task converted to WIP after create_time_log action used.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task2\"].status = data[\"status_rts\"]\n    data[\"test_task8\"].status = data[\"status_rts\"]\n\n    assert data[\"test_task8\"].parent == data[\"test_task2\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task8\"].create_time_log(\n        resource=data[\"test_task8\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    assert data[\"test_task8\"].status == data[\"status_wip\"]\n    assert data[\"test_task2\"].status == data[\"status_wip\"]\n\n\n# RTS -> root task no problem\ndef test_create_time_log_in_rts_root_task_no_parent_no_problem(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"RTS leaf task status converted to WIP if create_time_log action is used.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    resource = data[\"test_task3\"].resources[0]\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    data[\"test_task3\"].create_time_log(resource, start, end)\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n\n\n# WIP\ndef test_create_time_log_in_wip_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"no problem if create_time_log in a WIP task, and the status stays WIP.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task9\"].status = data[\"status_wip\"]\n    resource = data[\"test_task9\"].resources[0]\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    data[\"test_task9\"].create_time_log(resource, start, end)\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n\n# PREV\ndef test_create_time_log_in_prev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"no problem to call create_time_log for a PREV task and the status stays PREV.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_prev\"]\n    resource = data[\"test_task3\"].resources[0]\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    assert data[\"test_task3\"].status == data[\"status_prev\"]\n    tlog = data[\"test_task3\"].create_time_log(resource, start, end)\n    assert isinstance(tlog, TimeLog)\n    assert data[\"test_task3\"].status == data[\"status_prev\"]\n\n\n# HREV\ndef test_create_time_log_in_hrev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"status converted to WIP if create_time_log is used in a HREV task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task9\"].status = data[\"status_hrev\"]\n    resource = data[\"test_task9\"].resources[0]\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    data[\"test_task9\"].create_time_log(resource, start, end)\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n\n# DREV\ndef test_create_time_log_in_drev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"status will stay DREV if create_time_log is used in a DREV task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task9\"].status = data[\"status_drev\"]\n    resource = data[\"test_task9\"].resources[0]\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    data[\"test_task9\"].create_time_log(resource, start, end)\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\n# OH\ndef test_create_time_log_in_oh_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the create_time_log actions is used in a OH task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task9\"].status = data[\"status_oh\"]\n    resource = data[\"test_task9\"].resources[0]\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task9\"].create_time_log(resource, start, end)\n\n    assert (\n        str(cm.value) == \"Test Task 9 is a OH task, and it is not allowed to create \"\n        \"TimeLogs for a OH task, please supply a RTS, WIP, HREV or DREV \"\n        \"task!\"\n    )\n\n\n# STOP\ndef test_create_time_log_in_stop_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the create_time_log action is used in a STOP task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task9\"].status = data[\"status_stop\"]\n    resource = data[\"test_task9\"].resources[0]\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task9\"].create_time_log(resource, start, end)\n\n    assert (\n        str(cm.value) == \"Test Task 9 is a STOP task, and it is not allowed to create \"\n        \"TimeLogs for a STOP task, please supply a RTS, WIP, HREV or \"\n        \"DREV task!\"\n    )\n\n\n# CMPL\ndef test_create_time_log_in_cmpl_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the create_time_log action is used in a CMPL task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task9\"].status = data[\"status_cmpl\"]\n    resource = data[\"test_task9\"].resources[0]\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task9\"].create_time_log(resource, start, end)\n\n    assert (\n        str(cm.value) == \"Test Task 9 is a CMPL task, and it is not allowed to create \"\n        \"TimeLogs for a CMPL task, please supply a RTS, WIP, HREV or \"\n        \"DREV task!\"\n    )\n\n\n# On Container Task\ndef test_create_time_log_on_container_task(setup_task_status_workflow_tests):\n    \"\"\"ValueError raised if the create_time_log action used in a container task.\"\"\"\n    data = setup_task_status_workflow_tests\n    start = datetime.datetime.now(pytz.utc)\n    end = datetime.datetime.now(pytz.utc) + datetime.timedelta(hours=1)\n    data[\"test_task2\"].id = 36\n    with pytest.raises(ValueError) as cm:\n        data[\"test_task2\"].create_time_log(resource=None, start=start, end=end)\n\n    assert (\n        str(cm.value) == \"Test Task 2 (id: 36) is a container task, and it is not \"\n        \"allowed to create TimeLogs for a container task\"\n    )\n\n\ndef test_create_time_log_is_creating_time_logs(setup_task_status_workflow_tests):\n    \"\"\"create_time_log action is really creating some time logs.\"\"\"\n    data = setup_task_status_workflow_tests\n    # initial condition\n    assert len(data[\"test_task3\"].time_logs) == 0\n\n    now = datetime.datetime.now(pytz.utc)\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now,\n        end=now + datetime.timedelta(hours=1),\n    )\n    assert len(data[\"test_task3\"].time_logs) == 1\n    assert data[\"test_task3\"].total_logged_seconds == 3600\n\n    now = datetime.datetime.now(pytz.utc)\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + datetime.timedelta(hours=1),\n        end=now + datetime.timedelta(hours=2),\n    )\n    assert len(data[\"test_task3\"].time_logs) == 2\n    assert data[\"test_task3\"].total_logged_seconds == 7200\n\n\ndef test_create_time_log_returns_time_log_instance(setup_task_status_workflow_tests):\n    \"\"\"create_time_log returns a TimeLog instance.\"\"\"\n    data = setup_task_status_workflow_tests\n    assert len(data[\"test_task3\"].time_logs) == 0\n\n    now = datetime.datetime.now(pytz.utc)\n    tl = data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now,\n        end=now + datetime.timedelta(hours=1),\n    )\n    assert isinstance(tl, TimeLog)\n\n\n# request_review\n# WFD\ndef test_request_review_in_wfd_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the request_review action is used in a WFD leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wfd\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_review()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a WFD task, and it is not \"\n        \"suitable for requesting a review, please supply a WIP task \"\n        \"instead.\"\n    )\n\n\n# RTS\ndef test_request_review_in_rts_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the request_review action is used in a RTS leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_review()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a RTS task, and it is not \"\n        \"suitable for requesting a review, please supply a WIP task \"\n        \"instead.\"\n    )\n\n\n# PREV\ndef test_request_review_in_prev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the request_review action is used in a PREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_prev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_review()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a PREV task, and it is not \"\n        \"suitable for requesting a review, please supply a WIP task \"\n        \"instead.\"\n    )\n\n\n# HREV\ndef test_request_review_in_hrev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the request_review action is used in a HREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_hrev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_review()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a HREV task, and it is not \"\n        \"suitable for requesting a review, please supply a WIP task \"\n        \"instead.\"\n    )\n\n\n# DREV\ndef test_request_review_in_drev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the request_review action is used in a DREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_drev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_review()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a DREV task, and it is not \"\n        \"suitable for requesting a review, please supply a WIP task \"\n        \"instead.\"\n    )\n\n\n# OH\ndef test_request_review_in_oh_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the request_review action is used in a OH leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_oh\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_review()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a OH task, and it is not \"\n        \"suitable for requesting a review, please supply a WIP task \"\n        \"instead.\"\n    )\n\n\n# STOP\ndef test_request_review_in_stop_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the request_review action is used in a STOP leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_stop\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_review()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a STOP task, and it is not \"\n        \"suitable for requesting a review, please supply a WIP task \"\n        \"instead.\"\n    )\n\n\n# CMPL\ndef test_request_review_in_cmpl_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the request_review action is used in a CMPL leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_cmpl\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_review()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a CMPL task, and it is not \"\n        \"suitable for requesting a review, please supply a WIP task \"\n        \"instead.\"\n    )\n\n\n# request_revision\n# WFD\ndef test_request_revision_in_wfd_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if request_revision action is used in a WFD leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wfd\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_revision()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id: 37) is a WFD task, and it is not suitable for \"\n        \"requesting a revision, please supply a PREV or CMPL task\"\n    )\n\n\n# RTS\ndef test_request_revision_in_rts_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the request_revision action is used in a RTS leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_revision()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id: 37) is a RTS task, and it is not suitable for \"\n        \"requesting a revision, please supply a PREV or CMPL task\"\n    )\n\n\n# WIP\ndef test_request_revision_in_wip_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the request_revision action is used in a WIP leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_revision()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id: 37) is a WIP task, and it is not suitable for \"\n        \"requesting a revision, please supply a PREV or CMPL task\"\n    )\n\n\n# HREV\n@pytest.mark.parametrize(\"schedule_unit\", [\"h\", TimeUnit.Hour])\ndef test_request_revision_in_hrev_leaf_task(\n    setup_task_status_workflow_tests, schedule_unit\n):\n    \"\"\"StatusError raised if the request_revision action is used in a HREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_hrev\"]\n    data[\"test_task3\"].id = 37\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": schedule_unit,\n    }\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_revision(**kw)\n\n    assert str(cm.value) == (\n        \"Test Task 3 (id: 37) is a HREV task, and it is not suitable \"\n        \"for requesting a revision, please supply a PREV or CMPL task\"\n    )\n\n\n# OH\n@pytest.mark.parametrize(\"schedule_unit\", [\"h\", TimeUnit.Hour])\ndef test_request_revision_in_oh_leaf_task(\n    setup_task_status_workflow_tests,\n    schedule_unit,\n):\n    \"\"\"StatusError raised if the request_revision action is used in a OH leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_oh\"]\n    data[\"test_task3\"].id = 37\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": schedule_unit,\n    }\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_revision(**kw)\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id: 37) is a OH task, and it is not suitable for \"\n        \"requesting a revision, please supply a PREV or CMPL task\"\n    )\n\n\n# STOP\n@pytest.mark.parametrize(\"schedule_unit\", [\"h\", TimeUnit.Hour])\ndef test_request_revision_in_stop_leaf_task(\n    setup_task_status_workflow_tests, schedule_unit\n):\n    \"\"\"StatusError raised if the request_revision action is used in a STOP leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_stop\"]\n    data[\"test_task3\"].id = 37\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": schedule_unit,\n    }\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].request_revision(**kw)\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id: 37) is a STOP task, and it is not suitable \"\n        \"for requesting a revision, please supply a PREV or CMPL task\"\n    )\n\n\n# hold\n# WFD\ndef test_hold_in_wfd_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the hold action is used in a WFD leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wfd\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].hold()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37) is a WFD task, only WIP or DREV tasks can \"\n        \"be set to On Hold\"\n    )\n\n\n# RTS\ndef test_hold_in_rts_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the hold action is used in a RTS leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].hold()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37) is a RTS task, only WIP or DREV tasks can \"\n        \"be set to On Hold\"\n    )\n\n\n# WIP: Status updated to OH\ndef test_hold_in_wip_leaf_task_status(setup_task_status_workflow_tests):\n    \"\"\"status updated to OH if the hold action is used in a WIP leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    data[\"test_task3\"].hold()\n    assert data[\"test_task3\"].status == data[\"status_oh\"]\n\n\n# WIP: Schedule values are intact\ndef test_hold_in_wip_leaf_task_schedule_values(setup_task_status_workflow_tests):\n    \"\"\"schedule values intact if the hold action is used in a WIP leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    data[\"test_task3\"].hold()\n    assert data[\"test_task3\"].schedule_timing == 10\n    assert data[\"test_task3\"].schedule_unit == TimeUnit.Day\n\n\n# WIP: Priority is set to 0\ndef test_hold_in_wip_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"priority set to 0 if the hold action is used in a WIP leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    data[\"test_task3\"].hold()\n    assert data[\"test_task3\"].priority == 0\n\n\n# PREV\ndef test_hold_in_prev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the hold action is used in a PREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_prev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].hold()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37) is a PREV task, only WIP or DREV tasks can \"\n        \"be set to On Hold\"\n    )\n\n\n# HREV\ndef test_hold_in_hrev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the hold action is used in a HREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_hrev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].hold()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37) is a HREV task, only WIP or DREV tasks can \"\n        \"be set to On Hold\"\n    )\n\n\n# DREV: Status updated to OH\ndef test_hold_in_drev_leaf_task_status_updated_to_oh(setup_task_status_workflow_tests):\n    \"\"\"status updated to OH if the hold action is used in a DREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_drev\"]\n    data[\"test_task3\"].hold()\n    assert data[\"test_task3\"].status == data[\"status_oh\"]\n\n\n# DREV: Schedule values are intact\ndef test_hold_in_drev_leaf_task_schedule_values_are_intact(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"schedule values intact if the hold action is used in a DREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_drev\"]\n    data[\"test_task3\"].hold()\n    assert data[\"test_task3\"].schedule_timing == 10\n    assert data[\"test_task3\"].schedule_unit == TimeUnit.Day\n\n\n# DREV: Priority is set to 0\ndef test_hold_in_drev_leaf_task_priority_set_to_0(setup_task_status_workflow_tests):\n    \"\"\"priority set to 0 if the hold action is used in a DREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_drev\"]\n    data[\"test_task3\"].hold()\n    assert data[\"test_task3\"].priority == 0\n\n\n# OH\ndef test_hold_in_oh_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"status will stay on OH if the hold action is used in a OH leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_oh\"]\n    data[\"test_task3\"].hold()\n    assert data[\"test_task3\"].status == data[\"status_oh\"]\n\n\n# STOP\ndef test_hold_in_stop_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the hold action is used in a STOP leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_stop\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].hold()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37) is a STOP task, only WIP or DREV tasks can \"\n        \"be set to On Hold\"\n    )\n\n\n# CMPL\ndef test_hold_in_cmpl_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the hold action is used in a CMPL leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_cmpl\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].hold()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37) is a CMPL task, only WIP or DREV tasks can \"\n        \"be set to On Hold\"\n    )\n\n\n# stop\n# WFD\ndef test_stop_in_wfd_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the stop action is used in a WFD leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wfd\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].stop()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37)is a WFD task and it is not possible to \"\n        \"stop a WFD task.\"\n    )\n\n\n# RTS\ndef test_stop_in_rts_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the stop action is used in a RTS leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].stop()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37)is a RTS task and it is not possible to \"\n        \"stop a RTS task.\"\n    )\n\n\n# WIP: Status Test\ndef test_stop_in_wip_leaf_task_status_is_updated_to_stop(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"status updated to STOP if the stop action is used in a WIP leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    data[\"test_task3\"].hold()\n    assert data[\"test_task3\"].status == data[\"status_oh\"]\n\n\n# WIP: Schedule Timing Test\ndef test_stop_in_wip_leaf_task_schedule_values_clamped(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"stop action on a WIP task clamps the schedule values to total_logged_seconds.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task8\"].status = data[\"status_rts\"]\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now,\n        end=now + td(hours=1),\n    )\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n    data[\"test_task8\"].status = data[\"status_wip\"]\n    data[\"test_task8\"].stop()\n    assert data[\"test_task8\"].schedule_timing == 2\n    assert data[\"test_task8\"].schedule_unit == TimeUnit.Hour\n\n\n# WIP: Dependency Status: WFD -> RTS\ndef test_stop_in_wip_leaf_task_dependent_task_status_updated_from_wfd_to_rts(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"stop action updates dependent task status from WFD to RTS on a WIP task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task8\"].status = data[\"status_rts\"]\n\n    data[\"test_task9\"].depends_on = [data[\"test_task8\"]]\n\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now,\n        end=now + td(hours=1),\n    )\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n    data[\"test_task8\"].status = data[\"status_wip\"]\n    data[\"test_task8\"].stop()\n    assert data[\"test_task9\"].status == data[\"status_rts\"]\n\n\n# WIP: Dependency Status: DREV -> WIP\ndef test_stop_in_wip_leaf_task_status_from_drev_to_hrev(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"stop action updates dependent task status from DREV to HREV on a WIP task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task8\"].status = data[\"status_cmpl\"]\n\n    data[\"test_task9\"].depends_on = [data[\"test_task8\"]]\n\n    TimeLog(\n        task=data[\"test_task9\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now,\n        end=now + td(hours=1),\n    )\n    TimeLog(\n        task=data[\"test_task9\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n    data[\"test_task9\"].status = data[\"status_wip\"]\n\n    data[\"test_task8\"].status = data[\"status_hrev\"]\n    data[\"test_task9\"].status = data[\"status_drev\"]\n\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now + td(hours=4),\n        end=now + td(hours=5),\n    )\n\n    data[\"test_task8\"].status = data[\"status_wip\"]\n    data[\"test_task8\"].stop()\n    assert data[\"test_task9\"].status == data[\"status_hrev\"]\n\n\n# WIP: parent statuses\ndef test_stop_in_drev_leaf_task_check_parent_status(setup_task_status_workflow_tests):\n    \"\"\"parent status is updated okay if stop action is used in a DREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    TimeLog(\n        task=data[\"test_task9\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now,\n        end=now + td(hours=1),\n    )\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    data[\"test_task9\"].status = data[\"status_drev\"]\n    data[\"test_task9\"].stop()\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n    assert data[\"test_asset1\"].status == data[\"status_cmpl\"]\n\n\n# PREV\ndef test_stop_in_prev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the stop action is used in a PREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_prev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].stop()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37)is a PREV task and it is not possible to \"\n        \"stop a PREV task.\"\n    )\n\n\n# HREV\ndef test_stop_in_hrev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the stop action is used in a HREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_hrev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].stop()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37)is a HREV task and it is not possible to \"\n        \"stop a HREV task.\"\n    )\n\n\n# DREV: Status Test\ndef test_stop_in_drev_leaf_task_status_is_updated_to_stop(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"status set to STOP if the stop action is used in a DREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_drev\"]\n    data[\"test_task3\"].stop()\n    assert data[\"test_task3\"].status == data[\"status_stop\"]\n\n\n# DREV: Schedule Timing Test\ndef test_stop_in_drev_leaf_task_schedule_values_are_clamped(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"stop action clamps schedule_timing to current time logs in a DREV lef task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task8\"].status = data[\"status_rts\"]\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now,\n        end=now + td(hours=2),\n    )\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=4),\n    )\n    data[\"test_task8\"].status = data[\"status_drev\"]\n    data[\"test_task8\"].stop()\n    assert data[\"test_task8\"].schedule_timing == 4\n    assert data[\"test_task8\"].schedule_unit == TimeUnit.Hour\n\n\n# DREV: parent statuses\ndef test_stop_in_drev_leaf_task_parent_status(setup_task_status_workflow_tests):\n    \"\"\"parent status is updated okay if the stop action is used in a DREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    TimeLog(\n        task=data[\"test_task9\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now,\n        end=now + td(hours=1),\n    )\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    data[\"test_task9\"].status = data[\"status_wip\"]\n    data[\"test_task9\"].stop()\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n    assert data[\"test_asset1\"].status == data[\"status_cmpl\"]\n\n\n# DREV: Dependency Status: WFD -> RTS\ndef test_stop_in_drev_leaf_task_dependent_task_status_updated_from_wfd_to_rts(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"dependent task statuses updated okay if stop action taken on a DREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task8\"].status = data[\"status_rts\"]\n\n    data[\"test_task9\"].depends_on = [data[\"test_task8\"]]\n\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now,\n        end=now + td(hours=1),\n    )\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n    data[\"test_task8\"].status = data[\"status_wip\"]\n    data[\"test_task8\"].stop()\n    assert data[\"test_task9\"].status == data[\"status_rts\"]\n\n\n# DREV: Dependency Status: DREV -> WIP\ndef test_stop_in_drev_leaf_task_dependent_task_status_updated_from_drev_to_hrev(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"dependent task statuses updated okay if stop action taken on a DREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task8\"].status = data[\"status_rts\"]\n\n    data[\"test_task9\"].depends_on = [data[\"test_task8\"]]\n    data[\"test_task9\"].status = data[\"status_drev\"]  # this set by an\n    # action in normal run\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now,\n        end=now + td(hours=1),\n    )\n    TimeLog(\n        task=data[\"test_task8\"],\n        resource=data[\"test_task8\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n    data[\"test_task8\"].status = data[\"status_wip\"]\n    data[\"test_task8\"].stop()\n    assert data[\"test_task9\"].status == data[\"status_hrev\"]\n\n\n# OH\ndef test_stop_in_oh_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the stop action is used in a OH leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_oh\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].stop()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37)is a OH task and it is not possible to stop \"\n        \"a OH task.\"\n    )\n\n\n# STOP\ndef test_stop_in_stop_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"status will stay on STOP if the stop action is used in a STOP leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_stop\"]\n    data[\"test_task3\"].stop()\n    assert data[\"test_task3\"].status == data[\"status_stop\"]\n\n\n# CMPL\ndef test_stop_in_cmpl_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the stop action is used in a CMPL leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_cmpl\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].stop()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37)is a CMPL task and it is not possible to \"\n        \"stop a CMPL task.\"\n    )\n\n\n# resume\n# WFD\ndef test_resume_in_wfd_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the resume action is used in a WFD leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wfd\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].resume()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a WFD task, and it is not suitable to \"\n        \"be resumed, please supply an OH or STOP task\"\n    )\n\n\n# RTS\ndef test_resume_in_rts_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the resume action is used in a RTS leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].resume()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a RTS task, and it is not suitable to \"\n        \"be resumed, please supply an OH or STOP task\"\n    )\n\n\n# WIP\ndef test_resume_in_wip_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the resume action is used in a WIP leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].resume()\n\n    assert (\n        str(cm.value) == \"Test Task 3 (id:37) is a WIP task, and it is not suitable to \"\n        \"be resumed, please supply an OH or STOP task\"\n    )\n\n\n# PREV\ndef test_resume_in_prev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the resume action is used in a PREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_prev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].resume()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37) is a PREV task, and it is not suitable to \"\n        \"be resumed, please supply an OH or STOP task\"\n    )\n\n\n# HREV\ndef test_resume_in_hrev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the resume action is used in a HREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_hrev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].resume()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37) is a HREV task, and it is not suitable to \"\n        \"be resumed, please supply an OH or STOP task\"\n    )\n\n\n# DREV\ndef test_resume_in_drev_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the resume action is used in a DREV leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_drev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].resume()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37) is a DREV task, and it is not suitable to \"\n        \"be resumed, please supply an OH or STOP task\"\n    )\n\n\n# OH: no dependency -> WIP\ndef test_resume_in_oh_leaf_task_with_no_dependencies(setup_task_status_workflow_tests):\n    \"\"\"resume action on a OH leaf task with no dependencies updates status to WIP.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_oh\"]\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].resume()\n    # no time logs so it will return back to rts\n    # the test is wrong in the first place (no way to turn a task with no\n    # time logs in to a OH task),\n    # but checks a situation that the system needs to be more robust\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n\n\n# OH: STOP dependencies -> WIP\ndef test_resume_in_oh_leaf_task_with_stop_dependencies(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"resume action on a OH leaf task with STOP dependencies updates status to WIP.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    data[\"test_task3\"].status = data[\"status_stop\"]\n    data[\"test_task9\"].status = data[\"status_oh\"]\n\n    data[\"test_task9\"].resume()\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n\n# OH: CMPL dependencies -> WIP\ndef test_resume_in_oh_leaf_task_with_cmpl_dependencies(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"resume action on a OH leaf task with CMPL dependencies updates status to WIP.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    data[\"test_task3\"].status = data[\"status_cmpl\"]\n    data[\"test_task9\"].status = data[\"status_oh\"]\n\n    data[\"test_task9\"].resume()\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n\n# STOP: no dependency -> WIP\ndef test_resume_in_stop_leaf_task_with_no_dependencies(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"resume action on a STOP leaf task with no dependencies updates status to WIP.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_stop\"]\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].resume()\n    # no time logs so it will return back to rts\n    # the test is wrong in the first place (no way to turn a task with no\n    # time logs in to a OH task),\n    # but checks a situation that the system needs to be more robust\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n\n\n# STOP: STOP dependencies -> WIP\ndef test_resume_in_stop_leaf_task_with_stop_dependencies(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"resume action on a STOP leaf task with STOP dep.s updates status to WIP.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    data[\"test_task3\"].status = data[\"status_stop\"]\n    data[\"test_task9\"].status = data[\"status_stop\"]\n\n    data[\"test_task9\"].resume()\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n\n# STOP: CMPL dependencies -> WIP\ndef test_resume_in_stop_leaf_task_with_cmpl_dependencies(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"resume action on a STOP leaf task with CMPL dep.s updates status to WIP.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    data[\"test_task3\"].status = data[\"status_cmpl\"]\n    data[\"test_task9\"].status = data[\"status_stop\"]\n\n    data[\"test_task9\"].resume()\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n\n# CMPL\ndef test_resume_in_cmpl_leaf_task(setup_task_status_workflow_tests):\n    \"\"\"StatusError raised if the resume action is used in a CMPL leaf task.\"\"\"\n    data = setup_task_status_workflow_tests\n    data[\"test_task3\"].status = data[\"status_drev\"]\n    data[\"test_task3\"].id = 37\n    with pytest.raises(StatusError) as cm:\n        data[\"test_task3\"].resume()\n\n    assert (\n        str(cm.value)\n        == \"Test Task 3 (id:37) is a DREV task, and it is not suitable to \"\n        \"be resumed, please supply an OH or STOP task\"\n    )\n\n\ndef test_review_set_review_number_is_not_an_integer(setup_task_status_workflow_tests):\n    \"\"\"TypeError raised if the review_number arg is not an int in Task.review_set().\"\"\"\n    data = setup_task_status_workflow_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_task3\"].review_set(\"not an integer\")\n\n    assert (\n        str(cm.value)\n        == \"review_number argument in Task.review_set should be a positive \"\n        \"integer, not str: 'not an integer'\"\n    )\n\n\ndef test_review_set_review_number_is_a_negative_integer(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"ValueError raised if the review_number is a negative number.\"\"\"\n    data = setup_task_status_workflow_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_task3\"].review_set(-10)\n\n    assert (\n        str(cm.value)\n        == \"review_number argument in Task.review_set should be a positive \"\n        \"integer, not -10\"\n    )\n\n\ndef test_review_set_review_number_is_zero(setup_task_status_workflow_tests):\n    \"\"\"ValueError raised if the review_number is zero.\"\"\"\n    data = setup_task_status_workflow_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_task3\"].review_set(0)\n\n    assert (\n        str(cm.value)\n        == \"review_number argument in Task.review_set should be a positive \"\n        \"integer, not 0\"\n    )\n\n\ndef test_leaf_drev_task_with_no_dependency_and_no_timelogs_update_status_with_dependent_statuses_fixes_status(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"Task.update_status_with_dependent_statuses() fixes status of a leaf DREV task\n    with no deps. (something went wrong) to RTS if there is no TimeLog and to WIP if\n    there is a TimeLog.\n    \"\"\"\n    data = setup_task_status_workflow_tests\n    # use task6 and task5\n    data[\"test_task5\"].depends_on = []\n\n    # set the statuses\n    data[\"test_task5\"].status = data[\"status_drev\"]\n\n    assert data[\"status_drev\"] == data[\"test_task5\"].status\n\n    # fix status with dependencies\n    data[\"test_task5\"].update_status_with_dependent_statuses()\n\n    # check the status\n    assert data[\"status_rts\"] == data[\"test_task5\"].status\n\n\ndef test_leaf_drev_task_with_no_dependency_but_with_timelogs_update_status_with_dependent_statuses_fixes_status(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"Task.update_status_with_dependent_statuses() will fix the status of a leaf DREV\n    task with no dependency (something went wrong) to RTS if there is no TimeLog and to\n    WIP if there is a TimeLog.\n    \"\"\"\n    data = setup_task_status_workflow_tests\n    # use task6 and task5\n    data[\"test_task5\"].depends_on = []\n\n    # create some time logs for\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    data[\"test_task5\"].create_time_log(\n        resource=data[\"test_task5\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    # set the statuses\n    data[\"test_task5\"].status = data[\"status_drev\"]\n\n    assert data[\"status_drev\"] == data[\"test_task5\"].status\n\n    # fix status with dependencies\n    data[\"test_task5\"].update_status_with_dependent_statuses()\n\n    # check the status\n    assert data[\"status_wip\"] == data[\"test_task5\"].status\n\n\ndef test_leaf_wip_task_with_no_dependency_and_no_timelogs_update_status_with_dependent_statuses_fixes_status(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"Task.update_status_with_dependent_statuses() will fix the status of a leaf WIP\n    task with no dependency (something went wrong) to RTS if there is no TimeLog and to\n    WIP if there is a TimeLog.\n    \"\"\"\n    data = setup_task_status_workflow_tests\n    # use task6 and task5\n    data[\"test_task5\"].depends_on = []\n\n    # check if there is no time logs\n    assert data[\"test_task5\"].time_logs == []\n\n    # set the statuses\n    data[\"test_task5\"].status = data[\"status_wip\"]\n\n    assert data[\"status_wip\"] == data[\"test_task5\"].status\n\n    # fix status with dependencies\n    data[\"test_task5\"].update_status_with_dependent_statuses()\n\n    # check the status\n    assert data[\"status_rts\"] == data[\"test_task5\"].status\n\n\ndef test_container_task_update_status_with_dependent_status_will_skip(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"update_status_with_dependent_status() will skip container tasks.\"\"\"\n    data = setup_task_status_workflow_tests\n    # the following should do nothing\n    data[\"test_task1\"].update_status_with_dependent_statuses()\n\n\ndef test_update_status_with_children_statuses_with_leaf_task(\n    setup_task_status_workflow_tests,\n):\n    \"\"\"update_status_with_children_statuses will skip leaf tasks.\"\"\"\n    data = setup_task_status_workflow_tests\n    # the following should do nothing\n    data[\"test_task4\"].update_status_with_children_statuses()\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_task_status_workflow_db_tests(setup_postgresql_db):\n    \"\"\"Set up the Task status workflow tests with a database.\"\"\"\n    data = dict()\n\n    # test users\n    data[\"test_user1\"] = User(\n        name=\"Test User 1\", login=\"tuser1\", email=\"tuser1@test.com\", password=\"secret\"\n    )\n    DBSession.add(data[\"test_user1\"])\n\n    data[\"test_user2\"] = User(\n        name=\"Test User 2\", login=\"tuser2\", email=\"tuser2@test.com\", password=\"secret\"\n    )\n    DBSession.add(data[\"test_user2\"])\n\n    # create a couple of tasks\n    data[\"status_new\"] = Status.query.filter_by(code=\"NEW\").first()\n    data[\"status_wfd\"] = Status.query.filter_by(code=\"WFD\").first()\n    data[\"status_rts\"] = Status.query.filter_by(code=\"RTS\").first()\n    data[\"status_wip\"] = Status.query.filter_by(code=\"WIP\").first()\n    data[\"status_prev\"] = Status.query.filter_by(code=\"PREV\").first()\n    data[\"status_hrev\"] = Status.query.filter_by(code=\"HREV\").first()\n    data[\"status_drev\"] = Status.query.filter_by(code=\"DREV\").first()\n    data[\"status_oh\"] = Status.query.filter_by(code=\"OH\").first()\n    data[\"status_stop\"] = Status.query.filter_by(code=\"STOP\").first()\n    data[\"status_cmpl\"] = Status.query.filter_by(code=\"CMPL\").first()\n\n    data[\"status_rrev\"] = Status.query.filter_by(code=\"RREV\").first()\n    data[\"status_app\"] = Status.query.filter_by(code=\"APP\").first()\n\n    # repository\n    data[\"test_repo\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        linux_path=\"/mnt/T/\",\n        windows_path=\"T:/\",\n        macos_path=\"/Volumes/T\",\n    )\n    DBSession.add(data[\"test_repo\"])\n\n    # proj1\n    data[\"test_project1\"] = Project(\n        name=\"Test Project 1\",\n        code=\"TProj1\",\n        repository=data[\"test_repo\"],\n        start=datetime.datetime(2013, 6, 20, 0, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, 0, tzinfo=pytz.utc),\n    )\n    DBSession.add(data[\"test_project1\"])\n\n    # root tasks\n    data[\"test_task1\"] = Task(\n        name=\"Test Task 1\",\n        project=data[\"test_project1\"],\n        responsible=[data[\"test_user1\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n    DBSession.add(data[\"test_task1\"])\n\n    data[\"test_task2\"] = Task(\n        name=\"Test Task 2\",\n        project=data[\"test_project1\"],\n        responsible=[data[\"test_user1\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n    DBSession.add(data[\"test_task2\"])\n\n    data[\"test_task3\"] = Task(\n        name=\"Test Task 3\",\n        project=data[\"test_project1\"],\n        resources=[data[\"test_user1\"], data[\"test_user2\"]],\n        responsible=[data[\"test_user1\"], data[\"test_user2\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n    DBSession.add(data[\"test_task3\"])\n\n    # children tasks\n\n    # children of data[\"test_task1\"]\n    data[\"test_task4\"] = Task(\n        name=\"Test Task 4\",\n        parent=data[\"test_task1\"],\n        status=data[\"status_wfd\"],\n        resources=[data[\"test_user1\"]],\n        depends_on=[data[\"test_task3\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n    DBSession.add(data[\"test_task4\"])\n\n    data[\"test_task5\"] = Task(\n        name=\"Test Task 5\",\n        parent=data[\"test_task1\"],\n        resources=[data[\"test_user1\"]],\n        depends_on=[data[\"test_task4\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n    DBSession.add(data[\"test_task5\"])\n\n    data[\"test_task6\"] = Task(\n        name=\"Test Task 6\",\n        parent=data[\"test_task1\"],\n        resources=[data[\"test_user1\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n    DBSession.add(data[\"test_task6\"])\n\n    # children of data[\"test_task2\"]\n    data[\"test_task7\"] = Task(\n        name=\"Test Task 7\",\n        parent=data[\"test_task2\"],\n        resources=[data[\"test_user2\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n    DBSession.add(data[\"test_task7\"])\n\n    data[\"test_task8\"] = Task(\n        name=\"Test Task 8\",\n        parent=data[\"test_task2\"],\n        resources=[data[\"test_user2\"]],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n    DBSession.add(data[\"test_task8\"])\n\n    # create an asset in between\n    data[\"test_asset1\"] = Asset(\n        name=\"Test Asset 1\",\n        code=\"TA1\",\n        parent=data[\"test_task7\"],\n        type=Type(\n            name=\"Character\",\n            code=\"Char\",\n            target_entity_type=\"Asset\",\n        ),\n    )\n    DBSession.add(data[\"test_asset1\"])\n\n    # new task under asset\n    data[\"test_task9\"] = Task(\n        name=\"Test Task 9\",\n        parent=data[\"test_asset1\"],\n        start=datetime.datetime(2013, 6, 20, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 30, 0, 0, tzinfo=pytz.utc),\n        resources=[data[\"test_user2\"]],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        schedule_model=ScheduleModel.Effort,\n    )\n    DBSession.add(data[\"test_task9\"])\n    DBSession.commit()\n\n    # --------------\n    # Task Hierarchy\n    # --------------\n    #\n    # +-> Test Task 1\n    # |   |\n    # |   +-> Test Task 4\n    # |   |\n    # |   +-> Test Task 5\n    # |   |\n    # |   +-> Test Task 6\n    # |\n    # +-> Test Task 2\n    # |   |\n    # |   +-> Test Task 7\n    # |   |   |\n    # |   |   +-> Test Asset 1\n    # |   |       |\n    # |   |       +-> Test Task 9\n    # |   |\n    # |   +-> Test Task 8\n    # |\n    # +-> Test Task 3\n\n    # no children for data[\"test_task3\"]\n    data[\"all_tasks\"] = [\n        data[\"test_task1\"],\n        data[\"test_task2\"],\n        data[\"test_task3\"],\n        data[\"test_task4\"],\n        data[\"test_task5\"],\n        data[\"test_task6\"],\n        data[\"test_task7\"],\n        data[\"test_task8\"],\n        data[\"test_task9\"],\n        data[\"test_asset1\"],\n    ]\n    return data\n\n\ndef test_container_rts_task_updated_to_have_a_dependency_of_cmpl_task(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"set dependency between an RTS container task to a CMPL task and will stay RTS.\"\"\"\n    data = setup_task_status_workflow_db_tests\n    # make a task with CMPL status\n    data[\"test_task3\"].depends_on = []\n    data[\"test_task3\"].children.append(data[\"test_task6\"])\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    data[\"test_task8\"].create_time_log(\n        resource=data[\"test_task8\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    reviews = data[\"test_task8\"].request_review()\n    for review in reviews:\n        review.approve()\n\n    assert data[\"test_task8\"].status == data[\"status_cmpl\"]\n\n    # find a RTS container task\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n\n    # create dependency\n    data[\"test_task3\"].depends_on.append(data[\"test_task8\"])\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n\n\n# WIP: review instances\ndef test_request_review_in_wip_leaf_task_review_instances(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"request_review action returns reviews for each responsible on a WIP leaf task.\"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].responsible = [data[\"test_user1\"], data[\"test_user2\"]]\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    reviews = data[\"test_task3\"].request_review()\n    assert len(reviews) == 2\n    assert isinstance(reviews[0], Review)\n    assert isinstance(reviews[1], Review)\n\n\n# WIP: review instances review_number is correct\ndef test_request_review_in_wip_leaf_task_review_instances_review_number(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"review_number attribute of the created Reviews are correctly set.\"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].responsible = [data[\"test_user1\"], data[\"test_user2\"]]\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    # request a review\n    reviews = data[\"test_task3\"].request_review()\n    review1 = reviews[0]\n    review2 = reviews[1]\n    assert review1.review_number == 1\n    assert review2.review_number == 1\n\n    # finalize reviews\n    review1.approve()\n    review2.approve()\n\n    # request a revision\n    review3 = data[\"test_task3\"].request_revision(\n        reviewer=data[\"test_user1\"],\n        description=\"some description\",\n        schedule_timing=1,\n        schedule_unit=TimeUnit.Day,\n    )\n\n    # the new_review.revision number still should be 1\n    assert review3.review_number == 2\n\n    # and then ask a review again\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    reviews = data[\"test_task3\"].request_review()\n    assert reviews[0].review_number == 3\n    assert reviews[1].review_number == 3\n\n\n# WIP: status updated to PREV\ndef test_request_review_in_wip_leaf_task_status_updated_to_prev(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"request_review action updates WIP leaf task to PREV.\"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    data[\"test_task3\"].request_review()\n    assert data[\"test_task3\"].status == data[\"status_prev\"]\n\n\n# CMPL: dependent task dependency_target update CMPL -> DREV\ndef test_request_revision_in_cmpl_leaf_task_cmpl_dependent_task_dependency_target_updated_to_onstart(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"dependency_target attribute of the TaskDependency object between the revised task\n    and the dependent CMPL task set to 'onstart' if the request_revision action is used\n    in a CMPL leaf task.\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    # create a couple of TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    # remove any TaskDependency instances\n    # for i in TaskDependency.query.all():\n    #     DBSession.delete(i)\n    #\n    # DBSession.commit()\n\n    data[\"test_task3\"].depends_on = [data[\"test_task9\"]]  # PREV\n    data[\"test_task4\"].depends_on = [data[\"test_task9\"]]  # HREV\n    data[\"test_task5\"].depends_on = [data[\"test_task9\"]]  # STOP\n    data[\"test_task6\"].depends_on = [data[\"test_task9\"]]  # OH\n    data[\"test_task8\"].depends_on = [data[\"test_task9\"]]  # DREV\n    assert data[\"test_task9\"] in data[\"test_task5\"].depends_on\n    assert data[\"test_task9\"] in data[\"test_task6\"].depends_on\n    assert data[\"test_task9\"] in data[\"test_task8\"].depends_on\n\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0], start=now, end=now + td(hours=1)\n    )\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    reviews = data[\"test_task9\"].request_review()\n    for r in reviews:\n        r.approve()\n    assert data[\"test_task9\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task8\"].status == data[\"status_rts\"]\n\n    data[\"test_task8\"].create_time_log(\n        resource=data[\"test_task8\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n    assert data[\"test_task8\"].status == data[\"status_wip\"]\n\n    [r.approve() for r in data[\"test_task8\"].request_review()]\n    assert data[\"test_task8\"].status == data[\"status_cmpl\"]\n\n    # now work on task5\n    data[\"test_task5\"].create_time_log(\n        resource=data[\"test_task5\"].resources[0],\n        start=now + td(hours=3),\n        end=now + td(hours=4),\n    )\n    assert data[\"test_task5\"].status == data[\"status_wip\"]\n    data[\"test_task5\"].hold()\n    assert data[\"test_task5\"].status == data[\"status_oh\"]\n\n    # now work on task6\n    data[\"test_task6\"].create_time_log(\n        resource=data[\"test_task6\"].resources[0],\n        start=now + td(hours=4),\n        end=now + td(hours=5),\n    )\n    assert data[\"test_task6\"].status == data[\"status_wip\"]\n    data[\"test_task6\"].stop()\n    assert data[\"test_task6\"].status == data[\"status_stop\"]\n\n    # now work on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=5),\n        end=now + td(hours=6),\n    )\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    data[\"test_task3\"].request_review()\n    assert data[\"test_task3\"].status == data[\"status_prev\"]\n\n    # now work on task4\n    data[\"test_task4\"].create_time_log(\n        resource=data[\"test_task4\"].resources[0],\n        start=now + td(hours=6),\n        end=now + td(hours=7),\n    )\n    assert data[\"test_task4\"].status == data[\"status_wip\"]\n    reviews = data[\"test_task4\"].request_review()\n    DBSession.add_all(reviews)\n    DBSession.commit()\n\n    assert data[\"test_task4\"].status == data[\"status_prev\"]\n    for r in reviews:\n        r.request_revision(schedule_timing=1, schedule_unit=TimeUnit.Hour)\n    assert data[\"test_task4\"].status == data[\"status_hrev\"]\n\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": TimeUnit.Hour,\n    }\n    data[\"test_task9\"].request_revision(**kw)\n\n    tdep_t3 = (\n        TaskDependency.query.filter_by(task=data[\"test_task3\"])\n        .filter_by(depends_on=data[\"test_task9\"])\n        .first()\n    )\n    tdep_t4 = (\n        TaskDependency.query.filter_by(task=data[\"test_task4\"])\n        .filter_by(depends_on=data[\"test_task9\"])\n        .first()\n    )\n    tdep_t5 = (\n        TaskDependency.query.filter_by(task=data[\"test_task5\"])\n        .filter_by(depends_on=data[\"test_task9\"])\n        .first()\n    )\n    tdep_t6 = (\n        TaskDependency.query.filter_by(task=data[\"test_task6\"])\n        .filter_by(depends_on=data[\"test_task9\"])\n        .first()\n    )\n    tdep_t8 = (\n        TaskDependency.query.filter_by(task=data[\"test_task8\"])\n        .filter_by(depends_on=data[\"test_task9\"])\n        .first()\n    )\n    assert tdep_t3 is not None\n    assert tdep_t4 is not None\n    assert tdep_t5 is not None\n    assert tdep_t6 is not None\n    assert tdep_t8 is not None\n    assert tdep_t3.dependency_target == DependencyTarget.OnStart\n    assert tdep_t4.dependency_target == DependencyTarget.OnStart\n    assert tdep_t5.dependency_target == DependencyTarget.OnStart\n    assert tdep_t6.dependency_target == DependencyTarget.OnStart\n    assert tdep_t8.dependency_target == DependencyTarget.OnStart\n\n\n# CMPL: dependent task status update CMPL -> DREV\ndef test_request_revision_in_cmpl_leaf_task_cmpl_dependent_task_updated_to_drev(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status of the dependent CMPL task set to DREV\n    if the request_revision action is used in a CMPL leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task8\"].depends_on = [data[\"test_task9\"]]\n    assert data[\"test_task9\"] in data[\"test_task8\"].depends_on\n\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0], start=now, end=now + td(hours=1)\n    )\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    reviews = data[\"test_task9\"].request_review()\n    for r in reviews:\n        r.approve()\n    assert data[\"test_task9\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task8\"].status == data[\"status_rts\"]\n\n    data[\"test_task8\"].create_time_log(\n        resource=data[\"test_task8\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n    assert data[\"test_task8\"].status == data[\"status_wip\"]\n\n    [r.approve() for r in data[\"test_task8\"].request_review()]\n    assert data[\"test_task8\"].status == data[\"status_cmpl\"]\n\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": TimeUnit.Hour,\n    }\n    data[\"test_task9\"].request_revision(**kw)\n\n    assert data[\"test_task9\"].status == data[\"status_hrev\"]\n    assert data[\"test_task8\"].status == data[\"status_drev\"]\n\n\n# CMPL: dependent task parent status updated to WIP\ndef test_request_revision_in_cmpl_leaf_task_dependent_task_parent_status_updated_to_wip(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status of the dependent task parent updated to WIP\n    if the request_revision action is used in a CMPL leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task9\"].depends_on = [data[\"test_task8\"]]\n    data[\"test_task9\"].status = data[\"status_wfd\"]\n    data[\"test_asset1\"].status = data[\"status_wfd\"]\n    data[\"test_task8\"].status = data[\"status_rts\"]\n\n    data[\"test_task8\"].create_time_log(\n        resource=data[\"test_task8\"].resources[0], start=now, end=now + td(hours=1)\n    )\n    data[\"test_task8\"].create_time_log(\n        resource=data[\"test_task8\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    data[\"test_task8\"].status = data[\"status_cmpl\"]\n    data[\"test_task9\"].status = data[\"status_cmpl\"]\n    data[\"test_asset1\"].status = data[\"status_cmpl\"]\n    data[\"test_task7\"].status = data[\"status_cmpl\"]\n\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": TimeUnit.Hour,\n    }\n    review = data[\"test_task8\"].request_revision(**kw)\n\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n    assert data[\"test_asset1\"].status == data[\"status_wip\"]\n    assert data[\"test_task7\"].status == data[\"status_wip\"]\n\n\n# CMPL: parent status update\ndef test_request_revision_in_cmpl_leaf_task_parent_status_updated_to_wip(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status of the parent set to WIP if the\n    request_revision action is used in a CMPL leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    TimeLog(\n        task=data[\"test_task9\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now,\n        end=now + td(hours=1),\n    )\n    TimeLog(\n        task=data[\"test_task9\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    data[\"test_task9\"].status = data[\"status_cmpl\"]\n    data[\"test_asset1\"].status = data[\"status_cmpl\"]\n\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": TimeUnit.Hour,\n    }\n    review = data[\"test_task9\"].request_revision(**kw)\n    assert data[\"test_asset1\"].status == data[\"status_wip\"]\n\n\n# CMPL: dependent task status update RTS -> WFD\ndef test_request_revision_in_cmpl_leaf_task_rts_dependent_task_updated_to_wfd(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status of the dependent RTS task set to WFD\n    if the request_revision action is used in a CMPL leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task8\"].depends_on = [data[\"test_task9\"]]\n    data[\"test_task8\"].status = data[\"status_wfd\"]\n\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    TimeLog(\n        task=data[\"test_task9\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now,\n        end=now + td(hours=1),\n    )\n    TimeLog(\n        task=data[\"test_task9\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    data[\"test_task9\"].status = data[\"status_cmpl\"]\n\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": TimeUnit.Hour,\n    }\n    review = data[\"test_task9\"].request_revision(**kw)\n    assert data[\"test_task8\"].status == data[\"status_wfd\"]\n\n\n# CMPL: schedule info update\ndef test_request_revision_in_cmpl_leaf_task_schedule_info_update(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"timing values are extended with the supplied values\n    if the request_revision action is used in a CMPL leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    tlog0 = data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n    DBSession.add(tlog0)\n    tlog1 = data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n    DBSession.add(tlog1)\n    DBSession.commit()\n    assert data[\"test_task3\"].total_logged_seconds == 7200\n\n    reviews = data[\"test_task3\"].request_review()\n    review1 = reviews[0]\n    review2 = reviews[1]\n    review1.approve()\n    review2.approve()\n\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": TimeUnit.Hour,\n    }\n    revision = data[\"test_task3\"].request_revision(**kw)\n    DBSession.add(revision)\n    assert data[\"test_task3\"].schedule_timing == 6\n    assert data[\"test_task3\"].schedule_unit == TimeUnit.Hour\n\n\n# CMPL: status update\ndef test_request_revision_in_cmpl_leaf_task_status_updated_to_hrev(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status set to HREV and the timing values are\n    extended with the supplied values if the request_revision action is\n    used in a CMPL leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_cmpl\"]\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": TimeUnit.Hour,\n    }\n    review = data[\"test_task3\"].request_revision(**kw)\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n\n\n# CMPL: dependent task status update WIP -> DREV\ndef test_request_revision_in_cmpl_leaf_task_wip_dependent_task_updated_to_drev(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status of the dependent WIP task set to DREV\n    if the request_revision action is used in a CMPL leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task8\"].depends_on = [data[\"test_task9\"]]\n    data[\"test_task8\"].status = data[\"status_wip\"]\n\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    TimeLog(\n        task=data[\"test_task9\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now,\n        end=now + td(hours=1),\n    )\n    TimeLog(\n        task=data[\"test_task9\"],\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    data[\"test_task9\"].status = data[\"status_cmpl\"]\n\n    kw = {\n        \"reviewer\": data[\"test_user1\"],\n        \"description\": \"do something uleyn\",\n        \"schedule_timing\": 4,\n        \"schedule_unit\": TimeUnit.Hour,\n    }\n    review = data[\"test_task9\"].request_revision(**kw)\n    assert data[\"test_task8\"].status == data[\"status_drev\"]\n\n\ndef test_request_revision_in_deeper_dependency_setup(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"all the dependent task statuses are updated to DREV.\"\"\"\n    data = setup_task_status_workflow_db_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    # # remove any TaskDependency instances\n    # for i in TaskDependency.query.all():\n    #     DBSession.delete(i)\n    # DBSession.commit()\n\n    data[\"test_task5\"].depends_on = []\n    data[\"test_task6\"].depends_on = [data[\"test_task5\"]]\n    data[\"test_task3\"].depends_on = [data[\"test_task6\"]]\n    data[\"test_task8\"].depends_on = [data[\"test_task3\"]]\n    data[\"test_task9\"].depends_on = [data[\"test_task8\"]]\n\n    data[\"test_task5\"].update_status_with_dependent_statuses()\n    data[\"test_task6\"].update_status_with_dependent_statuses()\n    data[\"test_task3\"].update_status_with_dependent_statuses()\n    data[\"test_task8\"].update_status_with_dependent_statuses()\n    data[\"test_task9\"].update_status_with_dependent_statuses()\n\n    assert data[\"test_task5\"].status == data[\"status_rts\"]\n    assert data[\"test_task6\"].status == data[\"status_wfd\"]\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    # DBSession.commit()\n\n    # complete each of them first\n    # test_task5\n    data[\"test_task5\"].create_time_log(\n        data[\"test_task5\"].resources[0], now - td(hours=1), now\n    )\n    data[\"test_task5\"].schedule_timing = 1\n    data[\"test_task5\"].schedule_unit = TimeUnit.Hour\n    data[\"test_task5\"].status = data[\"status_cmpl\"]\n\n    # test_task6\n    data[\"test_task6\"].status = data[\"status_rts\"]\n    data[\"test_task6\"].create_time_log(\n        data[\"test_task6\"].resources[0], now, now + td(hours=1)\n    )\n    data[\"test_task6\"].schedule_timing = 1\n    data[\"test_task6\"].schedule_unit = TimeUnit.Hour\n    data[\"test_task6\"].status = data[\"status_cmpl\"]\n\n    # test_task3\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].create_time_log(\n        data[\"test_task3\"].resources[0], now + td(hours=1), now + td(hours=2)\n    )\n    data[\"test_task3\"].schedule_timing = 1\n    data[\"test_task3\"].schedule_unit = TimeUnit.Hour\n    data[\"test_task3\"].status = data[\"status_cmpl\"]\n\n    # test_task8\n    data[\"test_task8\"].status = data[\"status_rts\"]\n    data[\"test_task8\"].create_time_log(\n        data[\"test_task8\"].resources[0], now + td(hours=2), now + td(hours=3)\n    )\n    data[\"test_task8\"].schedule_timing = 1\n    data[\"test_task8\"].schedule_unit = TimeUnit.Hour\n    data[\"test_task8\"].status = data[\"status_cmpl\"]\n\n    # test_task9\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].create_time_log(\n        data[\"test_task9\"].resources[0], now + td(hours=3), now + td(hours=4)\n    )\n    data[\"test_task9\"].schedule_timing = 1\n    data[\"test_task9\"].schedule_unit = TimeUnit.Hour\n    data[\"test_task9\"].status = data[\"status_cmpl\"]\n\n    # now request a revision to the first task (test_task6)\n    # and expect all of the task dependency targets to be turned\n    # in to DependencyTarget.OnStart\n    data[\"test_task6\"].request_revision(data[\"test_user1\"])\n\n    assert (\n        data[\"test_task6\"].task_depends_on[0].dependency_target\n        == DependencyTarget.OnEnd\n    )\n    assert (\n        data[\"test_task3\"].task_depends_on[0].dependency_target\n        == DependencyTarget.OnStart\n    )\n    assert (\n        data[\"test_task8\"].task_depends_on[0].dependency_target\n        == DependencyTarget.OnStart\n    )\n    assert (\n        data[\"test_task9\"].task_depends_on[0].dependency_target\n        == DependencyTarget.OnStart\n    )\n\n\n# PREV: Review instances statuses are updated\ndef test_request_revision_in_prev_leaf_task_new_review_instance_is_created(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"statuses of review instances are correctly updated to\n    RREV if the request_revision action is used in a PREV leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    reviews = data[\"test_task3\"].request_review()\n    new_review = data[\"test_task3\"].request_revision(\n        reviewer=data[\"test_user2\"],\n        description=\"some description\",\n        schedule_timing=1,\n        schedule_unit=TimeUnit.Week,\n    )\n    assert isinstance(new_review, Review)\n\n\n# PREV: Review instances statuses are updated\ndef test_request_revision_in_prev_leaf_task_review_instances_are_deleted(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"NEW Review instances are deleted if the\n    request_revision action is used in a PREV leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    reviews = data[\"test_task3\"].request_review()\n    review1 = reviews[0]\n    review2 = reviews[1]\n\n    assert review1.status == data[\"status_new\"]\n    assert review2.status == data[\"status_new\"]\n\n    review3 = data[\"test_task3\"].request_revision(\n        reviewer=data[\"test_user2\"],\n        description=\"some description\",\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Hour,\n    )\n\n    # now check if the review instances are not in task3.reviews list\n    # anymore\n    assert review1 not in data[\"test_task3\"].reviews\n    assert review2 not in data[\"test_task3\"].reviews\n    assert review3 in data[\"test_task3\"].reviews\n\n\n# PREV: Schedule info update also consider RREV Reviews\ndef test_request_revision_in_prev_leaf_task_schedule_info_update_also_considers_other_rrev_reviews_with_same_review_number(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"timing values are extended with the supplied values\n    and also any RREV Review timings with the same revision number are\n    included if the request_revision action is used in a PREV leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    # create a couple TimeLogs\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].responsible = [data[\"test_user1\"], data[\"test_user2\"]]\n\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    # check the status\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n\n    # first request a review\n    reviews = data[\"test_task3\"].request_review()\n\n    # only finalize the first review\n    review1 = reviews[0]\n    review2 = reviews[1]\n\n    review1.request_revision(\n        schedule_timing=6, schedule_unit=TimeUnit.Hour, description=\"\"\n    )\n\n    # now request_revision using the task\n    review3 = data[\"test_task3\"].request_revision(\n        reviewer=data[\"test_user1\"],\n        description=\"do something uleyn\",\n        schedule_timing=4,\n        schedule_unit=TimeUnit.Hour,\n    )\n    assert len(data[\"test_task3\"].reviews) == 2\n\n    # check if they are in the same review set\n    assert review1.review_number == review3.review_number\n\n    # the final timing should be 12 hours\n    assert data[\"test_task3\"].schedule_timing == 10\n    assert data[\"test_task3\"].schedule_unit == TimeUnit.Day\n\n\n# PREV: Status updated to HREV\ndef test_request_revision_in_prev_leaf_task_status_updated_to_hrev(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"the status of the PREV leaf task converted to\n    HREV if the request_revision action is used in a PREV leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_prev\"]\n\n    reviewer = data[\"test_user1\"]\n    description = \"do something uleyn\"\n    schedule_timing = 4\n    schedule_unit = TimeUnit.Hour\n\n    data[\"test_task3\"].request_revision(\n        reviewer=reviewer,\n        description=description,\n        schedule_timing=schedule_timing,\n        schedule_unit=schedule_unit,\n    )\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n\n\n# PREV: Schedule info update\ndef test_request_revision_in_prev_leaf_task_timing_is_extended(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"timing extended as stated in the action when\n    the request_revision action is used in a PREV leaf task\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_prev\"]\n\n    reviewer = data[\"test_user1\"]\n    description = \"do something uleyn\"\n    schedule_timing = 4\n    schedule_unit = \"h\"\n\n    data[\"test_task3\"].request_revision(\n        reviewer=reviewer,\n        description=description,\n        schedule_timing=schedule_timing,\n        schedule_unit=schedule_unit,\n    )\n    assert data[\"test_task3\"].schedule_timing == 10\n    assert data[\"test_task3\"].schedule_unit == TimeUnit.Day\n\n\n# OH: DREV dependencies -> DREV\ndef test_resume_in_oh_leaf_task_with_drev_dependencies(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status updated to DREV if the resume action\n    is used in a OH leaf task with DREV dependencies\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task6\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n    data[\"test_task3\"].depends_on = [data[\"test_task6\"]]\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    data[\"test_task6\"].create_time_log(\n        resource=data[\"test_task6\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    # approve task6\n    reviews = data[\"test_task6\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    # start working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    # approve task3\n    reviews = data[\"test_task3\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_rts\"]\n\n    # start working on task9\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n    # hold task9\n    data[\"test_task9\"].hold()\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    # request a revision to task6\n    data[\"test_task6\"].request_revision(reviewer=data[\"test_user1\"])\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_hrev\"]\n    assert data[\"test_task3\"].status == data[\"status_drev\"]\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    # resume task9\n    data[\"test_task9\"].resume()\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_hrev\"]\n    assert data[\"test_task3\"].status == data[\"status_drev\"]\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\n# OH: HREV dependencies -> DREV\ndef test_resume_in_oh_leaf_task_with_hrev_dependencies(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status updated to DREV if the resume action\n    is used in a OH leaf task with HREV dependencies\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    reviews = data[\"test_task3\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # task3 should be cmpl\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n\n    # start working on task9\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    # now continue working on test_task3\n    data[\"test_task3\"].request_revision(reviewer=data[\"test_task3\"].resources[0])\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n    # hold task9\n    data[\"test_task9\"].hold()\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    # resume task9\n    data[\"test_task9\"].resume()\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\n# OH: OH dependencies -> DREV\ndef test_resume_in_oh_leaf_task_with_oh_dependencies(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status updated to WIP if the resume action\n    is used in a OH leaf task with OH dependencies\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n\n    # finish task3 first\n    now = datetime.datetime.now(pytz.utc)\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now,\n        end=now + datetime.timedelta(hours=1),\n    )\n    reviews = data[\"test_task3\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    # start working for task9\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + datetime.timedelta(hours=1),\n        end=now + datetime.timedelta(hours=2),\n    )\n\n    # now request a revision for task3\n    data[\"test_task3\"].request_revision(reviewer=data[\"test_user1\"])\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n    # enter a new time log for task3 to make it wip\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + datetime.timedelta(hours=3),\n        end=now + datetime.timedelta(hours=4),\n    )\n\n    # and hold task3 and task9\n    data[\"test_task9\"].hold()\n    data[\"test_task3\"].hold()\n\n    assert data[\"test_task3\"].status == data[\"status_oh\"]\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    data[\"test_task9\"].resume()\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\n# OH: PREV dependencies -> DREV\ndef test_resume_in_oh_leaf_task_with_prev_dependencies(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status updated to DREV if the resume action\n    is used in a OH leaf task with PREV dependencies\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    # start working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    # complete task3\n    reviews = data[\"test_task3\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_rts\"]\n\n    # start working on task9\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n    # hold task9\n    data[\"test_task9\"].hold()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    # request a revision to task3\n    data[\"test_task3\"].request_revision(reviewer=data[\"test_user1\"])\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    # now continue working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    # request a review for task3\n    data[\"test_task3\"].request_review()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_prev\"]\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    # now resume task9\n    data[\"test_task9\"].resume()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_prev\"]\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\n# OH: WIP dependencies -> DREV\ndef test_resume_in_oh_leaf_task_with_wip_dependencies(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status updated to DREV if the resume action\n    is used in a OH leaf task with WIP dependencies\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    # start working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    # complete task3\n    reviews = data[\"test_task3\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_rts\"]\n\n    # start working on task9\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n    # hold task9\n    data[\"test_task9\"].hold()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    # request a revision to task3\n    data[\"test_task3\"].request_revision(reviewer=data[\"test_user1\"])\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    # now continue working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_oh\"]\n\n    # now resume task9\n    data[\"test_task9\"].resume()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\n# STOP: DREV dependencies -> DREV\ndef test_resume_in_stop_leaf_task_with_drev_dependencies(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status updated to DREV if the resume action\n    is used in a STOP leaf task with DREV dependencies\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task6\"].status = data[\"status_rts\"]\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n    data[\"test_task3\"].depends_on = [data[\"test_task6\"]]\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_rts\"]\n    assert data[\"test_task3\"].status == data[\"status_wfd\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n    data[\"test_task6\"].create_time_log(\n        resource=data[\"test_task6\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    # approve task6\n    reviews = data[\"test_task6\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    # start working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    # approve task3\n    reviews = data[\"test_task3\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_rts\"]\n\n    # start working on task9\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n    # stop task9\n    data[\"test_task9\"].stop()\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # request a revision to task6\n    data[\"test_task6\"].request_revision(reviewer=data[\"test_user1\"])\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_hrev\"]\n    assert data[\"test_task3\"].status == data[\"status_drev\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # resume task9\n    data[\"test_task9\"].resume()\n\n    # check statuses\n    assert data[\"test_task6\"].status == data[\"status_hrev\"]\n    assert data[\"test_task3\"].status == data[\"status_drev\"]\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\n# STOP: HREV dependencies -> DREV\ndef test_resume_in_stop_leaf_task_with_hrev_dependencies(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status updated to DREV if the resume action\n    is used in a STOP leaf task with HREV dependencies\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    # start working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    # complete task3\n    reviews = data[\"test_task3\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_rts\"]\n\n    # start working on task9\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n    # stop task9\n    data[\"test_task9\"].stop()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # request a revision to task3\n    data[\"test_task3\"].request_revision(reviewer=data[\"test_user1\"])\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # now continue working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # request a review for task3\n    reviews = data[\"test_task3\"].request_review()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_prev\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # request revisions\n    for r in reviews:\n        r.request_revision()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # now resume task9\n    data[\"test_task9\"].resume()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\n# STOP: OH dependencies -> DREV\ndef test_resume_in_stop_leaf_task_with_oh_dependencies(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status updated to WIP if the resume action\n    is used in a STOP leaf task with OH dependencies\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    # start working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    # complete task3\n    reviews = data[\"test_task3\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_rts\"]\n\n    # start working on task9\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n    # stop task9\n    data[\"test_task9\"].stop()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # request a revision to task3\n    data[\"test_task3\"].request_revision(reviewer=data[\"test_user1\"])\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # now continue working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # hold task3\n    data[\"test_task3\"].hold()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_oh\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # now resume task9\n    data[\"test_task9\"].resume()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_oh\"]\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\n# STOP: PREV dependencies -> DREV\ndef test_resume_in_stop_leaf_task_with_prev_dependencies(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status updated to DREV if the resume action\n    is used in a STOP leaf task with PREV dependencies\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    # start working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    # complete task3\n    reviews = data[\"test_task3\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_rts\"]\n\n    # start working on task9\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n    # stop task9\n    data[\"test_task9\"].stop()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # request a revision to task3\n    data[\"test_task3\"].request_revision(reviewer=data[\"test_user1\"])\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # now continue working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # request a review for task3\n    data[\"test_task3\"].request_review()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_prev\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # now resume task9\n    data[\"test_task9\"].resume()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_prev\"]\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\n# STOP: WIP dependencies -> DREV\ndef test_resume_in_stop_leaf_task_with_wip_dependencies(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"status updated to DREV if the resume action\n    is used in a STOP leaf task with WIP dependencies\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].status = data[\"status_rts\"]\n    data[\"test_task9\"].depends_on = [data[\"test_task3\"]]\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_rts\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    dt = datetime.datetime\n    td = datetime.timedelta\n    now = dt.now(pytz.utc)\n\n    # start working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0], start=now, end=now + td(hours=1)\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_wfd\"]\n\n    # complete task3\n    reviews = data[\"test_task3\"].request_review()\n    for r in reviews:\n        r.approve()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_rts\"]\n\n    # start working on task9\n    data[\"test_task9\"].create_time_log(\n        resource=data[\"test_task9\"].resources[0],\n        start=now + td(hours=1),\n        end=now + td(hours=2),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_wip\"]\n\n    # stop task9\n    data[\"test_task9\"].stop()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_cmpl\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # request a revision to task3\n    data[\"test_task3\"].request_revision(reviewer=data[\"test_user1\"])\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # now continue working on task3\n    data[\"test_task3\"].create_time_log(\n        resource=data[\"test_task3\"].resources[0],\n        start=now + td(hours=2),\n        end=now + td(hours=3),\n    )\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_stop\"]\n\n    # now resume task9\n    data[\"test_task9\"].resume()\n\n    # check statuses\n    assert data[\"test_task3\"].status == data[\"status_wip\"]\n    assert data[\"test_task9\"].status == data[\"status_drev\"]\n\n\ndef test_review_set_method_is_working_as_expected(setup_task_status_workflow_db_tests):\n    \"\"\"review_set() method is working as expected\"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    # request a review\n    reviews = data[\"test_task3\"].request_review()\n    DBSession.add_all(reviews)\n    assert len(reviews) == 2\n\n    # check the review_set() method with no review number\n    assert data[\"test_task3\"].review_set() == reviews\n\n    # now finalize the reviews\n    reviews[0].approve()\n    reviews[1].request_revision()\n\n    # task should have been set to hrev\n    assert data[\"status_hrev\"] == data[\"test_task3\"].status\n\n    # set the status to wip again\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    # request a new set of reviews\n    reviews2 = data[\"test_task3\"].request_review()\n\n    # confirm that they it is a different set of review\n    assert reviews != reviews2\n\n    # now check if review_set() will return reviews2\n    assert data[\"test_task3\"].review_set() == reviews2\n\n    # and use a review_number\n    assert data[\"test_task3\"].review_set(1) == reviews\n\n    assert data[\"test_task3\"].review_set(2) == reviews2\n\n\ndef test_review_set_review_number_is_skipped(setup_task_status_workflow_db_tests):\n    \"\"\"latest review set returned if the\n    review_number argument is skipped in Task.review_set() method\n    \"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    # request a review\n    reviews = data[\"test_task3\"].request_review()\n    DBSession.add_all(reviews)\n    assert len(reviews) == 2\n\n    # check the review_set() method with no review number\n    assert data[\"test_task3\"].review_set() == reviews\n\n    # now finalize the reviews\n    reviews[0].approve()\n    reviews[1].request_revision()\n\n    # task should have been set to hrev\n    assert data[\"test_task3\"].status == data[\"status_hrev\"]\n\n    # set the status to wip again\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    # request a new set of reviews\n    reviews2 = data[\"test_task3\"].request_review()\n    DBSession.add_all(reviews2)\n\n    # confirm that it is a different set of review\n    assert reviews != reviews2\n\n    # now check if review_set() will return reviews2\n    assert data[\"test_task3\"].review_set() == reviews2\n\n\ndef test_request_review_version_arg_is_skipped(setup_task_status_workflow_db_tests):\n    \"\"\"request_review() version arg can be skipped.\"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    # request a review\n    reviews = data[\"test_task3\"].request_review()  # Version arg is skipped\n    assert len(reviews) == 2\n\n\ndef test_request_review_version_arg_is_none(setup_task_status_workflow_db_tests):\n    \"\"\"request_review() version arg can be None.\"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    # request a review\n    reviews = data[\"test_task3\"].request_review(version=None)\n    assert len(reviews) == 2\n\n\ndef test_request_review_version_arg_is_not_a_version_instance(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"request_review() version arg is not a Version instance raises TypeError.\"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n\n    # request a review\n    with pytest.raises(TypeError) as cm:\n        _ = data[\"test_task3\"].request_review(version=\"Not a version instance\")\n\n    assert str(cm.value) == (\n        \"Review.version should be a Version instance, \"\n        \"not str: 'Not a version instance'\"\n    )\n\n\ndef test_request_review_version_arg_is_not_related_to_the_task(\n    setup_task_status_workflow_db_tests,\n):\n    \"\"\"request_review() version arg is not related to the Task raises ValueError.\"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    version = Version(task=data[\"test_task2\"])\n\n    # request a review\n    with pytest.raises(ValueError) as cm:\n        _ = data[\"test_task3\"].request_review(version=version)\n\n    assert str(cm.value) == (\n        f\"Review.version should be a Version instance related to this Task: {version}\"\n    )\n\n\ndef test_request_review_accepts_version_instance(setup_task_status_workflow_db_tests):\n    \"\"\"request_review() a Version instance can be passed to it.\"\"\"\n    data = setup_task_status_workflow_db_tests\n    data[\"test_task3\"].status = data[\"status_wip\"]\n    version = Version(task=data[\"test_task3\"])\n\n    # request a review\n    reviews = data[\"test_task3\"].request_review(version=version)\n    assert reviews[0].version == version\n    assert reviews[1].version == version\n"
  },
  {
    "path": "tests/models/test_ticket.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Ticket class.\"\"\"\n\nimport logging\nimport sys\n\nimport pytest\n\nfrom stalker import log\nfrom stalker import Asset\nfrom stalker import Note\nfrom stalker import Project\nfrom stalker import Repository\nfrom stalker import Status\nfrom stalker import Task\nfrom stalker import Ticket\nfrom stalker import TicketLog\nfrom stalker import Type\nfrom stalker import User\nfrom stalker import Version\nfrom stalker.db.session import DBSession\nfrom stalker.exceptions import CircularDependencyError\n\nlogger = logging.getLogger(\"stalker.models.ticket\")\nlogger.setLevel(log.logging_level)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_ticket_tests(setup_postgresql_db):\n    \"\"\"Set up the tests for the Ticket class.\"\"\"\n    data = dict()\n    # create statuses\n    data[\"test_status1\"] = Status(name=\"N\", code=\"N\")\n    data[\"test_status2\"] = Status(name=\"R\", code=\"R\")\n\n    # get the ticket types\n    ticket_types = Type.query.filter(Type.target_entity_type == \"Ticket\").all()\n    data[\"ticket_type_1\"] = ticket_types[0]\n    data[\"ticket_type_2\"] = ticket_types[1]\n\n    # create a User\n    data[\"test_user\"] = User(\n        name=\"Test User\", login=\"test_user1\", email=\"test1@user.com\", password=\"secret\"\n    )\n\n    # create a Repository\n    data[\"test_repo\"] = Repository(name=\"Test Repo\", code=\"TR\")\n\n    # create a Project Type\n    data[\"test_project_type\"] = Type(\n        name=\"Commercial Project\",\n        code=\"comm\",\n        target_entity_type=\"Project\",\n    )\n\n    # create a Project StatusList\n    data[\"test_project_status1\"] = Status(name=\"PrjStat1\", code=\"PrjStat1\")\n    data[\"test_project_status2\"] = Status(name=\"PrjStat2\", code=\"PrjStat2\")\n\n    # create a Project\n    data[\"test_project\"] = Project(\n        name=\"Test Project 1\",\n        code=\"TEST_PROJECT_1\",\n        type=data[\"test_project_type\"],\n        repository=data[\"test_repo\"],\n    )\n    DBSession.add(data[\"test_project\"])\n    DBSession.commit()\n\n    data[\"test_asset_type\"] = Type(\n        name=\"Character Asset\", code=\"char\", target_entity_type=\"Asset\"\n    )\n\n    data[\"test_asset\"] = Asset(\n        name=\"Test Asset\",\n        code=\"ta\",\n        project=data[\"test_project\"],\n        type=data[\"test_asset_type\"],\n    )\n    DBSession.add(data[\"test_asset\"])\n    DBSession.commit()\n\n    # create a Task\n    data[\"test_task\"] = Task(\n        name=\"Modeling of Asset 1\",\n        resources=[data[\"test_user\"]],\n        parent=data[\"test_asset\"],\n    )\n    DBSession.add(data[\"test_task\"])\n    DBSession.commit()\n\n    data[\"test_version\"] = Version(\n        name=\"Test Version\", task=data[\"test_task\"], version=1, full_path=\"some/path\"\n    )\n\n    # create the Ticket\n    data[\"kwargs\"] = {\n        \"project\": data[\"test_project\"],\n        \"links\": [data[\"test_version\"]],\n        \"summary\": \"This is a test ticket\",\n        \"description\": \"This is the long description\",\n        \"priority\": \"TRIVIAL\",\n        \"reported_by\": data[\"test_user\"],\n    }\n\n    data[\"test_ticket\"] = Ticket(**data[\"kwargs\"])\n    DBSession.add(data[\"test_ticket\"])\n    DBSession.commit()\n\n    # get the Ticket Statuses\n    data[\"status_new\"] = Status.query.filter_by(name=\"New\").first()\n    data[\"status_accepted\"] = Status.query.filter_by(name=\"Accepted\").first()\n    data[\"status_assigned\"] = Status.query.filter_by(name=\"Assigned\").first()\n    data[\"status_reopened\"] = Status.query.filter_by(name=\"Reopened\").first()\n    data[\"status_closed\"] = Status.query.filter_by(name=\"Closed\").first()\n\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_true():\n    \"\"\"__auto_name__ class attribute is set to True for Ticket class.\"\"\"\n    assert Ticket.__auto_name__ is True\n\n\ndef test_name_argument_is_not_used(setup_ticket_tests):\n    \"\"\"name argument is not used.\"\"\"\n    data = setup_ticket_tests\n    test_value = \"Test Name\"\n    data[\"kwargs\"][\"name\"] = test_value\n    new_ticket = Ticket(**data[\"kwargs\"])\n    assert new_ticket.name != test_value\n\n\ndef test_name_argument_is_skipped_will_not_raise_error(setup_ticket_tests):\n    \"\"\"name arg skipped an automatically generated name is used.\"\"\"\n    data = setup_ticket_tests\n    if \"name\" in data[\"kwargs\"]:\n        data[\"kwargs\"].pop(\"name\")\n        # expect no errors\n    Ticket(**data[\"kwargs\"])\n\n\ndef test_number_attribute_is_not_created_per_project(setup_ticket_tests):\n    \"\"\"number attr is not per project and uniquely increment for every new ticket.\"\"\"\n    data = setup_ticket_tests\n    proj1 = Project(\n        name=\"Test Project 1\",\n        code=\"TP1\",\n        repository=data[\"test_repo\"],\n    )\n\n    proj2 = Project(\n        name=\"Test Project 2\",\n        code=\"TP2\",\n        repository=data[\"test_repo\"],\n    )\n\n    proj3 = Project(\n        name=\"Test Project 3\",\n        code=\"TP3\",\n        repository=data[\"test_repo\"],\n    )\n\n    p1_t1 = Ticket(project=proj1)\n    DBSession.add(p1_t1)\n    DBSession.commit()\n    assert p1_t1.number == 2\n\n    p1_t2 = Ticket(project=proj1)\n    DBSession.add(p1_t2)\n    DBSession.commit()\n    assert p1_t2.number == 3\n\n    p2_t1 = Ticket(project=proj2)\n    DBSession.add(p2_t1)\n    DBSession.commit()\n    assert p2_t1.number == 4\n\n    p1_t3 = Ticket(project=proj1)\n    DBSession.add(p1_t3)\n    DBSession.commit()\n    assert p1_t3.number == 5\n\n    p3_t1 = Ticket(project=proj3)\n    DBSession.add(p3_t1)\n    DBSession.commit()\n    assert p3_t1.number == 6\n\n    p2_t2 = Ticket(project=proj2)\n    DBSession.add(p2_t2)\n    DBSession.commit()\n    assert p2_t2.number == 7\n\n    p3_t2 = Ticket(project=proj3)\n    DBSession.add(p3_t2)\n    DBSession.commit()\n    assert p3_t2.number == 8\n\n    p2_t3 = Ticket(project=proj2)\n    DBSession.add(p2_t3)\n    DBSession.commit()\n    assert p2_t3.number == 9\n\n\ndef test_number_attribute_is_read_only(setup_ticket_tests):\n    \"\"\"number attribute is read-only.\"\"\"\n    data = setup_ticket_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_ticket\"].number = 234\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'Ticket' object has no setter\",\n        12: \"property of 'Ticket' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_number_getter' of 'Ticket' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_number_attribute_is_automatically_increased(setup_ticket_tests):\n    \"\"\"number attribute is automatically increased.\"\"\"\n    data = setup_ticket_tests\n    # create two new tickets\n    ticket1 = Ticket(**data[\"kwargs\"])\n    DBSession.add(ticket1)\n    DBSession.commit()\n\n    ticket2 = Ticket(**data[\"kwargs\"])\n    DBSession.add(ticket2)\n    DBSession.commit()\n\n    assert ticket1.number + 1 == ticket2.number\n    assert ticket1.number == 2\n    assert ticket2.number == 3\n\n\ndef test_links_argument_accepts_anything_derived_from_simple_entity(setup_ticket_tests):\n    \"\"\"links accepting anything derived from SimpleEntity.\"\"\"\n    data = setup_ticket_tests\n    data[\"kwargs\"][\"links\"] = [\n        data[\"test_project\"],\n        data[\"test_project_status1\"],\n        data[\"test_project_status2\"],\n        data[\"test_repo\"],\n        data[\"test_version\"],\n    ]\n\n    new_ticket = Ticket(**data[\"kwargs\"])\n    assert sorted(data[\"kwargs\"][\"links\"], key=lambda x: x.name) == sorted(\n        new_ticket.links, key=lambda x: x.name\n    )\n\n\ndef test_links_attribute_accepts_anything_derived_from_simple_entity(\n    setup_ticket_tests,\n):\n    \"\"\"links attribute is accepting anything derived from SimpleEntity.\"\"\"\n    data = setup_ticket_tests\n    links = [\n        data[\"test_project\"],\n        data[\"test_project_status1\"],\n        data[\"test_project_status2\"],\n        data[\"test_repo\"],\n        data[\"test_version\"],\n    ]\n    data[\"test_ticket\"].links = links\n    assert sorted(links, key=lambda x: x.name) == sorted(\n        data[\"test_ticket\"].links, key=lambda x: x.name\n    )\n\n\ndef test_related_tickets_attribute_is_an_empty_list_on_init(setup_ticket_tests):\n    \"\"\"related_tickets attribute is an empty list on init.\"\"\"\n    data = setup_ticket_tests\n    assert data[\"test_ticket\"].related_tickets == []\n\n\ndef test_related_tickets_attribute_is_set_to_something_other_then_a_list_of_tickets(\n    setup_ticket_tests,\n):\n    \"\"\"TypeError raised if the related_tickets attr is not a list of Tickets.\"\"\"\n    data = setup_ticket_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_ticket\"].related_tickets = [\"a ticket\"]\n\n    assert str(cm.value) == (\n        \"Ticket.related_ticket should only contain instances of \"\n        \"stalker.models.ticket.Ticket, not str: 'a ticket'\"\n    )\n\n\ndef test_related_tickets_attribute_accepts_list_of_ticket_instances(setup_ticket_tests):\n    \"\"\"related tickets attr accepts only list of Ticket instances.\"\"\"\n    data = setup_ticket_tests\n    new_ticket1 = Ticket(**data[\"kwargs\"])\n    DBSession.add(new_ticket1)\n    DBSession.commit()\n\n    new_ticket2 = Ticket(**data[\"kwargs\"])\n    DBSession.add(new_ticket2)\n    DBSession.commit()\n\n    data[\"test_ticket\"].related_tickets = [new_ticket1, new_ticket2]\n\n\ndef test_related_ticket_attribute_will_not_accept_self(setup_ticket_tests):\n    \"\"\"related_tickets attr don't accept the Ticket itself and raises ValueError.\"\"\"\n    data = setup_ticket_tests\n    with pytest.raises(CircularDependencyError) as cm:\n        data[\"test_ticket\"].related_tickets = [data[\"test_ticket\"]]\n\n    assert (\n        str(cm.value) == \"Ticket.related_ticket attribute cannot \"\n        \"have itself in the list\"\n    )\n\n\ndef test_priority_argument_is_skipped_will_set_it_to_zero(setup_ticket_tests):\n    \"\"\"priority arg is skipped will set the priority of the Ticket to 0 or TRIVIAL.\"\"\"\n    data = setup_ticket_tests\n    data[\"kwargs\"].pop(\"priority\")\n    new_ticket = Ticket(**data[\"kwargs\"])\n    assert new_ticket.priority == \"TRIVIAL\"\n\n\ndef test_comments_attribute_is_synonym_for_notes_attribute(setup_ticket_tests):\n    \"\"\"comments attr is the synonym for the notes attr.\"\"\"\n    data = setup_ticket_tests\n    note1 = Note(name=\"Test Note 1\", content=\"Test note 1\")\n    note2 = Note(name=\"Test Note 2\", content=\"Test note 2\")\n\n    data[\"test_ticket\"].comments.append(note1)\n    data[\"test_ticket\"].comments.append(note2)\n\n    assert note1 in data[\"test_ticket\"].notes\n    assert note2 in data[\"test_ticket\"].notes\n\n    data[\"test_ticket\"].notes.remove(note1)\n    assert note1 not in data[\"test_ticket\"].comments\n\n    data[\"test_ticket\"].notes.remove(note2)\n    assert note2 not in data[\"test_ticket\"].comments\n\n\ndef test_reported_by_attribute_is_synonym_of_created_by(setup_ticket_tests):\n    \"\"\"reported_by attribute is a synonym for the created_by attribute.\"\"\"\n    data = setup_ticket_tests\n    user1 = User(name=\"user1\", login=\"user1\", password=\"secret\", email=\"user1@test.com\")\n    data[\"test_ticket\"].reported_by = user1\n    assert user1 == data[\"test_ticket\"].created_by\n\n\ndef test_status_for_newly_created_tickets_will_be_new_if_skipped(setup_ticket_tests):\n    \"\"\"status of newly created tickets New.\"\"\"\n    data = setup_ticket_tests\n    # get the status NEW from the session\n    new_ticket = Ticket(**data[\"kwargs\"])\n    assert new_ticket.status == data[\"status_new\"]\n\n\ndef test_project_argument_is_skipped(setup_ticket_tests):\n    \"\"\"TypeError raised if the project argument is skipped.\"\"\"\n    data = setup_ticket_tests\n    data[\"kwargs\"].pop(\"project\")\n    with pytest.raises(TypeError) as cm:\n        Ticket(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Ticket.project should be an instance of \"\n        \"stalker.models.project.Project, not NoneType: 'None'\"\n    )\n\n\ndef test_project_argument_is_none(setup_ticket_tests):\n    \"\"\"TypeError raised if the project argument is None.\"\"\"\n    data = setup_ticket_tests\n    data[\"kwargs\"][\"project\"] = None\n    with pytest.raises(TypeError) as cm:\n        Ticket(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"Ticket.project should be an instance of \"\n        \"stalker.models.project.Project, not NoneType: 'None'\"\n    )\n\n\ndef test_project_argument_accepts_project_instances_only(setup_ticket_tests):\n    \"\"\"project argument accepts Project instances only.\"\"\"\n    data = setup_ticket_tests\n    data[\"kwargs\"][\"project\"] = \"Not a Project instance\"\n    with pytest.raises(TypeError) as cm:\n        Ticket(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Ticket.project should be an instance of \"\n        \"stalker.models.project.Project, not str: 'Not a Project instance'\"\n    )\n\n\ndef test_project_argument_is_working_as_expected(setup_ticket_tests):\n    \"\"\"project argument is working as expected.\"\"\"\n    data = setup_ticket_tests\n    data[\"kwargs\"][\"project\"] = data[\"test_project\"]\n    new_ticket = Ticket(**data[\"kwargs\"])\n    assert new_ticket.project == data[\"test_project\"]\n\n\ndef test_project_attribute_is_read_only(setup_ticket_tests):\n    \"\"\"project attribute is read only.\"\"\"\n    data = setup_ticket_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_ticket\"].project = data[\"test_project\"]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'Ticket' object has no setter\",\n        12: \"property of 'Ticket' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_project_getter' of 'Ticket' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\n# STATUSES\n\n\n# resolve\ndef test_resolve_method_will_change_the_status_from_new_to_closed_and_creates_a_log(\n    setup_ticket_tests,\n):\n    \"\"\"resolve action changes Ticket status from New to Closed.\"\"\"\n    data = setup_ticket_tests\n    assert data[\"test_ticket\"].status == data[\"status_new\"]\n    ticket_log = data[\"test_ticket\"].resolve()\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n    assert ticket_log.from_status == data[\"status_new\"]\n    assert ticket_log.to_status == data[\"status_closed\"]\n    assert ticket_log.action == \"resolve\"\n\n\ndef test_resolve_method_will_change_the_status_from_accepted_to_closed(\n    setup_ticket_tests,\n):\n    \"\"\"resolve action changes Ticket status from Accepted to Closed.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_accepted\"]\n    assert data[\"test_ticket\"].status == data[\"status_accepted\"]\n    ticket_log = data[\"test_ticket\"].resolve()\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n    assert ticket_log.from_status == data[\"status_accepted\"]\n    assert ticket_log.to_status == data[\"status_closed\"]\n    assert ticket_log.action == \"resolve\"\n\n\ndef test_resolve_method_will_change_the_status_from_assigned_to_closed(\n    setup_ticket_tests,\n):\n    \"\"\"resolve action changes Ticket status from Assigned to Closed.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_assigned\"]\n    assert data[\"test_ticket\"].status == data[\"status_assigned\"]\n    ticket_log = data[\"test_ticket\"].resolve()\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n    assert ticket_log.from_status == data[\"status_assigned\"]\n    assert ticket_log.to_status == data[\"status_closed\"]\n    assert ticket_log.action == \"resolve\"\n\n\ndef test_resolve_method_will_change_the_status_from_reopened_to_closed(\n    setup_ticket_tests,\n):\n    \"\"\"accept action changes Ticket status from Reopened to closed.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_reopened\"]\n    assert data[\"test_ticket\"].status == data[\"status_reopened\"]\n    ticket_log = data[\"test_ticket\"].resolve()\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n    assert ticket_log.from_status == data[\"status_reopened\"]\n    assert ticket_log.to_status == data[\"status_closed\"]\n    assert ticket_log.action == \"resolve\"\n\n\ndef test_resolve_method_will_not_change_the_status_from_closed_to_anything(\n    setup_ticket_tests,\n):\n    \"\"\"resolve action don't change Ticket status from Closed to anything.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_closed\"]\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n    ticket_log = data[\"test_ticket\"].resolve()\n    assert ticket_log is None\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n\n\n# reopen\ndef test_reopen_method_will_not_change_the_status_from_new_to_anything(\n    setup_ticket_tests,\n):\n    \"\"\"reopen action will not change Ticket status from New to anything.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_new\"]\n    assert data[\"test_ticket\"].status == data[\"status_new\"]\n    ticket_log = data[\"test_ticket\"].reopen()\n    assert ticket_log is None\n    assert data[\"test_ticket\"].status == data[\"status_new\"]\n\n\ndef test_reopen_method_will_not_change_the_status_from_accepted_to_anything(\n    setup_ticket_tests,\n):\n    \"\"\"reopen action will not change Ticket status from Accepted to anything.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_accepted\"]\n    assert data[\"test_ticket\"].status == data[\"status_accepted\"]\n    ticket_log = data[\"test_ticket\"].reopen()\n    assert ticket_log is None\n    assert data[\"test_ticket\"].status == data[\"status_accepted\"]\n\n\ndef test_reopen_method_will_not_change_the_status_from_assigned_to_anything(\n    setup_ticket_tests,\n):\n    \"\"\"reopen action will not change Ticket status from Assigned to anything.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_assigned\"]\n    assert data[\"test_ticket\"].status == data[\"status_assigned\"]\n    ticket_log = data[\"test_ticket\"].reopen()\n    assert ticket_log is None\n    assert data[\"test_ticket\"].status == data[\"status_assigned\"]\n\n\ndef test_reopen_method_will_not_change_the_status_from_reopened_to_anything(\n    setup_ticket_tests,\n):\n    \"\"\"reopen action will not change Ticket status from Reopened to anything.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_reopened\"]\n    assert data[\"test_ticket\"].status == data[\"status_reopened\"]\n    ticket_log = data[\"test_ticket\"].reopen()\n    assert ticket_log is None\n    assert data[\"test_ticket\"].status == data[\"status_reopened\"]\n\n\ndef test_reopen_method_will_change_the_status_from_closed_to_reopened(\n    setup_ticket_tests,\n):\n    \"\"\"reopen action changes Ticket status from Closed to \"Reopened\".\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_closed\"]\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n    ticket_log = data[\"test_ticket\"].reopen()\n    assert data[\"test_ticket\"].status == data[\"status_reopened\"]\n    assert ticket_log.from_status == data[\"status_closed\"]\n    assert ticket_log.to_status == data[\"status_reopened\"]\n    assert ticket_log.action == \"reopen\"\n\n\n# accept\ndef test_accept_method_will_change_the_status_from_new_to_accepted(setup_ticket_tests):\n    \"\"\"accept action changes Ticket status from New to Accepted.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_new\"]\n    assert data[\"test_ticket\"].status == data[\"status_new\"]\n    ticket_log = data[\"test_ticket\"].accept()\n    assert data[\"test_ticket\"].status == data[\"status_accepted\"]\n    assert ticket_log.from_status == data[\"status_new\"]\n    assert ticket_log.to_status == data[\"status_accepted\"]\n    assert ticket_log.action == \"accept\"\n\n\ndef test_accept_method_will_change_the_status_from_accepted_to_accepted(\n    setup_ticket_tests,\n):\n    \"\"\"accept action changes Ticket status from Accepted to Accepted.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_accepted\"]\n    assert data[\"test_ticket\"].status == data[\"status_accepted\"]\n    ticket_log = data[\"test_ticket\"].accept()\n    assert data[\"test_ticket\"].status == data[\"status_accepted\"]\n    assert ticket_log.from_status == data[\"status_accepted\"]\n    assert ticket_log.to_status == data[\"status_accepted\"]\n    assert ticket_log.action == \"accept\"\n\n\ndef test_accept_method_will_change_the_status_from_assigned_to_accepted(\n    setup_ticket_tests,\n):\n    \"\"\"accept action changes Ticket status from Assigned to Accepted.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_assigned\"]\n    assert data[\"test_ticket\"].status == data[\"status_assigned\"]\n    ticket_log = data[\"test_ticket\"].accept()\n    assert data[\"test_ticket\"].status == data[\"status_accepted\"]\n    assert ticket_log.from_status == data[\"status_assigned\"]\n    assert ticket_log.to_status == data[\"status_accepted\"]\n    assert ticket_log.action == \"accept\"\n\n\ndef test_accept_method_will_change_the_status_from_reopened_to_accepted(\n    setup_ticket_tests,\n):\n    \"\"\"accept action changes Ticket status from Reopened to Accepted.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_reopened\"]\n    assert data[\"test_ticket\"].status == data[\"status_reopened\"]\n    ticket_log = data[\"test_ticket\"].accept()\n    assert data[\"test_ticket\"].status == data[\"status_accepted\"]\n    assert ticket_log.from_status == data[\"status_reopened\"]\n    assert ticket_log.to_status == data[\"status_accepted\"]\n    assert ticket_log.action == \"accept\"\n\n\ndef test_accept_method_will_not_change_the_status_of_closed_to_anything(\n    setup_ticket_tests,\n):\n    \"\"\"accept action will not change Ticket status from Closed to Anything.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_closed\"]\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n    ticket_log = data[\"test_ticket\"].accept()\n    assert ticket_log is None\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n\n\n# reassign\ndef test_reassign_method_will_change_the_status_from_new_to_assigned(\n    setup_ticket_tests,\n):\n    \"\"\"reassign action changes Ticket status from New to Assigned.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_new\"]\n    assert data[\"test_ticket\"].status == data[\"status_new\"]\n    ticket_log = data[\"test_ticket\"].reassign()\n    assert data[\"test_ticket\"].status == data[\"status_assigned\"]\n    assert ticket_log.from_status == data[\"status_new\"]\n    assert ticket_log.to_status == data[\"status_assigned\"]\n    assert ticket_log.action == \"reassign\"\n\n\ndef test_reassign_method_will_change_the_status_from_accepted_to_assigned(\n    setup_ticket_tests,\n):\n    \"\"\"reassign action changes Ticket status from Accepted to Accepted.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_accepted\"]\n    assert data[\"test_ticket\"].status == data[\"status_accepted\"]\n    ticket_log = data[\"test_ticket\"].reassign()\n    assert data[\"test_ticket\"].status == data[\"status_assigned\"]\n    assert ticket_log.from_status == data[\"status_accepted\"]\n    assert ticket_log.to_status == data[\"status_assigned\"]\n    assert ticket_log.action == \"reassign\"\n\n\ndef test_reassign_method_will_change_the_status_from_assigned_to_assigned(\n    setup_ticket_tests,\n):\n    \"\"\"reassign action changes Ticket status from Assigned to Accepted.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_assigned\"]\n    assert data[\"test_ticket\"].status == data[\"status_assigned\"]\n    ticket_log = data[\"test_ticket\"].reassign()\n    assert data[\"test_ticket\"].status == data[\"status_assigned\"]\n    assert ticket_log.from_status == data[\"status_assigned\"]\n    assert ticket_log.to_status == data[\"status_assigned\"]\n    assert ticket_log.action == \"reassign\"\n\n\ndef test_reassign_method_will_change_the_status_from_reopened_to_assigned(\n    setup_ticket_tests,\n):\n    \"\"\"accept action changes Ticket status from Reopened to Assigned.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_reopened\"]\n    assert data[\"test_ticket\"].status == data[\"status_reopened\"]\n    ticket_log = data[\"test_ticket\"].reassign()\n    assert data[\"test_ticket\"].status == data[\"status_assigned\"]\n    assert ticket_log.from_status == data[\"status_reopened\"]\n    assert ticket_log.to_status == data[\"status_assigned\"]\n    assert ticket_log.action == \"reassign\"\n\n\ndef test_reassign_method_will_not_change_the_status_of_closed_to_anything(\n    setup_ticket_tests,\n):\n    \"\"\"reassign action will not change Ticket status from Closed to Anything.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].status = data[\"status_closed\"]\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n    ticket_log = data[\"test_ticket\"].reassign()\n    assert ticket_log is None\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n\n\ndef test_resolve_method_will_set_the_resolution(setup_ticket_tests):\n    \"\"\"resolve action changes Ticket status from New to Closed.\"\"\"\n    data = setup_ticket_tests\n    assert data[\"test_ticket\"].status == data[\"status_new\"]\n    ticket_log = data[\"test_ticket\"].resolve(resolution=\"fixed\")\n    assert data[\"test_ticket\"].status == data[\"status_closed\"]\n    assert ticket_log.from_status == data[\"status_new\"]\n    assert ticket_log.to_status == data[\"status_closed\"]\n    assert ticket_log.action == \"resolve\"\n    assert data[\"test_ticket\"].resolution == \"fixed\"\n\n\ndef test_reopen_will_clear_resolution(setup_ticket_tests):\n    \"\"\"reopen method will clear the timing_resolution.\"\"\"\n    data = setup_ticket_tests\n    assert data[\"test_ticket\"].status == data[\"status_new\"]\n    data[\"test_ticket\"].resolve(resolution=\"fixed\")\n    assert data[\"test_ticket\"].resolution == \"fixed\"\n    ticket_log = data[\"test_ticket\"].reopen()\n    assert isinstance(ticket_log, TicketLog)\n    assert data[\"test_ticket\"].resolution == \"\"\n\n\ndef test_reassign_will_set_the_owner(setup_ticket_tests):\n    \"\"\"reassign method will set the owner.\"\"\"\n    data = setup_ticket_tests\n    assert data[\"test_ticket\"].status == data[\"status_new\"]\n    assert data[\"test_ticket\"].owner != data[\"test_user\"]\n    ticket_log = data[\"test_ticket\"].reassign(assign_to=data[\"test_user\"])\n    assert isinstance(ticket_log, TicketLog)\n    assert data[\"test_ticket\"].owner == data[\"test_user\"]\n\n\ndef test_accept_will_set_the_owner(setup_ticket_tests):\n    \"\"\"accept method will set the owner.\"\"\"\n    data = setup_ticket_tests\n    assert data[\"test_ticket\"].status == data[\"status_new\"]\n    assert data[\"test_ticket\"].owner != data[\"test_user\"]\n    ticket_log = data[\"test_ticket\"].accept(created_by=data[\"test_user\"])\n    assert isinstance(ticket_log, TicketLog)\n    assert data[\"test_ticket\"].owner == data[\"test_user\"]\n\n\ndef test_summary_argument_skipped(setup_ticket_tests):\n    \"\"\"summary argument can be skipped.\"\"\"\n    data = setup_ticket_tests\n    try:\n        data[\"kwargs\"].pop(\"summary\")\n    except KeyError:\n        pass\n    new_ticket = Ticket(**data[\"kwargs\"])\n    assert new_ticket.summary == \"\"\n\n\ndef test_summary_argument_can_be_none(setup_ticket_tests):\n    \"\"\"summary argument can be None.\"\"\"\n    data = setup_ticket_tests\n    data[\"kwargs\"][\"summary\"] = None\n    new_ticket = Ticket(**data[\"kwargs\"])\n    assert new_ticket.summary == \"\"\n\n\ndef test_summary_attribute_can_be_set_to_none(setup_ticket_tests):\n    \"\"\"summary attribute can be set to None.\"\"\"\n    data = setup_ticket_tests\n    data[\"test_ticket\"].summary = None\n    assert data[\"test_ticket\"].summary == \"\"\n\n\ndef test_summary_argument_is_not_a_string(setup_ticket_tests):\n    \"\"\"TypeError raised if the summary argument value is not a str.\"\"\"\n    data = setup_ticket_tests\n    data[\"kwargs\"][\"summary\"] = [\"not a string instance\"]\n    with pytest.raises(TypeError) as cm:\n        Ticket(data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Ticket.project should be an instance of \"\n        \"stalker.models.project.Project, not dict: \"\n        \"'{'project': <Test Project 1 (Project)>, \"\n        \"'links': [<TEST_PROJECT_1_Test_Asset_Modeling_of_Asset_1_v001 \"\n        \"(Version)>], 'summary': ['not a string instance'], 'description': \"\n        \"'This is the long description', 'priority': 'TRIVIAL', 'reported_by': \"\n        \"<Test User ('testuser1') (User)>}'\"\n    )\n\n\ndef test_summary_attribute_is_set_to_a_value_other_than_a_string(setup_ticket_tests):\n    \"\"\"summary attribute is set to a value other than a str.\"\"\"\n    data = setup_ticket_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_ticket\"].summary = [\"not a string\"]\n\n    assert str(cm.value) == (\n        \"Ticket.summary should be an instance of str, not list: '['not a string']'\"\n    )\n\n\ndef test_summary_argument_is_working_as_expected(setup_ticket_tests):\n    \"\"\"summary argument value is passed to summary attribute correctly.\"\"\"\n    data = setup_ticket_tests\n    test_value = \"test summary\"\n    data[\"kwargs\"][\"summary\"] = test_value\n    new_ticket = Ticket(**data[\"kwargs\"])\n    assert new_ticket.summary == test_value\n\n\ndef test_summary_attribute_is_working_as_expected(setup_ticket_tests):\n    \"\"\"summary attribute is working as expected.\"\"\"\n    data = setup_ticket_tests\n    test_value = \"test_summary\"\n    assert data[\"test_ticket\"].summary != test_value\n    data[\"test_ticket\"].summary = test_value\n    assert data[\"test_ticket\"].summary == test_value\n\n\ndef test__hash__is_working_as_expected(setup_ticket_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_ticket_tests\n    result = hash(data[\"test_ticket\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_ticket\"].__hash__()\n\n\ndef test__eq__of_two_tickets_true_case(setup_ticket_tests):\n    \"\"\"__eq__() for two tickets.\"\"\"\n    data = setup_ticket_tests\n    ticket1 = data[\"test_ticket\"]\n    ticket2 = Ticket.query.filter_by(name=ticket1.name).first()\n    assert (ticket1 == ticket2) is True\n\n\ndef test__eq__of_two_tickets_false_case(setup_ticket_tests):\n    \"\"\"__eq__() for two tickets.\"\"\"\n    data = setup_ticket_tests\n    new_ticket = Ticket(**data[\"kwargs\"])\n    assert (data[\"test_ticket\"] == new_ticket) is False\n\n\ndef test_max_number_returns_0():\n    \"\"\"_maximum_number() returns 0 when there is no DB connection.\"\"\"\n    assert Ticket._maximum_number() == 0\n"
  },
  {
    "path": "tests/models/test_time_log.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the TimeLog class.\"\"\"\n\nimport copy\nimport datetime\n\nimport pytest\n\nimport pytz\n\nimport tzlocal\n\nfrom sqlalchemy.exc import IntegrityError\n\nfrom stalker import Project, Repository, Status, StatusList, Task, TimeLog, User\nfrom stalker.db.session import DBSession\nfrom stalker.exceptions import DependencyViolationError, OverBookedError, StatusError\nfrom stalker.models.enum import TimeUnit\nfrom stalker.models.enum import DependencyTarget\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_time_log_db_tests(setup_postgresql_db):\n    \"\"\"Set up the tests for the TimeLog class.\"\"\"\n    data = dict()\n    data[\"status_wfd\"] = Status.query.filter_by(code=\"WFD\").first()\n    data[\"status_rts\"] = Status.query.filter_by(code=\"RTS\").first()\n    data[\"status_wip\"] = Status.query.filter_by(code=\"WIP\").first()\n    data[\"status_prev\"] = Status.query.filter_by(code=\"PREV\").first()\n    data[\"status_hrev\"] = Status.query.filter_by(code=\"HREV\").first()\n    data[\"status_drev\"] = Status.query.filter_by(code=\"DREV\").first()\n    data[\"status_oh\"] = Status.query.filter_by(code=\"OH\").first()\n    data[\"status_stop\"] = Status.query.filter_by(code=\"STOP\").first()\n    data[\"status_cmpl\"] = Status.query.filter_by(code=\"CMPL\").first()\n\n    # create a resource\n    data[\"test_resource1\"] = User(\n        name=\"User1\",\n        login=\"user1\",\n        email=\"user1@users.com\",\n        password=\"1234\",\n    )\n    DBSession.add(data[\"test_resource1\"])\n\n    data[\"test_resource2\"] = User(\n        name=\"User2\", login=\"user2\", email=\"user2@users.com\", password=\"1234\"\n    )\n    DBSession.add(data[\"test_resource2\"])\n\n    data[\"test_repo\"] = Repository(name=\"test repository\", code=\"tr\")\n    DBSession.add(data[\"test_repo\"])\n\n    # create a Project\n    data[\"test_status1\"] = Status(name=\"Status1\", code=\"STS1\")\n    data[\"test_status2\"] = Status(name=\"Status2\", code=\"STS2\")\n    data[\"test_status3\"] = Status(name=\"Status3\", code=\"STS3\")\n    DBSession.add_all(\n        [data[\"test_status1\"], data[\"test_status2\"], data[\"test_status3\"]]\n    )\n\n    data[\"test_project\"] = Project(\n        name=\"test project\",\n        code=\"tp\",\n        repository=data[\"test_repo\"],\n    )\n    DBSession.add(data[\"test_project\"])\n\n    # create Tasks\n    data[\"test_task1\"] = Task(\n        name=\"test task 1\",\n        project=data[\"test_project\"],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        resources=[data[\"test_resource1\"]],\n    )\n    DBSession.add(data[\"test_task1\"])\n\n    data[\"test_task2\"] = Task(\n        name=\"test task 2\",\n        project=data[\"test_project\"],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        resources=[data[\"test_resource1\"]],\n    )\n    DBSession.add(data[\"test_task2\"])\n\n    data[\"kwargs\"] = {\n        \"task\": data[\"test_task1\"],\n        \"resource\": data[\"test_resource1\"],\n        \"start\": datetime.datetime(2013, 3, 22, 1, 0, tzinfo=pytz.utc),\n        \"duration\": datetime.timedelta(10),\n    }\n\n    # create a TimeLog\n    # and test it\n    data[\"test_time_log\"] = TimeLog(**data[\"kwargs\"])\n    DBSession.add(data[\"test_time_log\"])\n    DBSession.commit()\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_true():\n    \"\"\"__auto_name__ class attribute is set to True for TimeLog class.\"\"\"\n    assert TimeLog.__auto_name__ is True\n\n\ndef test_task_argument_is_skipped(setup_time_log_db_tests):\n    \"\"\"TypeError raised if the task argument is skipped.\"\"\"\n    data = setup_time_log_db_tests\n    td = datetime.timedelta\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"task\")\n    kwargs[\"start\"] = kwargs[\"start\"] - td(days=100)\n    kwargs[\"duration\"] = td(hours=10)\n    with pytest.raises(TypeError) as cm:\n        TimeLog(**kwargs)\n\n    assert str(cm.value) == (\n        \"TimeLog.task should be an instance of stalker.models.task.Task, \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test_task_argument_is_none(setup_time_log_db_tests):\n    \"\"\"TypeError raised if the task argument is None.\"\"\"\n    data = setup_time_log_db_tests\n    td = datetime.timedelta\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"task\"] = None\n    kwargs[\"start\"] = kwargs[\"start\"] - td(days=100)\n    kwargs[\"duration\"] = td(hours=10)\n    with pytest.raises(TypeError) as cm:\n        TimeLog(**kwargs)\n\n    assert str(cm.value) == (\n        \"TimeLog.task should be an instance of stalker.models.task.Task, \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test_task_attribute_is_none(setup_time_log_db_tests):\n    \"\"\"TypeError raised if the task attribute is None.\"\"\"\n    data = setup_time_log_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_time_log\"].task = None\n\n    assert str(cm.value) == (\n        \"TimeLog.task should be an instance of stalker.models.task.Task, \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test_task_argument_is_not_a_task_instance(setup_time_log_db_tests):\n    \"\"\"TypeError raised if the task arg is not a Task instance.\"\"\"\n    data = setup_time_log_db_tests\n    td = datetime.timedelta\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"task\"] = \"this is a task\"\n    kwargs[\"start\"] = kwargs[\"start\"] - td(days=100)\n    kwargs[\"duration\"] = td(hours=10)\n    with pytest.raises(TypeError) as cm:\n        TimeLog(**kwargs)\n\n    assert str(cm.value) == (\n        \"TimeLog.task should be an instance of stalker.models.task.Task, \"\n        \"not str: 'this is a task'\"\n    )\n\n\ndef test_task_attribute_is_not_a_task_instance(setup_time_log_db_tests):\n    \"\"\"TypeError raised if the task attribute is not a Task instance.\"\"\"\n    data = setup_time_log_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_time_log\"].task = \"this is a task\"\n\n    assert str(cm.value) == (\n        \"TimeLog.task should be an instance of stalker.models.task.Task, \"\n        \"not str: 'this is a task'\"\n    )\n\n\ndef test_task_attribute_is_working_as_expected(setup_time_log_db_tests):\n    \"\"\"task attribute is working as expected.\"\"\"\n    data = setup_time_log_db_tests\n    new_task = Task(\n        name=\"Test task 2\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_resource1\"]],\n    )\n    assert data[\"test_time_log\"].task != new_task\n    data[\"test_time_log\"].task = new_task\n    assert data[\"test_time_log\"].task == new_task\n\n\ndef test_task_argument_updates_backref(setup_time_log_db_tests):\n    \"\"\"setting Task in TimeLog task arg updates Task.timee_logs attr.\"\"\"\n    data = setup_time_log_db_tests\n    new_task = Task(\n        name=\"Test Task 3\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_resource1\"]],\n    )\n\n    # now create a new time_log for the new task\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"task\"] = new_task\n    kwargs[\"start\"] = kwargs[\"start\"] + kwargs[\"duration\"] + datetime.timedelta(120)\n    new_time_log = TimeLog(**kwargs)\n\n    # now check if the new_time_log is in task.time_logs\n    assert new_time_log in new_task.time_logs\n\n\ndef test_task_attribute_updates_backref(setup_time_log_db_tests):\n    \"\"\"setting Task in TimeLog.task attr updates Task.timee_logs attr.\"\"\"\n    data = setup_time_log_db_tests\n    new_task = Task(\n        name=\"Test Task 3\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_resource1\"]],\n    )\n\n    data[\"test_time_log\"].task = new_task\n    assert data[\"test_time_log\"] in new_task.time_logs\n\n\ndef test_resource_argument_is_skipped(setup_time_log_db_tests):\n    \"\"\"TypeError raised if the resource argument is skipped.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs.pop(\"resource\")\n    kwargs[\"start\"] -= datetime.timedelta(days=200)\n    kwargs[\"end\"] = kwargs[\"start\"] + datetime.timedelta(days=10)\n    with pytest.raises(TypeError) as cm:\n        TimeLog(**kwargs)\n\n    assert str(cm.value) == \"TimeLog.resource cannot be None\"\n\n\ndef test_resource_argument_is_none(setup_time_log_db_tests):\n    \"\"\"TypeError raised if the resource argument is None.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = None\n    with pytest.raises(TypeError) as cm:\n        TimeLog(**kwargs)\n\n    assert str(cm.value) == \"TimeLog.resource cannot be None\"\n\n\ndef test_resource_attribute_is_none(setup_time_log_db_tests):\n    \"\"\"TypeError raised if the resource attribute is set to None.\"\"\"\n    data = setup_time_log_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_time_log\"].resource = None\n\n    assert str(cm.value) == \"TimeLog.resource cannot be None\"\n\n\ndef test_resource_argument_is_not_a_user_instance(setup_time_log_db_tests):\n    \"\"\"TypeError raised if the resource arg is not a User instance.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = \"This is a resource\"\n    with pytest.raises(TypeError) as cm:\n        TimeLog(**kwargs)\n\n    assert str(cm.value) == (\n        \"TimeLog.resource should be a stalker.models.auth.User instance, \"\n        \"not str: 'This is a resource'\"\n    )\n\n\ndef test_resource_attribute_is_not_a_user_instance(setup_time_log_db_tests):\n    \"\"\"TypeError raised if the resource attr is not a User instance.\"\"\"\n    data = setup_time_log_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_time_log\"].resource = \"this is a resource\"\n\n    assert str(cm.value) == (\n        \"TimeLog.resource should be a stalker.models.auth.User instance, \"\n        \"not str: 'this is a resource'\"\n    )\n\n\ndef test_resource_attribute_is_working_as_expected(setup_time_log_db_tests):\n    \"\"\"resource attribute is working okay.\"\"\"\n    data = setup_time_log_db_tests\n    new_resource = User(\n        name=\"Test Resource\",\n        login=\"test resource 2\",\n        email=\"test@resource2.com\",\n        password=\"1234\",\n    )\n\n    assert data[\"test_time_log\"].resource != new_resource\n    data[\"test_time_log\"].resource = new_resource\n    assert data[\"test_time_log\"].resource == new_resource\n\n\ndef test_resource_argument_updates_backref(setup_time_log_db_tests):\n    \"\"\"setting User in TimeLog resource arg updates User.timee_logs attr.\"\"\"\n    data = setup_time_log_db_tests\n    new_resource = User(\n        name=\"Test Resource\",\n        login=\"test resource 2\",\n        email=\"test@resource2.com\",\n        password=\"1234\",\n    )\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = new_resource\n    new_time_log = TimeLog(**kwargs)\n    assert new_time_log.resource == new_resource\n\n\ndef test_resource_attribute_updates_backref(setup_time_log_db_tests):\n    \"\"\"setting User in TimeLog.resource attr updates User.timee_logs attr.\"\"\"\n    data = setup_time_log_db_tests\n    new_resource = User(\n        name=\"Test Resource\",\n        login=\"test resource 2\",\n        email=\"test@resource2.com\",\n        password=\"1234\",\n    )\n\n    assert data[\"test_time_log\"].resource != new_resource\n    data[\"test_time_log\"].resource = new_resource\n    assert data[\"test_time_log\"].resource == new_resource\n\n\ndef test_schedule_mixin_initialization(setup_time_log_db_tests):\n    \"\"\"DateRangeMixin part is initialized correctly.\"\"\"\n    data = setup_time_log_db_tests\n    # it should have schedule attributes\n    assert data[\"test_time_log\"].start == data[\"kwargs\"][\"start\"]\n    assert data[\"test_time_log\"].duration == data[\"kwargs\"][\"duration\"]\n\n    data[\"test_time_log\"].start = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n    data[\"test_time_log\"].end = data[\"test_time_log\"].start + datetime.timedelta(10)\n    assert data[\"test_time_log\"].duration == datetime.timedelta(10)\n\n\ndef test_overbooked_error_1(setup_time_log_db_tests):\n    \"\"\"OverBookedError raised if resource is already booked for the given time period.\n\n    Simple case diagram:\n    #####\n    #####\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = (datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc),)\n    kwargs[\"duration\"] = datetime.timedelta(10)\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n    DBSession.commit()\n\n    with pytest.raises(OverBookedError) as cm:\n        TimeLog(**kwargs)\n\n    local_tz = tzlocal.get_localzone()\n    assert str(cm.value) == \"The resource has another TimeLog between {} and {}\".format(\n        time_log1.start.astimezone(local_tz),\n        time_log1.end.astimezone(local_tz),\n    )\n\n\ndef test_overbooked_error_2(setup_time_log_db_tests):\n    \"\"\"OverBookedError raised if resource is already booked for the given time period.\n\n    Simple case diagram:\n    #######\n    #####\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n    kwargs[\"duration\"] = datetime.timedelta(10)\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n    DBSession.commit()\n\n    # time_log2\n    kwargs[\"duration\"] = datetime.timedelta(8)\n    with pytest.raises(OverBookedError) as cm:\n        TimeLog(**kwargs)\n\n    local_tz = tzlocal.get_localzone()\n    assert str(cm.value) == \"The resource has another TimeLog between {} and {}\".format(\n        time_log1.start.astimezone(local_tz),\n        time_log1.end.astimezone(local_tz),\n    )\n\n\ndef test_overbooked_error_3(setup_time_log_db_tests):\n    \"\"\"OverBookedError raised if resource is already booked for the given time period.\n\n    Simple case diagram:\n    #####\n    #######\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n    kwargs[\"duration\"] = datetime.timedelta(8)\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n    DBSession.commit()\n\n    # time_log2\n    kwargs[\"duration\"] = datetime.timedelta(10)\n    with pytest.raises(OverBookedError) as cm:\n        TimeLog(**kwargs)\n\n    local_tz = tzlocal.get_localzone()\n    assert str(cm.value) == \"The resource has another TimeLog between {} and {}\".format(\n        time_log1.start.astimezone(local_tz),\n        time_log1.end.astimezone(local_tz),\n    )\n\n\ndef test_overbooked_error_4(setup_time_log_db_tests):\n    \"\"\"OverBookedError raised if resource is already booked for the given time period.\n\n    Simple case diagram:\n    #######\n      #####\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(\n        2013, 3, 22, 4, 0, tzinfo=pytz.utc\n    ) - datetime.timedelta(2)\n    kwargs[\"duration\"] = datetime.timedelta(12)\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n    DBSession.commit()\n\n    # time_log2\n    kwargs[\"start\"] = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n    kwargs[\"duration\"] = datetime.timedelta(10)\n\n    with pytest.raises(OverBookedError) as cm:\n        TimeLog(**kwargs)\n\n    local_tz = tzlocal.get_localzone()\n    assert str(cm.value) == \"The resource has another TimeLog between {} and {}\".format(\n        datetime.datetime(2013, 3, 20, 4, 0, tzinfo=pytz.utc).astimezone(local_tz),\n        (\n            datetime.datetime(2013, 3, 20, 4, 0, tzinfo=pytz.utc)\n            + datetime.timedelta(12)\n        ).astimezone(local_tz),\n    )\n\n\ndef test_overbooked_error_5(setup_time_log_db_tests):\n    \"\"\"OverBookedError raised if resource is already booked for the given time period.\n\n    Simple case diagram:\n      #####\n    #######\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n    kwargs[\"duration\"] = datetime.timedelta(10)\n\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n    DBSession.commit()\n\n    # time_log2\n    kwargs[\"start\"] = datetime.datetime(\n        2013, 3, 22, 4, 0, tzinfo=pytz.utc\n    ) - datetime.timedelta(2)\n    kwargs[\"duration\"] = datetime.timedelta(12)\n\n    with pytest.raises(OverBookedError) as cm:\n        TimeLog(**kwargs)\n\n    local_tz = tzlocal.get_localzone()\n    assert str(cm.value) == \"The resource has another TimeLog between {} and {}\".format(\n        datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc).astimezone(local_tz),\n        datetime.datetime(2013, 4, 1, 4, 0, tzinfo=pytz.utc).astimezone(local_tz),\n    )\n\n\ndef test_overbooked_error_6(setup_time_log_db_tests):\n    \"\"\"OverBookedError raised if resource is already booked for the given time period.\n\n    Simple case diagram:\n      #######\n    #######\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n    kwargs[\"duration\"] = datetime.timedelta(15)\n\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n    DBSession.commit()\n\n    # time_log2\n    kwargs[\"start\"] = datetime.datetime(\n        2013, 3, 22, 4, 0, tzinfo=pytz.utc\n    ) - datetime.timedelta(5)\n    kwargs[\"duration\"] = datetime.timedelta(15)\n\n    with pytest.raises(OverBookedError) as cm:\n        TimeLog(**kwargs)\n\n    local_tz = tzlocal.get_localzone()\n    assert str(cm.value) == \"The resource has another TimeLog between {} and {}\".format(\n        datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc).astimezone(local_tz),\n        datetime.datetime(2013, 4, 6, 4, 0, tzinfo=pytz.utc).astimezone(local_tz),\n    )\n\n\ndef test_overbooked_error_7(setup_time_log_db_tests):\n    \"\"\"OverBookedError raised if resource is already booked for the given time period.\n\n    Simple case diagram:\n    #######\n      #######\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(\n        2013, 3, 22, 4, 0, tzinfo=pytz.utc\n    ) - datetime.timedelta(5)\n    kwargs[\"duration\"] = datetime.timedelta(15)\n\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n    DBSession.commit()\n\n    # time_log2\n    kwargs[\"start\"] = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n    kwargs[\"duration\"] = datetime.timedelta(15)\n\n    with pytest.raises(OverBookedError) as cm:\n        TimeLog(**kwargs)\n\n    local_tz = tzlocal.get_localzone()\n    assert str(cm.value) == \"The resource has another TimeLog between {} and {}\".format(\n        datetime.datetime(2013, 3, 17, 4, 0, tzinfo=pytz.utc).astimezone(local_tz),\n        datetime.datetime(2013, 4, 1, 4, 0, tzinfo=pytz.utc).astimezone(local_tz),\n    )\n\n\ndef test_overbooked_error_8(setup_time_log_db_tests):\n    \"\"\"OverBookedError raised if resource is already booked for the given time period.\n\n    Simple case diagram:\n    #######\n             #######\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n    kwargs[\"duration\"] = datetime.timedelta(5)\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n    DBSession.commit()\n\n    # time_log2\n    kwargs[\"start\"] = datetime.datetime(\n        2013, 3, 22, 4, 0, tzinfo=pytz.utc\n    ) + datetime.timedelta(20)\n    # no warning\n    time_log2 = TimeLog(**kwargs)\n    DBSession.add(time_log2)\n    DBSession.commit()\n\n\ndef test_overbooked_error_9(setup_time_log_db_tests):\n    \"\"\"OverBookedError raised if resource is already booked for the given time period.\n\n    Simple case diagram:\n             #######\n    #######\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(\n        2013, 3, 22, 4, 0, tzinfo=pytz.utc\n    ) + datetime.timedelta(20)\n    kwargs[\"duration\"] = datetime.timedelta(5)\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n    DBSession.commit()\n\n    # time_log2\n    kwargs[\"start\"] = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n\n    # no warning\n    time_log2 = TimeLog(**kwargs)\n    DBSession.add(time_log2)\n    DBSession.commit()\n\n\ndef test_overbooked_error_10(setup_time_log_db_tests):\n    \"\"\"no OverBookedError raised for the same TimeLog instance.\"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(2013, 5, 6, 14, 0, tzinfo=pytz.utc)\n    kwargs[\"duration\"] = datetime.timedelta(20)\n    time_log1 = TimeLog(**kwargs)\n\n    # no warning\n    data[\"test_resource2\"].time_logs.append(time_log1)\n\n\ndef test_overbooked_error_11(setup_time_log_db_tests):\n    \"\"\"DB backend raises IntegrityError if the resource is booked for time the period.\n\n    It is not caught in Python side.\n\n    Simple case diagram:\n    #######\n      #######\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(\n        2013, 3, 22, 4, 0, tzinfo=pytz.utc\n    ) - datetime.timedelta(5)\n    kwargs[\"duration\"] = datetime.timedelta(15)\n\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n\n    # time_log2\n    kwargs[\"start\"] = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n    kwargs[\"duration\"] = datetime.timedelta(15)\n\n    time_log2 = TimeLog(**kwargs)\n    DBSession.add(time_log2)\n\n    # there should be an DatabaseError raised\n    with pytest.raises(IntegrityError) as cm:\n        DBSession.commit()\n\n    assert (\n        \"(psycopg2.errors.ExclusionViolation) conflicting key value \"\n        'violates exclusion constraint \"overlapping_time_logs\"' in str(cm.value)\n    )\n\n\ndef test_overbooked_error_12(setup_time_log_db_tests):\n    \"\"\"DB backend raises IntegrityError if the resource is booked for the time period.\n\n    It is not caught in Python side. But this one ensures that the error is raised even\n    if the tasks are different.\n\n    Simple case diagram:\n    #######\n      #######\n    \"\"\"\n    data = setup_time_log_db_tests\n    # time_log1\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"resource\"] = data[\"test_resource2\"]\n    kwargs[\"start\"] = datetime.datetime(\n        2013, 3, 22, 4, 0, tzinfo=pytz.utc\n    ) - datetime.timedelta(5)\n    kwargs[\"duration\"] = datetime.timedelta(15)\n    kwargs[\"task\"] = data[\"test_task1\"]\n\n    time_log1 = TimeLog(**kwargs)\n\n    DBSession.add(time_log1)\n\n    # time_log2\n    kwargs[\"start\"] = datetime.datetime(2013, 3, 22, 4, 0, tzinfo=pytz.utc)\n    kwargs[\"duration\"] = datetime.timedelta(15)\n    kwargs[\"task\"] = data[\"test_task2\"]\n\n    time_log2 = TimeLog(**kwargs)\n    DBSession.add(time_log2)\n\n    # there should be an DatabaseError raised\n    with pytest.raises(IntegrityError) as cm:\n        DBSession.commit()\n\n    assert (\n        \"(psycopg2.errors.ExclusionViolation) conflicting key value \"\n        'violates exclusion constraint \"overlapping_time_logs\"' in str(cm.value)\n    )\n\n\ndef tests_overbooked_error_fallback_to_python_if_no_db_is_setup_self():\n    \"\"\"_validate_resource() will fallback to Python if no db.\"\"\"\n    data = dict()\n    data[\"status_wfd\"] = Status(name=\"Waiting For Dependency\", code=\"WFD\")\n    data[\"status_rts\"] = Status(name=\"Ready To Start\", code=\"RTS\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_prev\"] = Status(name=\"Pending Review\", code=\"PREV\")\n    data[\"status_hrev\"] = Status(name=\"Has Revision\", code=\"HREV\")\n    data[\"status_drev\"] = Status(name=\"Dependency Has Revision\", code=\"DREV\")\n    data[\"status_oh\"] = Status(name=\"On Hold\", code=\"OH\")\n    data[\"status_stop\"] = Status(name=\"Stop\", code=\"STOP\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n\n    data[\"project_status_list\"] = StatusList(\n        name=\"Project Statuses\",\n        statuses=[\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Project\",\n    )\n    data[\"task_status_list\"] = StatusList(\n        name=\"Task Statuses\",\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n\n    # create a resource\n    data[\"test_resource1\"] = User(\n        name=\"User1\", login=\"user1\", email=\"user1@users.com\", password=\"1234\"\n    )\n\n    data[\"test_resource2\"] = User(\n        name=\"User2\", login=\"user2\", email=\"user2@users.com\", password=\"1234\"\n    )\n\n    data[\"test_repo\"] = Repository(name=\"test repository\", code=\"tr\")\n\n    # create a Project\n    data[\"test_status1\"] = Status(name=\"Status1\", code=\"STS1\")\n    data[\"test_status2\"] = Status(name=\"Status2\", code=\"STS2\")\n    data[\"test_status3\"] = Status(name=\"Status3\", code=\"STS3\")\n\n    data[\"test_project\"] = Project(\n        name=\"test project\",\n        code=\"tp\",\n        repository=data[\"test_repo\"],\n        status_list=data[\"project_status_list\"],\n    )\n\n    # create Tasks\n    data[\"test_task1\"] = Task(\n        name=\"test task 1\",\n        project=data[\"test_project\"],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        resources=[data[\"test_resource1\"]],\n        status_list=data[\"task_status_list\"],\n    )\n\n    data[\"test_task2\"] = Task(\n        name=\"test task 2\",\n        project=data[\"test_project\"],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        resources=[data[\"test_resource1\"]],\n        status_list=data[\"task_status_list\"],\n    )\n\n    data[\"kwargs\"] = {\n        \"task\": data[\"test_task1\"],\n        \"resource\": data[\"test_resource1\"],\n        \"start\": datetime.datetime(2013, 3, 22, 1, 0, tzinfo=pytz.utc),\n        \"duration\": datetime.timedelta(10),\n    }\n\n    # create a TimeLog\n    # and test it\n    data[\"test_time_log\"] = TimeLog(**data[\"kwargs\"])\n\n    # assigning the same resource should skip self while searching for a timelog\n    data[\"test_time_log\"].resource = data[\"test_resource1\"]\n\n\ndef tests_overbooked_error_fallback_to_python_if_no_db_is_setup_new_tlog():\n    \"\"\"_validate_resource() will fallback to Python if no db.\"\"\"\n    data = dict()\n    data[\"status_wfd\"] = Status(name=\"Waiting For Dependency\", code=\"WFD\")\n    data[\"status_rts\"] = Status(name=\"Ready To Start\", code=\"RTS\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_prev\"] = Status(name=\"Pending Review\", code=\"PREV\")\n    data[\"status_hrev\"] = Status(name=\"Has Revision\", code=\"HREV\")\n    data[\"status_drev\"] = Status(name=\"Dependency Has Revision\", code=\"DREV\")\n    data[\"status_oh\"] = Status(name=\"On Hold\", code=\"OH\")\n    data[\"status_stop\"] = Status(name=\"Stop\", code=\"STOP\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n\n    data[\"project_status_list\"] = StatusList(\n        name=\"Project Statuses\",\n        statuses=[\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Project\",\n    )\n    data[\"task_status_list\"] = StatusList(\n        name=\"Task Statuses\",\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n\n    # create a resource\n    data[\"test_resource1\"] = User(\n        name=\"User1\", login=\"user1\", email=\"user1@users.com\", password=\"1234\"\n    )\n\n    data[\"test_resource2\"] = User(\n        name=\"User2\", login=\"user2\", email=\"user2@users.com\", password=\"1234\"\n    )\n\n    data[\"test_repo\"] = Repository(name=\"test repository\", code=\"tr\")\n\n    # create a Project\n    data[\"test_status1\"] = Status(name=\"Status1\", code=\"STS1\")\n    data[\"test_status2\"] = Status(name=\"Status2\", code=\"STS2\")\n    data[\"test_status3\"] = Status(name=\"Status3\", code=\"STS3\")\n\n    data[\"test_project\"] = Project(\n        name=\"test project\",\n        code=\"tp\",\n        repository=data[\"test_repo\"],\n        status_list=data[\"project_status_list\"],\n    )\n\n    # create Tasks\n    data[\"test_task1\"] = Task(\n        name=\"test task 1\",\n        project=data[\"test_project\"],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        resources=[data[\"test_resource1\"]],\n        status_list=data[\"task_status_list\"],\n    )\n\n    data[\"test_task2\"] = Task(\n        name=\"test task 2\",\n        project=data[\"test_project\"],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        resources=[data[\"test_resource1\"]],\n        status_list=data[\"task_status_list\"],\n    )\n\n    data[\"kwargs\"] = {\n        \"task\": data[\"test_task1\"],\n        \"resource\": data[\"test_resource1\"],\n        \"start\": datetime.datetime(2013, 3, 22, 1, 0, tzinfo=pytz.utc),\n        \"duration\": datetime.timedelta(10),\n    }\n\n    # create a TimeLog\n    # and test it\n    data[\"test_time_log\"] = TimeLog(**data[\"kwargs\"])\n\n    # creating another time log should raise Overbooked error\n    with pytest.raises(OverBookedError) as cm:\n        _ = TimeLog(**data[\"kwargs\"])\n\n\ndef test_timelog_prevents_auto_flush_if_expanding_task_schedule_timing(\n    setup_time_log_db_tests,\n):\n    \"\"\"timeLog prevents auto flush if expanding task schedule_timing attribute.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"start\"] = kwargs[\"start\"] - datetime.timedelta(days=100)\n    tlog1 = TimeLog(**kwargs)\n\n    DBSession.add(tlog1)\n    DBSession.commit()\n\n    # create a new time log\n    kwargs[\"start\"] = kwargs[\"start\"] + kwargs[\"duration\"]\n    _ = TimeLog(**kwargs)\n\n\ndef test_timelog_creation_for_a_child_task(setup_time_log_db_tests):\n    \"\"\"TimeLog creation for a child task which has a couple of parent tasks.\"\"\"\n    data = setup_time_log_db_tests\n    dt = datetime.datetime\n    parent_task1 = Task(\n        name=\"Parent Task 1\",\n        project=data[\"test_project\"],\n    )\n    parent_task2 = Task(\n        name=\"Parent Task 2\",\n        project=data[\"test_project\"],\n    )\n    child_task1 = Task(\n        name=\"Child Task 1\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_resource1\"]],\n    )\n    child_task2 = Task(\n        name=\"Child Task 1\",\n        project=data[\"test_project\"],\n        resources=[data[\"test_resource2\"]],\n    )\n\n    # Task hierarchy\n    # +-> p1\n    # |   |\n    # |   +-> p2\n    # |   |    |\n    # |   |    +-> c1\n    # |   |\n    # |   +-> c2\n    # |\n    # +-> data[\"test_task1\"]\n    parent_task2.parent = parent_task1\n    child_task2.parent = parent_task1\n    child_task1.parent = parent_task2\n\n    assert parent_task1.total_logged_seconds == 0\n    assert parent_task2.total_logged_seconds == 0\n    assert child_task1.total_logged_seconds == 0\n    assert child_task2.total_logged_seconds == 0\n\n    # now create a time log for child_task2\n    tlog1 = TimeLog(\n        task=child_task2,\n        resource=child_task2.resources[0],\n        start=dt(2013, 7, 31, 10, 0, tzinfo=pytz.utc),\n        end=dt(2013, 7, 31, 19, 0, tzinfo=pytz.utc),\n    )\n\n    # before commit\n    assert parent_task1.total_logged_seconds == 9 * 3600\n    assert parent_task2.total_logged_seconds == 0\n    assert child_task1.total_logged_seconds == 0\n    assert child_task2.total_logged_seconds == 0\n\n    # commit changes\n    DBSession.add(tlog1)\n    DBSession.commit()\n\n    # after \"commit\" it should not change\n    assert parent_task1.total_logged_seconds == 9 * 3600\n    assert parent_task2.total_logged_seconds == 0\n    assert child_task1.total_logged_seconds == 0\n    assert child_task2.total_logged_seconds == 9 * 3600\n\n    # add a new tlog to child_task2 and commit it\n    # now create a time log for child_task2\n    tlog2 = TimeLog(\n        task=child_task2,\n        resource=child_task2.resources[0],\n        start=dt(2013, 7, 31, 19, 0, tzinfo=pytz.utc),\n        end=dt(2013, 7, 31, 22, 0, tzinfo=pytz.utc),\n    )\n\n    assert parent_task1.total_logged_seconds == 12 * 3600\n    assert parent_task2.total_logged_seconds == 0\n    assert child_task1.total_logged_seconds == 0\n    assert child_task2.total_logged_seconds == 9 * 3600\n\n    # commit changes\n    DBSession.add(tlog2)\n    DBSession.commit()\n\n    assert parent_task1.total_logged_seconds == 12 * 3600\n    assert parent_task2.total_logged_seconds == 0\n    assert child_task1.total_logged_seconds == 0\n    assert child_task2.total_logged_seconds == 12 * 3600\n\n    # add a new time log to child_task1 and commit it\n    tlog3 = TimeLog(\n        task=child_task1,\n        resource=child_task1.resources[0],\n        start=dt(2013, 7, 31, 10, 0, tzinfo=pytz.utc),\n        end=dt(2013, 7, 31, 19, 0, tzinfo=pytz.utc),\n    )\n    # commit changes\n    DBSession.add(tlog3)\n    DBSession.commit()\n\n    assert parent_task1.total_logged_seconds == 21 * 3600\n    assert parent_task2.total_logged_seconds == 9 * 3600\n    assert child_task1.total_logged_seconds == 9 * 3600\n    assert child_task2.total_logged_seconds == 12 * 3600\n\n    # assert parent_task1.total_logged_seconds == 21 * 3600\n    # assert parent_task2.total_logged_seconds == 9 * 3600\n    # assert child_task1.total_logged_seconds == 9 * 3600\n    # assert child_task2.total_logged_seconds == 12 * 3600\n\n\ndef test_time_log_creation_for_a_wfd_leaf_task(setup_time_log_db_tests):\n    \"\"\"StatusError raised if TimeLog is created for a WFD leaf task.\"\"\"\n    data = setup_time_log_db_tests\n    new_task = Task(name=\"Test Task 2\", project=data[\"test_project\"])\n    new_task.depends_on = [data[\"test_task1\"]]\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"task\"] = new_task\n    with pytest.raises(StatusError) as cm:\n        TimeLog(**kwargs)\n\n    assert (\n        str(cm.value) == \"Test Task 2 is a WFD task, and it is not allowed to create \"\n        \"TimeLogs for a WFD task, please supply a RTS, WIP, HREV or \"\n        \"DREV task!\"\n    )\n\n\ndef test_time_log_creation_for_a_rts_leaf_task(setup_time_log_db_tests):\n    \"\"\"status updated to WIP if a TimeLog instance is created for an RTS leaf task.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    task = kwargs[\"task\"]\n    kwargs[\"start\"] -= datetime.timedelta(days=100)\n    task.status = data[\"status_rts\"]\n    assert task.status == data[\"status_rts\"]\n    TimeLog(**kwargs)\n    assert task.status == data[\"status_wip\"]\n\n\ndef test_time_log_creation_for_a_wip_leaf_task(setup_time_log_db_tests):\n    \"\"\"status will stay at WIP if a TimeLog instance is created for a WIP leaf task.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    task = kwargs[\"task\"]\n    kwargs[\"start\"] -= datetime.timedelta(days=10)\n    task.status = data[\"status_wip\"]\n    assert task.status == data[\"status_wip\"]\n    TimeLog(**kwargs)\n\n\ndef test_time_log_creation_for_a_prev_leaf_task(setup_time_log_db_tests):\n    \"\"\"status will stay PREV if a TimeLog instance is created for a PREV leaf task.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    task = kwargs[\"task\"]\n    kwargs[\"start\"] -= datetime.timedelta(days=100)\n    task.status = data[\"status_prev\"]\n    assert task.status == data[\"status_prev\"]\n    TimeLog(**kwargs)\n    assert task.status == data[\"status_prev\"]\n\n\ndef test_time_log_creation_for_a_hrev_leaf_task(setup_time_log_db_tests):\n    \"\"\"status updated to WIP if a TimeLog instance is created for a HREV leaf task.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    task = kwargs[\"task\"]\n    kwargs[\"start\"] -= datetime.timedelta(days=100)\n    task.status = data[\"status_hrev\"]\n    assert task.status == data[\"status_hrev\"]\n    TimeLog(**kwargs)\n\n\ndef test_time_log_creation_for_a_drev_leaf_task(setup_time_log_db_tests):\n    \"\"\"status will stay DREV if a TimeLog instance is created for a DREV leaf task.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    task = kwargs[\"task\"]\n    kwargs[\"start\"] -= datetime.timedelta(days=100)\n    task.status = data[\"status_drev\"]\n    assert task.status == data[\"status_drev\"]\n    TimeLog(**kwargs)\n\n\ndef test_time_log_creation_for_a_oh_leaf_task(setup_time_log_db_tests):\n    \"\"\"StatusError raised if a TimeLog instance is created for a OH leaf task.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    task = kwargs[\"task\"]\n    task.status = data[\"status_oh\"]\n    assert task.status == data[\"status_oh\"]\n    with pytest.raises(StatusError) as cm:\n        TimeLog(**kwargs)\n\n    assert (\n        str(cm.value) == \"test task 1 is a OH task, and it is not allowed to create \"\n        \"TimeLogs for a OH task, please supply a RTS, WIP, HREV or DREV \"\n        \"task!\"\n    )\n\n\ndef test_time_log_creation_for_a_stop_leaf_task(setup_time_log_db_tests):\n    \"\"\"StatusError raised if a TimeLog instance is created for a STOP leaf task.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    task = kwargs[\"task\"]\n    task.status = data[\"status_stop\"]\n    assert task.status == data[\"status_stop\"]\n    with pytest.raises(StatusError) as cm:\n        TimeLog(**kwargs)\n\n    assert (\n        str(cm.value) == \"test task 1 is a STOP task, and it is not allowed to create \"\n        \"TimeLogs for a STOP task, please supply a RTS, WIP, HREV or \"\n        \"DREV task!\"\n    )\n\n\ndef test_time_log_creation_for_a_cmpl_leaf_task(setup_time_log_db_tests):\n    \"\"\"StatusError raised if a TimeLog instance is created for a CMPL leaf task.\"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    task = kwargs[\"task\"]\n    task.status = data[\"status_cmpl\"]\n    assert task.status == data[\"status_cmpl\"]\n    with pytest.raises(StatusError) as cm:\n        TimeLog(**kwargs)\n\n    assert (\n        str(cm.value) == \"test task 1 is a CMPL task, and it is not allowed to create \"\n        \"TimeLogs for a CMPL task, please supply a RTS, WIP, HREV or \"\n        \"DREV task!\"\n    )\n\n\ndef test_time_log_creation_that_violates_dependency_condition_wip_cmpl_onend(\n    setup_time_log_db_tests,\n):\n    \"\"\"DependencyViolationError raised if the TimeLog violates dependency task relation.\n\n    +--------+\n    | Task 1 | ----+\n    |  CMPL  |     |\n    +--------+     |    +--------+\n                   +--->| Task 2 |\n                        |  WIP   |\n                        +--------+\n    \"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    task = kwargs[\"task\"]\n    task.status = data[\"status_cmpl\"]\n    task.start = datetime.datetime(2014, 3, 16, 10, 0, tzinfo=pytz.utc)\n    task.end = datetime.datetime(2014, 3, 25, 19, 0, tzinfo=pytz.utc)\n\n    dep_task = Task(\n        name=\"test task 2\",\n        project=data[\"test_project\"],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        depends_on=[task],\n        resources=[data[\"test_resource2\"]],\n    )\n\n    # set the dependency target to onend\n    dep_task.task_depends_on[0].dependency_target = \"onend\"\n\n    # entering a time log to the dates before 2014-03-25-19-0 should raise\n    # a ValueError\n    with pytest.raises(DependencyViolationError) as cm:\n        dep_task.create_time_log(\n            data[\"test_resource2\"],\n            datetime.datetime(2014, 3, 25, 18, 0, tzinfo=pytz.utc),\n            datetime.datetime(2014, 3, 25, 19, 0, tzinfo=pytz.utc),\n        )\n\n    assert str(cm.value) == (\n        \"It is not possible to create a TimeLog before {}, which \"\n        'violates the dependency relation of \"{}\" to \"{}\"'.format(\n            datetime.datetime(2014, 3, 25, 19, 0, tzinfo=pytz.utc),\n            dep_task.name,\n            task.name,\n        )\n    )\n\n    # and creating a TimeLog after that is possible\n    dep_task.create_time_log(\n        data[\"test_resource2\"],\n        datetime.datetime(2014, 3, 25, 19, 0, tzinfo=pytz.utc),\n        datetime.datetime(2014, 3, 25, 20, 0, tzinfo=pytz.utc),\n    )\n\n\ndef test_time_log_creation_that_violates_dependency_condition_wip_cmpl_onstart(\n    setup_time_log_db_tests,\n):\n    \"\"\"ValueError raised if the entered TimeLog violates the dependency relation tasks.\n\n      +--------+\n    +-| Task 1 |\n    | |  CMPL  |\n    | +--------+          +--------+\n    +-------------------->| Task 2 |\n                          |  WIP   |\n                          +--------+\n    \"\"\"\n    data = setup_time_log_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    task = kwargs[\"task\"]\n    task.status = data[\"status_cmpl\"]\n    task.start = datetime.datetime(2014, 3, 16, 10, 0, tzinfo=pytz.utc)\n    task.end = datetime.datetime(2014, 3, 25, 19, 0, tzinfo=pytz.utc)\n\n    dep_task = Task(\n        name=\"test task 2\",\n        project=data[\"test_project\"],\n        schedule_timing=10,\n        schedule_unit=TimeUnit.Day,\n        depends_on=[task],\n        resources=[data[\"test_resource2\"]],\n    )\n\n    # set the dependency target to onstart\n    dep_task.task_depends_on[0].dependency_target = DependencyTarget.OnStart\n\n    # entering a time log to the dates before 2014-03-16-10-0 should raise\n    # a ValueError\n    with pytest.raises(DependencyViolationError) as cm:\n        dep_task.create_time_log(\n            data[\"test_resource2\"],\n            datetime.datetime(2014, 3, 16, 9, 0, tzinfo=pytz.utc),\n            datetime.datetime(2014, 3, 16, 10, 0, tzinfo=pytz.utc),\n        )\n\n    assert str(cm.value) == (\n        \"It is not possible to create a TimeLog before {}, which \"\n        'violates the dependency relation of \"{}\" to \"{}\"'.format(\n            datetime.datetime(2014, 3, 16, 10, 0, tzinfo=pytz.utc),\n            dep_task.name,\n            task.name,\n        )\n    )\n\n    # and creating a TimeLog after that is possible\n    dep_task.create_time_log(\n        data[\"test_resource2\"],\n        datetime.datetime(2014, 3, 16, 10, 0, tzinfo=pytz.utc),\n        datetime.datetime(2014, 3, 16, 10, 0, tzinfo=pytz.utc),\n    )\n"
  },
  {
    "path": "tests/models/test_time_unit.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"TimeUnit related tests are here.\"\"\"\nfrom enum import Enum\nimport sys\n\nimport pytest\n\nfrom stalker.models.enum import TimeUnit, TimeUnitDecorator\n\n\n@pytest.mark.parametrize(\n    \"unit\",\n    [\n        TimeUnit.Minute,\n        TimeUnit.Hour,\n        TimeUnit.Day,\n        TimeUnit.Week,\n        TimeUnit.Month,\n        TimeUnit.Year,\n    ],\n)\ndef test_it_is_an_enum(unit):\n    \"\"\"TimeUnit is an Enum.\"\"\"\n    assert isinstance(unit, Enum)\n\n\n@pytest.mark.parametrize(\n    \"unit,expected_value\",\n    [\n        [TimeUnit.Minute, \"min\"],\n        [TimeUnit.Hour, \"h\"],\n        [TimeUnit.Day, \"d\"],\n        [TimeUnit.Week, \"w\"],\n        [TimeUnit.Month, \"m\"],\n        [TimeUnit.Year, \"y\"],\n    ],\n)\ndef test_enum_values(unit, expected_value):\n    \"\"\"Test enum values.\"\"\"\n    assert unit.value == expected_value\n\n\n@pytest.mark.parametrize(\n    \"unit,expected_name\",\n    [\n        [TimeUnit.Minute, \"Minute\"],\n        [TimeUnit.Hour, \"Hour\"],\n        [TimeUnit.Day, \"Day\"],\n        [TimeUnit.Week, \"Week\"],\n        [TimeUnit.Month, \"Month\"],\n        [TimeUnit.Year, \"Year\"],\n    ],\n)\ndef test_enum_names(unit, expected_name):\n    \"\"\"Test enum names.\"\"\"\n    assert unit.name == expected_name\n\n\n@pytest.mark.parametrize(\n    \"unit,expected_value\",\n    [\n        [TimeUnit.Minute, \"min\"],\n        [TimeUnit.Hour, \"h\"],\n        [TimeUnit.Day, \"d\"],\n        [TimeUnit.Week, \"w\"],\n        [TimeUnit.Month, \"m\"],\n        [TimeUnit.Year, \"y\"],\n    ],\n)\ndef test_enum_as_str(unit, expected_value):\n    \"\"\"Test enum names.\"\"\"\n    assert str(unit) == expected_value\n\n\ndef test_to_unit_unit_is_skipped():\n    \"\"\"TimeUnit.to_unit() unit is skipped.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = TimeUnit.to_unit()\n\n    py_error_message = {\n        8: \"to_unit() missing 1 required positional argument: 'unit'\",\n        9: \"to_unit() missing 1 required positional argument: 'unit'\",\n        10: \"TimeUnit.to_unit() missing 1 required positional argument: 'unit'\",\n        11: \"TimeUnit.to_unit() missing 1 required positional argument: 'unit'\",\n        12: \"TimeUnit.to_unit() missing 1 required positional argument: 'unit'\",\n        13: \"TimeUnit.to_unit() missing 1 required positional argument: 'unit'\",\n    }[sys.version_info.minor]\n    assert str(cm.value) == py_error_message\n\n\ndef test_to_unit_unit_is_none():\n    \"\"\"TimeUnit.to_unit() unit is None.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = TimeUnit.to_unit(None)\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not NoneType: 'None'\"\n    )\n\n\ndef test_to_unit_unit_is_not_a_str():\n    \"\"\"TimeUnit.to_unit() unit is not a str.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = TimeUnit.to_unit(12334.123)\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not float: '12334.123'\"\n    )\n\n\ndef test_to_unit_unit_is_not_a_valid_str():\n    \"\"\"TimeUnit.to_unit() unit is not a valid str.\"\"\"\n    with pytest.raises(ValueError) as cm:\n        _ = TimeUnit.to_unit(\"not a valid value\")\n\n    assert str(cm.value) == (\n        \"unit should be a TimeUnit enum value or one of ['Minute', 'Hour', \"\n        \"'Day', 'Week', 'Month', 'Year', 'min', 'h', 'd', 'w', 'm', 'y'], \"\n        \"not 'not a valid value'\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"unit_name,unit\",\n    [\n        # Minute\n        [\"Min\", TimeUnit.Minute],\n        [\"min\", TimeUnit.Minute],\n        [\"MIN\", TimeUnit.Minute],\n        [\"MiN\", TimeUnit.Minute],\n        [\"mIn\", TimeUnit.Minute],\n        [\"Minute\", TimeUnit.Minute],\n        [\"minute\", TimeUnit.Minute],\n        [\"MINUTE\", TimeUnit.Minute],\n        [\"MiNuTe\", TimeUnit.Minute],\n        [\"mInUtE\", TimeUnit.Minute],\n        # Hour\n        [\"H\", TimeUnit.Hour],\n        [\"h\", TimeUnit.Hour],\n        [\"Hour\", TimeUnit.Hour],\n        [\"hour\", TimeUnit.Hour],\n        [\"HOUR\", TimeUnit.Hour],\n        [\"HoUr\", TimeUnit.Hour],\n        [\"hOuR\", TimeUnit.Hour],\n        # Day\n        [\"D\", TimeUnit.Day],\n        [\"d\", TimeUnit.Day],\n        [\"Day\", TimeUnit.Day],\n        [\"day\", TimeUnit.Day],\n        [\"DAY\", TimeUnit.Day],\n        [\"DaY\", TimeUnit.Day],\n        [\"dAy\", TimeUnit.Day],\n        # Week\n        [\"W\", TimeUnit.Week],\n        [\"w\", TimeUnit.Week],\n        [\"Week\", TimeUnit.Week],\n        [\"week\", TimeUnit.Week],\n        [\"WEEK\", TimeUnit.Week],\n        [\"WeeK\", TimeUnit.Week],\n        [\"wEEk\", TimeUnit.Week],\n        # Month\n        [\"M\", TimeUnit.Month],\n        [\"m\", TimeUnit.Month],\n        [\"Month\", TimeUnit.Month],\n        [\"month\", TimeUnit.Month],\n        [\"MONTH\", TimeUnit.Month],\n        [\"MoNtH\", TimeUnit.Month],\n        [\"mOnTh\", TimeUnit.Month],\n        # Year\n        [\"Y\", TimeUnit.Year],\n        [\"y\", TimeUnit.Year],\n        [\"Year\", TimeUnit.Year],\n        [\"year\", TimeUnit.Year],\n        [\"YEAR\", TimeUnit.Year],\n        [\"YeAr\", TimeUnit.Year],\n        [\"yEaR\", TimeUnit.Year],\n    ],\n)\ndef test_schedule_unit_to_unit_is_working_properly(unit_name, unit):\n    \"\"\"TimeUnit can parse schedule unit names.\"\"\"\n    assert TimeUnit.to_unit(unit_name) == unit\n\n\ndef test_cache_ok_is_true_in_type_decorator():\n    \"\"\"TimeUnitDecorator.cache_ok is True.\"\"\"\n    assert TimeUnitDecorator.cache_ok is True\n"
  },
  {
    "path": "tests/models/test_traversal_direction.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"TraversalDirection related tests are here.\"\"\"\nfrom enum import IntEnum\nimport sys\n\nimport pytest\n\nfrom stalker.models.enum import TraversalDirection\n\n\n@pytest.mark.parametrize(\n    \"traversal_direction\",\n    [\n        TraversalDirection.DepthFirst,\n        TraversalDirection.BreadthFirst,\n    ],\n)\ndef test_it_is_an_int_enum(traversal_direction):\n    \"\"\"TraversalDirection is an IntEnum.\"\"\"\n    assert isinstance(traversal_direction, IntEnum)\n\n\n@pytest.mark.parametrize(\n    \"traversal_direction,expected_value\",\n    [\n        [TraversalDirection.DepthFirst, 0],\n        [TraversalDirection.BreadthFirst, 1],\n    ],\n)\ndef test_enum_values(traversal_direction, expected_value):\n    \"\"\"Test enum values.\"\"\"\n    assert traversal_direction == expected_value\n\n\n@pytest.mark.parametrize(\n    \"traversal_direction,expected_value\",\n    [\n        [TraversalDirection.DepthFirst, \"DepthFirst\"],\n        [TraversalDirection.BreadthFirst, \"BreadthFirst\"],\n    ],\n)\ndef test_enum_names(traversal_direction, expected_value):\n    \"\"\"Test enum names.\"\"\"\n    assert str(traversal_direction) == expected_value\n\n\ndef test_to_direction_direction_is_skipped():\n    \"\"\"TraversalDirection.to_direction() direction is skipped.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = TraversalDirection.to_direction()\n\n    py_error_message = {\n        8: \"to_direction() missing 1 required positional argument: 'direction'\",\n        9: \"to_direction() missing 1 required positional argument: 'direction'\",\n        10: \"TraversalDirection.to_direction() missing 1 required positional argument: 'direction'\",\n        11: \"TraversalDirection.to_direction() missing 1 required positional argument: 'direction'\",\n        12: \"TraversalDirection.to_direction() missing 1 required positional argument: 'direction'\",\n        13: \"TraversalDirection.to_direction() missing 1 required positional argument: 'direction'\",\n    }[sys.version_info.minor]\n    assert str(cm.value) == py_error_message\n\n\ndef test_to_direction_direction_is_none():\n    \"\"\"TraversalDirection.to_direction() direction is None.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = TraversalDirection.to_direction(None)\n    assert str(cm.value) == (\n        \"direction should be a TraversalDirection enum value or one \"\n        \"of ['DepthFirst', 'BreadthFirst', 0, 1], not NoneType: 'None'\"\n    )\n\n\ndef test_to_direction_direction_is_not_a_str():\n    \"\"\"TraversalDirection.to_direction() direction is not an int or str.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        _ = TraversalDirection.to_direction(12334.123)\n\n    assert str(cm.value) == (\n        \"direction should be a TraversalDirection enum value or one of \"\n        \"['DepthFirst', 'BreadthFirst', 0, 1], not float: '12334.123'\"\n    )\n\n\ndef test_to_direction_direction_is_not_a_valid_str():\n    \"\"\"TraversalDirection.to_direction() direction is not a valid str.\"\"\"\n    with pytest.raises(ValueError) as cm:\n        _ = TraversalDirection.to_direction(\"not a valid value\")\n\n    assert str(cm.value) == (\n        \"direction should be a TraversalDirection enum value or one of \"\n        \"['DepthFirst', 'BreadthFirst', 0, 1], not 'not a valid value'\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"direction_name,direction\",\n    [\n        # DepthFirst\n        [\"DepthFirst\", TraversalDirection.DepthFirst],\n        [\"depthfirst\", TraversalDirection.DepthFirst],\n        [\"DEPTHFIRST\", TraversalDirection.DepthFirst],\n        [\"DePtHfIrSt\", TraversalDirection.DepthFirst],\n        [\"dEpThFiRsT\", TraversalDirection.DepthFirst],\n        [0, TraversalDirection.DepthFirst],\n        # BreadthFirst\n        [\"BreadthFirst\", TraversalDirection.BreadthFirst],\n        [\"breadthfirst\", TraversalDirection.BreadthFirst],\n        [\"BREADTHFIRST\", TraversalDirection.BreadthFirst],\n        [\"BrEaDtHfIrSt\", TraversalDirection.BreadthFirst],\n        [\"bReAdThFiRsT\", TraversalDirection.BreadthFirst],\n        [1, TraversalDirection.BreadthFirst],\n    ],\n)\ndef test_to_direction_is_working_properly(direction_name, direction):\n    \"\"\"TraversalDirection can parse schedule direction names.\"\"\"\n    assert TraversalDirection.to_direction(direction_name) == direction\n"
  },
  {
    "path": "tests/models/test_type.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Type class.\"\"\"\n\nimport sys\nimport pytest\n\nfrom stalker import Asset, Entity, Type\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_type_tests():\n    \"\"\"Set up tests for the Type class.\"\"\"\n    data = dict()\n    data[\"kwargs\"] = {\n        \"name\": \"test type\",\n        \"code\": \"test\",\n        \"description\": \"this is a test type\",\n        \"target_entity_type\": \"SimpleEntity\",\n    }\n\n    data[\"test_type\"] = Type(**data[\"kwargs\"])\n\n    # create another Entity with the same name of the\n    # test_type for __eq__ and __ne__ tests\n    data[\"entity1\"] = Entity(**data[\"kwargs\"])\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for Ticket class.\"\"\"\n    assert Type.__auto_name__ is False\n\n\ndef test_equality(setup_type_tests):\n    \"\"\"equality operator.\"\"\"\n    data = setup_type_tests\n    new_type2 = Type(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"target_entity_type\"] = \"Asset\"\n    new_type3 = Type(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"name\"] = \"a different type\"\n    data[\"kwargs\"][\"description\"] = \"this is a different type\"\n    new_type4 = Type(**data[\"kwargs\"])\n\n    assert data[\"test_type\"] == new_type2\n    assert not data[\"test_type\"] == new_type3\n    assert not data[\"test_type\"] == new_type4\n    assert not data[\"test_type\"] == data[\"entity1\"]\n\n\ndef test_inequality(setup_type_tests):\n    \"\"\"inequality operator.\"\"\"\n    data = setup_type_tests\n    new_type2 = Type(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"target_entity_type\"] = \"Asset\"\n    new_type3 = Type(**data[\"kwargs\"])\n\n    data[\"kwargs\"][\"name\"] = \"a different type\"\n    data[\"kwargs\"][\"description\"] = \"this is a different type\"\n    new_type4 = Type(**data[\"kwargs\"])\n\n    assert not data[\"test_type\"] != new_type2\n    assert data[\"test_type\"] != new_type3\n    assert data[\"test_type\"] != new_type4\n    assert data[\"test_type\"] != data[\"entity1\"]\n\n\ndef test_plural_class_name(setup_type_tests):\n    \"\"\"plural name of Type class.\"\"\"\n    data = setup_type_tests\n    assert data[\"test_type\"].plural_class_name == \"Types\"\n\n\ndef test_target_entity_type_argument_cannot_be_skipped(setup_type_tests):\n    \"\"\"TypeError raised if the created Type doesn't have any target_entity_type.\"\"\"\n    data = setup_type_tests\n    data[\"kwargs\"].pop(\"target_entity_type\")\n    with pytest.raises(TypeError) as cm:\n        Type(**data[\"kwargs\"])\n    assert str(cm.value) == \"Type.target_entity_type cannot be None\"\n\n\ndef test_target_entity_type_argument_cannot_be_none(setup_type_tests):\n    \"\"\"TypeError raised if the target_entity_type argument is None.\"\"\"\n    data = setup_type_tests\n    data[\"kwargs\"][\"target_entity_type\"] = None\n    with pytest.raises(TypeError) as cm:\n        Type(**data[\"kwargs\"])\n    assert str(cm.value) == \"Type.target_entity_type cannot be None\"\n\n\ndef test_target_entity_type_argument_cannot_be_empty_string(setup_type_tests):\n    \"\"\"ValueError raised if the target_entity_type argument is an empty string.\"\"\"\n    data = setup_type_tests\n    data[\"kwargs\"][\"target_entity_type\"] = \"\"\n    with pytest.raises(ValueError) as cm:\n        Type(**data[\"kwargs\"])\n    assert str(cm.value) == \"Type.target_entity_type cannot be empty\"\n\n\ndef test_target_entity_type_argument_accepts_strings(setup_type_tests):\n    \"\"\"target_entity_type argument accepts strings.\"\"\"\n    data = setup_type_tests\n    data[\"kwargs\"][\"target_entity_type\"] = \"Asset\"\n    # no error should be raised\n    Type(**data[\"kwargs\"])\n\n\ndef test_target_entity_type_argument_accepts_python_classes(setup_type_tests):\n    \"\"\"target_entity_type argument is given as a Python class converted to a string.\"\"\"\n    data = setup_type_tests\n    data[\"kwargs\"][\"target_entity_type\"] = Asset\n    new_type = Type(**data[\"kwargs\"])\n    assert new_type.target_entity_type == \"Asset\"\n\n\ndef test_target_entity_type_attribute_is_read_only(setup_type_tests):\n    \"\"\"target_entity_type attribute is read-only.\"\"\"\n    data = setup_type_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_type\"].target_entity_type = \"Asset\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute\",\n        11: \"property of 'Type' object has no setter\",\n        12: \"property of 'Type' object has no setter\",\n    }.get(\n        sys.version_info.minor,\n        \"property '_target_entity_type_getter' of 'Type' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_target_entity_type_attribute_is_working_as_expected(setup_type_tests):\n    \"\"\"target_entity_type attribute is working as expected.\"\"\"\n    data = setup_type_tests\n    assert data[\"test_type\"].target_entity_type == data[\"kwargs\"][\"target_entity_type\"]\n\n\ndef test__hash__is_working_as_expected(setup_type_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_type_tests\n    result = hash(data[\"test_type\"])\n    assert isinstance(result, int)\n    assert result == data[\"test_type\"].__hash__()\n"
  },
  {
    "path": "tests/models/test_user.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the User class.\"\"\"\n\nimport copy\nimport datetime\nimport logging\nimport sys\n\nimport pytest\n\nimport pytz\n\nfrom stalker import (\n    Client,\n    Department,\n    Group,\n    Project,\n    Repository,\n    Sequence,\n    Status,\n    StatusList,\n    Task,\n    Ticket,\n    Type,\n    User,\n    Vacation,\n    Version,\n)\nfrom stalker.db.session import DBSession\nfrom stalker.models.ticket import FIXED, CANTFIX, INVALID\n\nfrom sqlalchemy.exc import IntegrityError\n\nfrom tests.utils import get_admin_user\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.DEBUG)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_user_db_tests(setup_postgresql_db):\n    \"\"\"Set up tests for the User class with a DB.\"\"\"\n    data = dict()\n\n    # need to have some test object for\n    # a department\n    data[\"test_department1\"] = Department(name=\"Test Department 1\")\n    data[\"test_department2\"] = Department(name=\"Test Department 2\")\n    data[\"test_department3\"] = Department(name=\"Test Department 3\")\n\n    DBSession.add_all(\n        [data[\"test_department1\"], data[\"test_department2\"], data[\"test_department3\"]]\n    )\n\n    # a couple of groups\n    data[\"test_group1\"] = Group(name=\"Test Group 1\")\n    data[\"test_group2\"] = Group(name=\"Test Group 2\")\n    data[\"test_group3\"] = Group(name=\"Test Group 3\")\n\n    DBSession.add_all([data[\"test_group1\"], data[\"test_group2\"], data[\"test_group3\"]])\n    DBSession.commit()\n\n    # a couple of statuses\n    data[\"status_cmpl\"] = Status.query.filter(Status.code == \"CMPL\").first()\n    data[\"status_wip\"] = Status.query.filter(Status.code == \"WIP\").first()\n    data[\"status_rts\"] = Status.query.filter(Status.code == \"RTS\").first()\n    data[\"status_prev\"] = Status.query.filter(Status.code == \"PREV\").first()\n\n    # a repository type\n    data[\"test_repository_type\"] = Type(\n        name=\"Test\",\n        code=\"test\",\n        target_entity_type=\"Repository\",\n    )\n\n    # a repository\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\", code=\"TR\", type=data[\"test_repository_type\"]\n    )\n\n    # a project type\n    data[\"commercial_project_type\"] = Type(\n        name=\"Commercial Project\",\n        code=\"comm\",\n        target_entity_type=\"Project\",\n    )\n\n    # a couple of projects\n    data[\"test_project1\"] = Project(\n        name=\"Test Project 1\",\n        code=\"tp1\",\n        type=data[\"commercial_project_type\"],\n        repository=data[\"test_repository\"],\n    )\n\n    data[\"test_project2\"] = Project(\n        name=\"Test Project 2\",\n        code=\"tp2\",\n        type=data[\"commercial_project_type\"],\n        repository=data[\"test_repository\"],\n    )\n\n    data[\"test_project3\"] = Project(\n        name=\"Test Project 3\",\n        code=\"tp3\",\n        type=data[\"commercial_project_type\"],\n        repository=data[\"test_repository\"],\n    )\n\n    DBSession.add_all(\n        [data[\"test_project1\"], data[\"test_project2\"], data[\"test_project3\"]]\n    )\n    DBSession.commit()\n\n    # a task status list\n    data[\"task_status_list\"] = StatusList.query.filter_by(\n        target_entity_type=\"Task\"\n    ).first()\n\n    data[\"test_lead\"] = User(\n        name=\"lead\", login=\"lead\", email=\"lead@lead.com\", password=\"12345\"\n    )\n\n    # a couple of tasks\n    data[\"test_task1\"] = Task(\n        name=\"Test Task 1\",\n        status_list=data[\"task_status_list\"],\n        project=data[\"test_project1\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    data[\"test_task2\"] = Task(\n        name=\"Test Task 2\",\n        status_list=data[\"task_status_list\"],\n        project=data[\"test_project1\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    data[\"test_task3\"] = Task(\n        name=\"Test Task 3\",\n        status_list=data[\"task_status_list\"],\n        project=data[\"test_project2\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    data[\"test_task4\"] = Task(\n        name=\"Test Task 4\",\n        status_list=data[\"task_status_list\"],\n        project=data[\"test_project3\"],\n        responsible=[data[\"test_lead\"]],\n    )\n\n    DBSession.add_all(\n        [data[\"test_task1\"], data[\"test_task2\"], data[\"test_task3\"], data[\"test_task4\"]]\n    )\n    DBSession.commit()\n\n    # for task1\n    data[\"test_version1\"] = Version(task=data[\"test_task1\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version1\"])\n    DBSession.commit()\n\n    data[\"test_version2\"] = Version(task=data[\"test_task1\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version2\"])\n    DBSession.commit()\n\n    data[\"test_version3\"] = Version(task=data[\"test_task1\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version3\"])\n    DBSession.commit()\n\n    # for task2\n    data[\"test_version4\"] = Version(task=data[\"test_task2\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version4\"])\n    DBSession.commit()\n\n    data[\"test_version5\"] = Version(task=data[\"test_task2\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version5\"])\n    DBSession.commit()\n\n    data[\"test_version6\"] = Version(task=data[\"test_task2\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version6\"])\n    DBSession.commit()\n\n    # for task3\n    data[\"test_version7\"] = Version(task=data[\"test_task3\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version7\"])\n    DBSession.commit()\n\n    data[\"test_version8\"] = Version(task=data[\"test_task3\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version8\"])\n    DBSession.commit()\n\n    data[\"test_version9\"] = Version(task=data[\"test_task3\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version9\"])\n    DBSession.commit()\n\n    # for task4\n    data[\"test_version10\"] = Version(task=data[\"test_task4\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version10\"])\n    DBSession.commit()\n\n    data[\"test_version11\"] = Version(task=data[\"test_task4\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version11\"])\n    DBSession.commit()\n\n    data[\"test_version12\"] = Version(task=data[\"test_task4\"], full_path=\"some/path\")\n    DBSession.add(data[\"test_version12\"])\n    DBSession.commit()\n\n    # *********************************************************************\n    # Tickets\n    # *********************************************************************\n\n    # no need to create status list for tickets because we have a database\n    # setup is running, so it will automatically be linked\n\n    # tickets for version1\n    data[\"test_ticket1\"] = Ticket(\n        project=data[\"test_project1\"],\n        links=[data[\"test_version1\"]],\n    )\n    DBSession.add(data[\"test_ticket1\"])\n    # set it to closed\n    data[\"test_ticket1\"].resolve()\n    DBSession.commit()\n\n    # create a new ticket and leave it open\n    data[\"test_ticket2\"] = Ticket(\n        project=data[\"test_project1\"],\n        links=[data[\"test_version1\"]],\n    )\n    DBSession.add(data[\"test_ticket2\"])\n    DBSession.commit()\n\n    # create a new ticket and close and then reopen it\n    data[\"test_ticket3\"] = Ticket(\n        project=data[\"test_project1\"],\n        links=[data[\"test_version1\"]],\n    )\n    DBSession.add(data[\"test_ticket3\"])\n    data[\"test_ticket3\"].resolve()\n    data[\"test_ticket3\"].reopen()\n    DBSession.commit()\n\n    # *********************************************************************\n    # tickets for version2\n    # create a new ticket and leave it open\n    data[\"test_ticket4\"] = Ticket(\n        project=data[\"test_project1\"],\n        links=[data[\"test_version2\"]],\n    )\n    DBSession.add(data[\"test_ticket4\"])\n    DBSession.commit()\n\n    # create a new Ticket and close it\n    data[\"test_ticket5\"] = Ticket(\n        project=data[\"test_project1\"],\n        links=[data[\"test_version2\"]],\n    )\n    DBSession.add(data[\"test_ticket5\"])\n    data[\"test_ticket5\"].resolve()\n    DBSession.commit()\n\n    # create a new Ticket and close it\n    data[\"test_ticket6\"] = Ticket(\n        project=data[\"test_project1\"],\n        links=[data[\"test_version3\"]],\n    )\n    DBSession.add(data[\"test_ticket6\"])\n    data[\"test_ticket6\"].resolve()\n    DBSession.commit()\n\n    # *********************************************************************\n    # tickets for version3\n    # create a new ticket and close it\n    data[\"test_ticket7\"] = Ticket(\n        project=data[\"test_project1\"],\n        links=[data[\"test_version3\"]],\n    )\n    DBSession.add(data[\"test_ticket7\"])\n    data[\"test_ticket7\"].resolve()\n    DBSession.commit()\n\n    # create a new ticket and close it\n    data[\"test_ticket8\"] = Ticket(\n        project=data[\"test_project1\"],\n        links=[data[\"test_version3\"]],\n    )\n    DBSession.add(data[\"test_ticket8\"])\n    data[\"test_ticket8\"].resolve()\n    DBSession.commit()\n\n    # *********************************************************************\n    # tickets for version4\n    # create a new ticket and close it\n    data[\"test_ticket9\"] = Ticket(\n        project=data[\"test_project1\"],\n        links=[data[\"test_version4\"]],\n    )\n    DBSession.add(data[\"test_ticket9\"])\n    data[\"test_ticket9\"].resolve()\n    DBSession.commit()\n\n    # no tickets for any other version\n    # *********************************************************************\n\n    # a status list for sequence\n    with DBSession.no_autoflush:\n        data[\"sequence_status_list\"] = StatusList.query.filter_by(\n            target_entity_type=\"Sequence\"\n        ).first()\n\n    # a couple of sequences\n    data[\"test_sequence1\"] = Sequence(\n        name=\"Test Seq 1\",\n        code=\"ts1\",\n        project=data[\"test_project1\"],\n        status_list=data[\"sequence_status_list\"],\n    )\n\n    data[\"test_sequence2\"] = Sequence(\n        name=\"Test Seq 2\",\n        code=\"ts2\",\n        project=data[\"test_project1\"],\n        status_list=data[\"sequence_status_list\"],\n    )\n\n    data[\"test_sequence3\"] = Sequence(\n        name=\"Test Seq 3\",\n        code=\"ts3\",\n        project=data[\"test_project1\"],\n        status_list=data[\"sequence_status_list\"],\n    )\n\n    data[\"test_sequence4\"] = Sequence(\n        name=\"Test Seq 4\",\n        code=\"ts4\",\n        project=data[\"test_project1\"],\n        status_list=data[\"sequence_status_list\"],\n    )\n\n    DBSession.add_all(\n        [\n            data[\"test_sequence1\"],\n            data[\"test_sequence2\"],\n            data[\"test_sequence3\"],\n            data[\"test_sequence4\"],\n        ]\n    )\n    DBSession.commit()\n\n    data[\"test_admin\"] = get_admin_user()\n    assert data[\"test_admin\"] is not None\n\n    # create test company\n    data[\"test_company\"] = Client(name=\"Test Company\")\n\n    # create the default values for parameters\n    data[\"kwargs\"] = {\n        \"name\": \"Erkan Ozgur Yilmaz\",\n        \"login\": \"eoyilmaz\",\n        \"description\": \"this is a test user\",\n        \"password\": \"hidden\",\n        \"email\": \"eoyilmaz@fake.com\",\n        \"departments\": [data[\"test_department1\"]],\n        \"groups\": [data[\"test_group1\"], data[\"test_group2\"]],\n        \"created_by\": data[\"test_admin\"],\n        \"updated_by\": data[\"test_admin\"],\n        \"efficiency\": 1.0,\n        \"companies\": [data[\"test_company\"]],\n    }\n\n    # create a proper user object\n    data[\"test_user\"] = User(**data[\"kwargs\"])\n    DBSession.add(data[\"test_user\"])\n    DBSession.commit()\n\n    # just change the kwargs for other tests\n    data[\"kwargs\"][\"name\"] = \"some other name\"\n    data[\"kwargs\"][\"email\"] = \"some@other.email\"\n    return data\n\n\ndef test___auto_name__class_attribute_is_set_to_false():\n    \"\"\"__auto_name__ class attribute is set to False for User class.\"\"\"\n    assert User.__auto_name__ is False\n\n\ndef test_email_argument_accepting_only_string(setup_user_db_tests):\n    \"\"\"email argument accepting only string values.\"\"\"\n    data = setup_user_db_tests\n    # try to create a new user with wrong attribute\n    data[\"kwargs\"][\"email\"] = 1.3\n    with pytest.raises(TypeError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"User.email should be an instance of str, not float: '1.3'\"\n\n\ndef test_email_attribute_accepting_only_string_1(setup_user_db_tests):\n    \"\"\"email attribute accepting only string values.\"\"\"\n    data = setup_user_db_tests\n    # try to assign something else than a string\n    test_value = 1\n\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].email = test_value\n\n    assert str(cm.value) == \"User.email should be an instance of str, not int: '1'\"\n\n\ndef test_email_attribute_accepting_only_string_2(setup_user_db_tests):\n    \"\"\"email attribute accepting only string values.\"\"\"\n    data = setup_user_db_tests\n    # try to assign something else than a string\n    test_value = [\"an email\"]\n\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].email = test_value\n\n    assert str(cm.value) == (\n        \"User.email should be an instance of str, not list: '['an email']'\"\n    )\n\n\ndef test_email_argument_format_1(setup_user_db_tests):\n    \"\"\"Given an email in wrong format will raise a ValueError.\"\"\"\n    data = setup_user_db_tests\n    # any of this values should raise a ValueError\n    data[\"kwargs\"][\"email\"] = \"an email in no format\"\n    with pytest.raises(ValueError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"check the formatting of User.email, there is no @ sign\"\n\n\ndef test_email_argument_format_2(setup_user_db_tests):\n    \"\"\"given an email in wrong format will raise a ValueError.\"\"\"\n    data = setup_user_db_tests\n    # any of this values should raise a ValueError\n    data[\"kwargs\"][\"email\"] = \"an_email_with_no_part2\"\n    with pytest.raises(ValueError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"check the formatting of User.email, there is no @ sign\"\n\n\ndef test_email_argument_format_3(setup_user_db_tests):\n    \"\"\"given an email in wrong format will raise a ValueError.\"\"\"\n    data = setup_user_db_tests\n    # any of this values should raise a ValueError\n    data[\"kwargs\"][\"email\"] = \"@an_email_with_only_part2\"\n    with pytest.raises(ValueError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"check the formatting of User.email, the name part is missing\"\n    )\n\n\ndef test_email_argument_format_4(setup_user_db_tests):\n    \"\"\"given an email in wrong format will raise a ValueError.\"\"\"\n    data = setup_user_db_tests\n    # any of this values should raise a ValueError\n    data[\"kwargs\"][\"email\"] = \"@\"\n    with pytest.raises(ValueError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"check the formatting of User.email, the name part is missing\"\n    )\n\n\ndef test_email_attribute_format_1(setup_user_db_tests):\n    \"\"\"given an email in wrong format will raise a ValueError.\"\"\"\n    data = setup_user_db_tests\n    # any of these email values should raise a ValueError\n    with pytest.raises(ValueError) as cm:\n        data[\"test_user\"].email = \"an email in no format\"\n\n    assert str(cm.value) == \"check the formatting of User.email, there is no @ sign\"\n\n\ndef test_email_attribute_format_2(setup_user_db_tests):\n    \"\"\"given an email in wrong format will raise a ValueError.\"\"\"\n    data = setup_user_db_tests\n    # any of these email values should raise a ValueError\n    with pytest.raises(ValueError) as cm:\n        data[\"test_user\"].email = \"an_email_with_no_part2\"\n\n    assert str(cm.value) == \"check the formatting of User.email, there is no @ sign\"\n\n\ndef test_email_attribute_format_3(setup_user_db_tests):\n    \"\"\"given an email in wrong format will raise a ValueError.\"\"\"\n    data = setup_user_db_tests\n    # any of these email values should raise a ValueError\n    with pytest.raises(ValueError) as cm:\n        data[\"test_user\"].email = \"@an_email_with_only_part2\"\n\n    assert (\n        str(cm.value) == \"check the formatting of User.email, the name part is missing\"\n    )\n\n\ndef test_email_attribute_format_4(setup_user_db_tests):\n    \"\"\"given an email in wrong format will raise a ValueError.\"\"\"\n    data = setup_user_db_tests\n    # any of these email values should raise a ValueError\n    with pytest.raises(ValueError) as cm:\n        data[\"test_user\"].email = \"@\"\n\n    assert (\n        str(cm.value) == \"check the formatting of User.email, the name part is missing\"\n    )\n\n\ndef test_email_attribute_format_5(setup_user_db_tests):\n    \"\"\"given an email in wrong format will raise a ValueError.\"\"\"\n    data = setup_user_db_tests\n    # any of these email values should raise a ValueError\n    with pytest.raises(ValueError) as cm:\n        data[\"test_user\"].email = \"eoyilmaz@\"\n\n    assert (\n        str(cm.value) == \"check the formatting User.email, the domain part is missing\"\n    )\n\n\ndef test_email_attribute_format_6(setup_user_db_tests):\n    \"\"\"given an email in wrong format will raise a ValueError.\"\"\"\n    data = setup_user_db_tests\n    # any of these email values should raise a ValueError\n    with pytest.raises(ValueError) as cm:\n        data[\"test_user\"].email = \"eoyilmaz@some.compony@com\"\n\n    assert (\n        str(cm.value)\n        == \"check the formatting of User.email, there are more than one @ sign\"\n    )\n\n\ndef test_email_argument_should_be_a_unique_value(setup_user_db_tests):\n    \"\"\"email argument should be a unique value.\"\"\"\n    data = setup_user_db_tests\n    # this test should include a database\n    test_email = \"test@email.com\"\n    data[\"kwargs\"][\"login\"] = \"test_user1\"\n    data[\"kwargs\"][\"email\"] = test_email\n    user1 = User(**data[\"kwargs\"])\n    DBSession.add(user1)\n    DBSession.commit()\n\n    data[\"kwargs\"][\"login\"] = \"test_user2\"\n    user2 = User(**data[\"kwargs\"])\n    DBSession.add(user2)\n\n    with pytest.raises(IntegrityError) as cm:\n        DBSession.commit()\n\n    assert (\n        \"(psycopg2.errors.UniqueViolation) duplicate key value \"\n        'violates unique constraint \"Users_email_key\"' in str(cm.value)\n    )\n\n\ndef test_email_attribute_is_working_as_expected(setup_user_db_tests):\n    \"\"\"email attribute works as expected.\"\"\"\n    data = setup_user_db_tests\n    test_email = \"eoyilmaz@somemail.com\"\n    data[\"test_user\"].email = test_email\n    assert data[\"test_user\"].email == test_email\n\n\ndef test_login_argument_conversion_to_strings(setup_user_db_tests):\n    \"\"\"ValueError raised if login converted to string results an empty string.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"login\"] = \"----++==#@#$\"\n    with pytest.raises(ValueError) as cm:\n        User(**data[\"kwargs\"])\n    assert str(cm.value) == \"User.login cannot be an empty string\"\n\n\ndef test_login_argument_for_empty_string(setup_user_db_tests):\n    \"\"\"ValueError raised if trying to assign an empty string to login argument.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"login\"] = \"\"\n    with pytest.raises(ValueError) as cm:\n        User(**data[\"kwargs\"])\n    assert str(cm.value) == \"User.login cannot be an empty string\"\n\n\ndef test_login_attribute_for_empty_string(setup_user_db_tests):\n    \"\"\"ValueError raised if trying to assign an empty string to login attribute.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_user\"].login = \"\"\n    assert str(cm.value) == \"User.login cannot be an empty string\"\n\n\ndef test_login_argument_is_skipped(setup_user_db_tests):\n    \"\"\"TypeError raised if the login argument is skipped.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"].pop(\"login\")\n    with pytest.raises(TypeError) as cm:\n        User(**data[\"kwargs\"])\n    assert str(cm.value) == \"User.login cannot be None\"\n\n\ndef test_login_argument_is_none(setup_user_db_tests):\n    \"\"\"TypeError raised if trying to assign None to login argument.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"login\"] = None\n    with pytest.raises(TypeError) as cm:\n        User(**data[\"kwargs\"])\n    assert str(cm.value) == \"User.login cannot be None\"\n\n\ndef test_login_attribute_is_none(setup_user_db_tests):\n    \"\"\"TypeError raised if trying to assign None to login attribute.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].login = None\n\n    assert str(cm.value) == \"User.login cannot be None\"\n\n\n@pytest.mark.parametrize(\n    \"test_value,expected\",\n    [\n        (\"e. ozgur\", \"eozgur\"),\n        (\"erkan\", \"erkan\"),\n        (\"Ozgur\", \"ozgur\"),\n        (\"Erkan ozgur\", \"erkanozgur\"),\n        (\"eRKAN\", \"erkan\"),\n        (\"eRkaN\", \"erkan\"),\n        (\" eRkAn\", \"erkan\"),\n        (\" eRkan ozGur\", \"erkanozgur\"),\n        (\"213 e.ozgur\", \"eozgur\"),\n    ],\n)\ndef test_login_argument_formatted_correctly(test_value, expected, setup_user_db_tests):\n    \"\"\"login argument formatted correctly.\"\"\"\n    data = setup_user_db_tests\n    # set the input and expect the expected output\n    data[\"kwargs\"][\"login\"] = test_value\n    test_user = User(**data[\"kwargs\"])\n    assert expected == test_user.login\n\n\n@pytest.mark.parametrize(\n    \"test_value,expected\",\n    [\n        (\"e. ozgur\", \"eozgur\"),\n        (\"erkan\", \"erkan\"),\n        (\"Ozgur\", \"ozgur\"),\n        (\"Erkan ozgur\", \"erkanozgur\"),\n        (\"eRKAN\", \"erkan\"),\n        (\"eRkaN\", \"erkan\"),\n        (\" eRkAn\", \"erkan\"),\n        (\" eRkan ozGur\", \"erkanozgur\"),\n    ],\n)\ndef test_login_attribute_formatted_correctly(test_value, expected, setup_user_db_tests):\n    \"\"\"login attribute formatted correctly.\"\"\"\n    data = setup_user_db_tests\n    # set the input and expect the expected output\n    data[\"test_user\"].login = test_value\n    assert expected == data[\"test_user\"].login\n\n\ndef test_login_argument_should_be_a_unique_value(setup_user_db_tests):\n    \"\"\"login argument should be a unique value.\"\"\"\n    data = setup_user_db_tests\n    # this test should include a database\n    test_login = \"test_user1\"\n    data[\"kwargs\"][\"login\"] = test_login\n    data[\"kwargs\"][\"email\"] = \"test1@email.com\"\n\n    user1 = User(**data[\"kwargs\"])\n    DBSession.add(user1)\n    DBSession.commit()\n\n    data[\"kwargs\"][\"email\"] = \"test2@email.com\"\n    user2 = User(**data[\"kwargs\"])\n    DBSession.add(user2)\n    with pytest.raises(IntegrityError) as cm:\n        DBSession.commit()\n\n    assert (\n        \"(psycopg2.errors.UniqueViolation) duplicate key value \"\n        'violates unique constraint \"Users_login_key\"' in str(cm.value)\n    )\n\n\ndef test_login_argument_is_working_as_expected(setup_user_db_tests):\n    \"\"\"login argument is working as expected.\"\"\"\n    data = setup_user_db_tests\n    assert data[\"test_user\"].login == data[\"kwargs\"][\"login\"]\n\n\ndef test_login_attribute_is_working_as_expected(setup_user_db_tests):\n    \"\"\"login attribute is working as expected.\"\"\"\n    data = setup_user_db_tests\n    test_value = \"newlogin\"\n    data[\"test_user\"].login = test_value\n    assert data[\"test_user\"].login == test_value\n\n\ndef test_last_login_attribute_none(setup_user_db_tests):\n    \"\"\"nothing happens if the last login attribute is set to None.\"\"\"\n    data = setup_user_db_tests\n    # nothing should happen\n    data[\"test_user\"].last_login = None\n\n\ndef test_departments_argument_is_skipped(setup_user_db_tests):\n    \"\"\"User can be created without a Department instance.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"].pop(\"departments\")\n\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.departments == []\n\n\ndef test_departments_argument_is_none(setup_user_db_tests):\n    \"\"\"User can be created with the departments argument value is to None.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"departments\"] = None\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.departments == []\n\n\ndef test_departments_attribute_is_set_none(setup_user_db_tests):\n    \"\"\"TypeError raised if the User's departments attribute set to None.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].departments = None\n    assert str(cm.value) == \"'NoneType' object is not iterable\"\n\n\ndef test_departments_argument_is_an_empty_list(setup_user_db_tests):\n    \"\"\"User can be created with the departments argument is an empty list.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"departments\"] = []\n    User(**data[\"kwargs\"])\n\n\ndef test_departments_attribute_is_an_empty_list(setup_user_db_tests):\n    \"\"\"departments attribute can be set to an empty list.\"\"\"\n    data = setup_user_db_tests\n    data[\"test_user\"].departments = []\n    assert data[\"test_user\"].departments == []\n\n\ndef test_departments_argument_only_accepts_list_of_department_objects(\n    setup_user_db_tests,\n):\n    \"\"\"TypeError raised if departments arg is not a Department instance.\"\"\"\n    data = setup_user_db_tests\n    # try to assign something other than a department object\n    test_values = [\"A department\", 1, 1.0, [\"a department\"], {\"a\": \"department\"}]\n    data[\"kwargs\"][\"departments\"] = test_values\n    with pytest.raises(TypeError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"DepartmentUser.department should be a \"\n        \"stalker.models.department.Department instance, not str: 'A department'\"\n    )\n\n\ndef test_departments_attribute_only_accepts_department_objects(setup_user_db_tests):\n    \"\"\"TypeError raised if department attribute is not a Department instance.\"\"\"\n    data = setup_user_db_tests\n    # try to assign something other than a department\n    test_value = \"a department\"\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].departments = test_value\n\n    assert str(cm.value) == (\n        \"DepartmentUser.department should be a \"\n        \"stalker.models.department.Department instance, not str: 'a'\"\n    )\n\n\ndef test_departments_attribute_works_as_expected(setup_user_db_tests):\n    \"\"\"departments attribute works as expected.\"\"\"\n    data = setup_user_db_tests\n    # try to set and get the same value back\n    data[\"test_user\"].departments = [data[\"test_department2\"]]\n    assert sorted(data[\"test_user\"].departments, key=lambda x: x.name) == sorted(\n        [data[\"test_department2\"]], key=lambda x: x.name\n    )\n\n\ndef test_departments_attribute_supports_appending(setup_user_db_tests):\n    \"\"\"departments attribute supports appending.\"\"\"\n    data = setup_user_db_tests\n    data[\"test_user\"].departments = []\n    data[\"test_user\"].departments.append(data[\"test_department1\"])\n    data[\"test_user\"].departments.append(data[\"test_department2\"])\n    assert sorted(data[\"test_user\"].departments, key=lambda x: x.name) == sorted(\n        [data[\"test_department1\"], data[\"test_department2\"]], key=lambda x: x.name\n    )\n\n\ndef test_password_arg_is_none(setup_user_db_tests):\n    \"\"\"TypeError raised if password arg value is None.\"\"\"\n    data = setup_user_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"password\"] = None\n    with pytest.raises(TypeError) as cm:\n        User(**kwargs)\n    assert str(cm.value) == \"User.password cannot be None\"\n\n\ndef test_password_argument_is_an_empty_string(setup_user_db_tests):\n    \"\"\"ValueError raised the password argument is an empty string.\"\"\"\n    data = setup_user_db_tests\n    kwargs = copy.copy(data[\"kwargs\"])\n    kwargs[\"password\"] = \"\"\n    with pytest.raises(ValueError) as cm:\n        User(**kwargs)\n    assert str(cm.value) == \"User.password cannot be an empty string\"\n\n\ndef test_password_attribute_being_none(setup_user_db_tests):\n    \"\"\"TypeError raised if tyring to assign None to the password attribute.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].password = None\n    assert str(cm.value) == \"User.password cannot be None\"\n\n\ndef test_password_attribute_works_as_expected(setup_user_db_tests):\n    \"\"\"password attribute works as expected.\"\"\"\n    data = setup_user_db_tests\n    test_password = \"a new test password\"\n    data[\"test_user\"].password = test_password\n    assert data[\"test_user\"].password != test_password\n\n\ndef test_password_argument_being_scrambled(setup_user_db_tests):\n    \"\"\"password is scrambled if trying to store it.\"\"\"\n    data = setup_user_db_tests\n    test_password = \"a new test password\"\n    data[\"kwargs\"][\"password\"] = test_password\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.password != test_password\n\n\ndef test_password_attribute_being_scrambled(setup_user_db_tests):\n    \"\"\"password is scrambled if trying to store it.\"\"\"\n    data = setup_user_db_tests\n    test_password = \"a new test password\"\n    data[\"test_user\"].password = test_password\n\n    # test if they are not the same anymore\n    assert data[\"test_user\"].password != test_password\n\n\ndef test_check_password_works_as_expected(setup_user_db_tests):\n    \"\"\"check_password method works as expected.\"\"\"\n    data = setup_user_db_tests\n    test_password = \"a new test password\"\n    data[\"test_user\"].password = test_password\n\n    # check if it is scrambled\n    assert data[\"test_user\"].password != test_password\n\n    # check if check_password returns True\n    assert data[\"test_user\"].check_password(test_password) is True\n\n    # check if check_password returns False\n    assert data[\"test_user\"].check_password(\"wrong pass\") is False\n\n\ndef test_groups_argument_for_none(setup_user_db_tests):\n    \"\"\"groups attribute an empty list if the groups argument is None.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"groups\"] = None\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.groups == []\n\n\ndef test_groups_attribute_for_none(setup_user_db_tests):\n    \"\"\"TypeError raised if groups attribute is set to None.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].groups = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_groups_argument_accepts_only_group_instances(setup_user_db_tests):\n    \"\"\"TypeError raised if group arg is not a Group instance.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"groups\"] = \"a_group\"\n    with pytest.raises(TypeError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_groups_attribute_accepts_only_group_instances(setup_user_db_tests):\n    \"\"\"TypeError raised if group attr is not a Group instance.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].groups = \"a_group\"\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_groups_attribute_works_as_expected(setup_user_db_tests):\n    \"\"\"groups attribute works as expected.\"\"\"\n    data = setup_user_db_tests\n    test_pg = [data[\"test_group3\"]]\n    data[\"test_user\"].groups = test_pg\n    assert data[\"test_user\"].groups == test_pg\n\n\ndef test_groups_attribute_elements_accepts_group_only_1(setup_user_db_tests):\n    \"\"\"TypeError raised if groups list appended a non Group object.\"\"\"\n    data = setup_user_db_tests\n    # append\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].groups.append(0)\n    assert str(cm.value) == (\n        \"Any group in User.groups should be an instance of \"\n        \"stalker.models.auth.Group, not int: '0'\"\n    )\n\n\ndef test_groups_attribute_elements_accepts_group_only_2(setup_user_db_tests):\n    \"\"\"TypeError raised if groups list set an item other than a Group instance.\"\"\"\n    data = setup_user_db_tests\n    # __setitem__\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].groups[0] = 0\n    assert str(cm.value) == (\n        \"Any group in User.groups should be an instance of \"\n        \"stalker.models.auth.Group, not int: '0'\"\n    )\n\n\ndef test_projects_attribute_is_none(setup_user_db_tests):\n    \"\"\"TypeError raised if the projects attribute is set to None.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].projects = None\n    assert str(cm.value) == \"'NoneType' object is not iterable\"\n\n\ndef test_projects_attribute_is_set_to_a_value_which_is_not_a_list(setup_user_db_tests):\n    \"\"\"projects attribute is accepting lists only.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].projects = \"not a list\"\n\n    assert str(cm.value) == (\n        \"ProjectUser.project should be a stalker.models.project.Project \"\n        \"instance, not str: 'n'\"\n    )\n\n\ndef test_projects_attribute_is_set_to_list_of_other_objects_than_project_instances(\n    setup_user_db_tests,\n):\n    \"\"\"TypeError raised if the projects attr is not all Project instances.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].projects = [\"not\", \"a\", \"list\", \"of\", \"projects\", 32]\n\n    assert (\n        str(cm.value)\n        == \"ProjectUser.project should be a stalker.models.project.Project \"\n        \"instance, not str: 'not'\"\n    )\n\n\ndef test_projects_attribute_is_working_as_expected(setup_user_db_tests):\n    \"\"\"projects attribute is working as expected.\"\"\"\n    data = setup_user_db_tests\n    data[\"test_user\"].rate = 102.0\n    test_list = [data[\"test_project1\"], data[\"test_project2\"]]\n    data[\"test_user\"].projects = test_list\n    assert sorted(test_list, key=lambda x: x.name) == sorted(\n        data[\"test_user\"].projects, key=lambda x: x.name\n    )\n    data[\"test_user\"].projects.append(data[\"test_project3\"])\n    assert data[\"test_project3\"] in data[\"test_user\"].projects\n    # also check the back ref\n    assert data[\"test_user\"] in data[\"test_project1\"].users\n    assert data[\"test_user\"] in data[\"test_project2\"].users\n    assert data[\"test_user\"] in data[\"test_project3\"].users\n\n\ndef test_tasks_attribute_none(setup_user_db_tests):\n    \"\"\"TypeError raised if the tasks attribute is set to None.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].tasks = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_tasks_attribute_accepts_only_list_of_task_objects(setup_user_db_tests):\n    \"\"\"TypeError raised if tasks arg is not all Task instances.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].tasks = \"aTask1\"\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_tasks_attribute_accepts_an_empty_list(setup_user_db_tests):\n    \"\"\"nothing happens if trying to assign an empty list to tasks attribute.\"\"\"\n    data = setup_user_db_tests\n    # this should work without any error\n    data[\"test_user\"].tasks = []\n\n\ndef test_tasks_attribute_works_as_expected(setup_user_db_tests):\n    \"\"\"tasks attribute is working as expected.\"\"\"\n    data = setup_user_db_tests\n    tasks = [\n        data[\"test_task1\"],\n        data[\"test_task2\"],\n        data[\"test_task3\"],\n        data[\"test_task4\"],\n    ]\n    data[\"test_user\"].tasks = tasks\n    assert data[\"test_user\"].tasks == tasks\n\n\ndef test_tasks_attribute_elements_accepts_tasks_only(setup_user_db_tests):\n    \"\"\"TypeError raised if tasks is not all Task instances.\"\"\"\n    data = setup_user_db_tests\n    # append\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].tasks.append(0)\n\n    assert str(cm.value) == (\n        \"Any element in User.tasks should be an instance of \"\n        \"stalker.models.task.Task, not int: '0'\"\n    )\n\n\ndef test_equality_operator(setup_user_db_tests):\n    \"\"\"equality of two users.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"].update(\n        {\n            \"name\": \"Generic User\",\n            \"description\": \"this is a different user\",\n            \"login\": \"guser\",\n            \"email\": \"generic.user@generic.com\",\n            \"password\": \"verysecret\",\n        }\n    )\n    new_user = User(**data[\"kwargs\"])\n    assert not data[\"test_user\"] == new_user\n\n\ndef test_inequality_operator(setup_user_db_tests):\n    \"\"\"inequality of two users.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"].update(\n        {\n            \"name\": \"Generic User\",\n            \"description\": \"this is a different user\",\n            \"login\": \"guser\",\n            \"email\": \"generic.user@generic.com\",\n            \"password\": \"verysecret\",\n        }\n    )\n    new_user = User(**data[\"kwargs\"])\n    assert data[\"test_user\"] != new_user\n\n\ndef test___repr__(setup_user_db_tests):\n    \"\"\"representation.\"\"\"\n    data = setup_user_db_tests\n    assert data[\"test_user\"].__repr__() == \"<{} ('{}') (User)>\".format(\n        data[\"test_user\"].name, data[\"test_user\"].login\n    )\n\n\ndef test_tickets_attribute_is_an_empty_list_by_default(setup_user_db_tests):\n    \"\"\"User.tickets is an empty list by default.\"\"\"\n    data = setup_user_db_tests\n    assert data[\"test_user\"].tickets == []\n\n\ndef test_open_tickets_attribute_is_an_empty_list_by_default(setup_user_db_tests):\n    \"\"\"User.open_tickets is an empty list by default.\"\"\"\n    data = setup_user_db_tests\n    assert data[\"test_user\"].open_tickets == []\n\n\ndef test_tickets_attribute_is_read_only(setup_user_db_tests):\n    \"\"\"User.tickets attribute is a read only attribute.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_user\"].tickets = []\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'tickets'\",\n    }.get(sys.version_info.minor, \"property 'tickets' of 'User' object has no setter\")\n\n    assert str(cm.value) == error_message\n\n\ndef test_open_tickets_attribute_is_read_only(setup_user_db_tests):\n    \"\"\"User.open_tickets attribute is a read only attribute.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_user\"].open_tickets = []\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'open_tickets'\",\n    }.get(\n        sys.version_info.minor, \"property 'open_tickets' of 'User' object has no setter\"\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_tickets_attribute_returns_all_tickets_owned_by_this_user(setup_user_db_tests):\n    \"\"\"User.tickets returns all the tickets owned by this user.\"\"\"\n    data = setup_user_db_tests\n    assert len(data[\"test_user\"].tasks) == 0\n    # there should be no tickets assigned to this user\n    assert data[\"test_user\"].tickets == []\n\n    # be careful not all of these are open tickets\n    data[\"test_ticket1\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket2\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket3\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket4\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket5\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket6\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket7\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket8\"].reassign(data[\"test_user\"], data[\"test_user\"])\n\n    # now we should have some tickets\n    assert len(data[\"test_user\"].tickets) > 0\n\n    # now check for exact items\n    assert sorted(data[\"test_user\"].tickets, key=lambda x: x.name) == sorted(\n        [data[\"test_ticket2\"], data[\"test_ticket3\"], data[\"test_ticket4\"]],\n        key=lambda x: x.name,\n    )\n\n\ndef test_open_tickets_attribute_returns_all_open_tickets_owned_by_this_user(\n    setup_user_db_tests,\n):\n    \"\"\"User.open_tickets returns all the open tickets owned by this user.\"\"\"\n    data = setup_user_db_tests\n    assert len(data[\"test_user\"].tasks) == 0\n\n    # there should be no tickets assigned to this user\n    assert data[\"test_user\"].open_tickets == []\n\n    # assign the user to some tickets\n    data[\"test_ticket1\"].reopen(data[\"test_user\"])\n    data[\"test_ticket2\"].reopen(data[\"test_user\"])\n    data[\"test_ticket3\"].reopen(data[\"test_user\"])\n    data[\"test_ticket4\"].reopen(data[\"test_user\"])\n    data[\"test_ticket5\"].reopen(data[\"test_user\"])\n    data[\"test_ticket6\"].reopen(data[\"test_user\"])\n    data[\"test_ticket7\"].reopen(data[\"test_user\"])\n    data[\"test_ticket8\"].reopen(data[\"test_user\"])\n\n    # be careful not all of these are open tickets\n    data[\"test_ticket1\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket2\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket3\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket4\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket5\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket6\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket7\"].reassign(data[\"test_user\"], data[\"test_user\"])\n    data[\"test_ticket8\"].reassign(data[\"test_user\"], data[\"test_user\"])\n\n    # now we should have some open tickets\n    assert len(data[\"test_user\"].open_tickets) > 0\n\n    # now check for exact items\n    assert sorted(data[\"test_user\"].open_tickets, key=lambda x: x.name) == sorted(\n        [\n            data[\"test_ticket1\"],\n            data[\"test_ticket2\"],\n            data[\"test_ticket3\"],\n            data[\"test_ticket4\"],\n            data[\"test_ticket5\"],\n            data[\"test_ticket6\"],\n            data[\"test_ticket7\"],\n            data[\"test_ticket8\"],\n        ],\n        key=lambda x: x.name,\n    )\n\n    # close a couple of them\n\n    data[\"test_ticket1\"].resolve(data[\"test_user\"], FIXED)\n    data[\"test_ticket2\"].resolve(data[\"test_user\"], INVALID)\n    data[\"test_ticket3\"].resolve(data[\"test_user\"], CANTFIX)\n\n    # new check again\n    assert sorted(data[\"test_user\"].open_tickets, key=lambda x: x.name) == sorted(\n        [\n            data[\"test_ticket4\"],\n            data[\"test_ticket5\"],\n            data[\"test_ticket6\"],\n            data[\"test_ticket7\"],\n            data[\"test_ticket8\"],\n        ],\n        key=lambda x: x.name,\n    )\n\n\ndef test_tjp_id_is_working_as_expected(setup_user_db_tests):\n    \"\"\"tjp_id is working as expected.\"\"\"\n    data = setup_user_db_tests\n    assert data[\"test_user\"].tjp_id == \"User_{}\".format(data[\"test_user\"].id)\n\n\ndef test_to_tjp_is_working_as_expected(setup_user_db_tests):\n    \"\"\"to_tjp property is working as expected.\"\"\"\n    data = setup_user_db_tests\n    expected_tjp = 'resource User_{} \"User_{}\" {{\\n    efficiency 1.0\\n}}'.format(\n        data[\"test_user\"].id, data[\"test_user\"].id\n    )\n    assert data[\"test_user\"].to_tjp == expected_tjp\n\n\ndef test_to_tjp_is_working_as_expected_for_a_user_with_vacations(setup_user_db_tests):\n    \"\"\"to_tjp property is working as expected for a user with vacations.\"\"\"\n    data = setup_user_db_tests\n    personal_vacation = Type(\n        name=\"Personal\", code=\"PERS\", target_entity_type=\"Vacation\"\n    )\n\n    vac1 = Vacation(\n        user=data[\"test_user\"],\n        type=personal_vacation,\n        start=datetime.datetime(2013, 6, 7, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 21, 0, 0, tzinfo=pytz.utc),\n    )\n    DBSession.add(vac1)\n\n    vac2 = Vacation(\n        user=data[\"test_user\"],\n        type=personal_vacation,\n        start=datetime.datetime(2013, 7, 1, 0, 0, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 7, 15, 0, 0, tzinfo=pytz.utc),\n    )\n    DBSession.add(vac2)\n\n    expected_tjp = \"\"\"resource User_{} \"User_{}\" {{\n    efficiency 1.0\n    vacation 2013-06-07-00:00:00 - 2013-06-21-00:00:00\n    vacation 2013-07-01-00:00:00 - 2013-07-15-00:00:00\n}}\"\"\".format(\n        data[\"test_user\"].id,\n        data[\"test_user\"].id,\n    )\n\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print('---------------')\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(data[\"test_user\"].to_tjp)\n\n    assert data[\"test_user\"].to_tjp == expected_tjp\n\n\ndef test_vacations_attribute_is_set_to_none(setup_user_db_tests):\n    \"\"\"TypeError raised if the vacations attribute is set to None.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].vacations = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_vacations_attribute_is_not_a_list(setup_user_db_tests):\n    \"\"\"TypeError raised if the vacations attr is set to a value other than a list.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].vacations = \"not a list of Vacation instances\"\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_vacations_attribute_is_not_a_list_of_vacation_instances(setup_user_db_tests):\n    \"\"\"TypeError raised if the vacations attr is not a list of all Vacation objects.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].vacations = [\"list of\", \"other\", \"instances\", 1]\n\n    assert str(cm.value) == (\n        \"All of the elements in User.vacations should be a \"\n        \"stalker.models.studio.Vacation instance, not str: 'list of'\"\n    )\n\n\ndef test_vacations_attribute_is_working_as_expected(setup_user_db_tests):\n    \"\"\"vacations attribute is working as expected.\"\"\"\n    data = setup_user_db_tests\n    some_other_user = User(\n        name=\"Some Other User\",\n        login=\"sou\",\n        email=\"some@other.user.com\",\n        password=\"my password\",\n    )\n\n    personal_vac_type = Type(\n        name=\"Personal Vacation\", code=\"PERS\", target_entity_type=\"Vacation\"\n    )\n\n    vac1 = Vacation(\n        user=some_other_user,\n        type=personal_vac_type,\n        start=datetime.datetime(2013, 6, 7, tzinfo=pytz.utc),\n        end=datetime.datetime(2013, 6, 10, tzinfo=pytz.utc),\n    )\n\n    assert vac1 not in data[\"test_user\"].vacations\n    data[\"test_user\"].vacations.append(vac1)\n    assert vac1 in data[\"test_user\"].vacations\n\n\ndef test_efficiency_argument_skipped(setup_user_db_tests):\n    \"\"\"efficiency attribute value 1.0 if the efficiency argument is skipped.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"].pop(\"efficiency\")\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.efficiency == 1.0\n\n\ndef test_efficiency_argument_is_none(setup_user_db_tests):\n    \"\"\"efficiency attribute value 1.0 if the efficiency argument is None.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"efficiency\"] = None\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.efficiency == 1.0\n\n\ndef test_efficiency_attribute_is_set_to_none(setup_user_db_tests):\n    \"\"\"efficiency attribute value 1.0 if it is set to None.\"\"\"\n    data = setup_user_db_tests\n    data[\"test_user\"].efficiency = 4.0\n    data[\"test_user\"].efficiency = None\n    assert data[\"test_user\"].efficiency == 1.0\n\n\ndef test_efficiency_argument_is_not_a_float_or_integer(setup_user_db_tests):\n    \"\"\"TypeError raised if the efficiency argument is not a float or integer.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"efficiency\"] = \"not a float or integer\"\n    with pytest.raises(TypeError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"User.efficiency should be a float number greater or equal to 0.0, \"\n        \"not str: 'not a float or integer'\"\n    )\n\n\ndef test_efficiency_attribute_is_not_a_float_or_integer(setup_user_db_tests):\n    \"\"\"TypeError raised if the efficiency attr is not a float or int.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].efficiency = \"not a float or integer\"\n\n    assert str(cm.value) == (\n        \"User.efficiency should be a float number greater or equal to 0.0, \"\n        \"not str: 'not a float or integer'\"\n    )\n\n\ndef test_efficiency_argument_is_a_negative_float_or_integer(setup_user_db_tests):\n    \"\"\"ValueError raised if the efficiency argument is a negative float or integer.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"efficiency\"] = -1\n    with pytest.raises(ValueError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value)\n        == \"User.efficiency should be a float number greater or equal to 0.0, not -1\"\n    )\n\n\ndef test_efficiency_attribute_is_a_negative_float_or_integer(setup_user_db_tests):\n    \"\"\"ValueError raised if the efficiency attr is set to a negative float or int.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_user\"].efficiency = -2.0\n\n    assert (\n        str(cm.value)\n        == \"User.efficiency should be a float number greater or equal to 0.0, not -2.0\"\n    )\n\n\ndef test_efficiency_argument_is_working_as_expected(setup_user_db_tests):\n    \"\"\"efficiency argument value is correctly passed to the efficiency attribute.\"\"\"\n    data = setup_user_db_tests\n    # integer value\n    data[\"kwargs\"][\"efficiency\"] = 2\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.efficiency == 2.0\n\n    # float value\n    data[\"kwargs\"][\"efficiency\"] = 2.3\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.efficiency == 2.3\n\n\ndef test_efficiency_attribute_is_working_as_expected(setup_user_db_tests):\n    \"\"\"efficiency attribute value can correctly be changed\"\"\"\n    data = setup_user_db_tests\n    # integer\n    assert data[\"test_user\"].efficiency != 2\n\n    data[\"test_user\"].efficiency = 2\n    assert data[\"test_user\"].efficiency == 2.0\n\n    # float\n    assert data[\"test_user\"].efficiency != 2.3\n\n    data[\"test_user\"].efficiency = 2.3\n    assert data[\"test_user\"].efficiency == 2.3\n\n\ndef test_companies_argument_is_skipped(setup_user_db_tests):\n    \"\"\"companies attribute set to an empty list if the company argument is skipped.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"].pop(\"companies\")\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.companies == []\n\n\ndef test_companies_argument_is_none(setup_user_db_tests):\n    \"\"\"companies argument is set to None the companies attribute an empty list.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"companies\"] = None\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.companies == []\n\n\ndef test_companies_attribute_is_set_to_none(setup_user_db_tests):\n    \"\"\"the companies attribute an empty list if it is set to None.\"\"\"\n    data = setup_user_db_tests\n    assert data[\"test_user\"].companies is not None\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].companies = None\n    assert str(cm.value) == \"'NoneType' object is not iterable\"\n\n\ndef test_companies_argument_is_not_a_list(setup_user_db_tests):\n    \"\"\"TypeError raised if the companies argument is not a list.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"companies\"] = \"not a list of clients\"\n    with pytest.raises(TypeError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"ClientUser.client should be instance of stalker.models.client.Client, \"\n        \"not str: 'n'\"\n    )\n\n\ndef test_companies_argument_is_not_a_list_of_client_instances(setup_user_db_tests):\n    \"\"\"TypeError raised if the companies argument is not a list of Client instances.\"\"\"\n    data = setup_user_db_tests\n    test_value = [1, 1.2, \"a user\", [\"a\", \"user\"], {\"a\": \"user\"}]\n    data[\"kwargs\"][\"companies\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"ClientUser.client should be instance of \"\n        \"stalker.models.client.Client, not int: '1'\"\n    )\n\n\ndef test_companies_attribute_is_set_to_a_value_other_than_a_list_of_client_instances(\n    setup_user_db_tests,\n):\n    \"\"\"TypeError raised if the companies attr is not list of all Client instances.\"\"\"\n    data = setup_user_db_tests\n    test_value = [1, 1.2, \"a user\", [\"a\", \"user\"], {\"a\": \"user\"}]\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].companies = test_value\n\n    assert str(cm.value) == (\n        \"ClientUser.client should be instance of stalker.models.client.Client, \"\n        \"not int: '1'\"\n    )\n\n\ndef test_companies_attribute_is_working_as_expected(setup_user_db_tests):\n    \"\"\"from issue #27.\"\"\"\n    new_companies = []\n    c1 = Client(name=\"Company X\")\n    c2 = Client(name=\"Company Y\")\n    c3 = Client(name=\"Company Z\")\n    DBSession.add_all([c1, c2, c3])\n    DBSession.commit()\n\n    c1 = Client.query.filter_by(name=\"Company X\").first()\n    c2 = Client.query.filter_by(name=\"Company Y\").first()\n    c3 = Client.query.filter_by(name=\"Company Z\").first()\n\n    user = User(\n        name=\"test_user\",\n        password=\"1234\",\n        email=\"a@a.com\",\n        login=\"test_user\",\n        clients=[c3],\n    )\n    DBSession.commit()\n\n    new_companies.append(c1)\n    new_companies.append(c2)\n    user.companies = new_companies\n    DBSession.commit()\n\n    assert c1 in user.companies\n    assert c2 in user.companies\n    assert c3 not in user.companies\n\n\ndef test_companies_attribute_is_working_as_expected_2(setup_user_db_tests):\n    \"\"\"from issue #27.\"\"\"\n    c1 = Client(name=\"Company X\")\n    c2 = Client(name=\"Company Y\")\n    c3 = Client(name=\"Company Z\")\n\n    user = User(\n        name=\"Fredrik\",\n        login=\"fredrik\",\n        email=\"f@f.f\",\n        password=\"pass\",\n        companies=[c1, c2, c3],\n    )\n\n    DBSession.add(user)\n\n    assert c1 in DBSession\n    assert c2 in DBSession\n    assert c3 in DBSession\n\n    DBSession.commit()\n\n    c1 = Client(name=\"New Company X\")\n    c2 = Client(name=\"New Company Y\")\n    c3 = Client(name=\"New Company Z\")\n\n    DBSession.add_all([c1, c2, c3])\n    DBSession.commit()\n\n    user = User.query.filter_by(name=\"Fredrik\").first()\n    user.companies = [c1, c2, c3]\n\n    DBSession.commit()\n\n\ndef test_watching_attribute_is_a_list_of_other_values_than_task(setup_user_db_tests):\n    \"\"\"TypeError raised if the watching attr not a list of all Task instances.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].watching = [\"not\", 1, \"list of tasks\"]\n\n    assert str(cm.value) == (\n        \"Any element in User.watching should be an instance of \"\n        \"stalker.models.task.Task, not str: 'not'\"\n    )\n\n\ndef test_watching_attribute_is_working_as_expected(setup_user_db_tests):\n    \"\"\"watching attribute is working as expected.\"\"\"\n    data = setup_user_db_tests\n    test_value = [data[\"test_task1\"], data[\"test_task2\"]]\n    assert data[\"test_user\"].watching == []\n    data[\"test_user\"].watching = test_value\n    assert sorted(test_value, key=lambda x: x.name) == sorted(\n        data[\"test_user\"].watching, key=lambda x: x.name\n    )\n\n\ndef test_rate_argument_is_skipped(setup_user_db_tests):\n    \"\"\"rate attribute 0 if the rate argument is skipped.\"\"\"\n    data = setup_user_db_tests\n    if \"rate\" in data[\"kwargs\"]:\n        data[\"kwargs\"].pop(\"rate\")\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.rate == 0\n\n\ndef test_rate_argument_is_none(setup_user_db_tests):\n    \"\"\"rate attribute 0 if the rate argument is None.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"rate\"] = None\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.rate == 0\n\n\ndef test_rate_attribute_is_set_to_none(setup_user_db_tests):\n    \"\"\"rate set to 0 if it is set to None.\"\"\"\n    data = setup_user_db_tests\n    assert data[\"test_user\"].rate is not None\n\n    data[\"test_user\"].rate = None\n    assert data[\"test_user\"].rate == 0\n\n\ndef test_rate_argument_is_not_a_float_or_integer_value(setup_user_db_tests):\n    \"\"\"TypeError raised if the rate argument is not an integer or float value.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"rate\"] = \"some string\"\n    with pytest.raises(TypeError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"User.rate should be a float number greater or equal to 0.0, \"\n        \"not str: 'some string'\"\n    )\n\n\ndef test_rate_attribute_is_not_a_float_or_integer_value(setup_user_db_tests):\n    \"\"\"TypeError raised if the rate attr is not an int or float.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_user\"].rate = \"some string\"\n\n    assert str(cm.value) == (\n        \"User.rate should be a float number greater or equal to 0.0, \"\n        \"not str: 'some string'\"\n    )\n\n\ndef test_rate_argument_is_a_negative_number(setup_user_db_tests):\n    \"\"\"ValueError raised if the rate argument is a negative value.\"\"\"\n    data = setup_user_db_tests\n    data[\"kwargs\"][\"rate\"] = -1\n    with pytest.raises(ValueError) as cm:\n        User(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value)\n        == \"User.rate should be a float number greater or equal to 0.0, not -1\"\n    )\n\n\ndef test_rate_attribute_is_set_to_a_negative_number(setup_user_db_tests):\n    \"\"\"ValueError raised if the rate attribute is set to a negative number.\"\"\"\n    data = setup_user_db_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_user\"].rate = -1\n\n    assert (\n        str(cm.value)\n        == \"User.rate should be a float number greater or equal to 0.0, not -1\"\n    )\n\n\ndef test_rate_argument_is_working_as_expected(setup_user_db_tests):\n    \"\"\"rate argument is working as expected.\"\"\"\n    data = setup_user_db_tests\n    test_value = 102.3\n    data[\"kwargs\"][\"rate\"] = test_value\n    new_user = User(**data[\"kwargs\"])\n    assert new_user.rate == test_value\n\n\ndef test_rate_attribute_is_working_as_expected(setup_user_db_tests):\n    \"\"\"rate attribute is working as expected.\"\"\"\n    data = setup_user_db_tests\n    test_value = 212.5\n    assert data[\"test_user\"].rate != test_value\n    data[\"test_user\"].rate = test_value\n    assert data[\"test_user\"].rate == test_value\n\n\ndef test_hash_value():\n    \"\"\"__hash__ returns the hash of the User instance.\"\"\"\n    user = User(\n        name=\"Erkan Ozgur Yilmaz\",\n        login=\"eoyilmaz\",\n        password=\"hidden\",\n        email=\"eoyilmaz@fake.com\",\n    )\n    result = hash(user)\n    assert isinstance(result, int)\n"
  },
  {
    "path": "tests/models/test_vacation.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Vacation class.\"\"\"\nimport datetime\nimport sys\n\nimport pytest\nimport pytz\n\nfrom stalker import Type\nfrom stalker import User\nfrom stalker import Vacation\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_vacation_tests():\n    \"\"\"Set up test for the Vacation class.\"\"\"\n    data = dict()\n    # create a user\n    data[\"test_user\"] = User(\n        name=\"Test User\",\n        login=\"testuser\",\n        email=\"testuser@test.com\",\n        password=\"secret\",\n    )\n\n    # vacation type\n    data[\"personal_vacation\"] = Type(\n        name=\"Personal\", code=\"PERS\", target_entity_type=\"Vacation\"\n    )\n\n    data[\"studio_vacation\"] = Type(\n        name=\"Studio Wide\", code=\"STD\", target_entity_type=\"Vacation\"\n    )\n\n    data[\"kwargs\"] = {\n        \"user\": data[\"test_user\"],\n        \"type\": data[\"personal_vacation\"],\n        \"start\": datetime.datetime(2013, 6, 6, 10, 0, tzinfo=pytz.utc),\n        \"end\": datetime.datetime(2013, 6, 10, 19, 0, tzinfo=pytz.utc),\n    }\n\n    data[\"test_vacation\"] = Vacation(**data[\"kwargs\"])\n    return data\n\n\ndef test_strictly_typed_is_false():\n    \"\"\"__strictly_typed_ attribute is False for Vacation class.\"\"\"\n    assert Vacation.__strictly_typed__ is False\n\n\ndef test_user_argument_is_skipped(setup_vacation_tests):\n    \"\"\"user argument can be skipped skipped.\"\"\"\n    data = setup_vacation_tests\n    data[\"kwargs\"].pop(\"user\")\n    Vacation(**data[\"kwargs\"])\n\n\ndef test_user_argument_is_none(setup_vacation_tests):\n    \"\"\"user argument can be set to None.\"\"\"\n    data = setup_vacation_tests\n    data[\"kwargs\"][\"user\"] = None\n    Vacation(**data[\"kwargs\"])\n\n\ndef test_user_attribute_is_none(setup_vacation_tests):\n    \"\"\"user attribute cat be set to None.\"\"\"\n    data = setup_vacation_tests\n    data[\"test_vacation\"].user = None\n\n\ndef test_user_argument_is_not_a_user_instance(setup_vacation_tests):\n    \"\"\"TypeError raised if the user arg is not a User instance.\"\"\"\n    data = setup_vacation_tests\n    data[\"kwargs\"][\"user\"] = \"not a user instance\"\n    with pytest.raises(TypeError) as cm:\n        Vacation(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Vacation.user should be an instance of stalker.models.auth.User, \"\n        \"not str: 'not a user instance'\"\n    )\n\n\ndef test_user_attribute_is_not_a_user_instance(setup_vacation_tests):\n    \"\"\"TypeError raised if the user attr is not a User instance.\"\"\"\n    data = setup_vacation_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_vacation\"].user = \"not a user instance\"\n\n    assert str(cm.value) == (\n        \"Vacation.user should be an instance of stalker.models.auth.User, \"\n        \"not str: 'not a user instance'\"\n    )\n\n\ndef test_user_argument_is_working_as_expected(setup_vacation_tests):\n    \"\"\"user argument value is correctly passed to the user attribute.\"\"\"\n    data = setup_vacation_tests\n    assert data[\"test_vacation\"].user == data[\"kwargs\"][\"user\"]\n\n\ndef test_user_attribute_is_working_as_expected(setup_vacation_tests):\n    \"\"\"user attribute is working as expected.\"\"\"\n    data = setup_vacation_tests\n    new_user = User(\n        name=\"test user 2\", login=\"testuser2\", email=\"test@user.com\", password=\"secret\"\n    )\n\n    assert data[\"test_vacation\"].user != new_user\n    data[\"test_vacation\"].user = new_user\n    assert data[\"test_vacation\"].user == new_user\n\n\ndef test_user_argument_back_populates_vacations_attribute(setup_vacation_tests):\n    \"\"\"user argument back populates vacations attribute of the User instance.\"\"\"\n    data = setup_vacation_tests\n    assert data[\"test_vacation\"] in data[\"kwargs\"][\"user\"].vacations\n\n\ndef test_user_attribute_back_populates_vacations_attribute(setup_vacation_tests):\n    \"\"\"user attribute back populates vacations attribute of the User instance.\"\"\"\n    data = setup_vacation_tests\n    new_user = User(\n        name=\"test user 2\", login=\"testuser2\", email=\"test@user.com\", password=\"secret\"\n    )\n    data[\"test_vacation\"].user = new_user\n    assert data[\"test_vacation\"] in new_user.vacations\n\n\ndef test_to_tjp_attribute_is_a_read_only_property(setup_vacation_tests):\n    \"\"\"to_tjp is a read-only attribute.\"\"\"\n    data = setup_vacation_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_vacation\"].to_tjp = \"some value\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'to_tjp'\",\n    }.get(\n        sys.version_info.minor, \"property 'to_tjp' of 'Vacation' object has no setter\"\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_to_tjp_attribute_is_working_as_expected(setup_vacation_tests):\n    \"\"\"to_tjp attribute is working as expected.\"\"\"\n    data = setup_vacation_tests\n    # TODO: Vacation should also use time zone info\n    expected_tjp = \"vacation 2013-06-06-10:00:00 - 2013-06-10-19:00:00\"\n    assert data[\"test_vacation\"].to_tjp == expected_tjp\n"
  },
  {
    "path": "tests/models/test_variant.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the Variant class.\"\"\"\n\nimport pytest\n\nfrom stalker.models.auth import User\nfrom stalker.models.project import Project\nfrom stalker.models.repository import Repository\nfrom stalker.models.type import Type\nfrom stalker.models.variant import Variant\nfrom stalker.models.status import Status, StatusList\nfrom stalker.models.task import Task\nfrom stalker.models.version import Version\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_variant_tests():\n    \"\"\"Setup Variant tests.\"\"\"\n    data = dict()\n\n    data[\"status_wfd\"] = Status(name=\"Waiting For Dependency\", code=\"WFD\")\n    data[\"status_rts\"] = Status(name=\"Ready To Start\", code=\"RTS\")\n    data[\"status_wip\"] = Status(name=\"Work In Progress\", code=\"WIP\")\n    data[\"status_prev\"] = Status(name=\"Pending Review\", code=\"PREV\")\n    data[\"status_hrev\"] = Status(name=\"Has Revision\", code=\"HREV\")\n    data[\"status_drev\"] = Status(name=\"Dependency Has Revision\", code=\"DREV\")\n    data[\"status_oh\"] = Status(name=\"On Hold\", code=\"OH\")\n    data[\"status_stop\"] = Status(name=\"Stopped\", code=\"STOP\")\n    data[\"status_cmpl\"] = Status(name=\"Completed\", code=\"CMPL\")\n\n    data[\"task_status_list\"] = StatusList(\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_oh\"],\n            data[\"status_stop\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n\n    data[\"variant_status_list\"] = StatusList(\n        statuses=[\n            data[\"status_wfd\"],\n            data[\"status_rts\"],\n            data[\"status_wip\"],\n            data[\"status_prev\"],\n            data[\"status_hrev\"],\n            data[\"status_drev\"],\n            data[\"status_oh\"],\n            data[\"status_stop\"],\n            data[\"status_cmpl\"],\n        ],\n        target_entity_type=\"Variant\",\n    )\n\n    data[\"test_project_status_list\"] = StatusList(\n        name=\"Project Statuses\",\n        statuses=[data[\"status_wip\"], data[\"status_prev\"], data[\"status_cmpl\"]],\n        target_entity_type=\"Project\",\n    )\n\n    data[\"test_variant_status_list\"] = StatusList(\n        name=\"Variant Statuses\",\n        statuses=[data[\"status_wip\"], data[\"status_prev\"], data[\"status_cmpl\"]],\n        target_entity_type=\"Variant\",\n    )\n\n    data[\"test_movie_project_type\"] = Type(\n        name=\"Movie Project\",\n        code=\"movie\",\n        target_entity_type=\"Project\",\n    )\n\n    data[\"test_repository_type\"] = Type(\n        name=\"Test Repository Type\",\n        code=\"test\",\n        target_entity_type=\"Repository\",\n    )\n\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"test_repository_type\"],\n        linux_path=\"/mnt/T/\",\n        windows_path=\"T:/\",\n        macos_path=\"/Volumes/T/\",\n    )\n\n    data[\"test_user1\"] = User(\n        name=\"User1\", login=\"user1\", email=\"user1@user1.com\", password=\"1234\"\n    )\n\n    data[\"test_user2\"] = User(\n        name=\"User2\", login=\"user2\", email=\"user2@user2.com\", password=\"1234\"\n    )\n\n    data[\"test_user3\"] = User(\n        name=\"User3\", login=\"user3\", email=\"user3@user3.com\", password=\"1234\"\n    )\n\n    data[\"test_user4\"] = User(\n        name=\"User4\", login=\"user4\", email=\"user4@user4.com\", password=\"1234\"\n    )\n\n    data[\"test_user5\"] = User(\n        name=\"User5\", login=\"user5\", email=\"user5@user5.com\", password=\"1234\"\n    )\n\n    data[\"test_project1\"] = Project(\n        name=\"Test Project1\",\n        code=\"tp1\",\n        type=data[\"test_movie_project_type\"],\n        status_list=data[\"test_project_status_list\"],\n        repositories=[data[\"test_repository\"]],\n    )\n\n    data[\"test_variant\"] = Variant(\n        name=\"Main\",\n        project=data[\"test_project1\"],\n        status_list=data[\"test_variant_status_list\"],\n    )\n    yield data\n\n\n# @pytest.fixture(scope=\"function\")\n# def setup_variant_db_tests(setup_postgresql_db):\n#     \"\"\"Setup Variant tests with a test DB.\"\"\"\n#     data = dict()\n\n\ndef test_variant_is_derived_from_task():\n    \"\"\"Variant is deriving from Task class.\"\"\"\n    assert Task in Variant.__mro__\n\n\ndef test_variant_entity_type_is_variant(setup_variant_tests):\n    \"\"\"Variant.entity_type is \"Variant\".\"\"\"\n    data = setup_variant_tests\n    assert data[\"test_variant\"].entity_type == \"Variant\"\n\n\ndef test_variant_is_not_auto_named():\n    \"\"\"Variant.__auto_name__ is False.\"\"\"\n    assert Variant.__auto_name__ is False\n\n\ndef test_variant_can_be_used_in_task_hierarchies(setup_variant_tests):\n    \"\"\"Variant instances can be used in task hierarchies.\"\"\"\n    data = setup_variant_tests\n    task = Task(\n        name=\"Test Task 1\",\n        project=data[\"test_project1\"],\n        status_list=data[\"task_status_list\"],\n    )\n    # should not raise any errors\n    variant = data[\"test_variant\"]\n    variant.parent = task\n    assert variant.parent == task\n\n\ndef test_variant_accepts_version_instances(setup_variant_tests):\n    \"\"\"Variant instances accepts Version instances.\"\"\"\n    data = setup_variant_tests\n    variant = data[\"test_variant\"]\n    version = Version(task=variant)\n    assert version in variant.versions\n"
  },
  {
    "path": "tests/models/test_version.py",
    "content": "# -*- coding: utf-8 -*-\nimport copy\nimport logging\nfrom pathlib import Path\nimport sys\n\nimport pytest\n\nfrom stalker import (\n    Asset,\n    FilenameTemplate,\n    File,\n    Project,\n    Repository,\n    Scene,\n    Sequence,\n    Shot,\n    Status,\n    StatusList,\n    Structure,\n    Task,\n    Type,\n    User,\n    Variant,\n    Version,\n    defaults,\n    log,\n)\nfrom stalker.db.session import DBSession\nfrom stalker.exceptions import CircularDependencyError\nfrom stalker.models.entity import Entity\n\nfrom tests.utils import PlatformPatcher\n\nlogger = logging.getLogger(\"stalker.models.version.Version\")\nlogger.setLevel(log.logging_level)\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_version_db_tests(setup_postgresql_db):\n    \"\"\"Set up the tests for the Version class with a DB.\"\"\"\n    data = dict()\n    data[\"patcher\"] = PlatformPatcher()\n\n    # Users\n    data[\"test_user1\"] = User(\n        name=\"Test User 1\",\n        login=\"tuser1\",\n        email=\"tuser1@test.com\",\n        password=\"secret\",\n    )\n    data[\"test_user2\"] = User(\n        name=\"Test User 2\",\n        login=\"tuser2\",\n        email=\"tuser2@test.com\",\n        password=\"secret\",\n    )\n\n    # statuses\n    data[\"status_wip\"] = Status.query.filter_by(code=\"WIP\").first()\n\n    # repository\n    data[\"test_repo\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        linux_path=\"/mnt/T/\",\n        windows_path=\"T:/\",\n        macos_path=\"/Volumes/T/\",\n    )\n    DBSession.add(data[\"test_repo\"])\n\n    # a project type\n    data[\"test_project_type\"] = Type(\n        name=\"Test\",\n        code=\"test\",\n        target_entity_type=\"Project\",\n    )\n    DBSession.add(data[\"test_project_type\"])\n\n    # create a filename template for Variants\n    data[\"test_filename_template\"] = FilenameTemplate(\n        name=\"Variant Filename Template\",\n        target_entity_type=\"Variant\",\n        path=\"{{project.code}}/{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/{%- endfor -%}\",\n        filename=\"{{version.nice_name}}\"\n        '_r{{\"%02d\"|format(version.revision_number)}}'\n        '_v{{\"%03d\"|format(version.version_number)}}'\n        \"{{extension}}\",\n    )\n    DBSession.add(data[\"test_filename_template\"])\n    DBSession.commit()\n    # create a structure\n    data[\"test_structure\"] = Structure(\n        name=\"Test Project Structure\", templates=[data[\"test_filename_template\"]]\n    )\n    DBSession.add(data[\"test_structure\"])\n\n    # create a project\n    data[\"test_project\"] = Project(\n        name=\"Test Project\",\n        code=\"tp\",\n        type=data[\"test_project_type\"],\n        repositories=[data[\"test_repo\"]],\n        structure=data[\"test_structure\"],\n    )\n    DBSession.add(data[\"test_project\"])\n    DBSession.commit()\n\n    # create a sequence\n    data[\"test_sequence\"] = Sequence(\n        name=\"Test Sequence\",\n        code=\"SEQ1\",\n        project=data[\"test_project\"],\n    )\n    DBSession.add(data[\"test_sequence\"])\n    DBSession.commit()\n\n    data[\"test_scene\"] = Scene(\n        name=\"Test Scene\",\n        code=\"SC001\",\n        project=data[\"test_project\"],\n    )\n    DBSession.add(data[\"test_scene\"])\n    DBSession.commit()\n\n    # create a shot\n    data[\"test_shot1\"] = Shot(\n        name=\"SH001\",\n        code=\"SH001\",\n        project=data[\"test_project\"],\n        sequence=data[\"test_sequence\"],\n        scene=data[\"test_scene\"],\n    )\n    DBSession.add(data[\"test_shot1\"])\n    DBSession.commit()\n\n    # create a group of Tasks for the shot\n    data[\"test_task1\"] = Task(name=\"FX\", parent=data[\"test_shot1\"])\n    DBSession.add(data[\"test_task1\"])\n    DBSession.commit()\n\n    data[\"test_variant1\"] = Variant(name=\"Main\", parent=data[\"test_task1\"])\n    DBSession.add(data[\"test_variant1\"])\n    DBSession.commit()\n\n    # a File for the files attribute\n    data[\"test_file1\"] = File(\n        name=\"File 1\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"SH001_FX_Main_r01_v001.ma\",\n    )\n    DBSession.add(data[\"test_file1\"])\n\n    data[\"test_file2\"] = File(\n        name=\"File 2\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"SH001_FX_Main_r01_v002.ma\",\n    )\n    DBSession.add(data[\"test_file2\"])\n\n    # a File for the input file\n    data[\"test_input_file1\"] = File(\n        name=\"Input File 1\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"Outputs/SH001_beauty_v001.###.exr\",\n    )\n    DBSession.add(data[\"test_input_file1\"])\n\n    data[\"test_input_file2\"] = File(\n        name=\"Input File 2\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"Outputs/SH001_occ_v001.###.exr\",\n    )\n    DBSession.add(data[\"test_input_file2\"])\n\n    # a File for the output file\n    data[\"test_output_file1\"] = File(\n        name=\"Output File 1\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"Outputs/SH001_beauty_v001.###.exr\",\n    )\n    DBSession.add(data[\"test_output_file1\"])\n\n    data[\"test_output_file2\"] = File(\n        name=\"Output File 2\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"Outputs/SH001_occ_v001.###.exr\",\n    )\n    DBSession.add(data[\"test_output_file2\"])\n    DBSession.commit()\n\n    # now create a version for the Task\n    data[\"kwargs\"] = {\n        \"files\": [data[\"test_file1\"], data[\"test_file2\"]],\n        \"task\": data[\"test_variant1\"],\n    }\n\n    # and the Version\n    data[\"test_version\"] = Version(**data[\"kwargs\"])\n    DBSession.add(data[\"test_version\"])\n\n    # set the published to False\n    data[\"test_version\"].is_published = False\n    DBSession.commit()\n    yield data\n    # clean up test\n    data[\"patcher\"].restore()\n\n\ndef test___auto_name__class_attribute_is_set_to_true():\n    \"\"\"__auto_name__ class attribute is set to True for Version class.\"\"\"\n    assert Version.__auto_name__ is True\n\n\ndef test_version_derives_from_entity():\n    \"\"\"Version class derives from Entity.\"\"\"\n    assert Entity == Version.__mro__[1]\n\n\ndef test_task_argument_is_skipped(setup_version_db_tests):\n    \"\"\"TypeError raised if the task argument is skipped.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"].pop(\"task\")\n    with pytest.raises(TypeError) as cm:\n        Version(**data[\"kwargs\"])\n    assert str(cm.value) == \"Version.task cannot be None\"\n\n\ndef test_task_argument_is_none(setup_version_db_tests):\n    \"\"\"TypeError raised if the task argument is None.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"task\"] = None\n    with pytest.raises(TypeError) as cm:\n        Version(**data[\"kwargs\"])\n    assert str(cm.value) == \"Version.task cannot be None\"\n\n\ndef test_task_attribute_is_none(setup_version_db_tests):\n    \"\"\"TypeError raised if the task attribute is None.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_version\"].task = None\n    assert str(cm.value) == \"Version.task cannot be None\"\n\n\ndef test_task_argument_is_not_a_task(setup_version_db_tests):\n    \"\"\"TypeError raised if the task argument is not a Task instance.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"task\"] = \"a task\"\n    with pytest.raises(TypeError) as cm:\n        Version(**data[\"kwargs\"])\n    assert str(cm.value) == (\n        \"Version.task should be a Task, Asset, Shot, Scene, Sequence or Variant \"\n        \"instance, not str: 'a task'\"\n    )\n\n\ndef test_task_attribute_is_not_a_task(setup_version_db_tests):\n    \"\"\"TypeError raised if the task attribute is not a Task instance.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_version\"].task = \"a task\"\n    assert str(cm.value) == (\n        \"Version.task should be a Task, Asset, Shot, Scene, Sequence or Variant \"\n        \"instance, not str: 'a task'\"\n    )\n\n\ndef test_task_attribute_is_working_as_expected(setup_version_db_tests):\n    \"\"\"task attribute is working as expected.\"\"\"\n    data = setup_version_db_tests\n    new_task = Variant(\n        name=\"New Test Variant\",\n        parent=data[\"test_shot1\"],\n    )\n    DBSession.add(new_task)\n    assert data[\"test_version\"].task is not new_task\n    data[\"test_version\"].task = new_task\n    assert data[\"test_version\"].task is new_task\n\n\ndef test_revision_number_arg_is_skipped(setup_version_db_tests):\n    \"\"\"revision_number arg can be skipped.\"\"\"\n    data = setup_version_db_tests\n    new_version = Version(**data[\"kwargs\"])\n    assert isinstance(new_version, Version)\n    assert new_version.revision_number == 1\n\n\ndef test_revision_number_arg_is_none(setup_version_db_tests):\n    \"\"\"revision_number arg can be None.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = None\n    new_version = Version(**data[\"kwargs\"])\n    assert isinstance(new_version, Version)\n    assert new_version.revision_number == 1\n\n\ndef test_revision_number_attr_cannot_be_set_to_none(setup_version_db_tests):\n    \"\"\"revision_number can be None.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 12\n    new_version = Version(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_version.revision_number = None\n    assert str(cm.value) == (\n        \"Version.revision_number should be a positive integer, not NoneType: 'None'\"\n    )\n\n\ndef test_revision_number_arg_is_not_an_integer(setup_version_db_tests):\n    \"\"\"revision_number arg is not an integer raises TypeError.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = \"not an integer\"\n    with pytest.raises(TypeError) as cm:\n        _ = Version(**data[\"kwargs\"])\n    assert str(cm.value) == (\n        \"Version.revision_number should be a positive integer, \"\n        \"not str: 'not an integer'\"\n    )\n\n\ndef test_revision_number_attr_is_not_an_integer(setup_version_db_tests):\n    \"\"\"revision_number attr is not an integer raises TypeError.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 14\n    new_version = Version(**data[\"kwargs\"])\n    with pytest.raises(TypeError) as cm:\n        new_version.revision_number = \"not an integer\"\n    assert str(cm.value) == (\n        \"Version.revision_number should be a positive integer, \"\n        \"not str: 'not an integer'\"\n    )\n\n\ndef test_revision_number_arg_is_not_a_positive_integer(setup_version_db_tests):\n    \"\"\"revision_number arg is not a positive integer raises ValueError.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = -109\n    with pytest.raises(ValueError) as cm:\n        _ = Version(**data[\"kwargs\"])\n    assert str(cm.value) == (\n        \"Version.revision_number should be a positive integer, \" \"not int: '-109'\"\n    )\n\n\ndef test_revision_number_attr_is_not_a_positive_integer(setup_version_db_tests):\n    \"\"\"revision_number attr is not a positive integer raises ValueError.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 153\n    new_version = Version(**data[\"kwargs\"])\n    with pytest.raises(ValueError) as cm:\n        new_version.revision_number = -109\n    assert str(cm.value) == (\n        \"Version.revision_number should be a positive integer, \" \"not int: '-109'\"\n    )\n\n\ndef test_revision_number_arg_can_be_non_sequential(setup_version_db_tests):\n    \"\"\"revision_number arg can be set to any positive number.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 21\n    new_version = Version(**data[\"kwargs\"])\n    assert isinstance(new_version, Version)\n    assert new_version.revision_number == 21\n\n\ndef test_revision_number_attr_can_be_non_sequential(setup_version_db_tests):\n    \"\"\"revision_number attr can be set to any positive number.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 21\n    new_version = Version(**data[\"kwargs\"])\n    assert new_version.revision_number != 13\n    new_version.revision_number = 13\n    assert new_version.revision_number == 13\n\n\ndef test_revision_number_attr_changed_will_reset_version_number(setup_version_db_tests):\n    \"\"\"revision_number attr can be set to any positive number.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 21\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.version_number == 1\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.version_number == 2\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.version_number == 3\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.version_number == 4\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.version_number == 5\n    assert new_version.revision_number != 13\n    new_version.revision_number = 13\n    DBSession.save(new_version)\n    assert new_version.revision_number == 13\n    assert new_version.version_number == 1\n    new_version.revision_number = 21\n    assert new_version.version_number == 5\n\n\ndef test_revision_number_attr_not_changed_will_not_reset_version_number(\n    setup_version_db_tests,\n):\n    \"\"\"revision_number attr can be set to any positive number.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 21\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    new_version.revision_number = 13\n    DBSession.save(new_version)\n    new_version.revision_number = 21\n    DBSession.save(new_version)\n    new_version.revision_number = 21\n    assert new_version.version_number == 5\n\n\ndef test_revision_number_arg_value_is_passed_to_revision_number_attr(\n    setup_version_db_tests,\n):\n    \"\"\"revision_number arg value is passed to revision_number attr.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 21\n    new_version = Version(**data[\"kwargs\"])\n    assert isinstance(new_version, Version)\n    assert new_version.revision_number == 21\n\n\ndef test_revision_number_arg_effects_version_number(setup_version_db_tests):\n    \"\"\"revision_number arg effects version_number value.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 1\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert isinstance(new_version, Version)\n    assert new_version.revision_number == 1\n    assert new_version.version_number == 2\n\n    # second version\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert isinstance(new_version, Version)\n    assert new_version.revision_number == 1\n    assert new_version.version_number == 3\n\n    # third version\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert isinstance(new_version, Version)\n    assert new_version.revision_number == 1\n    assert new_version.version_number == 4\n\n    # new revision_number series\n    data[\"kwargs\"][\"revision_number\"] = 2\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert isinstance(new_version, Version)\n    assert new_version.revision_number == 2\n    assert new_version.version_number == 1\n\n    # second version\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert isinstance(new_version, Version)\n    assert new_version.revision_number == 2\n    assert new_version.version_number == 2\n\n    # back to revision_number 1\n    data[\"kwargs\"][\"revision_number\"] = 1\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert isinstance(new_version, Version)\n    assert new_version.revision_number == 1\n    assert new_version.version_number == 5\n\n\ndef test_max_revision_number_returns_the_maximum_revision_number_in_the_db(\n    setup_version_db_tests,\n):\n    \"\"\"max_revision_number returns the maximum value of the revision_number in the db.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 1\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.revision_number == 1\n    assert new_version.version_number == 2\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.revision_number == 1\n    assert new_version.version_number == 3\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.revision_number == 1\n    assert new_version.version_number == 4\n\n    # new revision_number series\n    data[\"kwargs\"][\"revision_number\"] = 2\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.revision_number == 2\n    assert new_version.version_number == 1\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.revision_number == 2\n    assert new_version.version_number == 2\n\n    # back to revision_number 1\n    data[\"kwargs\"][\"revision_number\"] = 1\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.save(new_version)\n    assert new_version.revision_number == 1\n\n    assert new_version.max_revision_number == 2\n\n\ndef test_max_revision_number_returns_the_maximum_revision_number_in_the_db_when_no_version(\n    setup_version_db_tests,\n):\n    \"\"\"max_revision_number returns the maximum value of the revision_number in the db when no version is created.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 1\n    new_version = Version(**data[\"kwargs\"])\n    assert new_version.max_revision_number == 1\n\n\ndef test_version_number_attribute_is_automatically_generated(setup_version_db_tests):\n    \"\"\"version_number attribute is automatically generated.\"\"\"\n    data = setup_version_db_tests\n    assert data[\"test_version\"].version_number == 1\n    DBSession.add(data[\"test_version\"])\n    DBSession.commit()\n\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.add(new_version)\n    DBSession.commit()\n\n    assert data[\"test_version\"].task == new_version.task\n    assert new_version.version_number == 2\n\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.add(new_version)\n    DBSession.commit()\n\n    assert data[\"test_version\"].task == new_version.task\n    assert new_version.version_number == 3\n\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.add(new_version)\n    DBSession.commit()\n\n    assert data[\"test_version\"].task == new_version.task\n    assert new_version.version_number == 4\n\n\ndef test_version_number_attribute_is_starting_from_1(setup_version_db_tests):\n    \"\"\"version_number attribute is starting from 1.\"\"\"\n    data = setup_version_db_tests\n    assert data[\"test_version\"].version_number == 1\n\n\ndef test_version_number_attribute_is_set_to_a_lower_then_it_should_be(\n    setup_version_db_tests,\n):\n    \"\"\"version_number attr is set to unique number if it smaller than what it\n    should be.\"\"\"\n    data = setup_version_db_tests\n    data[\"test_version\"].version_number = -1\n    assert data[\"test_version\"].version_number == 1\n\n    data[\"test_version\"].version_number = -10\n    assert data[\"test_version\"].version_number == 1\n\n    DBSession.add(data[\"test_version\"])\n    DBSession.commit()\n\n    data[\"test_version\"].version_number = -100\n    # it should be 1 again\n    assert data[\"test_version\"].version_number == 1\n\n    new_version = Version(**data[\"kwargs\"])\n    assert new_version.version_number == 2\n\n    new_version.version_number = 1\n    assert new_version.version_number == 2\n\n    new_version.version_number = 100\n    assert new_version.version_number == 100\n\n\ndef test_files_argument_is_skipped(setup_version_db_tests):\n    \"\"\"files attribute an empty list if the files argument is skipped.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"].pop(\"files\")\n    new_version = Version(**data[\"kwargs\"])\n    assert new_version.files == []\n\n\ndef test_files_argument_is_none(setup_version_db_tests):\n    \"\"\"files attribute an empty list if the files argument is None.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"files\"] = None\n    new_version = Version(**data[\"kwargs\"])\n    assert new_version.files == []\n\n\ndef test_files_attribute_is_none(setup_version_db_tests):\n    \"\"\"TypeError raised if the files argument is set to None.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_version\"].files = None\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_files_argument_is_not_a_list_of_file_instances(setup_version_db_tests):\n    \"\"\"TypeError raised if the files attr is not a list of File instances.\"\"\"\n    data = setup_version_db_tests\n    test_value = [132, \"231123\"]\n    data[\"kwargs\"][\"files\"] = test_value\n    with pytest.raises(TypeError) as cm:\n        Version(**data[\"kwargs\"])\n\n    assert (\n        str(cm.value) == \"Version.files should only contain instances of \"\n        \"stalker.models.file.File, not int: '132'\"\n    )\n\n\ndef test_files_attribute_is_not_a_list_of_file_instances(setup_version_db_tests):\n    \"\"\"TypeError raised if the files attr is set to something other than a File.\"\"\"\n    data = setup_version_db_tests\n    test_value = [132, \"231123\"]\n    with pytest.raises(TypeError) as cm:\n        data[\"test_version\"].files = test_value\n\n    assert (\n        str(cm.value) == \"Version.files should only contain instances of \"\n        \"stalker.models.file.File, not int: '132'\"\n    )\n\n\ndef test_files_attribute_is_working_as_expected(setup_version_db_tests):\n    \"\"\"files attribute is working as expected.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"].pop(\"files\")\n    new_version = Version(**data[\"kwargs\"])\n    assert data[\"test_file1\"] not in new_version.files\n    assert data[\"test_file2\"] not in new_version.files\n\n    new_version.files = [data[\"test_file1\"], data[\"test_file2\"]]\n    assert data[\"test_file1\"] in new_version.files\n    assert data[\"test_file2\"] in new_version.files\n\n\ndef test_is_published_attribute_is_false_by_default(setup_version_db_tests):\n    \"\"\"is_published attribute is False by default.\"\"\"\n    data = setup_version_db_tests\n    assert data[\"test_version\"].is_published is False\n\n\ndef test_is_published_attribute_is_working_as_expected(setup_version_db_tests):\n    \"\"\"is_published attribute is working as expected.\"\"\"\n    data = setup_version_db_tests\n    data[\"test_version\"].is_published = True\n    assert data[\"test_version\"].is_published is True\n    data[\"test_version\"].is_published = False\n    assert data[\"test_version\"].is_published is False\n\n\ndef test_parent_argument_is_skipped(setup_version_db_tests):\n    \"\"\"parent attribute None if the parent argument is skipped.\"\"\"\n    data = setup_version_db_tests\n    try:\n        data[\"kwargs\"].pop(\"parent\")\n    except KeyError:\n        pass\n    new_version = Version(**data[\"kwargs\"])\n    assert new_version.parent is None\n\n\ndef test_parent_argument_is_none(setup_version_db_tests):\n    \"\"\"parent attribute None if the parent argument is skipped.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = None\n    new_version = Version(**data[\"kwargs\"])\n    assert new_version.parent is None\n\n\ndef test_parent_attribute_is_none(setup_version_db_tests):\n    \"\"\"parent attribute value None if it is set to None.\"\"\"\n    data = setup_version_db_tests\n    data[\"test_version\"].parent = None\n    assert data[\"test_version\"].parent is None\n\n\ndef test_parent_argument_is_not_a_version_instance(setup_version_db_tests):\n    \"\"\"TypeError raised if the parent argument is not a Version instance.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = \"not a version instance\"\n    with pytest.raises(TypeError) as cm:\n        Version(**data[\"kwargs\"])\n\n    assert str(cm.value) == (\n        \"Version.parent should be an instance of Version class or \"\n        \"derivative, not str: 'not a version instance'\"\n    )\n\n\ndef test_parent_attribute_is_not_set_to_a_version_instance(setup_version_db_tests):\n    \"\"\"TypeError raised if the parent attribute is not set to a Version instance.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_version\"].parent = \"not a version instance\"\n\n    assert str(cm.value) == (\n        \"Version.parent should be an instance of Version class or \"\n        \"derivative, not str: 'not a version instance'\"\n    )\n\n\ndef test_parent_argument_is_working_as_expected(setup_version_db_tests):\n    \"\"\"parent argument is working as expected.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = data[\"test_version\"]\n    new_version = Version(**data[\"kwargs\"])\n    assert new_version.parent == data[\"test_version\"]\n\n\ndef test_parent_attribute_is_working_as_expected(setup_version_db_tests):\n    \"\"\"parent attribute is working as expected.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = None\n    new_version = Version(**data[\"kwargs\"])\n    assert new_version.parent != data[\"test_version\"]\n    new_version.parent = data[\"test_version\"]\n    assert new_version.parent == data[\"test_version\"]\n\n\ndef test_parent_argument_updates_the_children_attribute(setup_version_db_tests):\n    \"\"\"parent argument updates the children attribute of the parent Version.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = data[\"test_version\"]\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.add(new_version)\n    assert new_version in data[\"test_version\"].children\n\n\ndef test_parent_attribute_updates_the_children_attribute(setup_version_db_tests):\n    \"\"\"parent attr updates the children attribute of the parent Version.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = None\n    new_version = Version(**data[\"kwargs\"])\n    DBSession.add(new_version)\n    assert new_version.parent != data[\"test_version\"]\n    new_version.parent = data[\"test_version\"]\n    assert new_version in data[\"test_version\"].children\n\n\ndef test_parent_attribute_will_not_allow_circular_dependencies(setup_version_db_tests):\n    \"\"\"CircularDependency raised if parent attr is a child of the current Version.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = data[\"test_version\"]\n    version1 = Version(**data[\"kwargs\"])\n    DBSession.add(version1)\n    with pytest.raises(CircularDependencyError) as cm:\n        data[\"test_version\"].parent = version1\n\n    assert (\n        str(cm.value) == \"<tp_SH001_FX_Main_v001 (Version)> (Version) and \"\n        \"<tp_SH001_FX_Main_v002 (Version)> (Version) are in a \"\n        'circular dependency in their \"children\" attribute'\n    )\n\n\ndef test_parent_attribute_will_not_allow_deeper_circular_dependencies(\n    setup_version_db_tests,\n):\n    \"\"\"CircularDependency raised if the Version is a parent of the current parent.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = data[\"test_version\"]\n    version1 = Version(**data[\"kwargs\"])\n    DBSession.add(version1)\n\n    data[\"kwargs\"][\"parent\"] = version1\n    version2 = Version(**data[\"kwargs\"])\n    DBSession.add(version2)\n\n    # now create circular dependency\n    with pytest.raises(CircularDependencyError) as cm:\n        data[\"test_version\"].parent = version2\n\n    assert (\n        str(cm.value) == \"<tp_SH001_FX_Main_v001 (Version)> (Version) and \"\n        \"<tp_SH001_FX_Main_v002 (Version)> (Version) are in a \"\n        'circular dependency in their \"children\" attribute'\n    )\n\n\ndef test_children_attribute_is_set_to_none(setup_version_db_tests):\n    \"\"\"TypeError raised if the children attribute is set to None.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_version\"].children = None\n\n    assert str(cm.value) == \"Incompatible collection type: None is not list-like\"\n\n\ndef test_children_attribute_is_not_set_to_a_list(setup_version_db_tests):\n    \"\"\"TypeError raised if the children attribute is not set to a list.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_version\"].children = \"not a list of Version instances\"\n\n    assert str(cm.value) == \"Incompatible collection type: str is not list-like\"\n\n\ndef test_children_attribute_is_not_set_to_a_list_of_version_instances(\n    setup_version_db_tests,\n):\n    \"\"\"TypeError raised if the children attr is not all Version instances.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_version\"].children = [\"not a Version instance\", 3]\n\n    assert str(cm.value) == (\n        \"Version.children should only contain instances of \"\n        \"Version (or derivative), not str: 'not a Version instance'\"\n    )\n\n\ndef test_children_attribute_is_working_as_expected(setup_version_db_tests):\n    \"\"\"children attribute is working as expected.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = None\n    new_version1 = Version(**data[\"kwargs\"])\n    data[\"test_version\"].children = [new_version1]\n    assert new_version1 in data[\"test_version\"].children\n\n    new_version2 = Version(**data[\"kwargs\"])\n    data[\"test_version\"].children.append(new_version2)\n    assert new_version2 in data[\"test_version\"].children\n\n\ndef test_children_attribute_updates_parent_attribute(setup_version_db_tests):\n    \"\"\"children attribute updates the parent attribute of the children Versions.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = None\n    new_version1 = Version(**data[\"kwargs\"])\n    data[\"test_version\"].children = [new_version1]\n    assert new_version1.parent == data[\"test_version\"]\n\n    new_version2 = Version(**data[\"kwargs\"])\n    data[\"test_version\"].children.append(new_version2)\n    assert new_version2.parent == data[\"test_version\"]\n\n\ndef test_children_attribute_will_not_allow_circular_dependencies(\n    setup_version_db_tests,\n):\n    \"\"\"CircularDependency error raised if a parent is set as a child to its child.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = None\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n    new_version2 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version2)\n    DBSession.commit()\n\n    new_version1.parent = new_version2\n    with pytest.raises(CircularDependencyError) as cm:\n        new_version1.children.append(new_version2)\n\n    assert (\n        str(cm.value) == \"<tp_SH001_FX_Main_v003 (Version)> (Version) and \"\n        \"<tp_SH001_FX_Main_v002 (Version)> (Version) are in a \"\n        'circular dependency in their \"children\" attribute'\n    )\n\n\ndef test_children_attribute_will_not_allow_deeper_circular_dependencies(\n    setup_version_db_tests,\n):\n    \"\"\"CircularDependency error raised if a parent Version of a parent Version is set as\n    a children to its grand child.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"parent\"] = None\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n    new_version2 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version2)\n    DBSession.commit()\n    new_version3 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version2)\n    DBSession.commit()\n\n    new_version1.parent = new_version2\n    new_version2.parent = new_version3\n\n    with pytest.raises(CircularDependencyError) as cm:\n        new_version1.children.append(new_version3)\n\n    assert (\n        str(cm.value) == \"<tp_SH001_FX_Main_v004 (Version)> (Version) and \"\n        \"<tp_SH001_FX_Main_v002 (Version)> (Version) are in a \"\n        'circular dependency in their \"children\" attribute'\n    )\n\n\ndef test_generate_path_extension_can_be_skipped(setup_version_db_tests):\n    \"\"\"generate_path() extension can be skipped.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n    # extension can be skipped\n    _ = new_version1.generate_path()\n\n\ndef test_generate_path_extension_can_be_None(setup_version_db_tests):\n    \"\"\"generate_path() extension can be None.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n    # extension can be skipped\n    path = new_version1.generate_path(extension=None)\n    assert path.suffix == \"\"\n\n\ndef test_generate_path_extension_is_not_a_str(setup_version_db_tests):\n    \"\"\"generate_path() extension is not a str will raise a TypeError.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n    # extension is not str raises TypeError\n    with pytest.raises(TypeError) as cm:\n        _ = new_version1.generate_path(extension=1234)\n\n    assert str(cm.value) == \"extension should be a str, not int: '1234'\"\n\n\ndef test_generate_path_extension_can_be_an_empty_str(setup_version_db_tests):\n    \"\"\"generate_path() extension can be an empty str.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n    # extension can be an empty string\n    path = new_version1.generate_path(extension=\"\")\n    assert path.suffix == \"\"\n\n\ndef test_generate_path_will_render_the_appropriate_template_from_the_related_project(\n    setup_version_db_tests,\n):\n    \"\"\"generate_path() generates a Path by rendering the related Project FilenameTemplate.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n    path = new_version1.generate_path()\n    assert isinstance(path, Path)\n    assert str(path.parent) == \"tp/SH001/FX/Main\"\n\n    path = path.with_suffix(\".ma\")\n    assert str(path.name) == \"SH001_FX_Main_r01_v002.ma\"\n\n\ndef test_generate_path_will_use_the_given_extension(setup_version_db_tests):\n    \"\"\"generate_path method uses the given extension.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n    path = new_version1.generate_path(extension=\".ma\")\n    assert isinstance(path, Path)\n    assert str(path.parent) == \"tp/SH001/FX/Main\"\n    assert str(path.name) == \"SH001_FX_Main_r01_v002.ma\"\n\n\ndef test_generate_path_will_raise_a_runtime_error_if_there_is_no_suitable_filename_template(\n    setup_version_db_tests,\n):\n    \"\"\"generate_path method raises a RuntimeError if there is no suitable\n    FilenameTemplate instance found.\"\"\"\n    data = setup_version_db_tests\n    data[\"test_structure\"].templates.remove(data[\"test_filename_template\"])\n    DBSession.commit()\n\n    DBSession.delete(data[\"test_filename_template\"])\n    DBSession.commit()\n    data[\"kwargs\"][\"parent\"] = None\n    new_version1 = Version(**data[\"kwargs\"])\n    with pytest.raises(RuntimeError) as cm:\n        new_version1.generate_path()\n\n    assert (\n        str(cm.value)\n        == \"There are no suitable FilenameTemplate (target_entity_type == \"\n        \"'Variant') defined in the Structure of the related Project \"\n        \"instance, please create a new \"\n        \"stalker.models.template.FilenameTemplate instance with its \"\n        \"'target_entity_type' attribute is set to 'Variant' and add it \"\n        \"to the `templates` attribute of the structure of the project\"\n    )\n\n\ndef test_template_variables_project(setup_version_db_tests):\n    \"\"\"project in template variables is correct.\"\"\"\n    data = setup_version_db_tests\n    kwargs = data[\"test_version\"]._template_variables()\n    assert kwargs[\"project\"] == data[\"test_version\"].task.project\n\n\ndef test_template_variables_sequence(setup_version_db_tests):\n    \"\"\"sequence in template variables is correct.\"\"\"\n    data = setup_version_db_tests\n    kwargs = data[\"test_version\"]._template_variables()\n    assert kwargs[\"sequence\"] == data[\"test_sequence\"]\n\n\ndef test_template_variables_scene(setup_version_db_tests):\n    \"\"\"scene in template variables is correct.\"\"\"\n    data = setup_version_db_tests\n    kwargs = data[\"test_version\"]._template_variables()\n    assert kwargs[\"scene\"] == data[\"test_scene\"]\n\n\ndef test_template_variables_shot(setup_version_db_tests):\n    \"\"\"shot in template variables is correct.\"\"\"\n    data = setup_version_db_tests\n    kwargs = data[\"test_version\"]._template_variables()\n    assert kwargs[\"shot\"] is None\n\n\ndef test_template_variables_asset(setup_version_db_tests):\n    \"\"\"asset in template variables is correct.\"\"\"\n    data = setup_version_db_tests\n    kwargs = data[\"test_version\"]._template_variables()\n    assert kwargs[\"asset\"] is None\n\n\ndef test_template_variables_task(setup_version_db_tests):\n    \"\"\"task in template variables is correct.\"\"\"\n    data = setup_version_db_tests\n    kwargs = data[\"test_version\"]._template_variables()\n    assert kwargs[\"task\"] == data[\"test_version\"].task\n\n\ndef test_template_variables_parent_tasks(setup_version_db_tests):\n    \"\"\"parent_tasks in template variables is correct.\"\"\"\n    data = setup_version_db_tests\n    kwargs = data[\"test_version\"]._template_variables()\n    parents = data[\"test_version\"].task.parents\n    parents.append(data[\"test_version\"].task)\n    assert kwargs[\"parent_tasks\"] == parents\n\n\ndef test_template_variables_version(setup_version_db_tests):\n    \"\"\"version in template variables is correct.\"\"\"\n    data = setup_version_db_tests\n    kwargs = data[\"test_version\"]._template_variables()\n    assert kwargs[\"version\"] == data[\"test_version\"]\n\n\ndef test_template_variables_type(setup_version_db_tests):\n    \"\"\"type in template variables is correct.\"\"\"\n    data = setup_version_db_tests\n    kwargs = data[\"test_version\"]._template_variables()\n    assert kwargs[\"type\"] == data[\"test_version\"].type\n\n\ndef test_template_variables_for_a_shot_version_contains_scene(setup_version_db_tests):\n    \"\"\"template_variables for a Shot version contains scene.\"\"\"\n    data = setup_version_db_tests\n    v = Version(task=data[\"test_shot1\"])\n    template_variables = v._template_variables()\n    assert data[\"test_shot1\"].scene is not None\n    assert \"scene\" in template_variables\n    assert template_variables[\"scene\"] == data[\"test_shot1\"].scene\n\n\ndef test_template_variables_for_a_shot_version_contains_sequence(\n    setup_version_db_tests,\n):\n    \"\"\"template_variables for a Shot version contains sequence.\"\"\"\n    data = setup_version_db_tests\n    v = Version(task=data[\"test_shot1\"])\n    template_variables = v._template_variables()\n    assert data[\"test_shot1\"].sequence is not None\n    assert \"sequence\" in template_variables\n    assert template_variables[\"sequence\"] == data[\"test_shot1\"].sequence\n\n\ndef test_absolute_path_works_as_expected(setup_version_db_tests):\n    \"\"\"absolute_path attribute works as expected.\"\"\"\n    data = setup_version_db_tests\n    # data[\"patcher\"].patch(\"Linux\")\n\n    data[\"test_structure\"].templates.remove(data[\"test_filename_template\"])\n    DBSession.delete(data[\"test_filename_template\"])\n    DBSession.commit()\n\n    ft = FilenameTemplate(\n        name=\"Task Filename Template\",\n        target_entity_type=\"Variant\",\n        path=\"$REPO{{project.repositories[0].code}}/{{project.code}}/\"\n        \"{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/\"\n        \"{%- endfor -%}\",\n        filename=\"{{task.nice_name}}\"\n        '_r{{\"%02d\"|format(version.revision_number)}}'\n        '_v{{\"%03d\"|format(version.version_number)}}',\n    )\n    data[\"test_project\"].structure.templates.append(ft)\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n\n    repo_path = data[\"test_repo\"].path\n    assert new_version1.absolute_path == Path(f\"{repo_path}/tp/SH001/FX/Main\")\n\n\ndef test_absolute_full_path_works_as_expected(setup_version_db_tests):\n    \"\"\"absolute_full_path attribute works as expected.\"\"\"\n    data = setup_version_db_tests\n    # data[\"patcher\"].patch(\"Linux\")\n\n    data[\"test_structure\"].templates.remove(data[\"test_filename_template\"])\n    DBSession.delete(data[\"test_filename_template\"])\n    DBSession.commit()\n\n    ft = FilenameTemplate(\n        name=\"Task Filename Template\",\n        target_entity_type=\"Variant\",\n        path=\"$REPO{{project.repositories[0].code}}/{{project.code}}/\"\n        \"{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/\"\n        \"{%- endfor -%}\",\n        filename=\"{{task.nice_name}}\"\n        '_r{{\"%02d\"|format(version.revision_number)}}'\n        '_v{{\"%03d\"|format(version.version_number)}}',\n    )\n    data[\"test_project\"].structure.templates.append(ft)\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n\n    repo_path = data[\"test_repo\"].path\n    assert new_version1.absolute_full_path == Path(\n        f\"{repo_path}/tp/SH001/FX/Main/Main_r01_v002\"\n    )\n\n\ndef test_path_works_as_expected(setup_version_db_tests):\n    \"\"\"path attribute works as expected.\"\"\"\n    data = setup_version_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n\n    data[\"test_structure\"].templates.remove(data[\"test_filename_template\"])\n    DBSession.delete(data[\"test_filename_template\"])\n    DBSession.commit()\n\n    ft = FilenameTemplate(\n        name=\"Task Filename Template\",\n        target_entity_type=\"Variant\",\n        path=\"$REPO{{project.repositories[0].code}}/{{project.code}}/\"\n        \"{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/\"\n        \"{%- endfor -%}\",\n        filename=\"{{task.nice_name}}\"\n        '_r{{\"%02d\"|format(version.revision_number)}}'\n        '_v{{\"%03d\"|format(version.version_number)}}',\n    )\n    data[\"test_project\"].structure.templates.append(ft)\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n\n    assert new_version1.path == Path(\"$REPOTR/tp/SH001/FX/Main\")\n\n\ndef test_full_path_works_as_expected(setup_version_db_tests):\n    \"\"\"full_path attribute works as expected.\"\"\"\n    data = setup_version_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n\n    data[\"test_structure\"].templates.remove(data[\"test_filename_template\"])\n    DBSession.delete(data[\"test_filename_template\"])\n    DBSession.commit()\n\n    ft = FilenameTemplate(\n        name=\"Task Filename Template\",\n        target_entity_type=\"Variant\",\n        path=\"$REPO{{project.repositories[0].code}}/{{project.code}}/\"\n        \"{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/\"\n        \"{%- endfor -%}\",\n        filename=\"{{task.nice_name}}\"\n        '_r{{\"%02d\"|format(version.revision_number)}}'\n        '_v{{\"%03d\"|format(version.version_number)}}',\n    )\n    data[\"test_project\"].structure.templates.append(ft)\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n\n    assert new_version1.full_path == Path(\"$REPOTR/tp/SH001/FX/Main/Main_r01_v002\")\n\n\ndef test_filename_works_as_expected(setup_version_db_tests):\n    \"\"\"filename attribute works as expected.\"\"\"\n    data = setup_version_db_tests\n    data[\"patcher\"].patch(\"Linux\")\n\n    data[\"test_structure\"].templates.remove(data[\"test_filename_template\"])\n    DBSession.delete(data[\"test_filename_template\"])\n    DBSession.commit()\n\n    ft = FilenameTemplate(\n        name=\"Task Filename Template\",\n        target_entity_type=\"Variant\",\n        path=\"$REPO{{project.repositories[0].code}}/{{project.code}}/\"\n        \"{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/\"\n        \"{%- endfor -%}\",\n        filename=\"{{task.nice_name}}\"\n        '_r{{\"%02d\"|format(version.revision_number)}}'\n        '_v{{\"%03d\"|format(version.version_number)}}',\n    )\n    data[\"test_project\"].structure.templates.append(ft)\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n\n    assert new_version1.filename == \"Main_r01_v002\"\n    assert isinstance(new_version1.filename, str)\n\n\ndef test_latest_published_version_is_read_only(setup_version_db_tests):\n    \"\"\"latest_published_version is a read only attribute.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_version\"].latest_published_version = True\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'latest_published_version'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'latest_published_version' of 'Version' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_latest_published_version_is_working_as_expected(setup_version_db_tests):\n    \"\"\"is_latest_published_version is working as expected.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version1)\n\n    new_version2 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version2)\n\n    new_version3 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version3)\n\n    new_version4 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version4)\n\n    new_version5 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version5)\n\n    # with new revision number\n    data[\"kwargs\"][\"revision_number\"] = 2\n    new_version6 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version6)\n    new_version7 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version7)\n    new_version8 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version8)\n\n    new_version1.is_published = True\n    new_version3.is_published = True\n    new_version4.is_published = True\n\n    new_version7.is_published = True\n\n    assert new_version1.latest_published_version == new_version4\n    assert new_version2.latest_published_version == new_version4\n    assert new_version3.latest_published_version == new_version4\n    assert new_version4.latest_published_version == new_version4\n    assert new_version5.latest_published_version == new_version4\n\n    assert new_version6.latest_published_version == new_version7\n    assert new_version7.latest_published_version == new_version7\n    assert new_version8.latest_published_version == new_version7\n\n\ndef test_is_latest_published_version_is_working_as_expected(setup_version_db_tests):\n    \"\"\"is_latest_published_version is working as expected.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version1)\n\n    new_version2 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version2)\n\n    new_version3 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version3)\n\n    new_version4 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version4)\n\n    new_version5 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version5)\n\n    # with new revision number\n    data[\"kwargs\"][\"revision_number\"] = 2\n    new_version6 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version6)\n    new_version7 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version7)\n    new_version8 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version8)\n\n    new_version1.is_published = True\n    new_version3.is_published = True\n    new_version4.is_published = True\n\n    new_version7.is_published = True\n\n    assert new_version1.is_latest_published_version() is False\n    assert new_version2.is_latest_published_version() is False\n    assert new_version3.is_latest_published_version() is False\n    assert new_version4.is_latest_published_version() is True\n    assert new_version5.is_latest_published_version() is False\n\n    assert new_version6.is_latest_published_version() is False\n    assert new_version7.is_latest_published_version() is True\n    assert new_version8.is_latest_published_version() is False\n\n\ndef test_equality_operator(setup_version_db_tests):\n    \"\"\"equality of two Version instances.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version1)\n\n    new_version2 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version2)\n\n    new_version3 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version3)\n\n    new_version4 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version4)  #\n\n    new_version5 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version5)\n\n    # with new revision number\n    data[\"kwargs\"][\"revision_number\"] = 2\n    new_version6 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version6)\n    new_version7 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version7)\n    new_version8 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version8)\n\n    new_version1.is_published = True\n    new_version3.is_published = True\n    new_version4.is_published = True\n\n    new_version7.is_published = True\n\n    assert (new_version1 == new_version1) is True\n    assert (new_version1 == new_version2) is False\n    assert (new_version1 == new_version3) is False\n    assert (new_version1 == new_version4) is False\n    assert (new_version1 == new_version5) is False\n    assert (new_version1 == new_version6) is False\n    assert (new_version1 == new_version7) is False\n    assert (new_version1 == new_version8) is False\n\n    assert (new_version2 == new_version2) is True\n    assert (new_version2 == new_version3) is False\n    assert (new_version2 == new_version4) is False\n    assert (new_version2 == new_version5) is False\n    assert (new_version2 == new_version6) is False\n    assert (new_version2 == new_version7) is False\n    assert (new_version2 == new_version8) is False\n\n    assert (new_version3 == new_version3) is True\n    assert (new_version3 == new_version4) is False\n    assert (new_version3 == new_version5) is False\n    assert (new_version3 == new_version6) is False\n    assert (new_version3 == new_version7) is False\n    assert (new_version3 == new_version8) is False\n\n    assert (new_version4 == new_version4) is True\n    assert (new_version4 == new_version5) is False\n    assert (new_version4 == new_version6) is False\n    assert (new_version4 == new_version7) is False\n    assert (new_version4 == new_version8) is False\n\n    assert (new_version5 == new_version5) is True\n    assert (new_version5 == new_version6) is False\n    assert (new_version5 == new_version7) is False\n    assert (new_version5 == new_version8) is False\n\n    assert (new_version6 == new_version6) is True\n    assert (new_version6 == new_version7) is False\n    assert (new_version6 == new_version8) is False\n\n    assert (new_version7 == new_version7) is True\n    assert (new_version6 == new_version8) is False\n\n    assert (new_version8 == new_version8) is True\n\n\ndef test_inequality_operator(setup_version_db_tests):\n    \"\"\"inequality of two Version instances.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version1)\n\n    new_version2 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version2)\n\n    new_version3 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version3)\n\n    new_version4 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version4)\n\n    new_version5 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version5)\n\n    # with new revision number\n    data[\"kwargs\"][\"revision_number\"] = 2\n    new_version6 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version6)\n    new_version7 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version7)\n    new_version8 = Version(**data[\"kwargs\"])\n    DBSession.save(new_version8)\n\n    new_version1.is_published = True\n    new_version3.is_published = True\n    new_version4.is_published = True\n\n    new_version7.is_published = True\n\n    assert (new_version1 != new_version1) is False\n    assert (new_version1 != new_version2) is True\n    assert (new_version1 != new_version3) is True\n    assert (new_version1 != new_version4) is True\n    assert (new_version1 != new_version5) is True\n    assert (new_version1 != new_version6) is True\n    assert (new_version1 != new_version7) is True\n    assert (new_version1 != new_version8) is True\n\n    assert (new_version2 != new_version2) is False\n    assert (new_version2 != new_version3) is True\n    assert (new_version2 != new_version4) is True\n    assert (new_version2 != new_version5) is True\n    assert (new_version2 != new_version6) is True\n    assert (new_version2 != new_version7) is True\n    assert (new_version2 != new_version8) is True\n\n    assert (new_version3 != new_version3) is False\n    assert (new_version3 != new_version4) is True\n    assert (new_version3 != new_version5) is True\n    assert (new_version3 != new_version6) is True\n    assert (new_version3 != new_version7) is True\n    assert (new_version3 != new_version8) is True\n\n    assert (new_version4 != new_version4) is False\n    assert (new_version4 != new_version5) is True\n    assert (new_version4 != new_version6) is True\n    assert (new_version4 != new_version7) is True\n    assert (new_version4 != new_version8) is True\n\n    assert (new_version5 != new_version5) is False\n    assert (new_version5 != new_version6) is True\n    assert (new_version5 != new_version7) is True\n    assert (new_version5 != new_version8) is True\n\n    assert (new_version6 != new_version6) is False\n    assert (new_version6 != new_version7) is True\n    assert (new_version6 != new_version8) is True\n\n    assert (new_version7 != new_version7) is False\n    assert (new_version6 != new_version8) is True\n\n    assert (new_version8 != new_version8) is False\n\n\ndef test_max_version_number_attribute_is_read_only(setup_version_db_tests):\n    \"\"\"max_version_number attribute is read only.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_version\"].max_version_number = 20\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'max_version_number'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'max_version_number' of 'Version' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_max_version_number_attribute_is_working_as_expected(setup_version_db_tests):\n    \"\"\"max_version_number attribute is working as expected.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n\n    new_version2 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version2)\n    DBSession.commit()\n\n    new_version3 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version3)\n    DBSession.commit()\n\n    new_version4 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version4)\n    DBSession.commit()\n\n    new_version5 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version5)\n    DBSession.commit()\n\n    assert new_version5.version_number == 6\n\n    assert new_version1.max_version_number == 6\n    assert new_version2.max_version_number == 6\n    assert new_version3.max_version_number == 6\n    assert new_version4.max_version_number == 6\n    assert new_version5.max_version_number == 6\n\n\ndef test_latest_version_attribute_is_read_only(setup_version_db_tests):\n    \"\"\"latest_version attribute is a read only attribute.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_version\"].latest_version = 3453\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'latest_version'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'latest_version' of 'Version' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_latest_version_attribute_is_working_as_expected(setup_version_db_tests):\n    \"\"\"latest_version attribute is working as expected.\"\"\"\n    data = setup_version_db_tests\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n\n    new_version2 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version2)\n    DBSession.commit()\n\n    new_version3 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version3)\n    DBSession.commit()\n\n    new_version4 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version4)\n    DBSession.commit()\n\n    new_version5 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version5)\n    DBSession.commit()\n\n    assert new_version5.version_number == 6\n\n    assert new_version1.latest_version == new_version5\n    assert new_version2.latest_version == new_version5\n    assert new_version3.latest_version == new_version5\n    assert new_version4.latest_version == new_version5\n    assert new_version5.latest_version == new_version5\n\n\ndef test_latest_version_attribute_is_working_as_expected_for_different_revision_numbers(\n    setup_version_db_tests,\n):\n    \"\"\"latest_version attribute is working as expected for different revision_numbers.\"\"\"\n    data = setup_version_db_tests\n    data[\"kwargs\"][\"revision_number\"] = 1\n    new_version1 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version1)\n    DBSession.commit()\n\n    new_version2 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version2)\n    DBSession.commit()\n\n    data[\"kwargs\"][\"revision_number\"] = 2\n    new_version3 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version3)\n    DBSession.commit()\n\n    new_version4 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version4)\n    DBSession.commit()\n\n    data[\"kwargs\"][\"revision_number\"] = 3\n    new_version5 = Version(**data[\"kwargs\"])\n    DBSession.add(new_version5)\n    DBSession.commit()\n\n    assert new_version5.version_number == 1\n\n    assert new_version1.latest_version == new_version2\n    assert new_version2.latest_version == new_version2\n    assert new_version3.latest_version == new_version4\n    assert new_version4.latest_version == new_version4\n    assert new_version5.latest_version == new_version5\n\n\ndef test_naming_parents_attribute_is_a_read_only_property(setup_version_db_tests):\n    \"\"\"naming_parents attribute is a read only property.\"\"\"\n    data = setup_version_db_tests\n    with pytest.raises(AttributeError) as cm:\n        data[\"test_version\"].naming_parents = [data[\"test_task1\"]]\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'naming_parents'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'naming_parents' of 'Version' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_naming_parents_attribute_is_working_as_expected(setup_version_db_tests):\n    \"\"\"naming_parents attribute is working as expected.\"\"\"\n    data = setup_version_db_tests\n    # for data[\"test_version\"]\n    assert data[\"test_version\"].naming_parents == [\n        data[\"test_shot1\"],\n        data[\"test_task1\"],\n        data[\"test_variant1\"],\n    ]\n\n    # for a new version of a task\n    task1 = Task(\n        name=\"Test Task 1\",\n        project=data[\"test_project\"],\n    )\n\n    task2 = Task(\n        name=\"Test Task 2\",\n        parent=task1,\n    )\n\n    task3 = Task(\n        name=\"Test Task 3\",\n        parent=task2,\n    )\n    DBSession.add_all([task1, task2, task3])\n    DBSession.commit()\n\n    version1 = Version(task=task3)\n    DBSession.add(version1)\n    DBSession.commit()\n\n    assert version1.naming_parents == [task1, task2, task3]\n\n    # for an asset version\n    character_type = Type(target_entity_type=\"Asset\", name=\"Character\", code=\"Char\")\n    asset1 = Asset(name=\"Asset1\", code=\"Asset1\", parent=task1, type=character_type)\n    DBSession.add(asset1)\n    DBSession.commit()\n\n    version2 = Version(task=asset1)\n    assert version2.naming_parents == [asset1]\n\n    # for a version of a task of a shot\n    shot2 = Shot(\n        name=\"SH002\",\n        code=\"SH002\",\n        parent=task3,\n    )\n    DBSession.add(shot2)\n    DBSession.commit()\n\n    task4 = Task(\n        name=\"Test Task 4\",\n        parent=shot2,\n    )\n    DBSession.add(task4)\n    DBSession.commit()\n\n    version3 = Version(task=task4)\n\n    assert version3.naming_parents == [shot2, task4]\n\n    # for an asset of a shot\n    asset2 = Asset(name=\"Asset2\", code=\"Asset2\", parent=shot2, type=character_type)\n    DBSession.add(asset2)\n    DBSession.commit()\n\n    version4 = Version(task=asset2)\n    assert version4.naming_parents == [asset2]\n\n\ndef test_nice_name_attribute_is_working_as_expected(setup_version_db_tests):\n    \"\"\"nice_name attribute is working as expected.\"\"\"\n    data = setup_version_db_tests\n    # for data[\"test_version\"]\n    assert data[\"test_version\"].naming_parents == [\n        data[\"test_shot1\"],\n        data[\"test_task1\"],\n        data[\"test_variant1\"],\n    ]\n\n    # for a new version of a task\n    task1 = Task(\n        name=\"Test Task 1\",\n        project=data[\"test_project\"],\n    )\n\n    task2 = Task(\n        name=\"Test Task 2\",\n        parent=task1,\n    )\n\n    task3 = Task(\n        name=\"Test Task 3\",\n        parent=task2,\n    )\n    DBSession.add_all([task1, task2, task3])\n    DBSession.commit()\n\n    version1 = Version(task=task3)\n    DBSession.add(version1)\n    DBSession.commit()\n\n    assert version1.nice_name == \"{}_{}_{}\".format(\n        task1.nice_name,\n        task2.nice_name,\n        task3.nice_name,\n    )\n\n    # for an asset version\n    character_type = Type(target_entity_type=\"Asset\", name=\"Character\", code=\"Char\")\n    asset1 = Asset(name=\"Asset1\", code=\"Asset1\", parent=task1, type=character_type)\n    DBSession.add(asset1)\n    DBSession.commit()\n\n    version2 = Version(task=asset1)\n    assert version2.nice_name == \"{}\".format(asset1.nice_name)\n\n    # for a version of a task of a shot\n    shot2 = Shot(\n        name=\"SH002\",\n        code=\"SH002\",\n        parent=task3,\n    )\n    DBSession.add(shot2)\n    DBSession.commit()\n\n    task4 = Task(\n        name=\"Test Task 4\",\n        parent=shot2,\n    )\n    DBSession.add(task4)\n    DBSession.commit()\n\n    version3 = Version(task=task4)\n\n    assert version3.nice_name == \"{}_{}\".format(\n        shot2.nice_name,\n        task4.nice_name,\n    )\n\n    # for an asset of a shot\n    asset2 = Asset(name=\"Asset2\", code=\"Asset2\", parent=shot2, type=character_type)\n    DBSession.add(asset2)\n    DBSession.commit()\n\n    version4 = Version(task=asset2)\n    assert version4.nice_name == \"{}\".format(asset2.nice_name)\n\n\ndef test_string_representation_is_a_little_bit_meaningful(setup_version_db_tests):\n    \"\"\"__str__ or __repr__ result is meaningful.\"\"\"\n    data = setup_version_db_tests\n    assert \"<tp_SH001_FX_Main_v001 (Version)>\" == f'{data[\"test_version\"]}'\n\n\ndef test_walk_hierarchy_is_working_as_expected_in_dfs_mode(setup_version_db_tests):\n    \"\"\"walk_hierarchy() method is working in DFS mode correctly.\"\"\"\n    data = setup_version_db_tests\n    v1 = Version(task=data[\"test_task1\"])\n    v2 = Version(task=data[\"test_task1\"], parent=v1)\n    v3 = Version(task=data[\"test_task1\"], parent=v2)\n    v4 = Version(task=data[\"test_task1\"], parent=v3)\n    v5 = Version(task=data[\"test_task1\"], parent=v1)\n    expected_result = [v1, v2, v3, v4, v5]\n    visited_versions = []\n    for v in v1.walk_hierarchy():\n        visited_versions.append(v)\n    assert expected_result == visited_versions\n\n\n# def test_path_attribute_value_is_calculated_on_init(setup_version_db_tests):\n#     \"\"\"path attribute value is automatically calculated on\n#     Version instance initialize\n#     \"\"\"\n#     ft = FilenameTemplate(\n#         name='Task Filename Template',\n#         target_entity_type='Task',\n#         path='{{project.code}}/{%- for p in parent_tasks -%}'\n#              '{{p.nice_name}}/{%- endfor -%}',\n#         filename='{{version.nice_name}}_v{{\"%03d\"|format(version.version_number)}}{{extension}}'\n#     )\n#     data[\"test_project\"].structure.templates.append(ft)\n#     DBSession.add(data[\"test_project\"])\n#     DBSession.commit()\n#\n#     print('entity_type: {}'.format(data[\"test_task1\"].entity_type))\n#\n#     # v1 = Version(task=data[\"test_task1\"])\n#     # assert 'tp/SH001/task1/task1_Main_v001' == v1.path\n#     data[\"fail\"]()\n\n\ndef test_reviews_attribute_is_a_list_of_reviews(setup_version_db_tests):\n    \"\"\"Version.reviews attribute is filled with Review instances.\"\"\"\n    data = setup_version_db_tests\n    data[\"test_variant1\"].status = data[\"status_wip\"]\n    data[\"test_variant1\"].responsible = [data[\"test_user1\"], data[\"test_user2\"]]\n    version = Version(task=data[\"test_variant1\"])\n\n    # request a review\n    reviews = data[\"test_variant1\"].request_review(version=version)\n    assert reviews[0].version == version\n    assert reviews[1].version == version\n    assert isinstance(version.reviews, list)\n    assert len(version.reviews) == 2\n    assert version.reviews == reviews\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_version_tests():\n    \"\"\"Set up non-DB related tests of Version class.\"\"\"\n    data = dict()\n    data[\"patcher\"] = PlatformPatcher()\n\n    # users\n    # test users\n    data[\"test_user1\"] = User(\n        name=\"Test User 1\",\n        login=\"tuser1\",\n        email=\"tuser1@test.com\",\n        password=\"secret\",\n    )\n    data[\"test_user2\"] = User(\n        name=\"Test User 2\",\n        login=\"tuser2\",\n        email=\"tuser2@test.com\",\n        password=\"secret\",\n    )\n\n    # statuses\n    data[\"test_status1\"] = Status(name=\"Status1\", code=\"STS1\")\n    data[\"test_status2\"] = Status(name=\"Status2\", code=\"STS2\")\n    data[\"test_status3\"] = Status(name=\"Status3\", code=\"STS3\")\n    data[\"test_status4\"] = Status(name=\"Status4\", code=\"STS4\")\n    data[\"test_status5\"] = Status(name=\"Status5\", code=\"STS5\")\n\n    # status lists\n    data[\"test_task_status_list\"] = StatusList(\n        name=\"Task Status List\",\n        statuses=[\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ],\n        target_entity_type=\"Task\",\n    )\n\n    data[\"test_asset_status_list\"] = StatusList(\n        name=\"Asset Status List\",\n        statuses=[\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ],\n        target_entity_type=\"Asset\",\n    )\n\n    data[\"test_shot_status_list\"] = StatusList(\n        name=\"Shot Status List\",\n        statuses=[\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ],\n        target_entity_type=\"Shot\",\n    )\n\n    data[\"test_sequence_status_list\"] = StatusList(\n        name=\"Sequence Status List\",\n        statuses=[\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ],\n        target_entity_type=\"Sequence\",\n    )\n\n    data[\"test_project_status_list\"] = StatusList(\n        name=\"Project Status List\",\n        statuses=[\n            data[\"test_status1\"],\n            data[\"test_status2\"],\n            data[\"test_status3\"],\n            data[\"test_status4\"],\n            data[\"test_status5\"],\n        ],\n        target_entity_type=\"Project\",\n    )\n\n    # repository\n    data[\"test_repo\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        linux_path=\"/mnt/T/\",\n        windows_path=\"T:/\",\n        macos_path=\"/Volumes/T/\",\n    )\n\n    # a project type\n    data[\"test_project_type\"] = Type(\n        name=\"Test\",\n        code=\"test\",\n        target_entity_type=\"Project\",\n    )\n\n    # create a structure\n    data[\"test_structure\"] = Structure(name=\"Test Project Structure\")\n\n    # create a project\n    data[\"test_project\"] = Project(\n        name=\"Test Project\",\n        code=\"tp\",\n        type=data[\"test_project_type\"],\n        status_list=data[\"test_project_status_list\"],\n        repositories=[data[\"test_repo\"]],\n        structure=data[\"test_structure\"],\n    )\n\n    # create a sequence\n    data[\"test_sequence\"] = Sequence(\n        name=\"Test Sequence\",\n        code=\"SEQ1\",\n        project=data[\"test_project\"],\n        status_list=data[\"test_sequence_status_list\"],\n    )\n\n    # create a shot\n    data[\"test_shot1\"] = Shot(\n        name=\"SH001\",\n        code=\"SH001\",\n        project=data[\"test_project\"],\n        sequence=data[\"test_sequence\"],\n        status_list=data[\"test_shot_status_list\"],\n    )\n\n    # create a group of Tasks for the shot\n    data[\"test_task1\"] = Task(\n        name=\"Task1\",\n        parent=data[\"test_shot1\"],\n        status_list=data[\"test_task_status_list\"],\n    )\n\n    data[\"test_task2\"] = Task(\n        name=\"Task2\",\n        parent=data[\"test_shot1\"],\n        status_list=data[\"test_task_status_list\"],\n    )\n\n    # a File for the input file\n    data[\"test_input_file1\"] = File(\n        name=\"Input File 1\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"Outputs/SH001_beauty_v001.###.exr\",\n    )\n\n    data[\"test_input_file2\"] = File(\n        name=\"Input File 2\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"Outputs/SH001_occ_v001.###.exr\",\n    )\n\n    # a File for the output file\n    data[\"test_output_file1\"] = File(\n        name=\"Output File 1\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"Outputs/SH001_beauty_v001.###.exr\",\n    )\n\n    data[\"test_output_file2\"] = File(\n        name=\"Output File 2\",\n        full_path=\"/mnt/M/JOBs/TestProj/Seqs/TestSeq/Shots/SH001/FX/\"\n        \"Outputs/SH001_occ_v001.###.exr\",\n    )\n\n    # now create a version for the Task\n    data[\"kwargs\"] = {\n        \"task\": data[\"test_task1\"],\n    }\n\n    # and the Version\n    data[\"test_version\"] = Version(**data[\"kwargs\"])\n\n    # set the published to False\n    data[\"test_version\"].is_published = False\n    yield data\n    # clean up test\n    data[\"patcher\"].restore()\n\n\ndef test_children_attribute_will_not_allow_circular_dependencies_2(\n    setup_version_tests,\n):\n    \"\"\"CircularDependency error raised if a parent is set as a child to its child.\"\"\"\n    data = setup_version_tests\n    data[\"kwargs\"][\"parent\"] = None\n    new_version1 = Version(**data[\"kwargs\"])\n    new_version2 = Version(**data[\"kwargs\"])\n\n    new_version1.parent = new_version2\n    with pytest.raises(CircularDependencyError) as cm:\n        new_version1.children.append(new_version2)\n\n    assert (\n        str(cm.value) == \"<tp_SH001_Task1_v003 (Version)> (Version) and \"\n        \"<tp_SH001_Task1_v002 (Version)> (Version) are in a \"\n        'circular dependency in their \"children\" attribute'\n    )\n\n\ndef test_children_attribute_will_not_allow_deeper_circular_dependencies_2(\n    setup_version_tests,\n):\n    \"\"\"CircularDependency error raised if the parent of a parent Version is set as a\n    children to its grand child.\"\"\"\n    data = setup_version_tests\n    data[\"kwargs\"][\"parent\"] = None\n    new_version1 = Version(**data[\"kwargs\"])\n    new_version2 = Version(**data[\"kwargs\"])\n    new_version3 = Version(**data[\"kwargs\"])\n\n    new_version1.parent = new_version2\n    new_version2.parent = new_version3\n\n    with pytest.raises(CircularDependencyError) as cm:\n        new_version1.children.append(new_version3)\n\n    assert (\n        str(cm.value) == \"<tp_SH001_Task1_v004 (Version)> (Version) and \"\n        \"<tp_SH001_Task1_v002 (Version)> (Version) are in a \"\n        'circular dependency in their \"children\" attribute'\n    )\n\n\ndef test_version_number_without_a_db(setup_version_tests):\n    \"\"\"version_number without a db is not None.\"\"\"\n    data = setup_version_tests\n    v = Version(task=data[\"test_task2\"])\n    assert v.version_number is not None\n\n\ndef test_version_number_without_a_db(setup_version_tests):\n    \"\"\"version_number without a db is not None.\"\"\"\n    data = setup_version_tests\n    v1 = Version(task=data[\"test_task2\"])\n    assert v1.version_number == 1\n    v2 = Version(task=data[\"test_task2\"])\n    assert v2.version_number == 2\n    v3 = Version(task=data[\"test_task2\"])\n    assert v3.version_number == 3\n\n\ndef test_latest_version_without_a_db(setup_version_tests):\n    \"\"\"latest_version without a db returns self.\"\"\"\n    data = setup_version_tests\n    v = Version(task=data[\"test_task2\"])\n    assert v.latest_version is v\n\n\ndef test_max_version_number_without_a_db(setup_version_tests):\n    \"\"\"max_version_number without a db returns self.version_number.\"\"\"\n    data = setup_version_tests\n    v = Version(task=data[\"test_task2\"])\n    assert v.max_version_number == v.version_number\n\n\ndef test__hash__is_working_as_expected(setup_version_tests):\n    \"\"\"__hash__ is working as expected.\"\"\"\n    data = setup_version_tests\n    v = Version(task=data[\"test_task2\"])\n    result = hash(v)\n    assert isinstance(result, int)\n    assert result == v.__hash__()\n\n\ndef test_request_review_method_calls_task_request_review_method(\n    setup_version_tests, monkeypatch\n):\n    \"\"\"request_review() calls Task.request_review() method.\"\"\"\n    data = setup_version_tests\n    called = []\n\n    def patched_request_review(self, version=None):\n        \"\"\"Patch the request review method.\"\"\"\n        called.append(version)\n\n    data[\"test_task2\"].responsible = [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n    ]\n\n    monkeypatch.setattr(\n        \"stalker.models.version.Task.request_review\", patched_request_review\n    )\n    v = Version(task=data[\"test_task2\"])\n\n    assert len(called) == 0\n    _ = v.request_review()\n    assert len(called) == 1\n    assert called[0] == v\n\n\ndef test_request_review_method_returns_reviews(setup_version_db_tests):\n    \"\"\"request_review() returns Reviews.\"\"\"\n    data = setup_version_db_tests\n    task = data[\"test_variant1\"]\n    task.responsible = [\n        data[\"test_user1\"],\n        data[\"test_user2\"],\n    ]\n    task.status = data[\"status_wip\"]\n    v = Version(task=task)\n    reviews = v.request_review()\n    assert len(reviews) == 2\n    from stalker.models.review import Review\n\n    assert isinstance(reviews[0], Review)\n    assert isinstance(reviews[1], Review)\n\n\ndef test_variant_name_attr_does_not_exist(setup_version_tests):\n    \"\"\"Version.variant_name does not exist anymore.\"\"\"\n    data = setup_version_tests\n    assert hasattr(data[\"test_version\"], \"variant_name\") is False\n"
  },
  {
    "path": "tests/models/test_wiki.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests related to the Wiki class.\"\"\"\n\nimport pytest\n\nfrom stalker import Page, Project, Repository, Status, StatusList, Type\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_page_tests():\n    \"\"\"Set up tests for the Page class.\"\"\"\n    data = dict()\n    # create a repository\n    data[\"repository_type\"] = Type(\n        name=\"Test Repository Type\", code=\"test_repo\", target_entity_type=\"Repository\"\n    )\n\n    data[\"test_repository\"] = Repository(\n        name=\"Test Repository\",\n        code=\"TR\",\n        type=data[\"repository_type\"],\n    )\n\n    # statuses\n    data[\"status1\"] = Status(name=\"Status1\", code=\"STS1\")\n    data[\"status2\"] = Status(name=\"Status2\", code=\"STS2\")\n    data[\"status3\"] = Status(name=\"Status3\", code=\"STS3\")\n\n    # project status list\n    data[\"project_status_list\"] = StatusList(\n        name=\"Project Status List\",\n        statuses=[\n            data[\"status1\"],\n            data[\"status2\"],\n            data[\"status3\"],\n        ],\n        target_entity_type=\"Project\",\n    )\n\n    # project type\n    data[\"test_project_type\"] = Type(\n        name=\"Test Project Type\",\n        code=\"testproj\",\n        target_entity_type=\"Project\",\n    )\n\n    # create projects\n    data[\"test_project1\"] = Project(\n        name=\"Test Project 1\",\n        code=\"tp1\",\n        type=data[\"test_project_type\"],\n        status_list=data[\"project_status_list\"],\n        repository=data[\"test_repository\"],\n    )\n\n    data[\"kwargs\"] = {\n        \"title\": \"Test Page Title\",\n        \"content\": \"Test content\",\n        \"project\": data[\"test_project1\"],\n    }\n\n    data[\"test_page\"] = Page(**data[\"kwargs\"])\n    return data\n\n\ndef test_title_argument_is_skipped(setup_page_tests):\n    \"\"\"ValueError is raised if the title argument is skipped.\"\"\"\n    data = setup_page_tests\n    data[\"kwargs\"].pop(\"title\")\n    with pytest.raises(ValueError) as cm:\n        Page(**data[\"kwargs\"])\n    assert str(cm.value) == \"Page.title cannot be empty\"\n\n\ndef test_title_argument_is_none(setup_page_tests):\n    \"\"\"TypeError is raised if the title argument is None.\"\"\"\n    data = setup_page_tests\n    data[\"kwargs\"][\"title\"] = None\n    with pytest.raises(TypeError) as cm:\n        Page(**data[\"kwargs\"])\n    assert str(cm.value) == \"Page.title should be a string, not NoneType: 'None'\"\n\n\ndef test_title_attribute_is_set_to_none(setup_page_tests):\n    \"\"\"TypeError is raised if the title attribute is set to None.\"\"\"\n    data = setup_page_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_page\"].title = None\n    assert str(cm.value) == \"Page.title should be a string, not NoneType: 'None'\"\n\n\ndef test_title_argument_is_an_empty_string(setup_page_tests):\n    \"\"\"ValueError is raised if the title argument is an empty string.\"\"\"\n    data = setup_page_tests\n    data[\"kwargs\"][\"title\"] = \"\"\n    with pytest.raises(ValueError) as cm:\n        Page(**data[\"kwargs\"])\n    assert str(cm.value) == \"Page.title cannot be empty\"\n\n\ndef test_title_attribute_is_set_to_empty_string(setup_page_tests):\n    \"\"\"ValueError is raised if the title attribute is set to empty string.\"\"\"\n    data = setup_page_tests\n    with pytest.raises(ValueError) as cm:\n        data[\"test_page\"].title = \"\"\n    assert str(cm.value) == \"Page.title cannot be empty\"\n\n\ndef test_title_argument_is_not_a_string(setup_page_tests):\n    \"\"\"TypeError is raised if the title argument is not a string.\"\"\"\n    data = setup_page_tests\n    data[\"kwargs\"][\"title\"] = 2165\n    with pytest.raises(TypeError) as cm:\n        Page(**data[\"kwargs\"])\n    assert str(cm.value) == \"Page.title should be a string, not int: '2165'\"\n\n\ndef test_title_attribute_is_not_a_string(setup_page_tests):\n    \"\"\"TypeError is raised if the title is set to a value other than a string.\"\"\"\n    data = setup_page_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_page\"].title = 2135\n    assert str(cm.value) == \"Page.title should be a string, not int: '2135'\"\n\n\ndef test_title_argument_is_working_as_expected(setup_page_tests):\n    \"\"\"title argument value is correctly passed to title attribute.\"\"\"\n    data = setup_page_tests\n    assert data[\"test_page\"].title == data[\"kwargs\"][\"title\"]\n\n\ndef test_title_attribute_is_working_as_expected(setup_page_tests):\n    \"\"\"title attribute is working as expected.\"\"\"\n    data = setup_page_tests\n    test_value = \"Test Title 2\"\n    data[\"test_page\"].title = test_value\n    assert data[\"test_page\"].title == test_value\n\n\ndef test_content_argument_skipped(setup_page_tests):\n    \"\"\"content attr value is an empty str if the content argument is skipped.\"\"\"\n    data = setup_page_tests\n    data[\"kwargs\"].pop(\"content\")\n    new_page = Page(**data[\"kwargs\"])\n    assert new_page.content == \"\"\n\n\ndef test_content_argument_is_None(setup_page_tests):\n    \"\"\"content attribute value is an empty string if the content argument is None.\"\"\"\n    data = setup_page_tests\n    data[\"kwargs\"][\"content\"] = None\n    new_page = Page(**data[\"kwargs\"])\n    assert new_page.content == \"\"\n\n\ndef test_content_attribute_is_set_to_None(setup_page_tests):\n    \"\"\"content attr value is an empty string if the content attribute is set to None.\"\"\"\n    data = setup_page_tests\n    assert data[\"test_page\"].content != \"\"\n    data[\"test_page\"].content = None\n    assert data[\"test_page\"].content == \"\"\n\n\ndef test_content_argument_is_empty_string(setup_page_tests):\n    \"\"\"content attr value is an empty string if the content arg is an empty string.\"\"\"\n    data = setup_page_tests\n    data[\"kwargs\"][\"content\"] = \"\"\n    new_page = Page(**data[\"kwargs\"])\n    assert new_page.content == \"\"\n\n\ndef test_content_attribute_is_set_to_an_empty_string(setup_page_tests):\n    \"\"\"content attribute can be set to an empty string.\"\"\"\n    data = setup_page_tests\n    data[\"test_page\"].content = \"\"\n    assert data[\"test_page\"].content == \"\"\n\n\ndef test_content_argument_is_not_a_string(setup_page_tests):\n    \"\"\"TypeError is raised if the content argument is not a str.\"\"\"\n    data = setup_page_tests\n    data[\"kwargs\"][\"content\"] = 1234\n    with pytest.raises(TypeError) as cm:\n        Page(**data[\"kwargs\"])\n    assert str(cm.value) == \"Page.content should be a string, not int: '1234'\"\n\n\ndef test_content_attribute_is_set_to_a_value_other_than_a_string(setup_page_tests):\n    \"\"\"TypeError is raised if the content attr is not a str.\"\"\"\n    data = setup_page_tests\n    with pytest.raises(TypeError) as cm:\n        data[\"test_page\"].content = [\"not\", \"a\", \"string\"]\n    assert str(cm.value) == (\n        \"Page.content should be a string, not list: '['not', 'a', 'string']'\"\n    )\n\n\ndef test_content_argument_is_working_as_expected(setup_page_tests):\n    \"\"\"content argument value is correctly passed to the content attribute.\"\"\"\n    data = setup_page_tests\n    assert data[\"test_page\"].content == data[\"kwargs\"][\"content\"]\n\n\ndef test_content_attribute_is_working_as_expected(setup_page_tests):\n    \"\"\"content attribute value can be correctly set.\"\"\"\n    data = setup_page_tests\n    test_value = \"This is a test content\"\n    assert data[\"test_page\"].content != test_value\n    data[\"test_page\"].content = test_value\n    assert data[\"test_page\"].content == test_value\n"
  },
  {
    "path": "tests/models/test_working_hours.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests related to the WorkingHours class.\"\"\"\nimport copy\nimport datetime\nimport sys\n\nimport pytest\n\nimport pytz\n\nfrom stalker import defaults\nfrom stalker.models.studio import WorkingHours\n\n\ndef test___auto_name___is_true():\n    \"\"\"WorkingHours.__auto_name__ is True\"\"\"\n    assert WorkingHours.__auto_name__ is True\n\n\ndef test_working_hours_argument_is_skipped():\n    \"\"\"WorkingHours is created with the default settings by default.\"\"\"\n    wh = WorkingHours()\n    assert wh.working_hours == defaults.working_hours\n\n\ndef test_working_hours_argument_is_none():\n    \"\"\"WorkingHours created with default settings if the working_hours arg is None.\"\"\"\n    wh = WorkingHours(working_hours=None)\n    assert wh.working_hours == defaults.working_hours\n\n\ndef test_working_hours_argument_is_not_a_dictionary():\n    \"\"\"TypeError is raised if the working_hours argument value is not a dictionary.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        WorkingHours(working_hours=\"not a dictionary of proper values\")\n\n    assert str(cm.value) == (\n        \"WorkingHours.working_hours should be a dictionary, \"\n        \"not str: 'not a dictionary of proper values'\"\n    )\n\n\ndef test_working_hours_attribute_is_not_a_dictionary():\n    \"\"\"TypeError raised if the working_hours attr is not a dictionary.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(TypeError) as cm:\n        wh.working_hours = \"not a dictionary of proper values\"\n\n    assert str(cm.value) == (\n        \"WorkingHours.working_hours should be a dictionary, \"\n        \"not str: 'not a dictionary of proper values'\"\n    )\n\n\ndef test_working_hours_argument_value_is_dictionary_of_other_formatted_data():\n    \"\"\"TypeError raised if the working_hours arg is not a dict of list of two int.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        WorkingHours(working_hours={\"not\": \"properly valued\"})\n\n    assert str(cm.value) == (\n        \"WorkingHours.working_hours should be a dictionary with keys \"\n        '\"mon, tue, wed, thu, fri, sat, sun\" and the values should a list '\n        \"of lists of two integers like [[540, 720], [800, 1080]], \"\n        \"not str: 'properly valued'\"\n    )\n\n\ndef test_working_hours_attribute_is_set_to_a_dictionary_of_other_formatted_data():\n    \"\"\"TypeError raised if the working hours attr is a dict of some other value.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(TypeError) as cm:\n        wh.working_hours = {\"not\": \"properly valued\"}\n\n    assert (\n        str(cm.value) == \"WorkingHours.working_hours should be a dictionary with keys \"\n        '\"mon, tue, wed, thu, fri, sat, sun\" and the values should a '\n        \"list of lists of two integers like [[540, 720], [800, 1080]], \"\n        \"not str: 'properly valued'\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"test_key, test_value\",\n    [\n        [\"sun\", [[-10, 1000]]],\n        [\"sat\", [[900, 1080], [1090, 1500]]],\n    ],\n)\ndef test_working_hours_argument_data_is_not_in_correct_range1(test_key, test_value):\n    \"\"\"ValueError raised if the time values are not correct in the working_hours arg.\"\"\"\n    wh = copy.copy(defaults.working_hours)\n    wh[test_key] = test_value\n    with pytest.raises(ValueError) as cm:\n        WorkingHours(working_hours=wh)\n\n    assert str(cm.value) == (\n        \"WorkingHours.working_hours value should be a list of lists of \"\n        \"two integers and the range of integers should be between 0-1440, \"\n        f\"not list: '{test_value}'\"\n    )\n\n\n@pytest.mark.parametrize(\n    \"test_key, test_value\",\n    [\n        [\"sun\", [[-10, 1000]]],\n        [\"sat\", [[900, 1080], [1090, 1500]]],\n    ],\n)\ndef test_working_hours_attribute_data_is_not_in_correct_range1(test_key, test_value):\n    \"\"\"ValueError raised if the times are not correct in the working_hours attr.\"\"\"\n    wh = copy.copy(defaults.working_hours)\n    wh[test_key] = test_value\n\n    wh_ins = WorkingHours()\n    with pytest.raises(ValueError) as cm:\n        wh_ins.working_hours = wh\n\n    assert str(cm.value) == (\n        \"WorkingHours.working_hours value should be a list of lists of \"\n        \"two integers and the range of integers should be between 0-1440, \"\n        f\"not list: '{test_value}'\"\n    )\n\n\ndef test_working_hours_argument_value_is_not_complete():\n    \"\"\"default values are used for missing days in the given working_hours arg.\"\"\"\n    working_hours = {\"sat\": [[900, 1080]], \"sun\": [[900, 1080]]}\n    wh = WorkingHours(working_hours=working_hours)\n    assert wh[\"mon\"] == defaults.working_hours[\"mon\"]\n    assert wh[\"tue\"] == defaults.working_hours[\"tue\"]\n    assert wh[\"wed\"] == defaults.working_hours[\"wed\"]\n    assert wh[\"thu\"] == defaults.working_hours[\"thu\"]\n    assert wh[\"fri\"] == defaults.working_hours[\"fri\"]\n\n\ndef test_working_hours_attribute_value_is_not_complete():\n    \"\"\"default values are used for missing days in the given working_hours attr.\"\"\"\n    working_hours = {\"sat\": [[900, 1080]], \"sun\": [[900, 1080]]}\n    wh = WorkingHours()\n    wh.working_hours = working_hours\n    assert wh[\"mon\"] == defaults.working_hours[\"mon\"]\n    assert wh[\"tue\"] == defaults.working_hours[\"tue\"]\n    assert wh[\"wed\"] == defaults.working_hours[\"wed\"]\n    assert wh[\"thu\"] == defaults.working_hours[\"thu\"]\n    assert wh[\"fri\"] == defaults.working_hours[\"fri\"]\n\n\ndef test_working_hours_can_be_indexed_with_day_number():\n    \"\"\"working hours for a day can be reached by an index.\"\"\"\n    wh = WorkingHours()\n    assert wh[6] == defaults.working_hours[\"sun\"]\n    # this should not raise any errors\n    wh[6] = [[540, 1080]]\n\n\ndef test_working_hours_day_0_is_monday():\n    \"\"\"day zero is monday.\"\"\"\n    wh = WorkingHours()\n    wh[0] = [[270, 980]]\n    assert wh[\"mon\"] == wh[0]\n\n\ndef test_working_hours_can_be_string_indexed_with_the_date_short_name():\n    \"\"\"working hours info can be reached by using the short date name as the index.\"\"\"\n    wh = WorkingHours()\n    assert wh[\"sun\"] == defaults.working_hours[\"sun\"]\n    # this should not raise any errors\n    wh[\"sun\"] = [[540, 1080]]\n\n\n@pytest.mark.parametrize(\n    \"test_key, test_value, error_type\",\n    [\n        [0, \"not a proper data\", TypeError],\n        [\"sun\", \"not a proper data\", TypeError],\n        [0, [\"no proper data\"], TypeError],\n        [\"sun\", [\"no proper data\"], TypeError],\n        [0, [[\"no proper data\"]], ValueError],\n        [\"sun\", [[\"no proper data\"]], ValueError],\n        [0, [[3]], ValueError],\n        [2, [[2, \"a\"]], TypeError],\n        [1, [[20, 10], [\"a\", 300]], TypeError],\n        [5, [[323, 1344], [2, \"d\"]], TypeError],\n        [0, [[4, 100, 3]], ValueError],\n        [\"mon\", [[3]], ValueError],\n        [\"mon\", [[2, \"a\"]], TypeError],\n        [\"tue\", [[20, 10], [\"a\", 300]], TypeError],\n        [\"fri\", [[323, 1344], [2, \"d\"]], TypeError],\n        [\"sat\", [[4, 100, 3]], ValueError],\n        [\"sun\", [[-10, 100]], ValueError],\n        [\"sat\", [[0, 1800]], ValueError],\n        [7, [[32, 23], [233, 324]], IndexError],\n        [7, [[32, 23], [233, 324]], IndexError],\n        [\"zon\", [[32, 23], [233, 324]], KeyError],\n    ],\n)\ndef test___setitem__checks_the_given_data(test_key, test_value, error_type):\n    \"\"\"__setitem__ checks the given data format.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(error_type) as cm:\n        wh[test_key] = test_value\n\n    error_message = {\n        TypeError: (\n            \"WorkingHours.working_hours value should be a list of lists of \"\n            \"two integers and the range of integers should be between 0-1440, \"\n            f\"not {test_value.__class__.__name__}: '{test_value}'\"\n        ),\n        ValueError: (\n            \"WorkingHours.working_hours value should be a list of lists of \"\n            \"two integers and the range of integers should be between 0-1440, \"\n            f\"not {test_value.__class__.__name__}: '{test_value}'\"\n        ),\n        IndexError: \"list index out of range\",\n        KeyError: (\n            \"\\\"WorkingHours accepts only ['mon', 'tue', 'wed', 'thu', \"\n            \"'fri', 'sat', 'sun'] as key, not 'zon'\\\"\"\n        ),\n    }[error_type]\n\n    assert str(cm.value) == error_message\n\n\ndef test_working_hours_argument_is_working_as_expected():\n    \"\"\"working_hours argument is working as expected,\"\"\"\n    working_hours = copy.copy(defaults.working_hours)\n    working_hours[\"sun\"] = [[540, 1000]]\n    working_hours[\"sat\"] = [[500, 800], [900, 1440]]\n    wh = WorkingHours(working_hours=working_hours)\n    assert wh.working_hours == working_hours\n    assert wh.working_hours[\"sun\"] == working_hours[\"sun\"]\n    assert wh.working_hours[\"sat\"] == working_hours[\"sat\"]\n\n\ndef test_working_hours_attribute_is_working_as_expected():\n    \"\"\"working_hours attribute is working as expected.\"\"\"\n    working_hours = copy.copy(defaults.working_hours)\n    working_hours[\"sun\"] = [[540, 1000]]\n    working_hours[\"sat\"] = [[500, 800], [900, 1440]]\n    wh = WorkingHours()\n    wh.working_hours = working_hours\n    assert wh.working_hours == working_hours\n    assert wh.working_hours[\"sun\"] == working_hours[\"sun\"]\n    assert wh.working_hours[\"sat\"] == working_hours[\"sat\"]\n\n\ndef test_to_tjp_attribute_is_read_only():\n    \"\"\"to_tjp attribute is read only.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(AttributeError) as cm:\n        wh.to_tjp = \"some value\"\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'to_tjp'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'to_tjp' of 'WorkingHours' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_to_tjp_attribute_is_working_as_expected():\n    \"\"\"to_tjp property is working as expected.\"\"\"\n    wh = WorkingHours()\n    wh[\"mon\"] = [[570, 1110]]\n    wh[\"tue\"] = [[570, 1110]]\n    wh[\"wed\"] = [[570, 1110]]\n    wh[\"thu\"] = [[570, 1110]]\n    wh[\"fri\"] = [[570, 1110]]\n    wh[\"sat\"] = []\n    wh[\"sun\"] = []\n\n    expected_tjp = \"\"\"workinghours mon 09:30 - 18:30\nworkinghours tue 09:30 - 18:30\nworkinghours wed 09:30 - 18:30\nworkinghours thu 09:30 - 18:30\nworkinghours fri 09:30 - 18:30\nworkinghours sat off\nworkinghours sun off\"\"\"\n\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print(\"--------------------\")\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(wh.to_tjp)\n\n    assert wh.to_tjp == expected_tjp\n\n\ndef test_to_tjp_attribute_is_working_as_expected_for_multiple_work_hour_ranges():\n    \"\"\"to_tjp property is working as expected.\"\"\"\n    wh = WorkingHours()\n    wh[\"mon\"] = [[570, 720], [780, 1110]]\n    wh[\"tue\"] = [[570, 720], [780, 1110]]\n    wh[\"wed\"] = [[570, 720], [780, 1110]]\n    wh[\"thu\"] = [[570, 720], [780, 1110]]\n    wh[\"fri\"] = [[570, 720], [780, 1110]]\n    wh[\"sat\"] = [[570, 720]]\n    wh[\"sun\"] = []\n\n    expected_tjp = \"\"\"workinghours mon 09:30 - 12:00, 13:00 - 18:30\nworkinghours tue 09:30 - 12:00, 13:00 - 18:30\nworkinghours wed 09:30 - 12:00, 13:00 - 18:30\nworkinghours thu 09:30 - 12:00, 13:00 - 18:30\nworkinghours fri 09:30 - 12:00, 13:00 - 18:30\nworkinghours sat 09:30 - 12:00\nworkinghours sun off\"\"\"\n\n    # print(\"Expected:\")\n    # print(\"---------\")\n    # print(expected_tjp)\n    # print(\"--------------------\")\n    # print(\"Result:\")\n    # print(\"-------\")\n    # print(wh.to_tjp)\n\n    assert wh.to_tjp == expected_tjp\n\n\ndef test_weekly_working_hours_attribute_is_read_only():\n    \"\"\"weekly_working_hours is a read-only attribute.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(AttributeError) as cm:\n        wh.weekly_working_hours = 232\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'weekly_working_hours'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'weekly_working_hours' of 'WorkingHours' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\ndef test_weekly_working_hours_attribute_is_working_as_expected():\n    \"\"\"weekly_working_hours attribute is working as expected.\"\"\"\n    wh = WorkingHours()\n    wh[\"mon\"] = [[570, 720], [780, 1110]]  # 480\n    wh[\"tue\"] = [[570, 720], [780, 1110]]  # 480\n    wh[\"wed\"] = [[570, 720], [780, 1110]]  # 480\n    wh[\"thu\"] = [[570, 720], [780, 1110]]  # 480\n    wh[\"fri\"] = [[570, 720], [780, 1110]]  # 480\n    wh[\"sat\"] = [[570, 720]]  # 150\n    wh[\"sun\"] = []  # 0\n\n    expected_value = 42.5\n    assert wh.weekly_working_hours == expected_value\n\n\ndef test_is_working_hour_is_working_as_expected():\n    \"\"\"is_working_hour method is working as expected.\"\"\"\n    wh = WorkingHours()\n\n    wh[\"mon\"] = [[570, 720], [780, 1110]]\n    wh[\"tue\"] = [[570, 720], [780, 1110]]\n    wh[\"wed\"] = [[570, 720], [780, 1110]]\n    wh[\"thu\"] = [[570, 720], [780, 1110]]\n    wh[\"fri\"] = [[570, 720], [780, 1110]]\n    wh[\"sat\"] = [[570, 720]]\n    wh[\"sun\"] = []\n\n    # monday\n    check_date = datetime.datetime(2013, 4, 8, 13, 55, tzinfo=pytz.utc)\n    assert wh.is_working_hour(check_date) is True\n\n    # sunday\n    check_date = datetime.datetime(2013, 4, 14, 13, 55, tzinfo=pytz.utc)\n    assert wh.is_working_hour(check_date) is False\n\n\ndef test_day_numbers_are_correct():\n    \"\"\"day numbers are correct.\"\"\"\n    wh = WorkingHours()\n    wh[\"mon\"] = [[1, 2]]\n    wh[\"tue\"] = [[3, 4]]\n    wh[\"wed\"] = [[5, 6]]\n    wh[\"thu\"] = [[7, 8]]\n    wh[\"fri\"] = [[9, 10]]\n    wh[\"sat\"] = [[11, 12]]\n    wh[\"sun\"] = [[13, 14]]\n\n    assert defaults.day_order[0] == \"mon\"\n    assert defaults.day_order[1] == \"tue\"\n    assert defaults.day_order[2] == \"wed\"\n    assert defaults.day_order[3] == \"thu\"\n    assert defaults.day_order[4] == \"fri\"\n    assert defaults.day_order[5] == \"sat\"\n    assert defaults.day_order[6] == \"sun\"\n\n    assert wh[\"mon\"] == wh[0]\n    assert wh[\"tue\"] == wh[1]\n    assert wh[\"wed\"] == wh[2]\n    assert wh[\"thu\"] == wh[3]\n    assert wh[\"fri\"] == wh[4]\n    assert wh[\"sat\"] == wh[5]\n    assert wh[\"sun\"] == wh[6]\n\n\ndef test_weekly_working_days_is_a_read_only_attribute():\n    \"\"\"weekly working days is a read-only attribute.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(AttributeError) as cm:\n        wh.weekly_working_days = 6\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'weekly_working_days'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'weekly_working_days' of 'WorkingHours' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\n@pytest.mark.parametrize(\n    \"test_data, expected_result\",\n    [\n        [\n            {\n                \"mon\": [[1, 2]],\n                \"tue\": [[3, 4]],\n                \"wed\": [[5, 6]],\n                \"thu\": [[7, 8]],\n                \"fri\": [[9, 10]],\n                \"sat\": [],\n                \"sun\": [],\n            },\n            5,\n        ],\n        [\n            {\n                \"mon\": [[1, 2]],\n                \"tue\": [[3, 4]],\n                \"wed\": [[5, 6]],\n                \"thu\": [[7, 8]],\n                \"fri\": [[9, 10]],\n                \"sat\": [[11, 12]],\n                \"sun\": [],\n            },\n            6,\n        ],\n        [\n            {\n                \"mon\": [[1, 2]],\n                \"tue\": [[3, 4]],\n                \"wed\": [[5, 6]],\n                \"thu\": [[7, 8]],\n                \"fri\": [[9, 10]],\n                \"sat\": [[11, 12]],\n                \"sun\": [[13, 14]],\n            },\n            7,\n        ],\n    ],\n)\ndef test_weekly_working_days_is_calculated_correctly(test_data, expected_result):\n    \"\"\"weekly working days are calculated correctly.\"\"\"\n    wh = WorkingHours()\n    for day in test_data:\n        wh[day] = test_data[day]\n    assert wh.weekly_working_days == expected_result\n\n\ndef test_yearly_working_days_is_a_read_only_attribute():\n    \"\"\"yearly_working_days attribute is a read only attribute.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(AttributeError) as cm:\n        wh.yearly_working_days = 260.1\n\n    error_message = {\n        8: \"can't set attribute\",\n        9: \"can't set attribute\",\n        10: \"can't set attribute 'yearly_working_days'\",\n    }.get(\n        sys.version_info.minor,\n        \"property 'yearly_working_days' of 'WorkingHours' object has no setter\",\n    )\n\n    assert str(cm.value) == error_message\n\n\n@pytest.mark.parametrize(\n    \"test_data, expected_result\",\n    [\n        [\n            {\n                \"mon\": [[1, 2]],\n                \"tue\": [[3, 4]],\n                \"wed\": [[5, 6]],\n                \"thu\": [[7, 8]],\n                \"fri\": [[9, 10]],\n                \"sat\": [],\n                \"sun\": [],\n            },\n            261,\n        ],\n        [\n            {\n                \"mon\": [[1, 2]],\n                \"tue\": [[3, 4]],\n                \"wed\": [[5, 6]],\n                \"thu\": [[7, 8]],\n                \"fri\": [[9, 10]],\n                \"sat\": [[11, 12]],\n                \"sun\": [],\n            },\n            313,\n        ],\n        [\n            {\n                \"mon\": [[1, 2]],\n                \"tue\": [[3, 4]],\n                \"wed\": [[5, 6]],\n                \"thu\": [[7, 8]],\n                \"fri\": [[9, 10]],\n                \"sat\": [[11, 12]],\n                \"sun\": [[13, 14]],\n            },\n            365,\n        ],\n    ],\n)\ndef test_yearly_working_days_is_calculated_correctly(test_data, expected_result):\n    \"\"\"yearly_working_days is calculated correctly.\"\"\"\n    wh = WorkingHours()\n    for day in test_data:\n        wh[day] = test_data[day]\n    assert wh.yearly_working_days == pytest.approx(expected_result)\n\n\ndef test_daily_working_hours_argument_is_skipped():\n    \"\"\"daily_working_hours arg is skipped, daily_working_hours attr is equal to the\n    default settings.\"\"\"\n    wh = WorkingHours()\n    assert wh.daily_working_hours == defaults.daily_working_hours\n\n\ndef test_daily_working_hours_argument_is_none():\n    \"\"\"daily_working_hours attr is equal to the default settings value if the\n    daily_working_hours argument is None.\"\"\"\n    kwargs = dict()\n    kwargs[\"daily_working_hours\"] = None\n    wh = WorkingHours(**kwargs)\n    assert wh.daily_working_hours == defaults.daily_working_hours\n\n\ndef test_daily_working_hours_attribute_is_none():\n    \"\"\"daily_working_hours attr is set to default if it is set to None.\"\"\"\n    wh = WorkingHours()\n    wh.daily_working_hours = None\n    assert wh.daily_working_hours == defaults.daily_working_hours\n\n\ndef test_daily_working_hours_argument_is_not_integer():\n    \"\"\"TypeError raised if the daily_working_hours argument is not an integer.\"\"\"\n    kwargs = dict()\n    kwargs[\"daily_working_hours\"] = \"not an integer\"\n    with pytest.raises(TypeError) as cm:\n        WorkingHours(**kwargs)\n    assert str(cm.value) == (\n        \"WorkingHours.daily_working_hours should be an integer, \"\n        \"not str: 'not an integer'\"\n    )\n\n\ndef test_daily_working_hours_attribute_is_not_an_integer():\n    \"\"\"TypeError raised if the daily_working hours attr is not an integer.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(TypeError) as cm:\n        wh.daily_working_hours = \"not an integer\"\n\n    assert str(cm.value) == (\n        \"WorkingHours.daily_working_hours should be an integer, \"\n        \"not str: 'not an integer'\"\n    )\n\n\ndef test_daily_working_hours_argument_is_working_fine():\n    \"\"\"daily working hours arg is correctly passed to daily_working_hours attr.\"\"\"\n    kwargs = dict()\n    kwargs[\"daily_working_hours\"] = 12\n    wh = WorkingHours(**kwargs)\n    assert wh.daily_working_hours == 12\n\n\ndef test_daily_working_hours_attribute_is_working_as_expected():\n    \"\"\"daily_working_hours attribute is working as expected.\"\"\"\n    wh = WorkingHours()\n    wh.daily_working_hours = 23\n    assert wh.daily_working_hours == 23\n\n\ndef test_daily_working_hours_argument_is_zero():\n    \"\"\"ValueError is raised if the daily_working_hours argument value is zero.\"\"\"\n    kwargs = dict()\n    kwargs[\"daily_working_hours\"] = 0\n    with pytest.raises(ValueError) as cm:\n        WorkingHours(**kwargs)\n\n    assert (\n        str(cm.value)\n        == \"WorkingHours.daily_working_hours should be a positive integer \"\n        \"value greater than 0 and smaller than or equal to 24\"\n    )\n\n\ndef test_daily_working_hours_attribute_is_zero():\n    \"\"\"ValueError is raised if the daily_working_hours attribute is set to zero.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(ValueError) as cm:\n        wh.daily_working_hours = 0\n\n    assert (\n        str(cm.value)\n        == \"WorkingHours.daily_working_hours should be a positive integer \"\n        \"value greater than 0 and smaller than or equal to 24\"\n    )\n\n\ndef test_daily_working_hours_argument_is_a_negative_number():\n    \"\"\"ValueError is raised if the daily_working_hours argument value is negative.\"\"\"\n    kwargs = dict()\n    kwargs[\"daily_working_hours\"] = -10\n    with pytest.raises(ValueError) as cm:\n        WorkingHours(**kwargs)\n\n    assert (\n        str(cm.value)\n        == \"WorkingHours.daily_working_hours should be a positive integer \"\n        \"value greater than 0 and smaller than or equal to 24\"\n    )\n\n\ndef test_daily_working_hours_attribute_is_a_negative_number():\n    \"\"\"ValueError raised if the daily_working_hours attr is set to a negative value.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(ValueError) as cm:\n        wh.daily_working_hours = -10\n\n    assert (\n        str(cm.value)\n        == \"WorkingHours.daily_working_hours should be a positive integer \"\n        \"value greater than 0 and smaller than or equal to 24\"\n    )\n\n\ndef test_daily_working_hours_argument_is_set_to_a_number_bigger_than_24():\n    \"\"\"ValueError is raised if the daily working hours argument is bigger than 24.\"\"\"\n    kwargs = dict()\n    kwargs[\"daily_working_hours\"] = 25\n    with pytest.raises(ValueError) as cm:\n        WorkingHours(**kwargs)\n\n    assert (\n        str(cm.value)\n        == \"WorkingHours.daily_working_hours should be a positive integer \"\n        \"value greater than 0 and smaller than or equal to 24\"\n    )\n\n\ndef test_daily_working_hours_attribute_is_set_to_a_number_bigger_than_24():\n    \"\"\"ValueError is raised if the daily working hours attr is bigger than 24.\"\"\"\n    wh = WorkingHours()\n    with pytest.raises(ValueError) as cm:\n        wh.daily_working_hours = 25\n\n    assert (\n        str(cm.value)\n        == \"WorkingHours.daily_working_hours should be a positive integer \"\n        \"value greater than 0 and smaller than or equal to 24\"\n    )\n\n\ndef test_split_in_to_working_hours_is_not_implemented_yet():\n    \"\"\"NotimplementedError is raised if the split_in_to_working_hours() is called.\"\"\"\n    with pytest.raises(NotImplementedError):\n        wh = WorkingHours()\n        start = datetime.datetime.now(pytz.utc)\n        end = start + datetime.timedelta(days=10)\n        wh.split_in_to_working_hours(start, end)\n"
  },
  {
    "path": "tests/test_exceptions.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"Tests for the exceptions module.\"\"\"\nimport pytest\n\nfrom stalker.exceptions import (\n    CircularDependencyError,\n    DependencyViolationError,\n    LoginError,\n    OverBookedError,\n    StatusError,\n)\n\n\ndef test_login_error_is_working_as_expected():\n    \"\"\"LoginError is working as expected.\"\"\"\n    test_message = \"testing LoginError\"\n    with pytest.raises(LoginError) as cm:\n        raise LoginError(test_message)\n\n    assert str(cm.value) == test_message\n\n\ndef test_circular_dependency_error_is_working_as_expected():\n    \"\"\"CircularDependencyError is working as expected.\"\"\"\n    test_message = \"testing CircularDependencyError\"\n    with pytest.raises(CircularDependencyError) as cm:\n        raise CircularDependencyError(test_message)\n\n    assert str(cm.value) == test_message\n\n\ndef test_over_booked_error_is_working_as_expected():\n    \"\"\"OverBookedError is working as expected.\"\"\"\n    test_message = \"testing OverBookedError\"\n    with pytest.raises(OverBookedError) as cm:\n        raise OverBookedError(test_message)\n\n    assert str(cm.value) == test_message\n\n\ndef test_status_error_is_working_as_expected():\n    \"\"\"StatusError is working as expected.\"\"\"\n    test_message = \"testing StatusError\"\n    with pytest.raises(StatusError) as cm:\n        raise StatusError(test_message)\n\n    assert str(cm.value) == test_message\n\n\ndef test_dependency_violation_error_is_working_as_expected():\n    \"\"\"DependencyViolationError is working as expected.\"\"\"\n    test_message = \"testing DependencyViolationError\"\n    with pytest.raises(DependencyViolationError) as cm:\n        raise DependencyViolationError(test_message)\n\n    assert str(cm.value) == test_message\n"
  },
  {
    "path": "tests/test_logging.py",
    "content": "# -*- coding: utf-8 -*-\n\nimport logging\nimport pytest\n\nfrom stalker import log\n\n\n@pytest.fixture(scope=\"function\")\ndef setup_logging():\n    \"\"\"Set up stalker log module.\"\"\"\n    log.loggers = []\n    yield\n    # clean loggers list after every test\n    log.loggers = []\n\n\ndef test_register_logger_simple(setup_logging):\n    \"\"\"register logger adds the given logger to the list.\"\"\"\n    logger = logging.getLogger(\"test_logger\")\n    assert logger not in log.loggers\n    log.register_logger(logger)\n    assert logger in log.loggers\n\n\ndef test_register_logger_called_multiple_times(setup_logging):\n    \"\"\"register logger adds the logger only once.\"\"\"\n    logger = logging.getLogger(\"test_logger\")\n    assert logger not in log.loggers\n    assert 0 == len(log.loggers)\n    log.register_logger(logger)\n    assert 1 == len(log.loggers)\n    log.register_logger(logger)\n    assert 1 == len(log.loggers)\n    log.register_logger(logger)\n    assert 1 == len(log.loggers)\n    log.register_logger(logger)\n    assert 1 == len(log.loggers)\n    assert logger in log.loggers\n\n\ndef test_register_logger_only_accept_loggers(setup_logging):\n    \"\"\"register_logger raise a TypeError if the logger is not a Logger instance.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        log.register_logger(\"not a logger\")\n\n    assert str(cm.value) == (\n        \"logger should be a logging.Logger instance, not str: 'not a logger'\"\n    )\n\n\ndef test_register_logger_sets_the_level_to_the_default_level(setup_logging):\n    \"\"\"register_logger set the level to the default level.\"\"\"\n    logger = logging.getLogger(\"logger1\")\n    logger.setLevel(logging.WARNING)\n    assert log.logging_level != logging.WARNING\n    log.register_logger(logger)\n    assert logger.level == log.logging_level\n\n\ndef test_set_level_sets_all_logger_levels(setup_logging):\n    \"\"\"set_level sets all logger levels all together.\"\"\"\n    logger1 = logging.getLogger(\"test_logger1\")\n    logger2 = logging.getLogger(\"test_logger2\")\n    logger3 = logging.getLogger(\"test_logger3\")\n    logger4 = logging.getLogger(\"test_logger4\")\n    logger1.setLevel(logging.DEBUG)\n    logger2.setLevel(logging.DEBUG)\n    logger3.setLevel(logging.DEBUG)\n    logger4.setLevel(logging.DEBUG)\n    log.register_logger(logger1)\n    log.register_logger(logger2)\n    log.register_logger(logger3)\n    log.register_logger(logger4)\n    assert logger1.level != logging.WARNING\n    assert logger2.level != logging.WARNING\n    assert logger3.level != logging.WARNING\n    assert logger4.level != logging.WARNING\n    log.set_level(logging.WARNING)\n    assert logger1.level == logging.WARNING\n    assert logger2.level == logging.WARNING\n    assert logger3.level == logging.WARNING\n    assert logger4.level == logging.WARNING\n\n\ndef test_set_level_level_is_not_an_integer(setup_logging):\n    \"\"\"TypeError raised if the logging level is not an integer.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        log.set_level(\"not a logging level\")\n\n    assert str(cm.value) == (\n        \"level should be an integer value one of [0, 10, 20, 30, 40, 50] or \"\n        \"[NOTSET, DEBUG, INFO, WARN, WARNING, ERROR, FATAL, CRITICAL] of the \"\n        \"logging library, not str: 'not a logging level'\"\n    )\n\n\ndef test_set_level_level_is_not_a_proper_logging_level(setup_logging):\n    \"\"\"ValueError raised if the logging level is not in correct value.\"\"\"\n    with pytest.raises(ValueError) as cm:\n        log.set_level(1000)\n\n    assert str(cm.value) == (\n        \"level should be an integer value one of [0, 10, 20, 30, 40, 50] or \"\n        \"[NOTSET, DEBUG, INFO, WARN, WARNING, ERROR, FATAL, CRITICAL] of the \"\n        \"logging library, not 1000.\"\n    )\n\n\ndef test_get_logger_name_is_not_a_string(setup_logging):\n    \"\"\"stalker.get_logger() raises a TypeError if the name attribute is not\n    a str.\"\"\"\n    with pytest.raises(TypeError) as cm:\n        log.get_logger(2123)\n    assert str(cm.value) == \"A logger name must be a string\"\n\n\ndef test_get_logger_creates_a_logger(setup_logging):\n    \"\"\"stalker.log.get_logger() returns a Logger instance.\"\"\"\n    logger = log.get_logger(\"logger\")\n    assert isinstance(logger, logging.Logger)\n\n\ndef test_get_logger_registers_the_new_logger_already(setup_logging):\n    \"\"\"stalker.log.get_logger() registers the new logger.\"\"\"\n    logger = log.get_logger(\"logger\")\n    assert logger in log.loggers\n\n\ndef test_get_logger_sets_the_logging_level_to_the_default_one(setup_logging):\n    \"\"\"stalker.log.get_logger() sets the logging level to the default one.\"\"\"\n    logger = log.get_logger(\"logger\")\n    assert logger.level == log.logging_level\n"
  },
  {
    "path": "tests/test_readme_tutorial.py",
    "content": "# -*- coding: utf-8 -*-\nimport stalker.db.setup\nfrom stalker import db\nfrom stalker.db.session import DBSession\nfrom stalker import (\n    Asset,\n    FilenameTemplate,\n    ImageFormat,\n    Repository,\n    Project,\n    Shot,\n    Structure,\n    Task,\n    Type,\n    User,\n    Version,\n)\nfrom stalker.models.enum import TimeUnit\n\n\ndef test_readme_tutorial_code(setup_sqlite3):\n    \"\"\"the tutorial code in README.rst.\"\"\"\n    stalker.db.setup.setup()\n    stalker.db.setup.init()\n\n    assert str(DBSession.connection().engine.url) == \"sqlite://\"\n\n    me = User(\n        name=\"Erkan Ozgur Yilmaz\",\n        login=\"erkanozgur\",\n        email=\"my_email@gmail.com\",\n        password=\"secretpass\",\n    )\n\n    # Save the user to database\n    DBSession.save(me)\n\n    repo = Repository(\n        name=\"Commercial Projects Repository\",\n        code=\"CPR\",\n        windows_path=\"Z:/Projects\",\n        linux_path=\"/mnt/Z/Projects\",\n        macos_path=\"/Volumes/Z/Projects\",\n    )\n\n    task_template = FilenameTemplate(\n        name=\"Standard Task Filename Template\",\n        target_entity_type=\"Task\",  # This is for files saved for Tasks\n        path=\"{{project.repository.path}}/{{project.code}}/\"\n        \"{%- for parent_task in parent_tasks -%}\"\n        \"{{parent_task.nice_name}}/\"\n        \"{%- endfor -%}\",  # This is Jinja2 template code\n        filename='{{version.nice_name}}_v{{\"%03d\"|format(version.version_number)}}',\n    )\n\n    standard_folder_structure = Structure(\n        name=\"Standard Project Folder Structure\",\n        templates=[task_template],\n        custom_template=\"{{project.code}}/References\",  # If you need extra folders\n    )\n\n    new_project = Project(\n        name=\"Test Project\",\n        code=\"TP\",\n        structure=standard_folder_structure,\n        repositories=[repo],  # if you have more than one repository you can do it\n    )\n\n    hd1080 = ImageFormat(name=\"1080p\", width=1920, height=1080)\n\n    new_project.image_format = hd1080\n\n    # Save the project and all the other data it is connected to it\n    DBSession.save(new_project)\n\n    # define Character asset type\n    char_type = Type(name=\"Character\", code=\"CHAR\", target_entity_type=\"Asset\")\n\n    character1 = Asset(\n        name=\"Character 1\", code=\"CHAR1\", type=char_type, project=new_project\n    )\n\n    # Save the Asset\n    DBSession.save(character1)\n\n    model = Task(name=\"Model\", parent=character1)\n\n    rigging = Task(\n        name=\"Rig\",\n        parent=character1,\n        depends_on=[model],  # For project management, define that Rig cannot start\n        # before Model ends.\n    )\n\n    # Save the new tasks\n    DBSession.save([model, rigging])\n\n    # A shot and some tasks for it\n    shot = Shot(name=\"SH001\", code=\"SH001\", project=new_project)\n\n    # Save the Shot\n    DBSession.save(shot)\n\n    animation = Task(\n        name=\"Animation\",\n        parent=shot,\n    )\n\n    lighting = Task(\n        name=\"Lighting\",\n        parent=shot,\n        depends_on=[animation],  # Lighting cannot start before Animation ends,\n        schedule_timing=1,\n        schedule_unit=TimeUnit.Day,  # The task expected to take 1 day to complete\n        resources=[me],\n    )\n    DBSession.save([animation, lighting])\n\n    new_version = Version(task=animation)\n    new_version.generate_path()  # to render the naming convention template\n    new_version.extension = \".ma\"  # let's say that we have created under Maya\n    DBSession.save(new_version)\n\n    path = new_version.generate_path(extension=\".ma\")\n    assert str(path) == f\"{repo.path}TP/SH001/Animation/SH001_Animation_v001.ma\"\n    assert new_version.version_number == 1\n\n    new_version2 = Version(task=animation)\n    DBSession.save(new_version2)\n\n    # to render the naming convention template\n    # let's say that we have created under Maya\n    # path = new_version2.generate_path(extension = \".ma\")\n    assert new_version2.version_number == 2\n"
  },
  {
    "path": "tests/test_testing.py",
    "content": "# -*- coding: utf-8 -*-\n\"\"\"stalker.testing module.\"\"\"\n\nimport pytest\nfrom tests.utils import get_server_details_from_url\n\n\n@pytest.mark.parametrize(\n    \"url,expected\",\n    [\n        (\n            \"postgresql://postgres:postgres@localhost:5432/stalker_test_e0b9bc6a\",\n            {\n                \"dialect\": \"postgresql\",\n                \"username\": \"postgres\",\n                \"password\": \"postgres\",\n                \"hostname\": \"localhost\",\n                \"port\": \"5432\",\n                \"database_name\": \"stalker_test_e0b9bc6a\",\n            },\n        ),\n        (\n            \"postgresql://postgres:postgres@localhost/stalker_test_e0b9bc6a\",\n            {\n                \"dialect\": \"postgresql\",\n                \"username\": \"postgres\",\n                \"password\": \"postgres\",\n                \"hostname\": \"localhost\",\n                \"port\": \"\",\n                \"database_name\": \"stalker_test_e0b9bc6a\",\n            },\n        ),\n    ],\n)\ndef test_get_server_details_from_url(url, expected):\n    \"\"\"get_server_details_from_url.\"\"\"\n    assert get_server_details_from_url(url) == expected\n"
  },
  {
    "path": "tests/test_version.py",
    "content": "# -*- coding: utf-8 -*-\nimport os\n\n# Local Imports\nimport stalker\nfrom stalker import version\n\n\ndef test_version_number_is_correct():\n    \"\"\"version.VERSION is correct.\"\"\"\n    version_file_path = os.path.join(os.path.dirname(stalker.__file__), \"VERSION\")\n    with open(version_file_path) as f:\n        expected_version = f.read().strip()\n    assert expected_version == version.__version__\n\n\ndef test_version_number_as_a_module_level_variable():\n    \"\"\"stalker.__version__ exists and value is correct.\"\"\"\n    assert version.__version__ == stalker.__version__\n"
  },
  {
    "path": "tests/utils.py",
    "content": "# -*- coding: utf-8 -*-\nimport datetime\nimport os\nimport platform\nimport re\nimport subprocess\nimport uuid\n\nfrom sqlalchemy.exc import OperationalError\nfrom sqlalchemy.orm import close_all_sessions\n\nfrom stalker import log, defaults, User\nfrom stalker.db.declarative import Base\nfrom stalker.db.session import DBSession\n\nlogger = log.get_logger(__name__)\nlogger.setLevel(log.logging_level)\n\n\n# {dialect}://{username}:{password}@{address}/{database_name}\nDB_REGEX = re.compile(\n    r\"(?P<dialect>\\w+)\"\n    r\"://\"\n    r\"(?P<username>\\w+)\"\n    r\":\"\n    r\"(?P<password>[\\w\\s#?!$%^&*\\-]+)\"\n    r\"@*\"\n    r\"(?P<hostname>[\\w.]+)\"\n    r\":*\"\n    r\"(?P<port>\\d*)\"\n    r\"/*\"\n    r\"(?P<database_name>[\\w_\\-]*)\"\n)\n\n\ndef run_db_command(\n    database_name=\"testdb\",\n    dialect=\"postgresql\",\n    hostname=\"localhost\",\n    port=5432,\n    username=\"postgres\",\n    password=\"postgres\",\n    command=\"\",\n):\n    \"\"\"Run db command on a Postgres database.\n\n    Args:\n        database_name (str): The database name to create.\n        dialect (str): The database dialect, default is postgresql and currently nothing\n            else is supported.\n        hostname (str): The DB server hostname, default is 'localhost'.\n        port (int): The port number, default is 5432.\n        username (str): The username, default is 'postgres'.\n        password (str): The password, default is 'postgres'.\n        command (str): The command to run.\n\n    Returns:\n        str: The database url.\n    \"\"\"\n    if port == \"\":\n        port = 5432\n\n    psql_command = [\n        \"psql\",\n        \"--host\",\n        hostname,\n        \"--port\",\n        str(port),\n        \"--username\",\n        username,\n        \"--no-password\",\n        \"--command\",\n        command,\n    ]\n\n    proc = subprocess.Popen(\n        psql_command,\n        stdout=subprocess.PIPE,\n        stderr=subprocess.PIPE,\n        env={\"PGPASSWORD\": password}.update(os.environ),\n    )\n    stdout_buffer = []\n    stderr_buffer = []\n    while True:\n        stdout = proc.stdout.readline().strip()\n        stderr = proc.stderr.readline().strip()\n        if not isinstance(stdout, str):\n            stdout = stdout.decode(\"utf-8\", \"replace\")\n        if not isinstance(stderr, str):\n            stderr = stderr.decode(\"utf-8\", \"replace\")\n\n        if stdout == \"\" and stderr == \"\" and proc.poll() is not None:\n            break\n\n        if stdout != \"\":\n            stdout_buffer.append(stdout)\n        if stderr != \"\":\n            stderr_buffer.append(stderr)\n\n    logger.debug(\"STDOUT BUFFER\")\n    logger.debug(\"=============\")\n    for line in stdout_buffer:\n        logger.debug(line)\n\n    logger.debug(\"STDERR BUFFER\")\n    logger.debug(\"=============\")\n    for line in stderr_buffer:\n        logger.debug(line)\n\n    return stdout_buffer, stderr_buffer\n\n\ndef create_db(\n    database_name=\"testdb\",\n    dialect=\"postgresql\",\n    hostname=\"localhost\",\n    port=5432,\n    username=\"postgres\",\n    password=\"postgres\",\n):\n    \"\"\"Create a new Postgres database.\n\n    Args:\n        database_name (str): The database name to create.\n        dialect (str): The database dialect, default is postgresql and currently nothing\n            else is supported.\n        hostname (str): The DB server hostname, default is 'localhost'.\n        port (int): The port number, default is 5432.\n        username (str): The username, default is 'postgres'.\n        password (str): The password, default is 'postgres'.\n\n    Returns:\n        str: The database url.\n    \"\"\"\n    logger.debug(\"Creating Database: {}\".format(database_name))\n    if port == \"\":\n        port = 5432  # use default\n\n    database_url = (\n        f\"{dialect}://{username}:{password}@{hostname}:{port}/{database_name}\"\n    )\n\n    command = \"CREATE DATABASE {};\".format(database_name)\n    run_db_command(\n        database_name=database_name,\n        dialect=dialect,\n        hostname=hostname,\n        port=port,\n        username=username,\n        password=password,\n        command=command,\n    )\n    return database_url\n\n\ndef get_server_details_from_url(url):\n    \"\"\"Return database details from the given url.\n\n    Args:\n        url (str): Database url in\n            dialect}://{user_name}:{password}@{address}/{database_name} format.\n\n    Returns:\n        dict: Returns a dictionary with \"dialect\", \"user_name\", \"password\", \"address\",\n            \"database_name\" keys.\n    \"\"\"\n    return_val = dict()\n    match = DB_REGEX.match(url)\n    if match:\n        return_val = match.groupdict()\n    return return_val\n\n\ndef create_random_db():\n    \"\"\"creates a random named Postgres database\n\n    :returns (str): db_url\n    \"\"\"\n    # create a new database for this test only\n    database_url = os.environ.get(\n        \"STALKER_TEST_DB\", \"postgresql://postgres:postgres@localhost/testdb\"\n    )\n    database_name = \"stalker_test_{}\".format(uuid.uuid4().hex[:8])\n\n    # get server details\n    db_kwargs = get_server_details_from_url(database_url)\n\n    # replace database name\n    db_kwargs[\"database_name\"] = database_name\n\n    return create_db(**db_kwargs)\n\n\ndef drop_db(\n    database_name=\"testdb\",\n    dialect=\"postgresql\",\n    hostname=\"localhost\",\n    port=5432,\n    username=\"postgres\",\n    password=\"postgres\",\n):\n    \"\"\"Drop the given Postgres database.\n\n    Args:\n        database_name (str): The database name to create.\n        dialect (str): The database dialect, default is postgresql and currently nothing\n            else is supported.\n        hostname (str): The DB server hostname, default is 'localhost'.\n        port (Union[str, int]): The port number, default is 5432.\n        username (str): The username, default is 'postgres'.\n        password (str): The password, default is 'postgres'.\n    \"\"\"\n    logger.debug(\"Dropping Database: {}\".format(database_name))\n    command = \"DROP DATABASE {};\".format(database_name)\n\n    run_db_command(\n        database_name=database_name,\n        dialect=dialect,\n        hostname=hostname,\n        port=port,\n        username=username,\n        password=password,\n        command=command,\n    )\n\n\nclass PlatformPatcher(object):\n    \"\"\"patches given callable\"\"\"\n\n    def __init__(self):\n        self.callable = None\n        self.original = None\n\n    def patch(self, desired_result):\n        \"\"\"Patch platform.\"\"\"\n        self.original = platform.system\n\n        def f():\n            return desired_result\n\n        platform.system = f\n\n    def restore(self):\n        \"\"\"restores the given callable_\"\"\"\n        if self.original:\n            platform.system = self.original\n\n\ndef tear_down_db(data):\n    \"\"\"Utility function to tear a test setup down.\"\"\"\n    # clean up test database\n    DBSession.rollback()\n    connection = DBSession.connection()\n    engine = connection.engine\n    connection.close()\n\n    try:\n        Base.metadata.drop_all(engine, checkfirst=True)\n        DBSession.remove()\n        close_all_sessions()\n        db_kwargs = get_server_details_from_url(data.get(\"database_url\", \"\"))\n        drop_db(**db_kwargs)\n    except OperationalError:\n        pass\n    finally:\n        defaults[\"timing_resolution\"] = datetime.timedelta(hours=1)\n\n\ndef get_admin_user():\n    \"\"\"Return admin user from database.\n\n    Returns:\n         stalker.User: The admin user\n    \"\"\"\n    with DBSession.no_autoflush:\n        return User.query.filter(User.login == defaults.admin_login).first()\n"
  },
  {
    "path": "whitelist.txt",
    "content": "autoflush\nautogenerate\nBFS\nCMPL\nCodeMixin\nCRUDL\ncsv\nDBSession\nDDL\nDEFERRABLE\nDFS\nDREV\nexpandvars\nfetchall\nFilenameTemplates\nformatter\nHREV\nLite3\nmacOS\nminallocated\nmixin\nMixins\nmyapp\nmymodel\nnormpath\nnullable\nnum\nOH\nonend\nonstart\noy\noyProjectManager\nPostgre\nPostgreSQL\npreliminarily\nPREV\nrepo\nRREV\nRTS\nsessionmaker\nSOM\nsqlalchemy\nSQLite3\nStatusList\nSTOP\ntablename\nTaskable\nTimeLog\nTJ\ntj3\ntjp\nUniqueConstraint\nunmanaged\nWFD\nWIP\nWorkingHours"
  }
]