├── .editorconfig ├── .github └── workflows │ └── pytest.yml ├── .gitignore ├── CHANGELOG.rst ├── CHANGELOG_OLD.rst ├── COPYING ├── COPYING.LESSER ├── Dockerfile-py3.5 ├── INSTALL ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── TODO.rst ├── VERSION ├── alembic.ini ├── alembic ├── README ├── env.py ├── script.py.mako └── versions │ ├── 0063f547dc2e_updated_version_inputs_table.py │ ├── 019378697b5b_rename_depends_to_to_depends_on.py │ ├── 101a789e38ad_created_task_responsible.py │ ├── 1181305d3001_added_client_id_column_to_goods_table.py │ ├── 130a7697cd79_vacation_user_can_now_be_nullable.py │ ├── 174567b9c159_note_content.py │ ├── 182f44ce5f07_added_users_company_and_projects_client.py │ ├── 1875136a2bfc_removed_version_variant_name_attribute.py │ ├── 1c9c9c28c102_price_lists_and_goods.py │ ├── 21b88ed3da95_added_referencemixin.py │ ├── 2252e51506de_multiple_repositories.py │ ├── 23dff41c95ff_removed_tasks_is_complete_column.py │ ├── 255ee1f9c7b3_added_payments_table.py │ ├── 258985128aff_create_entitygroups_table.py │ ├── 25b3eba6ffe7_derive_version_from.py │ ├── 275bdc106fd5_added_ticket_summary.py │ ├── 2aeab8b376dc_fg_color_bg_color.py │ ├── 2e4a3813ae76_created_daily_class.py │ ├── 2f55dc4f199f_wiki_page.py │ ├── 30c576f3691_budget_and_budget_entry.py │ ├── 31b1e22b455e_added_exclude_and_check_constraints_to_.py │ ├── 39d3c16ff005_budget_entries_good_id.py │ ├── 3be540ad3a93_added_version_revision_number_attribute.py │ ├── 409d2d73ca30_user_rate.py │ ├── 433d9caaafab_task_review_status_workflow.py │ ├── 4400871fa852_scene_is_now_deriving_from_task.py │ ├── 4664d72ce1e1_renamed_link_path_to_full_path.py │ ├── 46775e4a3d96_create_enum_types.py │ ├── 4a836cf73bcf_create_entitytype_accepts_references.py │ ├── 5078390e5527_shot_scene_relation_is_now_many_to_one.py │ ├── 5168cc8552a3_html_style_html_class.py │ ├── 5355b569237b_version_version_of_r.py │ ├── 53d8127d8560_parent_child_relatio.py │ ├── 57a5949c7f29_cache_for_total_logged_seconds.py │ ├── 5814290f49c7_added_shot_source_in_shot_source_out_record_in.py │ ├── 583875229230_good_task_relation.py │ ├── 59092d41175c_added_version_created_with.py │ ├── 5999269aad30_added_generic_text_attribute.py │ ├── 59bfe820c369_resource_efficiency.py │ ├── 6297277da38_added_vacation_class.py │ ├── 644f5251fc0d_remove_project_active_attribute.py │ ├── 745b210e6907_fix_non_existing_thumbnails.py │ ├── 856e70016b2_roles.py │ ├── 91ed52b72b82_created_variant_class.py │ ├── 92257ba439e1_budget_is_now_statusable.py │ ├── 9f9b88fef376_link_renamed_to_file.py │ ├── a2007ad7f535_added_review_version_id_column.py │ ├── a6598cde6b_versions_are_not_mix.py │ ├── a9319b19f7be_added_shot_fps.py │ ├── af869ddfdf9_entity_to_note_relation_is_now_many_to_many.py │ ├── bf67e6a234b4_added_revision_code_attribute.py │ ├── c5607b4cfb0a_added_support_for_time_zones.py │ ├── d8421de6a206_added_project_users_rate_column.py │ ├── e25ec9930632_shot_sequence_relation_is_now_many_to_.py │ ├── ea28a39ba3f5_added_invoices_table.py │ ├── eaed49db6d9_added_position_column_to_Project_Repositories.py │ ├── ec1eb2151bb9_rename_version_take_name_to_version_.py │ ├── ed0167fff399_added_workinghours_table.py │ ├── f16651477e64_added_authenticationlog_class.py │ ├── f2005d1fbadc_added_projectclients.py │ └── feca9bac7d5a_renamed_osx_to_macos.py ├── docs ├── Makefile ├── make.bat ├── make_html.bat └── source │ ├── _static │ └── images │ │ ├── Task_Status_Workflow.png │ │ ├── Task_Status_Workflow.vue │ │ └── stalker_design.vue │ ├── _templates │ └── autosummary │ │ ├── base.rst │ │ ├── class.rst │ │ └── module.rst │ ├── about.rst │ ├── changelog.rst │ ├── conf.py │ ├── configure.rst │ ├── contents.rst │ ├── contribute.rst │ ├── design.rst │ ├── index.rst │ ├── inheritance_diagram.rst │ ├── installation.rst │ ├── roadmap.rst │ ├── status_and_status_lists.rst │ ├── summary.rst │ ├── task_review_workflow.rst │ ├── todo.rst │ ├── tutorial.rst │ ├── tutorial │ ├── asset_management.rst │ ├── basics.rst │ ├── collaboration.rst │ ├── conclusion.rst │ ├── creating_simple_data.rst │ ├── extending_som.rst │ ├── pipeline.rst │ ├── query_update_delete_data.rst │ ├── scheduling.rst │ ├── task_and_resource_management.rst │ └── tutorial_files │ │ └── tutorial.py │ └── upgrade_db.rst ├── examples ├── __init__.py ├── extending │ ├── __init__.py │ ├── camera_lens.py │ ├── great_entity.py │ └── statused_entity.py └── flat_project_example.py ├── pyproject.toml ├── requirements-dev.txt ├── requirements.txt ├── setup.py ├── src └── stalker │ ├── VERSION │ ├── __init__.py │ ├── config.py │ ├── db │ ├── __init__.py │ ├── declarative.py │ ├── session.py │ ├── setup.py │ └── types.py │ ├── exceptions.py │ ├── log.py │ ├── models │ ├── __init__.py │ ├── asset.py │ ├── auth.py │ ├── budget.py │ ├── client.py │ ├── department.py │ ├── entity.py │ ├── enum.py │ ├── file.py │ ├── format.py │ ├── message.py │ ├── mixins.py │ ├── note.py │ ├── project.py │ ├── repository.py │ ├── review.py │ ├── scene.py │ ├── schedulers.py │ ├── sequence.py │ ├── shot.py │ ├── status.py │ ├── structure.py │ ├── studio.py │ ├── tag.py │ ├── task.py │ ├── template.py │ ├── ticket.py │ ├── type.py │ ├── variant.py │ ├── version.py │ └── wiki.py │ ├── py.typed │ ├── utils.py │ └── version.py ├── tests ├── __init__.py ├── benchmarks │ ├── __init__.py │ └── task_total_logged_seonds.py ├── config │ ├── __init__.py │ └── test_config.py ├── conftest.py ├── data │ ├── project_to_tjp_output.jinja2 │ ├── project_to_tjp_output_formatted │ └── project_to_tjp_output_rendered ├── db │ ├── __init__.py │ ├── test_db.py │ ├── test_dbsession.py │ └── test_types.py ├── mixins │ ├── __init__.py │ ├── test_acl_mixin.py │ ├── test_amount_mixin.py │ ├── test_code_mixin.py │ ├── test_create_secondary_table.py │ ├── test_dag_mixin.py │ ├── test_date_range_mixin.py │ ├── test_declarative_project_mixin.py │ ├── test_declarative_reference_mixin.py │ ├── test_declarative_schedule_mixin.py │ ├── test_declarative_status_mixin.py │ ├── test_project_mixin.py │ ├── test_reference_mixin.py │ ├── test_schedule_mixin.py │ ├── test_status_mixin.py │ ├── test_target_entity_type_mixin.py │ └── test_unit_mixin.py ├── models │ ├── __init__.py │ ├── test_asset.py │ ├── test_authentication_log.py │ ├── test_budget.py │ ├── test_client.py │ ├── test_client_user.py │ ├── test_daily.py │ ├── test_department.py │ ├── test_department_user.py │ ├── test_dependency_target.py │ ├── test_entity.py │ ├── test_entity_group.py │ ├── test_file.py │ ├── test_filename_template.py │ ├── test_generic.py │ ├── test_good.py │ ├── test_group.py │ ├── test_image_format.py │ ├── test_invoice.py │ ├── test_local_session.py │ ├── test_message.py │ ├── test_note.py │ ├── test_payment.py │ ├── test_permission.py │ ├── test_price_list.py │ ├── test_project.py │ ├── test_project_client.py │ ├── test_project_user.py │ ├── test_repository.py │ ├── test_review.py │ ├── test_role.py │ ├── test_scene.py │ ├── test_schedule_constraint.py │ ├── test_schedule_model.py │ ├── test_schedulers.py │ ├── test_sequence.py │ ├── test_shot.py │ ├── test_simple_entity.py │ ├── test_status.py │ ├── test_status_list.py │ ├── test_structure.py │ ├── test_studio.py │ ├── test_tag.py │ ├── test_task.py │ ├── test_task_dependency.py │ ├── test_task_juggler_scheduler.py │ ├── test_task_status_workflow.py │ ├── test_ticket.py │ ├── test_time_log.py │ ├── test_time_unit.py │ ├── test_traversal_direction.py │ ├── test_type.py │ ├── test_user.py │ ├── test_vacation.py │ ├── test_variant.py │ ├── test_version.py │ ├── test_wiki.py │ └── test_working_hours.py ├── test_exceptions.py ├── test_logging.py ├── test_readme_tutorial.py ├── test_testing.py ├── test_version.py └── utils.py └── whitelist.txt /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 4 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | charset = utf-8 11 | end_of_line = lf 12 | 13 | [*.yml] 14 | indent_size = 2 15 | 16 | [*.md] 17 | insert_final_newline = false 18 | trim_trailing_whitespace = false 19 | 20 | [*.bat] 21 | indent_style = tab 22 | end_of_line = crlf 23 | 24 | [LICENSE] 25 | insert_final_newline = false 26 | 27 | [Makefile] 28 | indent_style = tab 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache/* 2 | .coverage* 3 | .DS_Store 4 | .env 5 | .mypy_cache/ 6 | .pytest_cache 7 | .tox/ 8 | .venv/ 9 | .vscode/ 10 | *.pyc 11 | *.swp 12 | *~* 13 | build/* 14 | dist/ 15 | dist/* 16 | docs/build/* 17 | docs/doctrees/* 18 | docs/html/* 19 | docs/latex/* 20 | docs/source/generated/* 21 | docs/source/static/design 22 | docs/source/static/stalker_design*.vue 23 | htmlcov 24 | include/* 25 | local 26 | stalker.db* 27 | stalker.egg-info -------------------------------------------------------------------------------- /Dockerfile-py3.5: -------------------------------------------------------------------------------- 1 | # This Dockerfile is based on: https://docs.docker.com/examples/postgresql_service/ 2 | 3 | FROM ubuntu:16.04 4 | 5 | MAINTAINER fredrik@averpil.com 6 | 7 | # Add the PostgreSQL PGP key to verify their Debian packages. 8 | # It should be the same key as https://www.postgresql.org/media/keys/ACCC4CF8.asc 9 | RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8 10 | 11 | # Add PostgreSQL's repository. It contains the most recent stable release 12 | # of PostgreSQL, ``9.3``. 13 | RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ precise-pgdg main" > /etc/apt/sources.list.d/pgdg.list 14 | 15 | # Install everything in one enormous RUN command 16 | # There are some warnings (in red) that show up during the build. You can hide 17 | # them by prefixing each apt-get statement with DEBIAN_FRONTEND=noninteractive 18 | RUN apt-get update && \ 19 | 20 | apt-get install -y \ 21 | python3-software-properties python3-pip \ 22 | software-properties-common \ 23 | postgresql-9.3 postgresql-client-9.3 postgresql-contrib-9.3 postgresql-server-dev-9.3 \ 24 | rubygems && \ 25 | 26 | gem install taskjuggler && \ 27 | 28 | pip3 install -U pip && \ 29 | pip3 install sqlalchemy psycopg2-binary jinja2 alembic mako markupsafe python-editor nose coverage 30 | 31 | # Note: The official Debian and Ubuntu images automatically ``apt-get clean`` 32 | # after each ``apt-get`` 33 | 34 | # Run commands as the ``postgres`` user created by the ``postgres-9.3`` package when it was ``apt-get installed`` 35 | USER postgres 36 | 37 | RUN /etc/init.d/postgresql start && \ 38 | psql -c "CREATE DATABASE stalker_test;" -U postgres && \ 39 | psql -c "CREATE USER stalker_admin WITH PASSWORD 'stalker';" -U postgres && \ 40 | /etc/init.d/postgresql stop 41 | 42 | # Adjust PostgreSQL configuration so that remote connections to the 43 | # database are possible. 44 | # RUN echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/9.3/main/pg_hba.conf 45 | 46 | # And add ``listen_addresses`` to ``/etc/postgresql/9.3/main/postgresql.conf`` 47 | # RUN echo "listen_addresses='*'" >> /etc/postgresql/9.3/main/postgresql.conf 48 | 49 | # Expose the PostgreSQL port 50 | # EXPOSE 5432 51 | 52 | # Add VOLUMEs to allow backup of config, logs and databases 53 | # VOLUME ["/etc/postgresql", "/var/log/postgresql", "/var/lib/postgresql"] 54 | 55 | USER root 56 | 57 | # Create symlink to TaskJuggler 58 | # RUN ln -s $(which tj3) /usr/local/bin/tj3 59 | 60 | # Set working directory 61 | WORKDIR /workspace 62 | 63 | # Embed wait-for-postgres.sh script into Dockerfile 64 | RUN echo '\n\ 65 | # wait-for-postgres\n\ 66 | \n\ 67 | 68 | set -e\n\ 69 | \n\ 70 | cmd="$@"\n\ 71 | timer="5"\n\ 72 | \n\ 73 | until runuser -l postgres -c 'pg_isready' 2>/dev/null; do\n\ 74 | >&2 echo "Postgres is unavailable - sleeping for $timer seconds"\n\ 75 | sleep $timer\n\ 76 | done\n\ 77 | \n\ 78 | >&2 echo "Postgres is up - executing command"\n\ 79 | exec $cmd\n'\ 80 | >> /workspace/wait-for-postgres.sh 81 | 82 | # Make script executable 83 | RUN chmod +x /workspace/wait-for-postgres.sh 84 | 85 | # Execute this when running container 86 | ENTRYPOINT \ 87 | 88 | # Copy stalker into container's /workspace' 89 | cp -r /stalker /workspace && \ 90 | 91 | # Remove execution permissions within Stalker 92 | chmod -R -x /workspace/stalker && \ 93 | 94 | # Start PostgreSQL 95 | 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 & ' && \ 96 | 97 | # Wait for PostgresSQL 98 | ./wait-for-postgres.sh nosetests /workspace/stalker --verbosity=1 --cover-erase --with-coverage --cover-package=stalker && \ 99 | 100 | # Cleanly shut down PostgreSQL 101 | /etc/init.d/postgresql stop 102 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | See docs/installation.html. 2 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.ini *.cfg 2 | 3 | include alembic.ini 4 | include CHANGELOG.rst 5 | include COPYING 6 | include COPYING.LESSER 7 | include INSTALL 8 | include MANIFEST.in 9 | include README.rst 10 | include stalker/VERSION 11 | include TODO 12 | include VERSION 13 | 14 | prune docs/build 15 | prune docs/source/generated -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL:=bash 2 | PACKAGE_NAME=stalker 3 | NUM_CPUS = $(shell nproc || grep -c '^processor' /proc/cpuinfo) 4 | SETUP_PY_FLAGS = --use-distutils 5 | VERSION := $(shell cat VERSION) 6 | VERSION_FILE=$(CURDIR)/src/stalker/VERSION 7 | VIRTUALENV_DIR:=.venv 8 | SYSTEM_PYTHON?=python3 9 | 10 | all: build FORCE 11 | 12 | .PHONY: help 13 | help: 14 | @echo "" 15 | @echo "Available targets:" 16 | @make -qp | grep -o '^[a-z0-9-]\+' | sort 17 | 18 | .PHONY: venv 19 | venv: 20 | @printf "\n\033[36m--- $@: Creating Local virtualenv '$(VIRTUALENV_DIR)' using '$(SYSTEM_PYTHON)' ---\033[0m\n" 21 | $(SYSTEM_PYTHON) -m venv $(VIRTUALENV_DIR) 22 | 23 | build: 24 | @printf "\n\033[36m--- $@: Building ---\033[0m\n" 25 | echo -e "\n\033[36m--- $@: Local install into virtualenv '$(VIRTUALENV_DIR)' ---\033[0m\n"; 26 | source ./$(VIRTUALENV_DIR)/bin/activate; \ 27 | echo -e "\n\033[36m--- $@: Using python interpretter '`which python`' ---\033[0m\n"; \ 28 | pip install -r requirements.txt; \ 29 | pip install -r requirements-dev.txt; \ 30 | python -m build; 31 | 32 | .PHONY: install 33 | install: 34 | @printf "\n\033[36m--- $@: Installing $(PACKAGE_NAME) to virtualenv at '$(VIRTUALENV_DIR)' using '$(SYSTEM_PYTHON)' ---\033[0m\n" 35 | source ./$(VIRTUALENV_DIR)/bin/activate; \ 36 | pip install ./dist/$(PACKAGE_NAME)-$(VERSION)-*.whl --force-reinstall; 37 | 38 | clean: FORCE 39 | @printf "\n\033[36m--- $@: Clean ---\033[0m\n" 40 | -rm -rf .pytest_cache 41 | -rm -f .coverage* 42 | -rm -rf .mypy_cache 43 | -rm -rf .tox 44 | -rm -rf dist 45 | -rm -rf build 46 | -rm -rf docs/build 47 | -rm -rf docs/source/generated/* 48 | -rm -rf htmlcov 49 | 50 | clean-all: clean 51 | @printf "\n\033[36m--- $@: Clean All---\033[0m\n" 52 | -rm -f INSTALLED_FILES 53 | -rm -f setuptools-*.egg 54 | -rm -f use-distutils 55 | -rm -Rf src/$(PACKAGE_NAME).egg-info 56 | -rm -Rf $(VIRTUALENV_DIR) 57 | 58 | html: 59 | ./setup.py readme 60 | 61 | new-release: 62 | @printf "\n\033[36m--- $@: Generating New Release ---\033[0m\n" 63 | git add $(VERSION_FILE) 64 | git commit -m "Version $(VERSION)" 65 | git push 66 | git checkout main 67 | git pull 68 | git merge develop 69 | git tag $(VERSION) 70 | git push origin main --tags 71 | source ./$(VIRTUALENV_DIR)/bin/activate; \ 72 | echo -e "\n\033[36m--- $@: Using python interpretter '`which python`' ---\033[0m\n"; \ 73 | pip install -r requirements.txt; \ 74 | pip install -r requirements-dev.txt; \ 75 | python -m build; \ 76 | twine check dist/$(PACKAGE_NAME)-$(VERSION).tar.gz; \ 77 | twine upload dist/$(PACKAGE_NAME)-$(VERSION).tar.gz; 78 | 79 | .PHONY: tests 80 | tests: 81 | @printf "\n\033[36m--- $@: Run Tests ---\033[0m\n" 82 | echo -e "\n\033[36m--- $@: Using virtualenv at '$(VIRTUALENV_DIR)' ---\033[0m\n"; 83 | source ./$(VIRTUALENV_DIR)/bin/activate; \ 84 | echo -e "\n\033[36m--- $@: Using python interpretter '`which python`' ---\033[0m\n"; \ 85 | SQLALCHEMY_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; 86 | 87 | .PHONY: docs 88 | docs: 89 | cd docs && $(MAKE) html 90 | 91 | # https://www.gnu.org/software/make/manual/html_node/Force-Targets.html 92 | FORCE: 93 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | src/stalker/VERSION -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # set to 'true' to run the environment during 11 | # the 'revision' command, regardless of autogenerate 12 | # revision_environment = false 13 | 14 | #sqlalchemy.url = sqlite:///%(here)s/stalker.db 15 | sqlalchemy.url = postgresql://stalker_admin:stalker@localhost:5432/stalker 16 | 17 | 18 | # Logging configuration 19 | [loggers] 20 | keys = root,sqlalchemy,alembic 21 | 22 | [handlers] 23 | keys = console 24 | 25 | [formatters] 26 | keys = generic 27 | 28 | [logger_root] 29 | level = WARN 30 | handlers = console 31 | qualname = 32 | 33 | [logger_sqlalchemy] 34 | level = WARN 35 | handlers = 36 | qualname = sqlalchemy.engine 37 | 38 | [logger_alembic] 39 | level = INFO 40 | handlers = 41 | qualname = alembic 42 | 43 | [handler_console] 44 | class = StreamHandler 45 | args = (sys.stderr,) 46 | level = NOTSET 47 | formatter = generic 48 | 49 | [formatter_generic] 50 | format = %(levelname)-5.5s [%(name)s] %(message)s 51 | datefmt = %H:%M:%S 52 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Setup environment for migration.""" 3 | from logging.config import fileConfig 4 | 5 | from alembic import context 6 | 7 | from sqlalchemy import engine_from_config, pool 8 | 9 | from stalker.db.declarative import Base 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | 24 | target_metadata = Base.metadata 25 | 26 | # other values from the config, defined by the needs of env.py, 27 | # can be acquired: 28 | # my_important_option = config.get_main_option("my_important_option") 29 | # ... etc. 30 | 31 | 32 | def run_migrations_offline(): 33 | """Run migrations in 'offline' mode. 34 | 35 | This configures the context with just a URL 36 | and not an Engine, though an Engine is acceptable 37 | here as well. By skipping the Engine creation 38 | we don't even need a DBAPI to be available. 39 | 40 | Calls to context.execute() here emit the given string to the 41 | script output. 42 | 43 | """ 44 | url = config.get_main_option("sqlalchemy.url") 45 | context.configure(url=url) 46 | 47 | with context.begin_transaction(): 48 | context.run_migrations() 49 | 50 | 51 | def run_migrations_online(): 52 | """Run migrations in 'online' mode. 53 | 54 | In this scenario we need to create an Engine 55 | and associate a connection with the context. 56 | 57 | """ 58 | engine = engine_from_config( 59 | config.get_section(config.config_ini_section), 60 | prefix="sqlalchemy.", 61 | poolclass=pool.NullPool, 62 | ) 63 | 64 | connection = engine.connect() 65 | context.configure(connection=connection, target_metadata=target_metadata) 66 | 67 | try: 68 | with context.begin_transaction(): 69 | context.run_migrations() 70 | finally: 71 | connection.close() 72 | 73 | 74 | if context.is_offline_mode(): 75 | run_migrations_offline() 76 | else: 77 | run_migrations_online() 78 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision} 5 | Create Date: ${create_date} 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = ${repr(up_revision)} 16 | down_revision = ${repr(down_revision)} 17 | 18 | 19 | def upgrade(): 20 | """Upgrade the tables.""" 21 | ${upgrades if upgrades else "pass"} 22 | 23 | 24 | def downgrade(): 25 | """Downgrade the tables.""" 26 | ${downgrades if downgrades else "pass"} 27 | -------------------------------------------------------------------------------- /alembic/versions/0063f547dc2e_updated_version_inputs_table.py: -------------------------------------------------------------------------------- 1 | """updated version_inputs table. 2 | 3 | Revision ID: 0063f547dc2e 4 | Revises: a9319b19f7be 5 | Create Date: 2016-11-29 14:08:41.335000 6 | """ 7 | 8 | from alembic import op 9 | 10 | # revision identifiers, used by Alembic. 11 | revision = "0063f547dc2e" 12 | down_revision = "a9319b19f7be" 13 | 14 | 15 | def upgrade(): 16 | """Upgrade the tables.""" 17 | op.drop_constraint( 18 | "Version_Inputs_link_id_fkey", "Version_Inputs", type_="foreignkey" 19 | ) 20 | op.create_foreign_key( 21 | None, 22 | "Version_Inputs", 23 | "Links", 24 | ["link_id"], 25 | ["id"], 26 | onupdate="CASCADE", 27 | ondelete="CASCADE", 28 | ) 29 | 30 | 31 | def downgrade(): 32 | """Downgrade the tables.""" 33 | op.drop_constraint(None, "Version_Inputs", type_="foreignkey") 34 | op.create_foreign_key( 35 | "Version_Inputs_link_id_fkey", "Version_Inputs", "Links", ["link_id"], ["id"] 36 | ) 37 | -------------------------------------------------------------------------------- /alembic/versions/019378697b5b_rename_depends_to_to_depends_on.py: -------------------------------------------------------------------------------- 1 | """Rename depends_to to depends_on 2 | 3 | Revision ID: 019378697b5b 4 | Revises: feca9bac7d5a 5 | Create Date: 2024-11-01 13:59:11.513575 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "019378697b5b" 15 | down_revision = "feca9bac7d5a" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | op.alter_column( 21 | "Task_Dependencies", "depends_to_id", new_column_name="depends_on_id" 22 | ) 23 | 24 | 25 | def downgrade(): 26 | """Downgrade the tables.""" 27 | op.alter_column( 28 | "Task_Dependencies", "depends_on_id", new_column_name="depends_to_id" 29 | ) 30 | -------------------------------------------------------------------------------- /alembic/versions/101a789e38ad_created_task_responsible.py: -------------------------------------------------------------------------------- 1 | """Created "Task.responsible" attribute. 2 | 3 | Revision ID: 101a789e38ad 4 | Revises: 59092d41175c 5 | Create Date: 2013-06-24 12:32:04.852386 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "101a789e38ad" 14 | down_revision = "59092d41175c" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | try: 20 | op.drop_column("Sequences", "lead_id") 21 | op.add_column("Tasks", sa.Column("responsible_id", sa.Integer(), nullable=True)) 22 | except sa.exc.OperationalError: 23 | pass 24 | 25 | 26 | def downgrade(): 27 | """Downgrade the tables.""" 28 | try: 29 | op.drop_column("Tasks", "responsible_id") 30 | op.add_column("Sequences", sa.Column("lead_id", sa.INTEGER(), nullable=True)) 31 | except sa.exc.OperationalError: 32 | pass 33 | -------------------------------------------------------------------------------- /alembic/versions/1181305d3001_added_client_id_column_to_goods_table.py: -------------------------------------------------------------------------------- 1 | """Added client_id column to Goods table. 2 | 3 | Revision ID: 1181305d3001 4 | Revises: 31b1e22b455e 5 | Create Date: 2017-05-17 18:17:46.555000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "1181305d3001" 14 | down_revision = "31b1e22b455e" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("Goods", sa.Column("client_id", sa.Integer(), nullable=True)) 20 | op.create_foreign_key(None, "Goods", "Clients", ["client_id"], ["id"]) 21 | 22 | 23 | def downgrade(): 24 | """Downgrade the tables.""" 25 | op.drop_constraint(None, "Goods", type_="foreignkey") 26 | op.drop_column("Goods", "client_id") 27 | -------------------------------------------------------------------------------- /alembic/versions/130a7697cd79_vacation_user_can_now_be_nullable.py: -------------------------------------------------------------------------------- 1 | """Vacation.user can now be nullable. 2 | 3 | Revision ID: 130a7697cd79 4 | Revises: 57a5949c7f29 5 | Create Date: 2013-08-02 19:58:59.638085 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "130a7697cd79" 14 | down_revision = "57a5949c7f29" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.alter_column("Vacations", "user_id", existing_type=sa.INTEGER(), nullable=True) 20 | 21 | 22 | def downgrade(): 23 | """Downgrade the tables.""" 24 | op.alter_column("Vacations", "user_id", existing_type=sa.INTEGER(), nullable=False) 25 | -------------------------------------------------------------------------------- /alembic/versions/174567b9c159_note_content.py: -------------------------------------------------------------------------------- 1 | """'Note.content' is now a synonym of 'Note.description'. 2 | 3 | Revision ID: 174567b9c159 4 | Revises: a6598cde6b 5 | Create Date: 2013-11-14 13:38:02.566201 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "174567b9c159" 14 | down_revision = "a6598cde6b" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.drop_column("Notes", "content") 20 | 21 | 22 | def downgrade(): 23 | """Downgrade the tables.""" 24 | op.add_column("Notes", sa.Column("content", sa.VARCHAR(), nullable=True)) 25 | -------------------------------------------------------------------------------- /alembic/versions/182f44ce5f07_added_users_company_and_projects_client.py: -------------------------------------------------------------------------------- 1 | """added "Users.company" and "Projects.client" columns and a new Clients table. 2 | 3 | Revision ID: 182f44ce5f07 4 | Revises: 59bfe820c369 5 | Create Date: 2014-05-29 11:33:02.313000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "182f44ce5f07" 14 | down_revision = "59bfe820c369" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | # Create Clients table 20 | op.create_table( 21 | "Clients", 22 | sa.Column("id", sa.Integer(), nullable=False), 23 | sa.ForeignKeyConstraint( 24 | ["id"], 25 | ["Entities.id"], 26 | ), 27 | sa.PrimaryKeyConstraint("id"), 28 | ) 29 | 30 | # Users table 31 | op.add_column("Users", sa.Column("company_id", sa.Integer(), nullable=True)) 32 | op.create_foreign_key( 33 | name=None, 34 | source="Users", 35 | referent="Clients", 36 | local_cols=["company_id"], 37 | remote_cols=["id"], 38 | ) 39 | 40 | # Projects table 41 | op.add_column("Projects", sa.Column("client_id", sa.Integer(), nullable=True)) 42 | op.create_foreign_key( 43 | name=None, 44 | source="Projects", 45 | referent="Clients", 46 | local_cols=["client_id"], 47 | remote_cols=["id"], 48 | ) 49 | 50 | 51 | def downgrade(): 52 | """Downgrade the tables.""" 53 | op.drop_column("Users", "company_id") 54 | op.drop_column("Projects", "client_id") 55 | op.drop_table("Clients") 56 | -------------------------------------------------------------------------------- /alembic/versions/1c9c9c28c102_price_lists_and_goods.py: -------------------------------------------------------------------------------- 1 | """Add PriceLists and Goods. 2 | 3 | Revision ID: 1c9c9c28c102 4 | Revises: 856e70016b2 5 | Create Date: 2015-01-26 13:05:50.050345 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "1c9c9c28c102" 14 | down_revision = "856e70016b2" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.create_table( 20 | "PriceLists", 21 | sa.Column("id", sa.Integer(), nullable=False), 22 | sa.ForeignKeyConstraint( 23 | ["id"], 24 | ["Entities.id"], 25 | ), 26 | sa.PrimaryKeyConstraint("id"), 27 | ) 28 | op.create_table( 29 | "Goods", 30 | sa.Column("id", sa.Integer(), nullable=False), 31 | sa.Column("cost", sa.Float(), nullable=True), 32 | sa.Column("msrp", sa.Float(), nullable=True), 33 | sa.Column("unit", sa.String(length=64), nullable=True), 34 | sa.ForeignKeyConstraint( 35 | ["id"], 36 | ["Entities.id"], 37 | ), 38 | sa.PrimaryKeyConstraint("id"), 39 | ) 40 | op.create_table( 41 | "PriceList_Goods", 42 | sa.Column("price_list_id", sa.Integer(), nullable=False), 43 | sa.Column("good_id", sa.Integer(), nullable=False), 44 | sa.ForeignKeyConstraint( 45 | ["good_id"], 46 | ["Goods.id"], 47 | ), 48 | sa.ForeignKeyConstraint( 49 | ["price_list_id"], 50 | ["PriceLists.id"], 51 | ), 52 | sa.PrimaryKeyConstraint("price_list_id", "good_id"), 53 | ) 54 | with op.batch_alter_table("BudgetEntries", schema=None) as batch_op: 55 | batch_op.add_column(sa.Column("cost", sa.Float(), nullable=True)) 56 | batch_op.add_column(sa.Column("msrp", sa.Float(), nullable=True)) 57 | batch_op.add_column(sa.Column("price", sa.Float(), nullable=True)) 58 | batch_op.add_column(sa.Column("realized_total", sa.Float(), nullable=True)) 59 | batch_op.add_column(sa.Column("unit", sa.String(length=64), nullable=True)) 60 | 61 | with op.batch_alter_table("Budgets", schema=None) as batch_op: 62 | batch_op.add_column(sa.Column("parent_id", sa.Integer(), nullable=True)) 63 | 64 | 65 | def downgrade(): 66 | """Downgrade the tables.""" 67 | with op.batch_alter_table("Budgets", schema=None) as batch_op: 68 | batch_op.drop_column("parent_id") 69 | 70 | with op.batch_alter_table("BudgetEntries", schema=None) as batch_op: 71 | batch_op.drop_column("unit") 72 | batch_op.drop_column("realized_total") 73 | batch_op.drop_column("price") 74 | batch_op.drop_column("msrp") 75 | batch_op.drop_column("cost") 76 | 77 | op.drop_table("PriceList_Goods") 78 | op.drop_table("Goods") 79 | op.drop_table("PriceLists") 80 | -------------------------------------------------------------------------------- /alembic/versions/21b88ed3da95_added_referencemixin.py: -------------------------------------------------------------------------------- 1 | """Added ReferenceMixin to Task. 2 | 3 | Revision ID: 21b88ed3da95 4 | Revises: 4664d72ce1e1 5 | Create Date: 2013-05-31 12:08:59.425539 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "21b88ed3da95" 15 | down_revision = "4664d72ce1e1" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | try: 21 | op.create_table( 22 | "Task_References", 23 | sa.Column("task_id", sa.Integer(), nullable=False), 24 | sa.Column("link_id", sa.Integer(), nullable=False), 25 | sa.ForeignKeyConstraint( 26 | ["link_id"], 27 | ["Links.id"], 28 | ), 29 | sa.ForeignKeyConstraint( 30 | ["task_id"], 31 | ["Tasks.id"], 32 | ), 33 | sa.PrimaryKeyConstraint("task_id", "link_id"), 34 | ) 35 | except sa.exc.OperationalError: 36 | pass 37 | 38 | op.drop_table("Asset_References") 39 | op.drop_table("Shot_References") 40 | op.drop_table("Sequence_References") 41 | 42 | 43 | def downgrade(): 44 | """Downgrade the tables.""" 45 | op.create_table( 46 | "Sequence_References", 47 | sa.Column("sequence_id", sa.INTEGER(), autoincrement=False, nullable=False), 48 | sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), 49 | sa.ForeignKeyConstraint( 50 | ["link_id"], ["Links.id"], name="Sequence_References_link_id_fkey" 51 | ), 52 | sa.ForeignKeyConstraint( 53 | ["sequence_id"], 54 | ["Sequences.id"], 55 | name="Sequence_References_sequence_id_fkey", 56 | ), 57 | sa.PrimaryKeyConstraint( 58 | "sequence_id", "link_id", name="Sequence_References_pkey" 59 | ), 60 | ) 61 | op.create_table( 62 | "Shot_References", 63 | sa.Column("shot_id", sa.INTEGER(), autoincrement=False, nullable=False), 64 | sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), 65 | sa.ForeignKeyConstraint( 66 | ["link_id"], ["Links.id"], name="Shot_References_link_id_fkey" 67 | ), 68 | sa.ForeignKeyConstraint( 69 | ["shot_id"], ["Shots.id"], name="Shot_References_shot_id_fkey" 70 | ), 71 | sa.PrimaryKeyConstraint("shot_id", "link_id", name="Shot_References_pkey"), 72 | ) 73 | op.create_table( 74 | "Asset_References", 75 | sa.Column("asset_id", sa.INTEGER(), autoincrement=False, nullable=False), 76 | sa.Column("link_id", sa.INTEGER(), autoincrement=False, nullable=False), 77 | sa.ForeignKeyConstraint( 78 | ["asset_id"], ["Assets.id"], name="Asset_References_asset_id_fkey" 79 | ), 80 | sa.ForeignKeyConstraint( 81 | ["link_id"], ["Links.id"], name="Asset_References_link_id_fkey" 82 | ), 83 | sa.PrimaryKeyConstraint("asset_id", "link_id", name="Asset_References_pkey"), 84 | ) 85 | op.drop_table("Task_References") 86 | -------------------------------------------------------------------------------- /alembic/versions/2252e51506de_multiple_repositories.py: -------------------------------------------------------------------------------- 1 | """Multiple Repositories per Project. 2 | 3 | Revision ID: 2252e51506de 4 | Revises: 1c9c9c28c102 5 | Create Date: 2015-01-28 00:46:29.139946 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "2252e51506de" 14 | down_revision = "1c9c9c28c102" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.create_table( 20 | "Project_Repositories", 21 | sa.Column("project_id", sa.Integer(), nullable=False), 22 | sa.Column("repo_id", sa.Integer(), nullable=False), 23 | sa.ForeignKeyConstraint(["project_id"], ["Projects.id"]), 24 | sa.ForeignKeyConstraint(["repo_id"], ["Repositories.id"]), 25 | sa.PrimaryKeyConstraint("project_id", "repo_id"), 26 | ) 27 | 28 | # before dropping repository column, carry all the data to the new table 29 | op.execute( 30 | 'insert into "Project_Repositories"' 31 | " select id, repository_id " 32 | ' from "Projects"' 33 | ) 34 | 35 | with op.batch_alter_table("Projects", schema=None) as batch_op: 36 | batch_op.drop_column("repository_id") 37 | 38 | 39 | def downgrade(): 40 | """Downgrade the tables.""" 41 | with op.batch_alter_table("Projects", schema=None) as batch_op: 42 | batch_op.add_column( 43 | sa.Column("repository_id", sa.INTEGER(), autoincrement=False, nullable=True) 44 | ) 45 | 46 | # before dropping Project_Repositories, carry all the data back, 47 | # note that only the first repository found per project will be 48 | # restored to the Project.repository_id column 49 | op.execute(""" 50 | UPDATE "Projects" SET repository_id = ( 51 | SELECT 52 | repo_id 53 | FROM "Project_Repositories" 54 | WHERE project_id = "Projects".id LIMIT 1 55 | )""" 56 | ) 57 | 58 | op.drop_table("Project_Repositories") 59 | -------------------------------------------------------------------------------- /alembic/versions/23dff41c95ff_removed_tasks_is_complete_column.py: -------------------------------------------------------------------------------- 1 | """Removed Tasks.is_complete column. 2 | 3 | Revision ID: 23dff41c95ff 4 | Revises: 5999269aad30 5 | Create Date: 2014-06-11 14:00:00.559122 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "23dff41c95ff" 14 | down_revision = "5999269aad30" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.drop_column("Tasks", "is_complete") 20 | 21 | 22 | def downgrade(): 23 | """Downgrade the tables.""" 24 | op.add_column("Tasks", sa.Column("is_complete", sa.BOOLEAN(), nullable=True)) 25 | -------------------------------------------------------------------------------- /alembic/versions/255ee1f9c7b3_added_payments_table.py: -------------------------------------------------------------------------------- 1 | """Added Payments table. 2 | 3 | Revision ID: 255ee1f9c7b3 4 | Revises: ea28a39ba3f5 5 | Create Date: 2016-08-18 03:19:22.301000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "255ee1f9c7b3" 14 | down_revision = "ea28a39ba3f5" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.create_table( 20 | "Payments", 21 | sa.Column("id", sa.Integer(), nullable=False), 22 | sa.Column("invoice_id", sa.Integer(), nullable=True), 23 | sa.Column("amount", sa.Float(), nullable=True), 24 | sa.Column("unit", sa.String(length=64), nullable=True), 25 | sa.ForeignKeyConstraint( 26 | ["id"], 27 | ["Entities.id"], 28 | ), 29 | sa.ForeignKeyConstraint( 30 | ["invoice_id"], 31 | ["Invoices.id"], 32 | ), 33 | sa.PrimaryKeyConstraint("id"), 34 | ) 35 | 36 | 37 | def downgrade(): 38 | """Downgrade the tables.""" 39 | op.drop_table("Payments") 40 | -------------------------------------------------------------------------------- /alembic/versions/258985128aff_create_entitygroups_table.py: -------------------------------------------------------------------------------- 1 | """create EntityGroups table. 2 | 3 | Revision ID: 258985128aff 4 | Revises: 39d3c16ff005 5 | Create Date: 2016-05-16 16:06:39.389000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "258985128aff" 14 | down_revision = "39d3c16ff005" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.create_table( 20 | "EntityGroups", 21 | sa.Column("id", sa.Integer(), nullable=False), 22 | sa.ForeignKeyConstraint( 23 | ["id"], 24 | ["Entities.id"], 25 | ), 26 | sa.PrimaryKeyConstraint("id"), 27 | ) 28 | op.create_table( 29 | "EntityGroup_Entities", 30 | sa.Column("entity_group_id", sa.Integer(), nullable=False), 31 | sa.Column("other_entity_id", sa.Integer(), nullable=False), 32 | sa.ForeignKeyConstraint( 33 | ["entity_group_id"], 34 | ["EntityGroups.id"], 35 | ), 36 | sa.ForeignKeyConstraint( 37 | ["other_entity_id"], 38 | ["SimpleEntities.id"], 39 | ), 40 | sa.PrimaryKeyConstraint("entity_group_id", "other_entity_id"), 41 | ) 42 | 43 | 44 | def downgrade(): 45 | """Downgrade the tables.""" 46 | op.drop_table("EntityGroup_Entities") 47 | op.drop_table("EntityGroups") 48 | -------------------------------------------------------------------------------- /alembic/versions/275bdc106fd5_added_ticket_summary.py: -------------------------------------------------------------------------------- 1 | """Added "Ticket.summary". 2 | 3 | Revision ID: 275bdc106fd5 4 | Revises: 130a7697cd79 5 | Create Date: 2013-08-07 00:19:39.414232 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "275bdc106fd5" 14 | down_revision = "130a7697cd79" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("Tickets", sa.Column("summary", sa.String(), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | """Downgrade the tables.""" 24 | op.drop_column("Tickets", "summary") 25 | -------------------------------------------------------------------------------- /alembic/versions/2aeab8b376dc_fg_color_bg_color.py: -------------------------------------------------------------------------------- 1 | """Remove Statuses.bg_color and Statuses.fg_color columns. 2 | 3 | Revision ID: 2aeab8b376dc 4 | Revises: 5168cc8552a3 5 | Create Date: 2013-11-18 23:44:49.428028 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "2aeab8b376dc" 15 | down_revision = "5168cc8552a3" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | op.drop_column("Statuses", "bg_color") 21 | op.drop_column("Statuses", "fg_color") 22 | 23 | 24 | def downgrade(): 25 | """Downgrade the tables.""" 26 | op.add_column("Statuses", sa.Column("fg_color", sa.INTEGER(), nullable=True)) 27 | op.add_column("Statuses", sa.Column("bg_color", sa.INTEGER(), nullable=True)) 28 | -------------------------------------------------------------------------------- /alembic/versions/2f55dc4f199f_wiki_page.py: -------------------------------------------------------------------------------- 1 | """Add Wiki Page. 2 | 3 | Revision ID: 2f55dc4f199f 4 | Revises: 433d9caaafab 5 | Create Date: 2014-03-24 16:52:45.127579 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "2f55dc4f199f" 15 | down_revision = "433d9caaafab" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | op.create_table( 21 | "Pages", 22 | sa.Column("id", sa.Integer(), nullable=False), 23 | sa.Column("title", sa.String(), nullable=True), 24 | sa.Column("content", sa.String(), nullable=True), 25 | sa.Column("project_id", sa.Integer(), nullable=True), 26 | sa.ForeignKeyConstraint(["id"], ["Entities.id"]), 27 | sa.ForeignKeyConstraint( 28 | ["project_id"], ["Projects.id"], name="project_x_id", use_alter=True 29 | ), 30 | sa.PrimaryKeyConstraint("id"), 31 | ) 32 | 33 | 34 | def downgrade(): 35 | """Downgrade the tables.""" 36 | op.drop_table("Pages") 37 | -------------------------------------------------------------------------------- /alembic/versions/30c576f3691_budget_and_budget_entry.py: -------------------------------------------------------------------------------- 1 | """Added Budget and BudgetEntry tables. 2 | 3 | Revision ID: 30c576f3691 4 | Revises: 409d2d73ca30 5 | Create Date: 2014-11-20 22:49:37.015323 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "30c576f3691" 14 | down_revision = "409d2d73ca30" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.create_table( 20 | "Budgets", 21 | sa.Column("id", sa.Integer(), nullable=False), 22 | sa.Column("project_id", sa.Integer(), nullable=True), 23 | sa.ForeignKeyConstraint( 24 | ["id"], 25 | ["Entities.id"], 26 | ), 27 | sa.ForeignKeyConstraint( 28 | ["project_id"], 29 | ["Projects.id"], 30 | ), 31 | sa.PrimaryKeyConstraint("id"), 32 | ) 33 | op.create_table( 34 | "BudgetEntries", 35 | sa.Column("id", sa.Integer(), nullable=False), 36 | sa.Column("budget_id", sa.Integer(), nullable=True), 37 | sa.Column("amount", sa.Float(), nullable=True), 38 | sa.ForeignKeyConstraint( 39 | ["budget_id"], 40 | ["Budgets.id"], 41 | ), 42 | sa.ForeignKeyConstraint( 43 | ["id"], 44 | ["Entities.id"], 45 | ), 46 | sa.PrimaryKeyConstraint("id"), 47 | ) 48 | 49 | 50 | def downgrade(): 51 | """Downgrade the tables.""" 52 | op.drop_table("BudgetEntries") 53 | op.drop_table("Budgets") 54 | -------------------------------------------------------------------------------- /alembic/versions/39d3c16ff005_budget_entries_good_id.py: -------------------------------------------------------------------------------- 1 | """Added BudgetEntries.good_id. 2 | 3 | Revision ID: 39d3c16ff005 4 | Revises: eaed49db6d9 5 | Create Date: 2015-02-15 02:29:26.301437 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "39d3c16ff005" 14 | down_revision = "eaed49db6d9" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | with op.batch_alter_table("BudgetEntries", schema=None) as batch_op: 20 | batch_op.add_column(sa.Column("good_id", sa.Integer(), nullable=True)) 21 | 22 | 23 | def downgrade(): 24 | """Downgrade the tables.""" 25 | with op.batch_alter_table("BudgetEntries", schema=None) as batch_op: 26 | batch_op.drop_column("good_id") 27 | -------------------------------------------------------------------------------- /alembic/versions/3be540ad3a93_added_version_revision_number_attribute.py: -------------------------------------------------------------------------------- 1 | """Added Version.revision_number attribute 2 | 3 | Revision ID: 3be540ad3a93 4 | Revises: 1875136a2bfc 5 | Create Date: 2024-12-04 17:04:37.174269 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "3be540ad3a93" 15 | down_revision = "1875136a2bfc" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | op.execute( 21 | """ALTER TABLE "Versions" ADD revision_number integer NOT NULL DEFAULT 1;""" 22 | ) 23 | 24 | 25 | def downgrade(): 26 | """Downgrade the tables.""" 27 | # because we are removing the revision_number column, 28 | # add the 1000 * (revision_number - 1) to all the version numbers 29 | # to preserve the version sequences, intact... 30 | op.execute( 31 | """UPDATE "Versions" SET version_number = (1000 * (revision_number - 1) + version_number);""" 32 | ) 33 | op.execute("""ALTER TABLE "Versions" DROP COLUMN revision_number;""") 34 | -------------------------------------------------------------------------------- /alembic/versions/409d2d73ca30_user_rate.py: -------------------------------------------------------------------------------- 1 | """Added "Users.rate". 2 | 3 | Revision ID: 409d2d73ca30 4 | Revises: 5814290f49c7 5 | Create Date: 2014-11-20 22:47:56.013644 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "409d2d73ca30" 14 | down_revision = "5814290f49c7" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("Users", sa.Column("rate", sa.Float(), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | """Downgrade the tables.""" 24 | op.drop_column("Users", "rate") 25 | -------------------------------------------------------------------------------- /alembic/versions/4664d72ce1e1_renamed_link_path_to_full_path.py: -------------------------------------------------------------------------------- 1 | """Renamed "Link.path" to" Link.full_path". 2 | 3 | Revision ID: 4664d72ce1e1 4 | Revises: 25b3eba6ffe7 5 | Create Date: 2013-05-23 18:46:18.218662 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "4664d72ce1e1" 14 | down_revision = "25b3eba6ffe7" 15 | 16 | 17 | def upgrade(): 18 | """Create full_path column.""" 19 | try: 20 | op.alter_column("Links", "path", new_column_name="full_path") 21 | except sa.exc.OperationalError: 22 | # SQLite3 23 | # create new table 24 | op.create_table( 25 | "Links_Temp", 26 | sa.Column("id", sa.Integer, sa.ForeignKey("Entities.id"), primary_key=True), 27 | sa.Column("original_filename", sa.String(256), nullable=True), 28 | sa.Column("full_path", sa.String), 29 | ) 30 | 31 | sa.sql.table( 32 | "Links_Temp", 33 | sa.Column("id", sa.Integer, sa.ForeignKey("Entities.id"), primary_key=True), 34 | sa.Column("original_filename", sa.String(256), nullable=True), 35 | sa.Column("full_path", sa.String), 36 | ) 37 | 38 | sa.sql.table( 39 | "Links", 40 | sa.Column("id", sa.Integer, sa.ForeignKey("Entities.id"), primary_key=True), 41 | sa.Column("original_filename", sa.String(256), nullable=True), 42 | sa.Column("path", sa.String), 43 | ) 44 | 45 | # copy data from Links.path to Links_Temp.full_path 46 | op.execute( 47 | 'INSERT INTO "Links_Temp" ' 48 | 'SELECT "Links".id, "Links".original_filename, "Links".path ' 49 | 'FROM "Links"' 50 | ) 51 | 52 | # drop the Links table and rename Links_Temp to Links 53 | op.drop_table("Links") 54 | op.rename_table("Links_Temp", "Links") 55 | 56 | 57 | def downgrade(): 58 | """Downgrade the tables.""" 59 | try: 60 | op.alter_column("Links", "path", new_column_name="full_path") 61 | except sa.exc.OperationalError: 62 | # SQLite3 63 | # create new table 64 | op.create_table( 65 | "Links_Temp", 66 | sa.Column("id", sa.Integer, sa.ForeignKey("Entities.id"), primary_key=True), 67 | sa.Column("original_filename", sa.String(256), nullable=True), 68 | sa.Column("path", sa.String), 69 | ) 70 | 71 | sa.sql.table( 72 | "Links_Temp", 73 | sa.Column("id", sa.Integer, sa.ForeignKey("Entities.id"), primary_key=True), 74 | sa.Column("original_filename", sa.String(256), nullable=True), 75 | sa.Column("path", sa.String), 76 | ) 77 | 78 | sa.sql.table( 79 | "Links", 80 | sa.Column("id", sa.Integer, sa.ForeignKey("Entities.id"), primary_key=True), 81 | sa.Column("original_filename", sa.String(256), nullable=True), 82 | sa.Column("full_path", sa.String), 83 | ) 84 | 85 | # copy data from Links.path to Links_Temp.full_path 86 | op.execute( 87 | 'INSERT INTO "Links_Temp" ' 88 | 'SELECT "Links".id, "Links".original_filename, "Links".full_path ' 89 | 'FROM "Links"' 90 | ) 91 | 92 | # drop the Links table and rename Links_Temp to Links 93 | op.drop_table("Links") 94 | op.rename_table("Links_Temp", "Links") 95 | -------------------------------------------------------------------------------- /alembic/versions/46775e4a3d96_create_enum_types.py: -------------------------------------------------------------------------------- 1 | """Create enum types. 2 | 3 | Revision ID: 46775e4a3d96 4 | Revises: 2aeab8b376dc 5 | Create Date: 2014-01-31 03:08:36.445876 6 | """ 7 | 8 | from alembic import op 9 | 10 | # revision identifiers, used by Alembic. 11 | revision = "46775e4a3d96" 12 | down_revision = "2aeab8b376dc" 13 | 14 | 15 | def upgrade(): 16 | """Upgrade the tables.""" 17 | # rename types 18 | op.execute('ALTER TYPE "TaskScheduleUnit" RENAME TO "TimeUnit";') 19 | 20 | # create new types 21 | op.execute( 22 | """CREATE TYPE "ResourceAllocationStrategy" AS ENUM 23 | ('minallocated', 'maxloaded', 'minloaded', 'order', 'random'); 24 | CREATE TYPE "TaskDependencyGapModel" AS ENUM ('length', 'duration'); 25 | CREATE TYPE "TaskDependencyTarget" AS ENUM ('onend', 'onstart'); 26 | CREATE TYPE "ReviewScheduleModel" 27 | AS ENUM ('effort', 'length', 'duration'); 28 | """ 29 | ) 30 | 31 | # update the Task column to use the TimeUnit type instead of TaskBidUnit 32 | op.execute( 33 | """ 34 | ALTER TABLE "Tasks" ALTER COLUMN bid_unit TYPE "TimeUnit" 35 | USING ((bid_unit::text)::"TimeUnit"); 36 | """ 37 | ) 38 | 39 | # remove unnecessary types 40 | op.execute('DROP TYPE IF EXISTS "TaskBidUnit" CASCADE;') 41 | 42 | 43 | def downgrade(): 44 | """Downgrade the tables.""" 45 | # add necessary types 46 | op.execute( 47 | """CREATE TYPE "TaskBidUnit" AS ENUM 48 | ('min', 'h', 'd', 'w', 'm', 'y'); 49 | """ 50 | ) 51 | 52 | # update the Task column to use the TimeUnit type instead of TaskBidUnit 53 | op.execute( 54 | """ 55 | ALTER TABLE "Tasks" ALTER COLUMN bid_unit TYPE "TaskBidUnit" 56 | USING ((bid_unit::text)::"TaskBidUnit"); 57 | """ 58 | ) 59 | 60 | # rename types 61 | op.execute('ALTER TYPE "TimeUnit" RENAME TO "TaskScheduleUnit";') 62 | 63 | # create new types 64 | op.execute( 65 | """ 66 | DROP TYPE IF EXISTS "ResourceAllocationStrategy" CASCADE; 67 | DROP TYPE IF EXISTS "TaskDependencyGapModel" CASCADE; 68 | DROP TYPE IF EXISTS "TaskDependencyTarget" CASCADE; 69 | DROP TYPE IF EXISTS "ReviewScheduleModel" CASCADE; 70 | """ 71 | ) 72 | -------------------------------------------------------------------------------- /alembic/versions/4a836cf73bcf_create_entitytype_accepts_references.py: -------------------------------------------------------------------------------- 1 | """Create EntityType.accepts_references. 2 | 3 | Revision ID: 4a836cf73bcf 4 | Revises: None 5 | Create Date: 2013-05-15 16:27:05.983849 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "4a836cf73bcf" 15 | down_revision = None 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | try: 21 | op.add_column("EntityTypes", sa.Column("accepts_references", sa.Boolean)) 22 | except (sa.exc.OperationalError, sa.exc.ProgrammingError): 23 | # the column already exists 24 | pass 25 | 26 | try: 27 | op.add_column("Links", sa.Column("original_filename", sa.String(256))) 28 | except (sa.exc.OperationalError, sa.exc.ProgrammingError, sa.exc.InternalError): 29 | # the column already exists 30 | pass 31 | 32 | 33 | def downgrade(): 34 | """Downgrade the tables.""" 35 | # no drop column in SQLite so this will not work for SQLite databases 36 | op.drop_column("EntityTypes", "accepts_references") 37 | op.drop_column("Links", "original_filename") 38 | -------------------------------------------------------------------------------- /alembic/versions/5078390e5527_shot_scene_relation_is_now_many_to_one.py: -------------------------------------------------------------------------------- 1 | """Shot Scene relation is now many-to-one 2 | 3 | Revision ID: 5078390e5527 4 | Revises: e25ec9930632 5 | Create Date: 2024-11-18 11:35:10.872216 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "5078390e5527" 15 | down_revision = "e25ec9930632" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | # Add scene_id column 21 | op.add_column("Shots", sa.Column("scene_id", sa.Integer(), nullable=True)) 22 | 23 | # Create foreign key constraint 24 | op.create_foreign_key(None, "Shots", "Scenes", ["scene_id"], ["id"]) 25 | 26 | # Migrate the data 27 | op.execute( 28 | """UPDATE "Shots" SET scene_id = ( 29 | SELECT scene_id 30 | FROM "Shot_Scenes" 31 | WHERE "Shot_Scenes".shot_id = "Shots".id LIMIT 1 32 | )""" 33 | ) 34 | 35 | # Drop Shot_Scenes Table 36 | op.execute("""DROP TABLE "Shot_Scenes" """) 37 | 38 | 39 | def downgrade(): 40 | """Downgrade the tables.""" 41 | # Add Shot_Scenes Table 42 | op.create_table( 43 | "Shot_Scenes", 44 | sa.Column("shot_id", sa.Integer(), nullable=False), 45 | sa.Column("scene_id", sa.Integer(), nullable=False), 46 | sa.ForeignKeyConstraint( 47 | ["shot_id"], 48 | ["Shots.id"], 49 | ), 50 | sa.ForeignKeyConstraint( 51 | ["scene_id"], 52 | ["Scenes.id"], 53 | ), 54 | ) 55 | 56 | # Transfer Data 57 | op.execute( 58 | """ 59 | UPDATE "Shot_Scenes" SET shot_id, scene_id = ( 60 | SELECT id, scene_id FROM "Shots" WHERE "Shots".scene_id != NULL 61 | ) 62 | """ 63 | ) 64 | 65 | # Drop foreign key constraint 66 | op.drop_constraint("Shots_scene_id_fkey", "Shots", type_="foreignkey") 67 | 68 | # drop Shots.scene_id column 69 | op.drop_column("Shots", "scene_id") 70 | -------------------------------------------------------------------------------- /alembic/versions/5168cc8552a3_html_style_html_class.py: -------------------------------------------------------------------------------- 1 | """Added html_style and html_class columns to SimpleEntities. 2 | 3 | Revision ID: 5168cc8552a3 4 | Revises: 174567b9c159 5 | Create Date: 2013-11-14 23:03:55.413681 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "5168cc8552a3" 14 | down_revision = "174567b9c159" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("SimpleEntities", sa.Column("html_class", sa.String(), nullable=True)) 20 | op.add_column("SimpleEntities", sa.Column("html_style", sa.String(), nullable=True)) 21 | 22 | 23 | def downgrade(): 24 | """Downgrade the tables.""" 25 | op.drop_column("SimpleEntities", "html_style") 26 | op.drop_column("SimpleEntities", "html_class") 27 | -------------------------------------------------------------------------------- /alembic/versions/5355b569237b_version_version_of_r.py: -------------------------------------------------------------------------------- 1 | """'Version.version_of' renamed to "Version.task". 2 | 3 | Revision ID: 5355b569237b 4 | Revises: 6297277da38 5 | Create Date: 2013-06-10 11:47:28.984222 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "5355b569237b" 14 | down_revision = "6297277da38" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | try: 20 | op.alter_column("Versions", "version_of_id", new_column_name="task_id") 21 | except sa.exc.OperationalError: 22 | # SQLite3 23 | # just create the new column 24 | # and copy data 25 | op.add_column( 26 | "Versions", 27 | "task_id", 28 | sa.Column(sa.Integer, sa.ForeignKey("Tasks.id"), nullable=False), 29 | ) 30 | # copy data from Links.path to Links_Temp.full_path 31 | op.execute( 32 | """INSERT INTO "Versions".task_id 33 | SELECT "Versions".version_of_id FROM "Versions" 34 | """ 35 | ) 36 | 37 | 38 | def downgrade(): 39 | """Downgrade the tables.""" 40 | try: 41 | op.alter_column("Versions", "task_id", new_column_name="version_of_id") 42 | except sa.exc.OperationalError: 43 | # SQLite3 44 | # just create the new column 45 | # and copy data 46 | op.add_column( 47 | "Versions", 48 | "version_of_id", 49 | sa.Column(sa.Integer, sa.ForeignKey("Tasks.id"), nullable=False), 50 | ) 51 | op.execute( 52 | """INSERT INTO "Versions".version_of_id 53 | SELECT "Versions".task_id 54 | FROM "Versions" 55 | """ 56 | ) 57 | -------------------------------------------------------------------------------- /alembic/versions/53d8127d8560_parent_child_relatio.py: -------------------------------------------------------------------------------- 1 | """parent child relation in Versions. 2 | 3 | Revision ID: 53d8127d8560 4 | Revises: 4a836cf73bcf 5 | Create Date: 2013-05-22 12:44:05.626047 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "53d8127d8560" 14 | down_revision = "4a836cf73bcf" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | try: 20 | op.add_column("Versions", sa.Column("parent_id", sa.Integer(), nullable=True)) 21 | except (sa.exc.OperationalError, sa.exc.InternalError): 22 | pass 23 | 24 | 25 | def downgrade(): 26 | """Downgrade the tables.""" 27 | op.drop_column("Versions", "parent_id") 28 | -------------------------------------------------------------------------------- /alembic/versions/57a5949c7f29_cache_for_total_logged_seconds.py: -------------------------------------------------------------------------------- 1 | """Created cache columns for total_logged_seconds and schedule_seconds attributes. 2 | 3 | Revision ID: 57a5949c7f29 4 | Revises: 101a789e38ad 5 | Create Date: 2013-07-31 16:57:17.674995 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "57a5949c7f29" 14 | down_revision = "101a789e38ad" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("Tasks", sa.Column("_schedule_seconds", sa.Integer(), nullable=True)) 20 | op.add_column( 21 | "Tasks", sa.Column("_total_logged_seconds", sa.Integer(), nullable=True) 22 | ) 23 | 24 | 25 | def downgrade(): 26 | """Downgrade the tables.""" 27 | op.drop_column("Tasks", "_total_logged_seconds") 28 | op.drop_column("Tasks", "_schedule_seconds") 29 | -------------------------------------------------------------------------------- /alembic/versions/5814290f49c7_added_shot_source_in_shot_source_out_record_in.py: -------------------------------------------------------------------------------- 1 | """Added Shot.source_in, Shot.source_out and Shot.record_in attributes. 2 | 3 | Revision ID: 5814290f49c7 4 | Revises: 2e4a3813ae76 5 | Create Date: 2014-09-22 15:25:29.618377 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "5814290f49c7" 14 | down_revision = "2e4a3813ae76" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("Shots", sa.Column("record_in", sa.Integer(), nullable=True)) 20 | op.add_column("Shots", sa.Column("source_in", sa.Integer(), nullable=True)) 21 | op.add_column("Shots", sa.Column("source_out", sa.Integer(), nullable=True)) 22 | 23 | 24 | def downgrade(): 25 | """Downgrade the tables.""" 26 | op.drop_column("Shots", "source_out") 27 | op.drop_column("Shots", "source_in") 28 | op.drop_column("Shots", "record_in") 29 | -------------------------------------------------------------------------------- /alembic/versions/583875229230_good_task_relation.py: -------------------------------------------------------------------------------- 1 | """Added Tasks.good_id column. 2 | 3 | Revision ID: 583875229230 4 | Revises: 2252e51506de 5 | Create Date: 2015-02-07 18:53:04.343928 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "583875229230" 14 | down_revision = "2252e51506de" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | with op.batch_alter_table("Tasks", schema=None) as batch_op: 20 | batch_op.add_column(sa.Column("good_id", sa.Integer(), nullable=True)) 21 | 22 | 23 | def downgrade(): 24 | """Downgrade the tables.""" 25 | with op.batch_alter_table("Tasks", schema=None) as batch_op: 26 | batch_op.drop_column("good_id") 27 | -------------------------------------------------------------------------------- /alembic/versions/59092d41175c_added_version_created_with.py: -------------------------------------------------------------------------------- 1 | """Added Version.created_with. 2 | 3 | Revision ID: 59092d41175c 4 | Revises: 5355b569237b 5 | Create Date: 2013-06-19 15:31:53.547392 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "59092d41175c" 14 | down_revision = "5355b569237b" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | try: 20 | op.add_column( 21 | "Versions", sa.Column("created_with", sa.String(length=256), nullable=True) 22 | ) 23 | except sa.exc.OperationalError: 24 | pass 25 | 26 | 27 | def downgrade(): 28 | """Downgrade the tables.""" 29 | try: 30 | op.drop_column("Versions", "created_with") 31 | except sa.exc.OperationalError: 32 | pass 33 | -------------------------------------------------------------------------------- /alembic/versions/5999269aad30_added_generic_text_attribute.py: -------------------------------------------------------------------------------- 1 | """Added generic_text attribute on SimpleEntity. 2 | 3 | Revision ID: 5999269aad30 4 | Revises: 182f44ce5f07 5 | Create Date: 2014-06-02 15:17:27.961000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "5999269aad30" 14 | down_revision = "182f44ce5f07" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("SimpleEntities", sa.Column("generic_text", sa.Text())) 20 | 21 | 22 | def downgrade(): 23 | """Downgrade the tables.""" 24 | op.drop_column("SimpleEntities", "generic_text") 25 | -------------------------------------------------------------------------------- /alembic/versions/59bfe820c369_resource_efficiency.py: -------------------------------------------------------------------------------- 1 | """Added "User.efficiency" column. 2 | 3 | Revision ID: 59bfe820c369 4 | Revises: af869ddfdf9 5 | Create Date: 2014-04-26 23:50:53.880274 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "59bfe820c369" 14 | down_revision = "af869ddfdf9" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("Users", sa.Column("efficiency", sa.Float(), nullable=True)) 20 | # set default value 21 | op.execute('update "Users" set efficiency = 1.0') 22 | 23 | 24 | def downgrade(): 25 | """Downgrade the tables.""" 26 | op.drop_column("Users", "efficiency") 27 | -------------------------------------------------------------------------------- /alembic/versions/6297277da38_added_vacation_class.py: -------------------------------------------------------------------------------- 1 | """Added Vacation class. 2 | 3 | Revision ID: 6297277da38 4 | Revises: 21b88ed3da95 5 | Create Date: 2013-06-07 16:03:08.412610 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "6297277da38" 14 | down_revision = "21b88ed3da95" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | try: 20 | op.drop_table("User_Vacations") 21 | except sa.exc.OperationalError: 22 | pass 23 | 24 | 25 | def downgrade(): 26 | """Downgrade the tables.""" 27 | op.create_table( 28 | "User_Vacations", 29 | sa.Column("user_id", sa.INTEGER(), autoincrement=False, nullable=False), 30 | sa.Column("vacation_id", sa.INTEGER(), autoincrement=False, nullable=False), 31 | sa.ForeignKeyConstraint( 32 | ["user_id"], ["Users.id"], name="User_Vacations_user_id_fkey" 33 | ), 34 | sa.ForeignKeyConstraint( 35 | ["vacation_id"], ["Vacations.id"], name="User_Vacations_vacation_id_fkey" 36 | ), 37 | sa.PrimaryKeyConstraint("user_id", "vacation_id", name="User_Vacations_pkey"), 38 | ) 39 | -------------------------------------------------------------------------------- /alembic/versions/644f5251fc0d_remove_project_active_attribute.py: -------------------------------------------------------------------------------- 1 | """Remove Project.active attribute 2 | 3 | Revision ID: 644f5251fc0d 4 | Revises: 5078390e5527 5 | Create Date: 2024-11-18 12:47:09.673241 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "644f5251fc0d" 15 | down_revision = "5078390e5527" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | # just remove the "active" column 21 | with op.batch_alter_table("Projects", schema=None) as batch_op: 22 | batch_op.drop_column("active") 23 | 24 | 25 | def downgrade(): 26 | """Downgrade the tables.""" 27 | with op.batch_alter_table("Projects", schema=None) as batch_op: 28 | batch_op.add_column(sa.Column("active", sa.Boolean(), nullable=True)) 29 | 30 | # restore the value by checking the status 31 | op.execute( 32 | """UPDATE "Projects" SET active = ( 33 | SELECT 34 | ( 35 | CASE WHEN "Projects".status_id = ( 36 | SELECT 37 | "Statuses".id 38 | FROM "Statuses" 39 | WHERE "Statuses".code = 'WIP' 40 | ) 41 | THEN true ELSE false END 42 | ) as active 43 | FROM "Projects" 44 | ) 45 | """ 46 | ) 47 | -------------------------------------------------------------------------------- /alembic/versions/745b210e6907_fix_non_existing_thumbnails.py: -------------------------------------------------------------------------------- 1 | """Fix none-existing thumbnails. 2 | 3 | Revision ID: 745b210e6907 4 | Revises: f2005d1fbadc 5 | Create Date: 2016-06-27 17:52:24.381000 6 | """ 7 | 8 | from alembic import op 9 | 10 | # revision identifiers, used by Alembic. 11 | revision = "745b210e6907" 12 | down_revision = "258985128aff" 13 | 14 | 15 | def upgrade(): 16 | """Fix SimpleEntities with none-existing thumbnail_id's.""" 17 | op.execute( 18 | """ 19 | UPDATE "SimpleEntities" SET thumbnail_id = NULL 20 | WHERE "SimpleEntities".thumbnail_id is not NULL 21 | and not exists( 22 | select 23 | thum.id 24 | from "SimpleEntities" as thum 25 | where thum.id = "SimpleEntities".thumbnail_id 26 | ) 27 | """ 28 | ) 29 | 30 | 31 | def downgrade(): 32 | """Downgrade the tables.""" 33 | # do nothing 34 | pass 35 | -------------------------------------------------------------------------------- /alembic/versions/856e70016b2_roles.py: -------------------------------------------------------------------------------- 1 | """Added Roles. 2 | 3 | Revision ID: 856e70016b2 4 | Revises: 30c576f3691 5 | Create Date: 2014-11-26 00:25:29.543411 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "856e70016b2" 14 | down_revision = "30c576f3691" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.create_table( 20 | "Roles", 21 | sa.Column("id", sa.Integer(), nullable=False), 22 | sa.ForeignKeyConstraint( 23 | ["id"], 24 | ["Entities.id"], 25 | ), 26 | sa.PrimaryKeyConstraint("id"), 27 | ) 28 | 29 | op.create_table( 30 | "Client_Users", 31 | sa.Column("uid", sa.Integer(), nullable=False), 32 | sa.Column("cid", sa.Integer(), nullable=False), 33 | sa.Column("rid", sa.Integer(), nullable=True), 34 | sa.ForeignKeyConstraint( 35 | ["cid"], 36 | ["Clients.id"], 37 | ), 38 | sa.ForeignKeyConstraint( 39 | ["rid"], 40 | ["Roles.id"], 41 | ), 42 | sa.ForeignKeyConstraint( 43 | ["uid"], 44 | ["Users.id"], 45 | ), 46 | sa.PrimaryKeyConstraint("uid", "cid"), 47 | ) 48 | 49 | # 50 | # read Users.client_id and create Client_Users entries accordingly 51 | # 52 | 53 | op.rename_table("User_Groups", "Group_Users") 54 | op.rename_table("User_Departments", "Department_Users") 55 | 56 | op.add_column("Department_Users", sa.Column("rid", sa.Integer(), nullable=True)) 57 | op.add_column("Project_Users", sa.Column("rid", sa.Integer(), nullable=True)) 58 | 59 | op.drop_column("Departments", "lead_id") 60 | op.drop_column("Projects", "lead_id") 61 | op.drop_column("Users", "company_id") 62 | 63 | 64 | def downgrade(): 65 | """Downgrade the tables.""" 66 | op.add_column("Projects", sa.Column("lead_id", sa.INTEGER(), nullable=True)) 67 | op.add_column("Departments", sa.Column("lead_id", sa.INTEGER(), nullable=True)) 68 | 69 | op.drop_column("Project_Users", "rid") 70 | op.drop_column("Department_Users", "rid") 71 | 72 | op.rename_table("Department_Users", "User_Departments") 73 | op.rename_table("Group_Users", "User_Groups") 74 | 75 | op.add_column("Users", sa.Column("company_id", sa.INTEGER(), nullable=True)) 76 | 77 | op.drop_table("Client_Users") 78 | 79 | op.drop_table("Roles") 80 | -------------------------------------------------------------------------------- /alembic/versions/92257ba439e1_budget_is_now_statusable.py: -------------------------------------------------------------------------------- 1 | """Budget is now statusable. 2 | 3 | Revision ID: 92257ba439e1 4 | Revises: f2005d1fbadc 5 | Create Date: 2016-07-28 13:20:27.397000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "92257ba439e1" 14 | down_revision = "f2005d1fbadc" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("Budgets", sa.Column("status_id", sa.Integer(), nullable=True)) 20 | op.add_column("Budgets", sa.Column("status_list_id", sa.Integer(), nullable=True)) 21 | op.create_foreign_key(None, "Budgets", "Statuses", ["status_id"], ["id"]) 22 | op.create_foreign_key(None, "Budgets", "StatusLists", ["status_list_id"], ["id"]) 23 | 24 | # create a dummy status list for budgets 25 | op.execute( 26 | """insert into "SimpleEntities" (name, entity_type) 27 | values ('Dummy Budget StatusList', 'StatusList'); 28 | insert into "Entities" (id) 29 | select 30 | "SimpleEntities".id 31 | from "SimpleEntities" 32 | where "SimpleEntities".entity_type = 'StatusList' and 33 | "SimpleEntities".name = 'Dummy Budget StatusList' 34 | ; 35 | insert into "StatusLists" (id, target_entity_type) 36 | select 37 | "SimpleEntities".id, 38 | 'Budget' 39 | from "SimpleEntities" 40 | where "SimpleEntities".entity_type = 'StatusList' and 41 | "SimpleEntities".name = 'Dummy Budget StatusList' 42 | ; 43 | insert into "StatusList_Statuses" 44 | select 45 | "SimpleEntities".id, 46 | "Statuses".id 47 | from "SimpleEntities", "Statuses" 48 | where "SimpleEntities".name = 'Dummy Budget StatusList' 49 | order by "Statuses".id 50 | limit 1 51 | ; 52 | update "Budgets" 53 | set status_id = ( 54 | select "Statuses".id 55 | from "Statuses" 56 | order by "Statuses".id limit 1 57 | ) 58 | ; 59 | update "Budgets" 60 | set status_list_id = ( 61 | select "SimpleEntities".id 62 | from "SimpleEntities" 63 | where "SimpleEntities".name = 'Dummy Budget StatusList' 64 | ) 65 | ; 66 | """ 67 | ) 68 | # now alter column to be non nullable 69 | op.alter_column("Budgets", "status_id", nullable=False) 70 | op.alter_column("Budgets", "status_list_id", nullable=False) 71 | 72 | 73 | def downgrade(): 74 | """Downgrade the tables.""" 75 | op.execute( 76 | """ 77 | ALTER TABLE public."Budgets" DROP CONSTRAINT "Budgets_status_id_fkey"; 78 | ALTER TABLE public."Budgets" DROP CONSTRAINT "Budgets_status_list_id_fkey"; 79 | ALTER TABLE public."Budgets" DROP COLUMN status_id; 80 | ALTER TABLE public."Budgets" DROP COLUMN status_list_id; 81 | """ 82 | ) 83 | 84 | # remove 'Dummy Budget StatusList' if it exists 85 | op.execute( 86 | """ 87 | delete 88 | from "StatusList_Statuses" 89 | where "StatusList_Statuses".status_list_id = ( 90 | select 91 | id 92 | from "SimpleEntities" 93 | where "SimpleEntities".name = 'Dummy Budget StatusList' 94 | ) 95 | ; 96 | delete 97 | from "StatusLists" 98 | where "StatusLists".id = ( 99 | select 100 | id 101 | from "SimpleEntities" 102 | where "SimpleEntities".name = 'Dummy Budget StatusList' 103 | ) 104 | ; 105 | delete 106 | from "Entities" 107 | where "Entities".id = ( 108 | select 109 | id 110 | from "SimpleEntities" 111 | where "SimpleEntities".name = 'Dummy Budget StatusList' 112 | ) 113 | ; 114 | delete 115 | from "SimpleEntities" 116 | where "SimpleEntities".name = 'Dummy Budget StatusList' 117 | ; 118 | """ 119 | ) 120 | -------------------------------------------------------------------------------- /alembic/versions/a2007ad7f535_added_review_version_id_column.py: -------------------------------------------------------------------------------- 1 | """Added Review.version_id column 2 | 3 | Revision ID: a2007ad7f535 4 | Revises: 91ed52b72b82 5 | Create Date: 2024-11-26 11:36:07.776169 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "a2007ad7f535" 15 | down_revision = "91ed52b72b82" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | op.add_column("Reviews", sa.Column("version_id", sa.Integer(), nullable=True)) 21 | op.create_foreign_key( 22 | "Reviews_version_id_fkey", 23 | "Reviews", 24 | "Versions", 25 | ["version_id"], 26 | ["id"], 27 | ) 28 | 29 | 30 | def downgrade(): 31 | """Downgrade the tables.""" 32 | op.drop_constraint("Reviews_version_id_fkey", "Reviews", type_="foreignkey") 33 | op.drop_column("Reviews", "version_id") 34 | -------------------------------------------------------------------------------- /alembic/versions/a6598cde6b_versions_are_not_mix.py: -------------------------------------------------------------------------------- 1 | """Versions are not mixed with StatusMixin anymore. 2 | 3 | Revision ID: a6598cde6b 4 | Revises: 275bdc106fd5 5 | Create Date: 2013-10-25 17:35:42.953516 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "a6598cde6b" 14 | down_revision = "275bdc106fd5" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.drop_column("Versions", "status_list_id") 20 | op.drop_column("Versions", "status_id") 21 | 22 | 23 | def downgrade(): 24 | """Downgrade the tables.""" 25 | op.add_column("Versions", sa.Column("status_id", sa.INTEGER(), nullable=False)) 26 | op.add_column("Versions", sa.Column("status_list_id", sa.INTEGER(), nullable=False)) 27 | -------------------------------------------------------------------------------- /alembic/versions/a9319b19f7be_added_shot_fps.py: -------------------------------------------------------------------------------- 1 | """Added "shot.fps". 2 | 3 | Revision ID: a9319b19f7be 4 | Revises: f16651477e64 5 | Create Date: 2016-11-29 13:38:22.380000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "a9319b19f7be" 14 | down_revision = "f16651477e64" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("Shots", sa.Column("fps", sa.Float(precision=3), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | """Downgrade the tables.""" 24 | op.drop_column("Shots", "fps") 25 | -------------------------------------------------------------------------------- /alembic/versions/af869ddfdf9_entity_to_note_relation_is_now_many_to_many.py: -------------------------------------------------------------------------------- 1 | """Entity to note relation is now many-to-many. 2 | 3 | Revision ID: af869ddfdf9 4 | Revises: 2f55dc4f199f 5 | Create Date: 2014-04-06 09:20:44.509357 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "af869ddfdf9" 14 | down_revision = "2f55dc4f199f" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.create_table( 20 | "Entity_Notes", 21 | sa.Column("entity_id", sa.Integer(), nullable=False), 22 | sa.Column("note_id", sa.Integer(), nullable=False), 23 | sa.ForeignKeyConstraint( 24 | ["entity_id"], 25 | ["Entities.id"], 26 | ), 27 | sa.ForeignKeyConstraint( 28 | ["note_id"], 29 | ["Notes.id"], 30 | ), 31 | sa.PrimaryKeyConstraint("entity_id", "note_id"), 32 | ) 33 | # before dropping notes entity_id column 34 | # store all the entity_id values in the secondary table 35 | op.execute( 36 | """ 37 | insert into "Entity_Notes" 38 | select "Notes".entity_id, "Notes".id 39 | from "Notes" 40 | where "Notes".entity_id is not NULL 41 | and exists( 42 | select "Entities".id 43 | from "Entities" 44 | where "Entities".id = "Notes".entity_id 45 | )""" 46 | ) 47 | 48 | # now drop the entity_id column 49 | op.drop_column("Notes", "entity_id") 50 | 51 | 52 | def downgrade(): 53 | """Downgrade the tables.""" 54 | op.add_column("Notes", sa.Column("entity_id", sa.INTEGER(), nullable=True)) 55 | 56 | # restore data 57 | op.execute( 58 | """ 59 | UPDATE 60 | "Notes" 61 | SET 62 | entity_id = "Entity_Notes".entity_id 63 | FROM "Entity_Notes" 64 | WHERE "Notes".id = "Entity_Notes".note_id 65 | """ 66 | ) 67 | 68 | op.drop_table("Entity_Notes") 69 | -------------------------------------------------------------------------------- /alembic/versions/bf67e6a234b4_added_revision_code_attribute.py: -------------------------------------------------------------------------------- 1 | """Added "Repository.code" attribute. 2 | 3 | Revision ID: bf67e6a234b4 4 | Revises: ed0167fff399 5 | Create Date: 2020-01-01 09:50:19.086342 6 | """ 7 | 8 | import logging 9 | 10 | from alembic import op 11 | 12 | import sqlalchemy as sa 13 | 14 | 15 | # revision identifiers, used by Alembic. 16 | revision = "bf67e6a234b4" 17 | down_revision = "ed0167fff399" 18 | 19 | logger = logging.getLogger(__name__) 20 | logger.setLevel(logging.INFO) 21 | 22 | 23 | def upgrade(): 24 | """Upgrade the tables.""" 25 | # add the column 26 | logger.info("creating code column in Repositories table") 27 | op.add_column( 28 | "Repositories", sa.Column("code", sa.String(length=256), nullable=True) 29 | ) 30 | 31 | # copy the name as code 32 | logger.info( 33 | "filling data to the code column in Repositories table from " 34 | "Repositories.name column" 35 | ) 36 | op.execute( 37 | r"""UPDATE "Repositories" 38 | SET code = ( 39 | SELECT REGEXP_REPLACE(name, '\s+', '') 40 | FROM "SimpleEntities" WHERE id="Repositories".id 41 | )""" 42 | ) 43 | logger.info("set code column to not nullable") 44 | op.alter_column("Repositories", "code", nullable=False) 45 | 46 | 47 | def downgrade(): 48 | """Downgrade the tables.""" 49 | logger.info("removing code column from Repositories table") 50 | op.drop_column("Repositories", "code") 51 | -------------------------------------------------------------------------------- /alembic/versions/c5607b4cfb0a_added_support_for_time_zones.py: -------------------------------------------------------------------------------- 1 | """Added support for time zones. 2 | 3 | Revision ID: c5607b4cfb0a 4 | Revises: 0063f547dc2e 5 | Create Date: 2017-03-09 02:17:08.209000 6 | """ 7 | 8 | import logging 9 | 10 | from alembic import op 11 | 12 | import sqlalchemy as sa 13 | 14 | # revision identifiers, used by Alembic. 15 | revision = "c5607b4cfb0a" 16 | down_revision = "0063f547dc2e" 17 | 18 | logger = logging.getLogger(__name__) 19 | logger.setLevel(logging.INFO) 20 | 21 | tables_to_update = { 22 | "AuthenticationLogs": ["date"], 23 | "Tasks": ["computed_start", "computed_end", "start", "end"], 24 | "Studios": [ 25 | "computed_start", 26 | "computed_end", 27 | "start", 28 | "end", 29 | "scheduling_started_at", 30 | "last_scheduled_at", 31 | ], 32 | "SimpleEntities": ["date_created", "date_updated"], 33 | "Projects": ["computed_start", "computed_end", "start", "end"], 34 | "TimeLogs": ["computed_start", "computed_end", "start", "end"], 35 | "Vacations": ["computed_start", "computed_end", "start", "end"], 36 | } 37 | 38 | 39 | def upgrade(): 40 | """Upgrade the tables.""" 41 | # Directly updating the columns will set the timezone of the datetime 42 | # fields to the timezone of the machine that is running this code. 43 | # 44 | # Because the data in the database is already in UTC we need to update the 45 | # data also to have their time values correctly shifted to UTC. 46 | for table_name in tables_to_update: 47 | logger.info(f"upgrading table: {table_name}") 48 | with op.batch_alter_table(table_name) as batch_op: 49 | for column_name in tables_to_update[table_name]: 50 | logger.info(f"altering column: {column_name}") 51 | batch_op.alter_column(column_name, type_=sa.DateTime(timezone=True)) 52 | 53 | sql = """ 54 | -- Add the time zone offset 55 | UPDATE 56 | "{table_name}" 57 | SET 58 | """.format( 59 | table_name=table_name 60 | ) 61 | 62 | for i, column_name in enumerate(tables_to_update[table_name]): 63 | if i > 0: 64 | sql = "{sql},\n".format(sql=sql) 65 | 66 | # per column add 67 | sql = f"""{sql} 68 | "{column_name}" = ( 69 | SELECT 70 | aliased_table.{column_name}::timestamp at time zone 'utc' 71 | FROM "{table_name}" as aliased_table 72 | where aliased_table.id = "{table_name}".id 73 | )""" 74 | 75 | op.execute(sql) 76 | logger.info(f"done upgrading table: {table_name}") 77 | 78 | 79 | def downgrade(): 80 | """Downgrade the tables.""" 81 | # Removing the timezone info will not shift the time values. So shift the 82 | # values by hand 83 | for table_name in tables_to_update: 84 | logger.info(f"downgrading table: {table_name}") 85 | sql = f""" 86 | -- Add the time zone offset 87 | UPDATE 88 | "{table_name}" 89 | SET 90 | """ 91 | 92 | for i, column_name in enumerate(tables_to_update[table_name]): 93 | if i > 0: 94 | sql = f"{sql},\n" 95 | 96 | # per column add 97 | sql = f"""{sql} 98 | "{column_name}" = ( 99 | SELECT 100 | CAST(aliased_table.{column_name} at time zone 'utc' 101 | AS timestamp with time zone) 102 | FROM "{table_name}" as aliased_table 103 | where aliased_table.id = "{table_name}".id 104 | )""" 105 | op.execute(sql) 106 | logger.info(f"raw sql completed for table: {table_name}") 107 | 108 | with op.batch_alter_table(table_name) as batch_op: 109 | for column_name in tables_to_update[table_name]: 110 | batch_op.alter_column(column_name, type_=sa.DateTime(timezone=False)) 111 | 112 | logger.info(f"done downgrading table: {table_name}") 113 | -------------------------------------------------------------------------------- /alembic/versions/d8421de6a206_added_project_users_rate_column.py: -------------------------------------------------------------------------------- 1 | """Added "Project_Users.rate". 2 | 3 | Revision ID: d8421de6a206 4 | Revises: 92257ba439e1 5 | Create Date: 2016-08-17 19:27:00.358000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "d8421de6a206" 14 | down_revision = "92257ba439e1" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.add_column("Project_Users", sa.Column("rate", sa.Float(), nullable=True)) 20 | 21 | 22 | def downgrade(): 23 | """Downgrade the tables.""" 24 | op.execute("""ALTER TABLE public."Project_Users" DROP COLUMN IF EXISTS rate;""") 25 | -------------------------------------------------------------------------------- /alembic/versions/e25ec9930632_shot_sequence_relation_is_now_many_to_.py: -------------------------------------------------------------------------------- 1 | """Shot Sequence relation is now many-to-one 2 | 3 | Revision ID: e25ec9930632 4 | Revises: 4400871fa852 5 | Create Date: 2024-11-16 00:27:54.060738 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "e25ec9930632" 15 | down_revision = "4400871fa852" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | # Add sequence_id column 21 | op.add_column("Shots", sa.Column("sequence_id", sa.Integer(), nullable=True)) 22 | 23 | # Create foreign key constraint 24 | op.create_foreign_key(None, "Shots", "Sequences", ["sequence_id"], ["id"]) 25 | 26 | # Migrate the data 27 | op.execute( 28 | """UPDATE "Shots" SET sequence_id = ( 29 | SELECT sequence_id 30 | FROM "Shot_Sequences" 31 | WHERE "Shot_Sequences".shot_id = "Shots".id LIMIT 1 32 | )""" 33 | ) 34 | 35 | # Drop Shot_Sequences Table 36 | op.execute("""DROP TABLE "Shot_Sequences" """) 37 | 38 | 39 | def downgrade(): 40 | """Downgrade the tables.""" 41 | # Add Shot_Sequences Table 42 | op.create_table( 43 | "Shot_Sequences", 44 | sa.Column("shot_id", sa.Integer(), nullable=False), 45 | sa.Column("sequence_id", sa.Integer(), nullable=False), 46 | sa.ForeignKeyConstraint( 47 | ["shot_id"], 48 | ["Shots.id"], 49 | ), 50 | sa.ForeignKeyConstraint( 51 | ["sequence_id"], 52 | ["Sequences.id"], 53 | ), 54 | ) 55 | 56 | # Transfer Data 57 | op.execute( 58 | """ 59 | UPDATE "Shot_Sequences" SET shot_id, sequence_id = ( 60 | SELECT id, sequence_id FROM "Shots" WHERE "Shots".sequence_id != NULL 61 | ) 62 | """ 63 | ) 64 | 65 | # Drop foreign key constraint 66 | op.drop_constraint("Shots_sequence_id_fkey", "Shots", type_="foreignkey") 67 | 68 | # drop Shots.sequence_id column 69 | op.drop_column("Shots", "sequence_id") 70 | -------------------------------------------------------------------------------- /alembic/versions/ea28a39ba3f5_added_invoices_table.py: -------------------------------------------------------------------------------- 1 | """Added Invoices table. 2 | 3 | Revision ID: ea28a39ba3f5 4 | Revises: 92257ba439e1 5 | Create Date: 2016-08-17 19:21:40.428000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "ea28a39ba3f5" 14 | down_revision = "d8421de6a206" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | op.create_table( 20 | "Invoices", 21 | sa.Column("id", sa.Integer(), nullable=False), 22 | sa.Column("budget_id", sa.Integer(), nullable=True), 23 | sa.Column("client_id", sa.Integer(), nullable=True), 24 | sa.Column("amount", sa.Float(), nullable=True), 25 | sa.Column("unit", sa.String(length=64), nullable=True), 26 | sa.ForeignKeyConstraint( 27 | ["budget_id"], 28 | ["Budgets.id"], 29 | ), 30 | sa.ForeignKeyConstraint( 31 | ["client_id"], 32 | ["Clients.id"], 33 | ), 34 | sa.ForeignKeyConstraint( 35 | ["id"], 36 | ["Entities.id"], 37 | ), 38 | sa.PrimaryKeyConstraint("id"), 39 | ) 40 | 41 | 42 | def downgrade(): 43 | """Downgrade the tables.""" 44 | op.drop_table("Invoices") 45 | -------------------------------------------------------------------------------- /alembic/versions/eaed49db6d9_added_position_column_to_Project_Repositories.py: -------------------------------------------------------------------------------- 1 | """Added position column to Project_Repositories table. 2 | 3 | Revision ID: eaed49db6d9 4 | Revises: 583875229230 5 | Create Date: 2015-02-10 16:08:03.449570 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = "eaed49db6d9" 14 | down_revision = "583875229230" 15 | 16 | 17 | def upgrade(): 18 | """Upgrade the tables.""" 19 | with op.batch_alter_table("Project_Repositories", schema=None) as batch_op: 20 | batch_op.add_column(sa.Column("position", sa.Integer(), nullable=True)) 21 | batch_op.alter_column("repo_id", new_column_name="repository_id") 22 | 23 | # insert zeros as the position value 24 | op.execute( 25 | """update "Project_Repositories" 26 | set position=0 27 | """ 28 | ) 29 | 30 | 31 | def downgrade(): 32 | """Downgrade the tables.""" 33 | with op.batch_alter_table("Project_Repositories", schema=None) as batch_op: 34 | batch_op.alter_column("repository_id", new_column_name="repo_id") 35 | batch_op.drop_column("position") 36 | -------------------------------------------------------------------------------- /alembic/versions/ec1eb2151bb9_rename_version_take_name_to_version_.py: -------------------------------------------------------------------------------- 1 | """Rename Version.take_name to Version.variant_name 2 | 3 | Revision ID: ec1eb2151bb9 4 | Revises: 019378697b5b 5 | Create Date: 2024-11-01 16:37:18.048904 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "ec1eb2151bb9" 15 | down_revision = "019378697b5b" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | op.alter_column("Versions", "take_name", new_column_name="variant_name") 21 | 22 | 23 | def downgrade(): 24 | """Downgrade the tables.""" 25 | op.alter_column("Versions", "variant_name", new_column_name="take_name") 26 | -------------------------------------------------------------------------------- /alembic/versions/ed0167fff399_added_workinghours_table.py: -------------------------------------------------------------------------------- 1 | """Added WorkingHours table. 2 | 3 | Revision ID: ed0167fff399 4 | Revises: 1181305d3001 5 | Create Date: 2017-05-20 14:32:48.388000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | from sqlalchemy.dialects import postgresql 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "ed0167fff399" 15 | down_revision = "1181305d3001" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | op.create_table( 21 | "WorkingHours", 22 | sa.Column("id", sa.Integer(), nullable=False), 23 | sa.Column("working_hours", sa.JSON(), nullable=True), 24 | sa.Column("daily_working_hours", sa.Integer(), nullable=True), 25 | sa.ForeignKeyConstraint( 26 | ["id"], 27 | ["Entities.id"], 28 | ), 29 | sa.PrimaryKeyConstraint("id"), 30 | ) 31 | 32 | op.add_column("Studios", sa.Column("working_hours_id", sa.Integer(), nullable=True)) 33 | op.create_foreign_key(None, "Studios", "WorkingHours", ["working_hours_id"], ["id"]) 34 | op.drop_column("Studios", "working_hours") 35 | op.alter_column("Studios", "last_schedule_message", type_=sa.Text) 36 | 37 | # warn the user to recreate the working hours 38 | # because of the nature of Pickle it is very hard to do it here 39 | print("Warning! Can not keep WorkingHours data of Studios.") 40 | print("Please, recreate the WorkingHours for all Studio instances!") 41 | 42 | 43 | def downgrade(): 44 | """Downgrade the tables.""" 45 | op.add_column( 46 | "Studios", 47 | sa.Column( 48 | "working_hours", postgresql.BYTEA(), autoincrement=False, nullable=True 49 | ), 50 | ) 51 | op.drop_constraint("Studios_working_hours_id_fkey", "Studios", type_="foreignkey") 52 | op.drop_column("Studios", "working_hours_id") 53 | op.drop_table("WorkingHours") 54 | op.execute( 55 | 'ALTER TABLE "Studios"' 56 | "ALTER COLUMN last_schedule_message TYPE BYTEA " 57 | "USING last_schedule_message::bytea" 58 | ) 59 | print("Warning! Can not keep WorkingHours instances.") 60 | print("Please, recreate the WorkingHours for all Studio instances!") 61 | -------------------------------------------------------------------------------- /alembic/versions/f16651477e64_added_authenticationlog_class.py: -------------------------------------------------------------------------------- 1 | """Added AuthenticationLog class. 2 | 3 | Revision ID: f16651477e64 4 | Revises: 255ee1f9c7b3 5 | Create Date: 2016-11-15 00:22:16.438000 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | from sqlalchemy.dialects import postgresql 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "f16651477e64" 15 | down_revision = "255ee1f9c7b3" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | op.create_table( 21 | "AuthenticationLogs", 22 | sa.Column("id", sa.Integer(), nullable=False), 23 | sa.Column("uid", sa.Integer(), nullable=False), 24 | sa.Column( 25 | "action", 26 | sa.Enum("login", "logout", name="AuthenticationActions"), 27 | nullable=False, 28 | ), 29 | sa.Column("date", sa.DateTime(), nullable=False), 30 | sa.ForeignKeyConstraint( 31 | ["id"], 32 | ["SimpleEntities.id"], 33 | ), 34 | sa.ForeignKeyConstraint( 35 | ["uid"], 36 | ["Users.id"], 37 | ), 38 | sa.PrimaryKeyConstraint("id"), 39 | ) 40 | op.drop_column("Users", "last_login") 41 | 42 | 43 | def downgrade(): 44 | """Downgrade the tables.""" 45 | op.add_column( 46 | "Users", 47 | sa.Column( 48 | "last_login", postgresql.TIMESTAMP(), autoincrement=False, nullable=True 49 | ), 50 | ) 51 | op.drop_table("AuthenticationLogs") 52 | -------------------------------------------------------------------------------- /alembic/versions/feca9bac7d5a_renamed_osx_to_macos.py: -------------------------------------------------------------------------------- 1 | """Renamed OSX to macOS 2 | 3 | Revision ID: feca9bac7d5a 4 | Revises: bf67e6a234b4 5 | Create Date: 2024-11-01 12:22:24.818481 6 | """ 7 | 8 | from alembic import op 9 | 10 | import sqlalchemy as sa 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = "feca9bac7d5a" 15 | down_revision = "bf67e6a234b4" 16 | 17 | 18 | def upgrade(): 19 | """Upgrade the tables.""" 20 | op.alter_column("Repositories", "osx_path", new_column_name="macos_path") 21 | 22 | 23 | def downgrade(): 24 | """Downgrade the tables.""" 25 | op.alter_column("Repositories", "macos_path", new_column_name="osx_path") 26 | -------------------------------------------------------------------------------- /docs/make_html.bat: -------------------------------------------------------------------------------- 1 | ..\..\sphinx-build.exe -b html -D graphviz_dot="dot.exe" source build\html -------------------------------------------------------------------------------- /docs/source/_static/images/Task_Status_Workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/docs/source/_static/images/Task_Status_Workflow.png -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/base.rst: -------------------------------------------------------------------------------- 1 | {{ fullname }} 2 | {{ underline }} 3 | 4 | .. currentmodule:: {{ module }} 5 | 6 | .. auto{{ objtype }}:: {{ objname }} 7 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/class.rst: -------------------------------------------------------------------------------- 1 | {{ fullname }} 2 | {{ underline }} 3 | 4 | .. inheritance-diagram:: 5 | {{ fullname }} 6 | :parts: 1 7 | 8 | .. currentmodule:: {{ module }} 9 | 10 | .. autoclass:: {{ objname }} 11 | :show-inheritance: 12 | :inherited-members: 13 | 14 | {% block methods %} 15 | .. automethod:: __init__ 16 | 17 | {% if methods %} 18 | .. rubric:: Methods 19 | 20 | .. autosummary:: 21 | {% for item in methods %} 22 | ~{{ name }}.{{ item }} 23 | {%- endfor %} 24 | {% endif %} 25 | {% endblock %} 26 | 27 | {% block attributes %} 28 | {% if attributes %} 29 | .. rubric:: Attributes 30 | 31 | .. autosummary:: 32 | {% for item in attributes %} 33 | ~{{ name }}.{{ item }} 34 | {%- endfor %} 35 | {% endif %} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /docs/source/_templates/autosummary/module.rst: -------------------------------------------------------------------------------- 1 | {{ fullname }} 2 | {{ underline }} 3 | 4 | .. automodule:: {{ fullname }} 5 | 6 | {% block functions %} 7 | {% if functions %} 8 | .. rubric:: Functions 9 | 10 | .. autosummary:: 11 | {% for item in functions %} 12 | {{ item }} 13 | {%- endfor %} 14 | {% endif %} 15 | {% endblock %} 16 | 17 | {% block classes %} 18 | {% if classes %} 19 | .. rubric:: Classes 20 | 21 | .. autosummary:: 22 | {% for item in classes %} 23 | {{ item }} 24 | {%- endfor %} 25 | {% endif %} 26 | {% endblock %} 27 | 28 | {% block exceptions %} 29 | {% if exceptions %} 30 | .. rubric:: Exceptions 31 | 32 | .. autosummary:: 33 | {% for item in exceptions %} 34 | {{ item }} 35 | {%- endfor %} 36 | {% endif %} 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /docs/source/about.rst: -------------------------------------------------------------------------------- 1 | .. about_toplevel: 2 | 3 | .. include:: source/../../../README.rst -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | .. _changelog_toplevel: 2 | 3 | .. include:: source/../../../CHANGELOG.rst -------------------------------------------------------------------------------- /docs/source/contents.rst: -------------------------------------------------------------------------------- 1 | .. _contents: 2 | 3 | Table of Contents 4 | ================= 5 | 6 | .. toctree:: 7 | :maxdepth: 3 8 | 9 | about.rst 10 | installation.rst 11 | tutorial.rst 12 | design.rst 13 | configure.rst 14 | upgrade_db.rst 15 | contribute.rst 16 | roadmap.rst 17 | changelog.rst 18 | todo.rst 19 | summary.rst -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. _index_toplevel: 2 | 3 | ===================== 4 | Stalker Documentation 5 | ===================== 6 | 7 | .. include:: about.rst 8 | 9 | .. include:: contents.rst 10 | 11 | Indices and tables 12 | ------------------ 13 | 14 | * :ref:`genindex` 15 | * :ref:`modindex` 16 | * :ref:`search` -------------------------------------------------------------------------------- /docs/source/inheritance_diagram.rst: -------------------------------------------------------------------------------- 1 | .. _inheritance_diagram_toplevel: 2 | 3 | Inheritance Diagram 4 | =================== 5 | 6 | .. inheritance-diagram:: 7 | stalker.exceptions.CircularDependencyError 8 | stalker.exceptions.DependencyViolationError 9 | stalker.exceptions.LoginError 10 | stalker.exceptions.OverBookedError 11 | stalker.exceptions.StatusError 12 | stalker.models.asset.Asset 13 | stalker.models.auth.AuthenticationLog 14 | stalker.models.auth.Group 15 | stalker.models.auth.LocalSession 16 | stalker.models.auth.Permission 17 | stalker.models.auth.Role 18 | stalker.models.auth.User 19 | stalker.models.budget.Budget 20 | stalker.models.budget.BudgetEntry 21 | stalker.models.budget.Good 22 | stalker.models.budget.Invoice 23 | stalker.models.budget.Payment 24 | stalker.models.budget.PriceList 25 | stalker.models.department.Department 26 | stalker.models.department.DepartmentUser 27 | stalker.models.client.Client 28 | stalker.models.entity.Entity 29 | stalker.models.entity.EntityGroup 30 | stalker.models.entity.SimpleEntity 31 | stalker.models.file.File 32 | stalker.models.format.ImageFormat 33 | stalker.models.message.Message 34 | stalker.models.mixins.ACLMixin 35 | stalker.models.mixins.CodeMixin 36 | stalker.models.mixins.DateRangeMixin 37 | stalker.models.mixins.ProjectMixin 38 | stalker.models.mixins.ReferenceMixin 39 | stalker.models.mixins.ScheduleMixin 40 | stalker.models.mixins.StatusMixin 41 | stalker.models.mixins.TargetEntityTypeMixin 42 | stalker.models.mixins.WorkingHoursMixin 43 | stalker.models.note.Note 44 | stalker.models.project.Project 45 | stalker.models.project.ProjectClient 46 | stalker.models.project.ProjectRepository 47 | stalker.models.project.ProjectUser 48 | stalker.models.repository.Repository 49 | stalker.models.review.Review 50 | stalker.models.review.Daily 51 | stalker.models.review.DailyFile 52 | stalker.models.scene.Scene 53 | stalker.models.schedulers.SchedulerBase 54 | stalker.models.schedulers.TaskJugglerScheduler 55 | stalker.models.sequence.Sequence 56 | stalker.models.shot.Shot 57 | stalker.models.status.Status 58 | stalker.models.status.StatusList 59 | stalker.models.structure.Structure 60 | stalker.models.studio.Studio 61 | stalker.models.studio.Vacation 62 | stalker.models.studio.WorkingHours 63 | stalker.models.tag.Tag 64 | stalker.models.task.Task 65 | stalker.models.task.TaskDependency 66 | stalker.models.task.TimeLog 67 | stalker.models.template.FilenameTemplate 68 | stalker.models.ticket.Ticket 69 | stalker.models.ticket.TicketLog 70 | stalker.models.type.EntityType 71 | stalker.models.type.Type 72 | stalker.models.variant.Variant 73 | stalker.models.version.Version 74 | stalker.models.wiki.Page 75 | :parts: 1 76 | -------------------------------------------------------------------------------- /docs/source/installation.rst: -------------------------------------------------------------------------------- 1 | .. _installation_toplevel: 2 | 3 | ============ 4 | Installation 5 | ============ 6 | 7 | 8 | How to Install Stalker 9 | ====================== 10 | 11 | 12 | This document will help you install and run Stalker. 13 | 14 | Install Python 15 | ============== 16 | 17 | Stalker is completely written with Python, so it requires Python. It currently 18 | works with Python version 2.6 and 2.7. So you first need to have Python 19 | installed in your system. On Linux and macOS there is a system wide Python 20 | already installed. For Windows, you need to download the Python installer 21 | suitable for your Windows operating system (32 or 64 bit) from `Python.org`_ 22 | 23 | .. _Python.org: http://www.python.org/ 24 | 25 | Install Stalker 26 | =============== 27 | 28 | The easiest way to install the latest version of Stalker along with all its 29 | dependencies is to use the `setuptools`. If your system doesn't have setuptools 30 | (particularly Windows) you need to install `setuptools` by using `ez_setup` 31 | bootstrap script. 32 | 33 | Installing `setuptools` with `ez_setup`: 34 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 35 | 36 | These steps are generally needed just for Windows. Linux and macOS users can skip 37 | this part. 38 | 39 | 1. download `ez_setup.py`_ 40 | 2. run the following command in the command prompt/shell/terminal:: 41 | 42 | python ez_setup 43 | 44 | It will install or build the `setuptools` if there are no suitable installer 45 | for your operating system. 46 | 47 | .. _ez_setup.py: http://peak.telecommunity.com/dist/ez_setup.py 48 | 49 | Installing Stalker (All OSes): 50 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 51 | 52 | After installing the `setuptools` you can run the following command:: 53 | 54 | easy_install -U stalker 55 | 56 | Now you have installed Stalker along with all its dependencies. 57 | 58 | Checking the installation of Stalker 59 | ==================================== 60 | 61 | If everything went ok you should be able to import and check the version of 62 | Stalker by using the Python prompt like this:: 63 | 64 | >>> import stalker 65 | >>> stalker.__version__ 66 | 0.2.21 67 | 68 | For developers 69 | ============== 70 | 71 | It is highly recommended to create a `VirtualEnv` specific for Stalker 72 | development. So to setup a virtualenv for Stalker:: 73 | 74 | virtualenv --no-site-packages stalker 75 | 76 | Then clone the repository (you need git to do that):: 77 | 78 | cd stalker 79 | git clone https://github.com/eoyilmaz/stalker.git stalker 80 | 81 | And then to setup the virtual environment for development:: 82 | 83 | cd stalker 84 | ../bin/python setup.py develop 85 | 86 | This command should install any dependent package to the virtual environment. 87 | 88 | .. _VirtualEnv: https://pypi.python.org/pypi/virtualenv 89 | 90 | Installing a Database 91 | ===================== 92 | 93 | Stalker uses a database to store all the data. The only database backend that 94 | doesn't require any extra installation is SQLite3. You can setup Stalker to run 95 | with an SQLite3 database. But it is much suitable to have a dedicated database 96 | server in your studio. And it is recommended to use the same kind of database 97 | backend both in development and production to reduce any compatibility problems 98 | and any migration headaches. 99 | 100 | Although Stalker is mainly tested and developed on SQLite3, the developers of 101 | Stalker are using it in a studio environment where the main database is 102 | PosgreSQL, and it is the recommended database for any application based on 103 | Stalker. But, testing and using Stalker in any other database is encouraged. 104 | 105 | See the `SQLAlchemy documentation`_ for supported databases. 106 | 107 | .. _SQLAlchemy documentation: http://www.sqlalchemy.org/docs/core/engines.html#supported-dbapis 108 | -------------------------------------------------------------------------------- /docs/source/roadmap.rst: -------------------------------------------------------------------------------- 1 | .. _roadmap_toplevel: 2 | 3 | =========================== 4 | Stalker Development Roadmap 5 | =========================== 6 | 7 | This section describes the direction Stalker is going. 8 | 9 | Roadmap Based on Versions 10 | ========================= 11 | 12 | Below you can find the roadmap based on the version 13 | 14 | 0.1.0: 15 | ------ 16 | 17 | * A complete working set of models in SOM which are using 18 | SQLAlchemy.ext.declarative. 19 | 20 | 0.2.0: 21 | ------ 22 | * Web interface 23 | * Complete ProdAM capabilities. 24 | 25 | 0.3.0: 26 | ------ 27 | * Complete working Event system 28 | -------------------------------------------------------------------------------- /docs/source/status_and_status_lists.rst: -------------------------------------------------------------------------------- 1 | .. _status_and_status_lists_toplevel: 2 | 3 | Statuses and Status Lists 4 | ========================= 5 | 6 | In Stalker, classes mixed with :class:`.StatusMixin` needs to be created with a 7 | *suitable* :class:`.StatusList` instance. 8 | 9 | Because most of the *statusable* classes are going to be using the same 10 | :class:`.Status`\ es (ex: **WIP**, **Pending Review**, **Completed** etc.) over 11 | and over again, it is much efficient to create those Statuses only once and use 12 | them multiple times by grouping them in :class:`.StatusList`\ s. 13 | 14 | A *suitable status list* means, the :attr:`.StatusList.target_entity_type` is 15 | set to the name of that particular class. 16 | -------------------------------------------------------------------------------- /docs/source/summary.rst: -------------------------------------------------------------------------------- 1 | .. _summary_toplevel: 2 | 3 | Summary 4 | ======= 5 | 6 | .. autosummary:: 7 | :toctree: generated/ 8 | :nosignatures: 9 | 10 | stalker.db 11 | stalker.db.setup 12 | stalker.exceptions 13 | stalker.exceptions.CircularDependencyError 14 | stalker.exceptions.LoginError 15 | stalker.exceptions.OverBookedError 16 | stalker.exceptions.StatusError 17 | stalker.models 18 | stalker.models.asset.Asset 19 | stalker.models.auth.AuthenticationLog 20 | stalker.models.auth.Group 21 | stalker.models.auth.LocalSession 22 | stalker.models.auth.Role 23 | stalker.models.auth.Permission 24 | stalker.models.auth.User 25 | stalker.models.budget.Budget 26 | stalker.models.budget.BudgetEntry 27 | stalker.models.budget.Good 28 | stalker.models.budget.Invoice 29 | stalker.models.budget.Payment 30 | stalker.models.budget.PriceList 31 | stalker.models.department.Department 32 | stalker.models.department.DepartmentUser 33 | stalker.models.client.Client 34 | stalker.models.client.ClientUser 35 | stalker.models.entity.Entity 36 | stalker.models.entity.EntityGroup 37 | stalker.models.entity.SimpleEntity 38 | stalker.models.file.File 39 | stalker.models.format.ImageFormat 40 | stalker.models.message.Message 41 | stalker.models.mixins.ACLMixin 42 | stalker.models.mixins.CodeMixin 43 | stalker.models.mixins.DateRangeMixin 44 | stalker.models.mixins.ProjectMixin 45 | stalker.models.mixins.ReferenceMixin 46 | stalker.models.mixins.ScheduleMixin 47 | stalker.models.mixins.StatusMixin 48 | stalker.models.mixins.TargetEntityTypeMixin 49 | stalker.models.mixins.WorkingHoursMixin 50 | stalker.models.note.Note 51 | stalker.models.project.Project 52 | stalker.models.project.ProjectClient 53 | stalker.models.project.ProjectRepository 54 | stalker.models.project.ProjectUser 55 | stalker.models.repository.Repository 56 | stalker.models.review.Review 57 | stalker.models.review.Daily 58 | stalker.models.review.DailyFile 59 | stalker.models.scene.Scene 60 | stalker.models.schedulers.SchedulerBase 61 | stalker.models.schedulers.TaskJugglerScheduler 62 | stalker.models.sequence.Sequence 63 | stalker.models.shot.Shot 64 | stalker.models.status.Status 65 | stalker.models.status.StatusList 66 | stalker.models.structure.Structure 67 | stalker.models.studio.Studio 68 | stalker.models.studio.WorkingHours 69 | stalker.models.tag.Tag 70 | stalker.models.task.Task 71 | stalker.models.task.TaskDependency 72 | stalker.models.task.TimeLog 73 | stalker.models.template.FilenameTemplate 74 | stalker.models.ticket.Ticket 75 | stalker.models.ticket.TicketLog 76 | stalker.models.type.EntityType 77 | stalker.models.type.Type 78 | stalker.models.variant.Variant 79 | stalker.models.version.Version 80 | stalker.models.wiki.Page 81 | -------------------------------------------------------------------------------- /docs/source/todo.rst: -------------------------------------------------------------------------------- 1 | .. _todo_toplevel: 2 | 3 | .. include:: source/../../../TODO.rst 4 | -------------------------------------------------------------------------------- /docs/source/tutorial.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial_toplevel: 2 | 3 | ============ 4 | API Tutorial 5 | ============ 6 | 7 | .. _tutorial_contents: 8 | 9 | Table of Contents 10 | ================= 11 | 12 | .. toctree:: 13 | :maxdepth: 3 14 | 15 | tutorial/basics.rst 16 | tutorial/creating_simple_data.rst 17 | tutorial/query_update_delete_data.rst 18 | tutorial/pipeline.rst 19 | tutorial/task_and_resource_management.rst 20 | tutorial/scheduling.rst 21 | task_review_workflow.rst 22 | tutorial/asset_management.rst 23 | tutorial/collaboration.rst 24 | tutorial/extending_som.rst 25 | tutorial/conclusion.rst 26 | 27 | Introduction 28 | ============ 29 | 30 | Stalker leverages the powerful `SQLAlchemy ORM`_ to facilitate interaction with 31 | databases using the Stalker Object Model (SOM). This tutorial introduces you to 32 | the Stalker Python API and SOM. If you're familiar with SQLAlchemy, you'll find 33 | the transition smooth. Otherwise, SOM offers a user-friendly way to manage 34 | databases. 35 | 36 | .. _SQLAlchemy ORM: http://www.sqlalchemy.org/docs/orm/tutorial.html 37 | -------------------------------------------------------------------------------- /docs/source/tutorial/collaboration.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial_collaboration_toplevel: 2 | 3 | Collaboration in Stalker 4 | ======================== 5 | 6 | While we've covered the core functionalities of Stalker, effective 7 | collaboration is essential in any production pipeline. Stalker provides several 8 | tools to facilitate communication and knowledge sharing among team members. 9 | 10 | Note System: 11 | 12 | You can leave :class:`Note`\ s on any Stalker :class:`.Entity` (except 13 | other :class:`.Note`\ s and :class:`.Tag`\ s). This allows you to add 14 | comments, reminders, or specific instructions to tasks, assets, versions, 15 | and other objects. 16 | 17 | Messaging System: 18 | 19 | A direct messaging system (currently under development) will allow you to 20 | send private messages to individuals or groups of users. 21 | 22 | Ticket System: 23 | 24 | Create and track tickets for specific projects to report issues, request 25 | features, or discuss project-related matters. 26 | 27 | Wiki Pages: 28 | 29 | Create and maintain project-specific wiki pages to document procedures, 30 | best practices, and other important information. 31 | -------------------------------------------------------------------------------- /docs/source/tutorial/conclusion.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial_toplevel: 2 | 3 | Conclusion 4 | ========== 5 | 6 | In this tutorial, you have nearly learned a quarter of what Stalker supplies as 7 | a Python library. 8 | 9 | Stalker provides a robust framework for production asset management, serving 10 | the needs of both large and small studios. Its 16-years development history (as 11 | of 2025) and use in major feature films and countless commercials is a 12 | testament to its effectiveness. 13 | 14 | While Stalker itself lacks a graphical user interface (GUI), its power extends 15 | beyond raw code. Here are some additional tools that leverage Stalker's core 16 | functionality: 17 | 18 | `Stalker Pyramid`_: 19 | 20 | A web application built using the `Pyramid`_ framework, utilizing Stalker 21 | as its database model. This allows for user-friendly web-based interaction 22 | with project data. 23 | 24 | `Anima Pipeline`_: 25 | 26 | A pipeline library that incorporates Stalker, showcasing how its 27 | functionalities can be integrated into a pipeline management system. 28 | Notably, Anima demonstrates the creation of Qt UIs using Stalker. 29 | 30 | For a deeper dive into how Stalker interacts with UIs and web applications, 31 | consider exploring the repositories of `Stalker Pyramid`_ and 32 | `Anima Pipeline`_. 33 | 34 | By understanding how Stalker integrates with these tools, you can unlock its 35 | full potential for streamlining your production workflows. 36 | 37 | .. _Stalker Pyramid: https://www.github.com/eoyilmaz/stalker_pyramid 38 | .. _Anima Pipeline: https://github.com/eoyilmaz/anima 39 | .. _Pyramid: https://trypyramid.com/ 40 | -------------------------------------------------------------------------------- /docs/source/tutorial/extending_som.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial_extending_som_toplevel: 2 | 3 | Extending SOM (coming) 4 | ====================== 5 | 6 | This part will be covered soon 7 | -------------------------------------------------------------------------------- /docs/source/tutorial/pipeline.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial_pipeline_toplevel: 2 | 3 | Pipeline 4 | ======== 5 | 6 | So far, we've covered the basics of creating data in Stalker. However. to fully 7 | utilize Stalker's power, we need to define our studio's **pipeline**. This 8 | involves creating tasks and establishing dependencies between them. 9 | 10 | Creating Tasks 11 | -------------- 12 | 13 | Let's create some :class:`.Task`\ s for one of the shots we created earlier: 14 | 15 | .. code-block:: python 16 | 17 | from stalker import Task 18 | 19 | previs = Task( 20 | name="Previs", 21 | parent=sh001 22 | ) 23 | 24 | matchmove = Task( 25 | name="Matchmove", 26 | parent=sh001 27 | ) 28 | 29 | anim = Task( 30 | name="Animation", 31 | parent=sh001 32 | ) 33 | 34 | lighting = Task( 35 | name="Lighting", 36 | parent=sh001 37 | ) 38 | 39 | comp = Task( 40 | name="Comp", 41 | parent=sh001 42 | ) 43 | 44 | Defining Dependencies 45 | --------------------- 46 | 47 | Now, let's define the dependencies between these tasks: 48 | 49 | .. code-block:: python 50 | 51 | comp.depends_on = [lighting] 52 | lighting.depends_on = [anim] 53 | anim.depends_on = [previs, matchmove] 54 | 55 | By establishing these dependencies, we're telling Stalker that certain tasks 56 | need to be completed before others can begin. For example, the "Comp" task 57 | depends on the "Lighting" task, meaning the "Lighting" task must be finished 58 | before the "Comp" task can start. Stalker uses these dependencies to schedule 59 | tasks effectively. 60 | 61 | We'll delve deeper into task scheduling and other pipeline-related concepts 62 | later in this tutorial. 63 | -------------------------------------------------------------------------------- /docs/source/tutorial/query_update_delete_data.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial_query_update_delete_data_toplevel: 2 | 3 | Querying, Updating and Deleting Data 4 | ==================================== 5 | 6 | Now that you've created some data, let's explore how to update and delete it. 7 | 8 | Updating Data 9 | ------------- 10 | 11 | Imagine you created a shot with incorrect information: 12 | 13 | .. code-block:: python 14 | 15 | sh004 = Shot( 16 | code='SH004', 17 | project=new_project, 18 | sequences=[seq1] 19 | ) 20 | DBSession.add(sh004) 21 | DBSession.commit() 22 | 23 | Later, you realize you need to fix the code: 24 | 25 | .. code-block:: python 26 | 27 | sh004.code = "SH005" 28 | DBsession.commit() 29 | 30 | Retrieving Data 31 | --------------- 32 | 33 | To retrieve a shot from the database, you can use a query: 34 | 35 | .. code-block:: python 36 | 37 | wrong_shot = Shot.query.filter_by(code="SH005").first() 38 | 39 | This retrieves the first shot with the code "SH005". 40 | 41 | Updating Retrieved Data 42 | ----------------------- 43 | 44 | If you need to modify the retrieve data: 45 | 46 | .. code-block:: python 47 | 48 | wrong_shot.code = "SH004" # Correct the code 49 | DBsession.commit() # Save the changes 50 | 51 | Deleting Data 52 | ------------- 53 | 54 | To delete data, use the :meth:`DBSession.delete()` method: 55 | 56 | .. code-block:: python 57 | 58 | DBsession.delete(wrong_shot) 59 | DBsession.commit() 60 | 61 | After deleting data, you program variables might still hold references to the 62 | deleted objects, but those objects no longer exist in the database. 63 | 64 | .. code-block:: python 65 | 66 | wrong_shot = Shot.query.filter_by(code="SH005").first() 67 | print(wrong_shot) # This will print None 68 | 69 | For More information 70 | -------------------- 71 | 72 | For advanced update and delete options (like cascades) in SQLAlchemy, refer to 73 | the official `SQLAlchemy documentation`_. 74 | 75 | .. _SQLAlchemy documentation: http://www.sqlalchemy.org/docs/orm/session.html 76 | -------------------------------------------------------------------------------- /docs/source/tutorial/scheduling.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial_scheduling_toplevel: 2 | 3 | Scheduling 4 | ========== 5 | 6 | Now that we've defined tasks, resources, and dependencies, let's schedule our 7 | project! 8 | 9 | 10 | TaskJuggler Integration 11 | ----------------------- 12 | 13 | Stalker utilizes `TaskJuggler`_ to solve scheduling problems and determine when 14 | resources should work on specific tasks. 15 | 16 | .. warning:: 17 | 18 | * Ensure you have `TaskJuggler`_ installed on your system. 19 | 20 | * Configure Stalker to locate the ``tj3`` executable: 21 | 22 | * **Linux:** This is usually straightforward under Linux, just install 23 | `TaskJuggler`_ and Stalker will be able to use it. 24 | 25 | * **macOS & Windows:** Create a ``STALKER_PATH`` environment variable 26 | pointing to a folder containing a ``config.py`` file. Add the following 27 | line to ``config.py``: 28 | 29 | .. code-block:: python 30 | 31 | tj_command = r"C:\Path\to\tj3.exe" 32 | 33 | The default value for ``tj_command`` is ``/usr/local/bin/tj3``. If you run 34 | ``which tj3`` on Linux or macOS and it returns this value, no additional 35 | setup is needed. 36 | 37 | .. _TaskJuggler: http://www.taskjuggler.org/ 38 | 39 | Scheduling Your Project 40 | ----------------------- 41 | 42 | Let's schedule our project using the :class:`.Studio` instance that we've 43 | created at the beginning of this tutorial: 44 | 45 | .. code-block:: python 46 | 47 | from stalker import TaskJugglerScheduler 48 | 49 | my_studio.scheduler = TaskJugglerScheduler() 50 | # Set a large duration (e.g., 1 year) to avoid TaskJuggler complaining the 51 | # project is not fitting into the time frame. 52 | my_studio.duration = datetime.timedelta(days=365) 53 | my_studio.schedule(scheduled_by=me) 54 | DBsession.commit() # Save changes 55 | 56 | This process might take a few seconds for small project or long for larger 57 | ones. 58 | 59 | Viewing Scheduled Dates 60 | ----------------------- 61 | 62 | Once completed, each task will have its ``computed_start`` and ``computed_end`` 63 | values populated: 64 | 65 | .. code-block:: python 66 | 67 | for task in [previs, matchmove, anim, lighting, comp]: 68 | print("{:16s} {} -> {}".format( 69 | task.name, 70 | task.computed_start, 71 | task.computed_end 72 | )) 73 | 74 | Outputs: 75 | 76 | .. code-block:: shell 77 | 78 | Previs 2024-04-02 16:00 -> 2024-04-15 15:00 79 | Matchmove 2024-04-15 15:00 -> 2024-04-17 13:00 80 | Animation 2024-04-17 13:00 -> 2024-04-23 17:00 81 | Lighting 2024-04-23 17:00 -> 2024-04-24 11:00 82 | Comp 2024-04-24 11:00 -> 2024-04-24 17:00 83 | 84 | Understanding the Output 85 | ------------------------ 86 | 87 | The output will display start and end dates for each task, reflecting the 88 | dependencies. In this example, since each task has only one assigned resource 89 | (you), they follow one another. 90 | 91 | Further Explorations 92 | -------------------- 93 | 94 | Scheduling is complex topic. For in-depth information, refer to the 95 | `TaskJuggler`_ documentation. 96 | 97 | 98 | TaskJuggler Project Representation 99 | ---------------------------------- 100 | 101 | You can check the ``to_tjp`` values of the data objects: 102 | 103 | .. code-block:: python 104 | 105 | print(my_studio.to_tjp) 106 | print(me.to_tjp) 107 | print(comp.to_tjp) 108 | print(new_project.to_tjp) 109 | 110 | If you're familiar with TaskJuggler, you'll recognize the output format. 111 | Stalker maps its data to TaskJuggler-compatible strings. Although, Stalker is 112 | currently supporting a subset of directives, it is enough for scheduling 113 | complex projects with intricate dependencies and hierarchies. Support for 114 | additional TaskJuggler directives will grow with future Stalker versions. 115 | -------------------------------------------------------------------------------- /docs/source/tutorial/task_and_resource_management.rst: -------------------------------------------------------------------------------- 1 | .. _tutorial_task_resource_management_toplevel: 2 | 3 | Task and Resource Management 4 | ============================ 5 | 6 | Now that we have created shots and tasks, we need to assign resources (users) 7 | to these tasks to complete the work. 8 | 9 | Let's assign ourselves to all the tasks: 10 | 11 | .. code-block:: python 12 | 13 | previs.resources = [me] 14 | previs.schedule_timing = 10 15 | previs.schedule_unit = 'd' 16 | 17 | matchmove.resources = [me] 18 | matchmove.schedule_timing = 2 19 | matchmove.schedule_unit = 'd' 20 | 21 | anim.resources = [me] 22 | anim.schedule_timing = 5 23 | anim.schedule_unit = 'd' 24 | 25 | lighting.resources = [me] 26 | lighting.schedule_timing = 3 27 | lighting.schedule_unit = 'd' 28 | 29 | comp.resources = [me] 30 | comp.schedule_timing = 6 31 | comp.schedule_unit = 'h' 32 | 33 | Here, we've assigned ourselves as the resource for each task and specified the 34 | estimated time to complete the task using ``schedule_timing`` and 35 | ``schedule_unit`` attributes. 36 | 37 | Saving Changes 38 | -------------- 39 | 40 | To save these changes to the database: 41 | 42 | .. code-block:: python 43 | 44 | DBsession.commit() 45 | 46 | Note that we didn't explicitly add any new object to the session. Since all the 47 | tasks are related to the ``sh001`` shot, which is already tracked by the 48 | session, SQLAlchemy will automatically track and save the changes to the 49 | database. 50 | 51 | With this information, Stalker can now schedule these tasks, taking info 52 | account dependencies and resource availability. This will help you plan and 53 | manage your project more efficiently. 54 | -------------------------------------------------------------------------------- /docs/source/upgrade_db.rst: -------------------------------------------------------------------------------- 1 | .. upgrade_db_toplevel: 2 | 3 | ================== 4 | Upgrading Database 5 | ================== 6 | 7 | Introduction 8 | ============ 9 | 10 | From time to time, with new releases of Stalker, your Stalker database may need 11 | to be upgraded. This is done with the `Alembic`_ library, which is a database 12 | migration library for `SQLAlchemy`_. 13 | 14 | .. _Alembic: http://alembic.zzzcomputing.com/en/latest/ 15 | .. _SQLAlchemy: http://www.sqlalchemy.org 16 | 17 | Instructions 18 | ============ 19 | 20 | The upgrade is easy, just run the following command on the root of the stalker 21 | installation directory:: 22 | 23 | # for Windows 24 | ..\Scripts\alembic.exe upgrade head 25 | 26 | # for Linux or macOS 27 | ../bin/alembic upgrade head 28 | 29 | # this should output something like that: 30 | # 31 | # INFO [alembic.runtime.migration] Context impl PostgresqlImpl. 32 | # INFO [alembic.runtime.migration] Will assume transactional DDL. 33 | # INFO [alembic.runtime.migration] Running upgrade 745b210e6907 -> f2005d1fbadc, added ProjectClients 34 | 35 | That's it, your database is now migrated to the latest version. -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/examples/__init__.py -------------------------------------------------------------------------------- /examples/extending/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/examples/extending/__init__.py -------------------------------------------------------------------------------- /examples/extending/great_entity.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | In this example we are going to extend stalker with a new entity type, which 4 | is also mixed in with a :class:`stalker.models.mixins.ReferenceMixin`. 5 | 6 | To create your own data type, just derive it from a suitable SOM class. 7 | """ 8 | 9 | from sqlalchemy import Column, Integer, ForeignKey 10 | from stalker import SimpleEntity, ReferenceMixin 11 | 12 | 13 | class GreatEntity(SimpleEntity, ReferenceMixin): 14 | """The new great entity class, which is a new simpleEntity with ReferenceMixin.""" 15 | 16 | __tablename__ = "GreatEntities" 17 | __mapper_args__ = {"polymorphic_identity": "GreatEntity"} 18 | great_entity_id = Column( 19 | "id", Integer, ForeignKey("SimpleEntities.id"), primary_key=True 20 | ) 21 | -------------------------------------------------------------------------------- /examples/extending/statused_entity.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | In this example we are going to extend Stalker with a new entity type, which 4 | is also mixed in with :class:`stalker.models.mixins.StatusMixin`. 5 | """ 6 | 7 | from sqlalchemy import Column, Integer, ForeignKey 8 | from stalker import SimpleEntity, StatusMixin 9 | 10 | 11 | class NewStatusedEntity(SimpleEntity, StatusMixin): 12 | """The new statused entity class, which is a new simpleEntity with status abilities. 13 | """ 14 | 15 | __tablename__ = "NewStatusedEntities" 16 | __mapper_args__ = {"polymorphic_identity": "NewStatusedEntity"} 17 | 18 | new_statused_entity_id = Column( 19 | "id", Integer, ForeignKey("SimpleEntities.id"), primary_key=True 20 | ) 21 | 22 | 23 | # voilà now we have introduced a new type to the SOM and also mixed it with a 24 | # StatusMixin 25 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | authors = [ 7 | {name = "Erkan Özgür Yılmaz", email = "eoyilmaz@gmail.com"}, 8 | ] 9 | classifiers = [ 10 | "Programming Language :: Python", 11 | "Programming Language :: Python :: 3.8", 12 | "Programming Language :: Python :: 3.9", 13 | "Programming Language :: Python :: 3.10", 14 | "Programming Language :: Python :: 3.11", 15 | "Programming Language :: Python :: 3.12", 16 | "Programming Language :: Python :: 3.13", 17 | "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", 18 | "Operating System :: OS Independent", 19 | "Development Status :: 5 - Production/Stable", 20 | "Intended Audience :: Developers", 21 | "Intended Audience :: End Users/Desktop", 22 | "Topic :: Database", 23 | "Topic :: Software Development", 24 | "Topic :: Utilities", 25 | "Topic :: Office/Business :: Scheduling", 26 | ] 27 | description = "A Production Asset Management (ProdAM) System" 28 | dynamic = ["version", "dependencies"] 29 | keywords = [ 30 | "production", 31 | "asset", 32 | "management", 33 | "vfx", 34 | "animation", 35 | "maya", 36 | "houdini", 37 | "nuke", 38 | "fusion", 39 | "softimage", 40 | "blender", 41 | "vue", 42 | ] 43 | license = { file = "LICENSE" } 44 | maintainers = [ 45 | {name = "Erkan Özgür Yılmaz", email = "eoyilmaz@gmail.com"}, 46 | ] 47 | name = "stalker" 48 | readme = "README.md" 49 | requires-python = ">= 3.8" 50 | 51 | [project.urls] 52 | "Home Page" = "https://github.com/eoyilmaz/stalker" 53 | GitHub = "https://github.com/eoyilmaz/stalker" 54 | Documentation = "https://stalker.readthedocs.io" 55 | Repository = "https://github.com/eoyilmaz/stalker.git" 56 | 57 | [tool.setuptools] 58 | include-package-data = true 59 | 60 | [tool.setuptools.packages.find] 61 | where = ["src"] 62 | 63 | [tool.setuptools.package-data] 64 | stalker = ["VERSION", "py.typed"] 65 | 66 | [tool.setuptools.exclude-package-data] 67 | stalker = ["alembic", "docs", "tests"] 68 | 69 | [tool.setuptools.dynamic] 70 | dependencies = { file = ["requirements.txt"] } 71 | optional-dependencies.test = { file = ["requirements-dev.txt"] } 72 | version = { file = ["VERSION"] } 73 | 74 | [tool.distutils.bdist_wheel] 75 | universal = false 76 | 77 | [tool.pytest.ini_options] 78 | pythonpath = ["."] 79 | addopts = "-n auto -W ignore -W always::DeprecationWarning --color=yes --cov=src --cov-report term --cov-report html --cov-append tests" 80 | 81 | [tool.black] 82 | 83 | [tool.flake8] 84 | exclude = [ 85 | ".github", 86 | "__pycache__", 87 | ".coverage", 88 | ".DS_Store", 89 | ".pytest_cache", 90 | ".venv", 91 | ".vscode", 92 | "build", 93 | "dist", 94 | "stalker.egg-info", 95 | ] 96 | extend-select = ["B950"] 97 | ignore = ["D107", "E203", "E501", "E701", "SC200", "W503"] 98 | max-complexity = 12 99 | max-line-length = 80 100 | 101 | [tool.tox] 102 | requires = ["tox>=4.23.2"] 103 | env_list = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 104 | 105 | [tool.tox.env_run_base] 106 | description = "run the tests with pytest" 107 | package = "wheel" 108 | wheel_build_env = ".pkg" 109 | set_env = { SQLALCHEMY_WARN_20 = "1" } 110 | deps = [ 111 | "pytest>=6", 112 | "pytest-cov", 113 | "pytest-xdist", 114 | ] 115 | commands = [ 116 | ["pytest"], 117 | ] 118 | 119 | [tool.mypy] 120 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | 2 | black 3 | coverage 4 | darglint 5 | flake8 6 | flake8-bugbear 7 | flake8-docstrings 8 | flake8-import-order 9 | flake8-mutable 10 | flake8-pep3101 11 | flake8-spellcheck 12 | flake8-pyproject 13 | furo 14 | mypy 15 | pyglet 16 | pytest 17 | pytest-cov 18 | pytest-github-actions-annotate-failures 19 | pytest-xdist 20 | sphinx 21 | sphinx-autoapi 22 | # sphinx-findthedocs 23 | tox 24 | twine 25 | types-pytz 26 | wheel -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic 2 | build 3 | jinja2 4 | psycopg2-binary 5 | pytz 6 | six 7 | sqlalchemy >= 2 8 | tzlocal -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup 3 | 4 | setup() 5 | -------------------------------------------------------------------------------- /src/stalker/VERSION: -------------------------------------------------------------------------------- 1 | 1.1.2 -------------------------------------------------------------------------------- /src/stalker/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Stalker is a Production Asset Management (ProdAM) designed for Animation/VFX Studios. 3 | 4 | See docs for more information. 5 | """ 6 | from stalker.version import __version__ # noqa: F401 7 | from stalker import config, log # noqa: I100 8 | 9 | if True: 10 | defaults: config.Config = config.Config() 11 | from stalker.models.asset import Asset 12 | from stalker.models.auth import ( 13 | AuthenticationLog, 14 | Group, 15 | LocalSession, 16 | Permission, 17 | Role, 18 | User, 19 | ) 20 | from stalker.models.budget import Budget, BudgetEntry, Good, Invoice, Payment, PriceList 21 | from stalker.models.client import Client, ClientUser 22 | from stalker.models.department import Department, DepartmentUser 23 | from stalker.models.entity import Entity, EntityGroup, SimpleEntity 24 | from stalker.models.format import ImageFormat 25 | from stalker.models.file import File 26 | from stalker.models.message import Message 27 | from stalker.models.mixins import ( 28 | ACLMixin, 29 | AmountMixin, 30 | CodeMixin, 31 | DAGMixin, 32 | DateRangeMixin, 33 | ProjectMixin, 34 | ReferenceMixin, 35 | ScheduleMixin, 36 | StatusMixin, 37 | TargetEntityTypeMixin, 38 | UnitMixin, 39 | WorkingHoursMixin, 40 | ) 41 | from stalker.models.note import Note 42 | from stalker.models.project import ( 43 | Project, 44 | ProjectClient, 45 | ProjectRepository, 46 | ProjectUser, 47 | ) 48 | from stalker.models.repository import Repository 49 | from stalker.models.review import Daily, DailyFile, Review 50 | from stalker.models.scene import Scene 51 | from stalker.models.schedulers import SchedulerBase, TaskJugglerScheduler 52 | from stalker.models.sequence import Sequence 53 | from stalker.models.shot import Shot 54 | from stalker.models.status import Status, StatusList 55 | from stalker.models.structure import Structure 56 | from stalker.models.studio import Studio, Vacation, WorkingHours 57 | from stalker.models.tag import Tag 58 | from stalker.models.task import Task, TaskDependency, TimeLog 59 | from stalker.models.template import FilenameTemplate 60 | from stalker.models.ticket import Ticket, TicketLog 61 | from stalker.models.type import EntityType, Type 62 | from stalker.models.variant import Variant 63 | from stalker.models.version import Version 64 | from stalker.models.wiki import Page 65 | 66 | __all__ = [ 67 | "ACLMixin", 68 | "AmountMixin", 69 | "Asset", 70 | "AuthenticationLog", 71 | "Budget", 72 | "BudgetEntry", 73 | "Client", 74 | "ClientUser", 75 | "CodeMixin", 76 | "DAGMixin", 77 | "Daily", 78 | "DailyFile", 79 | "DateRangeMixin", 80 | "Department", 81 | "DepartmentUser", 82 | "Entity", 83 | "EntityGroup", 84 | "EntityType", 85 | "File", 86 | "FilenameTemplate", 87 | "Good", 88 | "Group", 89 | "ImageFormat", 90 | "Invoice", 91 | "LocalSession", 92 | "Message", 93 | "Note", 94 | "Page", 95 | "Payment", 96 | "Permission", 97 | "PriceList", 98 | "Project", 99 | "ProjectClient", 100 | "ProjectMixin", 101 | "ProjectRepository", 102 | "ProjectUser", 103 | "ReferenceMixin", 104 | "Repository", 105 | "Review", 106 | "Role", 107 | "Scene", 108 | "ScheduleMixin", 109 | "SchedulerBase", 110 | "Sequence", 111 | "Shot", 112 | "SimpleEntity", 113 | "Status", 114 | "StatusList", 115 | "StatusMixin", 116 | "Structure", 117 | "Studio", 118 | "Tag", 119 | "TargetEntityTypeMixin", 120 | "Task", 121 | "TaskDependency", 122 | "TaskJugglerScheduler", 123 | "Ticket", 124 | "TicketLog", 125 | "TimeLog", 126 | "Type", 127 | "UnitMixin", 128 | "User", 129 | "Vacation", 130 | "Variant", 131 | "Version", 132 | "WorkingHours", 133 | "WorkingHoursMixin", 134 | ] 135 | 136 | 137 | logger = log.get_logger(__name__) 138 | -------------------------------------------------------------------------------- /src/stalker/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/src/stalker/db/__init__.py -------------------------------------------------------------------------------- /src/stalker/db/declarative.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """The declarative base class is situated here.""" 3 | import logging 4 | from typing import Any, Type 5 | 6 | from sqlalchemy.orm import declarative_base 7 | 8 | from stalker.db.session import DBSession 9 | from stalker.log import get_logger 10 | from stalker.utils import make_plural 11 | 12 | logger: logging.Logger = get_logger(__name__) 13 | 14 | 15 | class ORMClass(object): 16 | """The base of the Base class.""" 17 | 18 | query = DBSession.query_property() 19 | 20 | @property 21 | def plural_class_name(self) -> str: 22 | """Return plural name of this class. 23 | 24 | Returns: 25 | str: The plural version of this class. 26 | """ 27 | return make_plural(self.__class__.__name__) 28 | 29 | 30 | Base: Type[Any] = declarative_base(cls=ORMClass) 31 | -------------------------------------------------------------------------------- /src/stalker/db/session.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """The venerable DBSession is situated here. 3 | 4 | This is a runtime storage for the DB session. Greatly simplifying the usage of a 5 | scoped session. 6 | """ 7 | from typing import Any, List, TYPE_CHECKING, Union 8 | 9 | from sqlalchemy.orm import scoped_session, sessionmaker 10 | 11 | if TYPE_CHECKING: # pragma: no cover 12 | from stalker.models.entity import SimpleEntity 13 | 14 | 15 | class ExtendedScopedSession(scoped_session): 16 | """A customized scoped_session which adds new functionality.""" 17 | 18 | def save(self, data: Union[None, List[Any], "SimpleEntity"] = None) -> None: 19 | """Add and commits data at once. 20 | 21 | Args: 22 | data (Union[list, stalker.models.entity.SimpleEntity]): Either a single or 23 | a list of :class:`stalker.models.entity.SimpleEntity` or derivatives. 24 | """ 25 | if data is not None: 26 | if isinstance(data, list): 27 | self.add_all(data) 28 | else: 29 | self.add(data) 30 | self.commit() 31 | 32 | 33 | DBSession = ExtendedScopedSession(sessionmaker(future=True)) 34 | -------------------------------------------------------------------------------- /src/stalker/db/types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Stalker specific data types are situated here.""" 3 | import datetime 4 | import json 5 | from typing import Any, Dict, TYPE_CHECKING, Union 6 | 7 | import pytz 8 | 9 | from sqlalchemy.types import DateTime, JSON, TEXT, TypeDecorator 10 | 11 | import tzlocal 12 | 13 | if TYPE_CHECKING: # pragma: no cover 14 | from sqlalchemy.engine.interfaces import Dialect 15 | 16 | 17 | class JSONEncodedDict(TypeDecorator): 18 | """Stores and retrieves JSON as TEXT.""" 19 | 20 | impl = TEXT 21 | 22 | def process_bind_param(self, value: Union[None, Any], dialect: "Dialect") -> str: 23 | """Process bind param. 24 | 25 | Args: 26 | value (Union[None, Any]): The object to convert to JSON. 27 | dialect (sqlalchemy.engine.interface.Dialect): The dialect. 28 | 29 | Returns: 30 | str: The str representation of the JSON data. 31 | """ 32 | if value is not None: 33 | value = json.dumps(value) 34 | return value 35 | 36 | def process_result_value( 37 | self, value: Union[None, str], dialect: "Dialect" 38 | ) -> Union[None, Dict[str, Any]]: 39 | """Process result value. 40 | 41 | Args: 42 | value (Union[None, Any]): The str representation of the JSON data. 43 | dialect (sqlalchemy.engine.interface.Dialect): The dialect. 44 | 45 | Returns: 46 | dict: The dict representation of the JSON data. 47 | """ 48 | return_value: Union[None, Dict[str, Any]] = None 49 | if value is not None: 50 | return_value = json.loads(value) 51 | return return_value 52 | 53 | 54 | GenericJSON = JSON().with_variant(JSONEncodedDict, "sqlite") 55 | """A JSON variant that can be used both for PostgreSQL and SQLite3 56 | 57 | It will be native JSON for PostgreSQL and JSONEncodedDict for SQLite3 58 | """ 59 | 60 | 61 | class DateTimeUTC(TypeDecorator): 62 | """Store UTC internally without the timezone info. 63 | 64 | Inject timezone info as the data comes back from db. 65 | """ 66 | 67 | impl = DateTime 68 | 69 | def process_bind_param(self, value: Any, dialect: str) -> datetime.datetime: 70 | """Process bind param. 71 | 72 | Args: 73 | value (Any): The value. 74 | dialect (str): The dialect. 75 | 76 | Returns: 77 | datetime.datetime: The datetime value with UTC timezone. 78 | """ 79 | if value is not None: 80 | # convert the datetime object to have UTC 81 | # and strip the datetime value out (which is automatic for SQLite3) 82 | value = value.astimezone(pytz.utc) 83 | return value 84 | 85 | def process_result_value(self, value: Any, dialect: str) -> datetime.datetime: 86 | """Process result value. 87 | 88 | Args: 89 | value (Any): The value. 90 | dialect (str): The dialect. 91 | 92 | Returns: 93 | datetime.datetime: The datetime value with UTC timezone. 94 | """ 95 | if value is not None: 96 | # inject utc and then convert to local timezone 97 | local_tz = tzlocal.get_localzone() 98 | value = value.replace(tzinfo=pytz.utc).astimezone(local_tz) 99 | return value 100 | 101 | 102 | GenericDateTime = DateTime(timezone=True).with_variant(DateTimeUTC, "sqlite") 103 | """A DateTime variant that can be used with both PostgreSQL and SQLite3 and 104 | adds support to timezones in SQLite3. 105 | """ 106 | -------------------------------------------------------------------------------- /src/stalker/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Errors for the system. 3 | 4 | This module contains the Errors in Stalker. 5 | """ 6 | 7 | 8 | class LoginError(Exception): 9 | """Raised when the login information is not correct.""" 10 | 11 | def __init__(self, value="") -> None: 12 | super(LoginError, self).__init__(value) 13 | self.value = value 14 | 15 | def __str__(self) -> str: 16 | """Return the string representation of this exception. 17 | 18 | Returns: 19 | str: The string representation of this exception. 20 | """ 21 | return self.value 22 | 23 | 24 | class CircularDependencyError(Exception): 25 | """Raised when there is circular dependencies within Tasks.""" 26 | 27 | def __init__(self, value="") -> None: 28 | super(CircularDependencyError, self).__init__(value) 29 | self.value = value 30 | 31 | def __str__(self) -> str: 32 | """Return the string representation of this exception. 33 | 34 | Returns: 35 | str: The string representation of this exception. 36 | """ 37 | return self.value 38 | 39 | 40 | class OverBookedError(Exception): 41 | """Raised when a resource is booked more than once for the same time period.""" 42 | 43 | def __init__(self, value="") -> None: 44 | super(OverBookedError, self).__init__(value) 45 | self.value = value 46 | 47 | def __str__(self) -> str: 48 | """Return the string representation of this exception. 49 | 50 | Returns: 51 | str: The string representation of this exception. 52 | """ 53 | return self.value 54 | 55 | 56 | class StatusError(Exception): 57 | """Raised when the status of an entity is not suitable for the desired action.""" 58 | 59 | def __init__(self, value="") -> None: 60 | super(StatusError, self).__init__(value) 61 | self.value = value 62 | 63 | def __str__(self) -> str: 64 | """Return the string representation of this exception. 65 | 66 | Returns: 67 | str: The string representation of this exception. 68 | """ 69 | return self.value 70 | 71 | 72 | class DependencyViolationError(Exception): 73 | """Raised when a TimeLog violates the dependency relation between tasks.""" 74 | 75 | def __init__(self, value="") -> None: 76 | super(DependencyViolationError, self).__init__(value) 77 | self.value = value 78 | 79 | def __str__(self) -> str: 80 | """Return the string representation of this exception. 81 | 82 | Returns: 83 | str: The string representation of this exception. 84 | """ 85 | return self.value 86 | -------------------------------------------------------------------------------- /src/stalker/log.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Logging related functions are situated here. 3 | 4 | This module allows registering any number of logger so that it is possible 5 | to update the logging level all together at runtime (without relaying on weird 6 | hacks). 7 | """ 8 | 9 | import logging 10 | 11 | logging.basicConfig() 12 | logging_level = logging.INFO 13 | loggers = [] 14 | 15 | 16 | def get_logger(name: str) -> logging.Logger: 17 | """Get a logger. 18 | 19 | Args: 20 | name (str): The name of the logger. 21 | 22 | Returns: 23 | logging.Logger: The logger. 24 | """ 25 | logger = logging.getLogger(name) 26 | register_logger(logger) 27 | return logger 28 | 29 | 30 | def register_logger(logger: logging.Logger) -> None: 31 | """Register logger. 32 | 33 | Args: 34 | logger (logging.Logger): A logging.Logger instance. 35 | 36 | Raises: 37 | TypeError: If the logger is not a logging.Logger instance. 38 | """ 39 | if not isinstance(logger, logging.Logger): 40 | raise TypeError( 41 | "logger should be a logging.Logger instance, not {}: '{}'".format( 42 | logger.__class__.__name__, logger 43 | ) 44 | ) 45 | 46 | if logger not in loggers: 47 | loggers.append(logger) 48 | 49 | logger.setLevel(logging_level) 50 | 51 | 52 | def set_level(level: int) -> None: 53 | """Update all registered loggers to the given level. 54 | 55 | Args: 56 | level (int): The logging level. The value should be valid with the 57 | logging library and should be one of [NOTSET, DEBUG, INFO, WARN, 58 | WARNING, ERROR, FATAL, CRITICAL] of the logging library (or anything 59 | that is registered as a proper logging level). 60 | 61 | Raises: 62 | TypeError: If level is not an integer. 63 | ValueError: If level is not a valid value for the logging library. 64 | """ 65 | if not isinstance(level, int): 66 | raise TypeError( 67 | "level should be an integer value one of [0, 10, 20, 30, 40, 50] " 68 | "or [NOTSET, DEBUG, INFO, WARN, WARNING, ERROR, FATAL, CRITICAL] " 69 | "of the logging library, not {}: '{}'".format( 70 | level.__class__.__name__, level 71 | ) 72 | ) 73 | 74 | level_names = logging._levelToName 75 | 76 | if level not in level_names: 77 | raise ValueError( 78 | "level should be an integer value one of [0, 10, 20, 30, 40, 50] " 79 | "or [NOTSET, DEBUG, INFO, WARN, WARNING, ERROR, FATAL, CRITICAL] " 80 | "of the logging library, not {}.".format(level) 81 | ) 82 | 83 | for logger in loggers: 84 | logger.setLevel(level) 85 | -------------------------------------------------------------------------------- /src/stalker/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/src/stalker/models/__init__.py -------------------------------------------------------------------------------- /src/stalker/models/asset.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Asset related classes.""" 3 | 4 | import logging 5 | from typing import Any 6 | 7 | from sqlalchemy import ForeignKey 8 | from sqlalchemy.orm import Mapped, mapped_column 9 | 10 | from stalker import log 11 | from stalker.models.mixins import CodeMixin, ReferenceMixin 12 | from stalker.models.task import Task 13 | 14 | logger: logging.Logger = log.get_logger(__name__) 15 | log.set_level(log.logging_level) 16 | 17 | 18 | class Asset(Task, CodeMixin): 19 | """The Asset class is the whole idea behind Stalker. 20 | 21 | *Assets* are containers of :class:`.Task` s. And :class:`.Task` s are the 22 | smallest meaningful part that should be accomplished to complete the 23 | :class:`.Project`. 24 | 25 | An example could be given as follows; you can create an asset for one of 26 | the characters in your project. Than you can divide this character asset in 27 | to :class:`.Task` s. These :class:`.Task` s can be defined by the type of 28 | the :class:`.Asset`, which is a :class:`.Type` object created specifically 29 | for :class:`.Asset` (ie. has its :attr:`.Type.target_entity_type` set to 30 | "Asset"), 31 | 32 | An :class:`.Asset` instance should be initialized with a :class:`.Project` 33 | instance (as the other classes which are mixed with the 34 | :class:`.TaskMixin`). And when a :class:`.Project` instance is given then 35 | the asset will append itself to the :attr:`.Project.assets` list. 36 | 37 | ..versionadded: 0.2.0: 38 | No more Asset to Shot connection: 39 | 40 | Assets now are not directly related to Shots. Instead a 41 | :class:`.Version` will reference the Asset and then it is easy to track 42 | which shots are referencing this Asset by querying with a join of Shot 43 | Versions referencing this Asset. 44 | """ 45 | 46 | __auto_name__ = False 47 | __strictly_typed__ = True 48 | __tablename__ = "Assets" 49 | __mapper_args__ = {"polymorphic_identity": "Asset"} 50 | 51 | asset_id: Mapped[int] = mapped_column( 52 | "id", ForeignKey("Tasks.id"), primary_key=True 53 | ) 54 | 55 | def __init__(self, code, **kwargs) -> None: 56 | kwargs["code"] = code 57 | 58 | super(Asset, self).__init__(**kwargs) 59 | CodeMixin.__init__(self, **kwargs) 60 | ReferenceMixin.__init__(self, **kwargs) 61 | 62 | def __eq__(self, other: Any) -> bool: 63 | """Check the equality. 64 | 65 | Args: 66 | other (Any): The other object. 67 | 68 | Returns: 69 | bool: True if the other object equals to this asset. 70 | """ 71 | return ( 72 | super(Asset, self).__eq__(other) 73 | and isinstance(other, Asset) 74 | and self.type == other.type 75 | ) 76 | 77 | def __hash__(self) -> int: 78 | """Return the hash value of this instance. 79 | 80 | Because the __eq__ is overridden the __hash__ also needs to be overridden. 81 | 82 | Returns: 83 | int: The hash value. 84 | """ 85 | return super(Asset, self).__hash__() 86 | -------------------------------------------------------------------------------- /src/stalker/models/message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """The Message related classes and functions are situated here.""" 3 | from typing import Any, Dict 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column 7 | 8 | from stalker.log import get_logger 9 | from stalker.models.entity import Entity 10 | from stalker.models.mixins import StatusMixin 11 | 12 | logger = get_logger(__name__) 13 | 14 | 15 | class Message(Entity, StatusMixin): 16 | """The base of the messaging system in Stalker. 17 | 18 | Messages are one of the ways to collaborate in Stalker. The model of the 19 | messages is taken from the e-mail system. So it is pretty similar to an 20 | e-mail message. 21 | 22 | Args: 23 | from (User): The :class:`.User` object sending the message. 24 | to (User): The list of :class:`.User` s to receive this message. 25 | subject (str): The subject of the message. 26 | body (str): tThe body of the message. 27 | in_reply_to (Message): The :class:`.Message` object which this message is a 28 | reply to. 29 | replies (Message): The list of :class:`.Message` objects which are the direct 30 | replies of this message. 31 | attachments (SimpleEntity): A list of :class:`.SimpleEntity` objects attached to 32 | this message (so anything can be attached to a message). 33 | """ 34 | 35 | __auto_name__ = True 36 | __tablename__ = "Messages" 37 | __mapper_args__ = {"polymorphic_identity": "Message"} 38 | message_id: Mapped[int] = mapped_column( 39 | "id", ForeignKey("Entities.id"), primary_key=True 40 | ) 41 | 42 | def __init__(self, **kwargs: Dict[str, Any]) -> None: 43 | super(Message, self).__init__(**kwargs) 44 | StatusMixin.__init__(self, **kwargs) 45 | -------------------------------------------------------------------------------- /src/stalker/models/note.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Note class lies here.""" 3 | from typing import Any, Dict, Optional 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column, synonym 7 | 8 | from stalker.log import get_logger 9 | from stalker.models.entity import SimpleEntity 10 | 11 | logger = get_logger(__name__) 12 | 13 | 14 | class Note(SimpleEntity): 15 | """Notes for any of the SOM objects. 16 | 17 | To leave notes in Stalker use the Note class. 18 | 19 | Args: 20 | content (str): The content of the note. 21 | attached_to (Entity): The object that this note is attached to. 22 | """ 23 | 24 | __auto_name__ = True 25 | __tablename__ = "Notes" 26 | __mapper_args__ = {"polymorphic_identity": "Note"} 27 | 28 | note_id: Mapped[int] = mapped_column( 29 | "id", 30 | ForeignKey("SimpleEntities.id"), 31 | primary_key=True, 32 | ) 33 | 34 | content: Mapped[Optional[str]] = synonym( 35 | "description", 36 | doc="""The content of this :class:`.Note` instance. 37 | 38 | Content is a string representing the content of this Note, can be an 39 | empty. 40 | """, 41 | ) 42 | 43 | def __init__(self, content: str = "", **kwargs: Dict[str, Any]) -> None: 44 | super(Note, self).__init__(**kwargs) 45 | self.content = content 46 | 47 | def __eq__(self, other: Any) -> bool: 48 | """Check the equality. 49 | 50 | Args: 51 | other (Any): The other object. 52 | 53 | Returns: 54 | bool: True if the other object is a Note instance and has the same content. 55 | """ 56 | return ( 57 | super(Note, self).__eq__(other) 58 | and isinstance(other, Note) 59 | and self.content == other.content 60 | ) 61 | 62 | def __hash__(self) -> int: 63 | """Return the hash value of this instance. 64 | 65 | Because the __eq__ is overridden the __hash__ also needs to be overridden. 66 | 67 | Returns: 68 | int: The hash value. 69 | """ 70 | return super(Note, self).__hash__() 71 | -------------------------------------------------------------------------------- /src/stalker/models/scene.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Scene related classes and functions are situated here.""" 3 | 4 | from typing import Any, Dict, List, Optional, TYPE_CHECKING 5 | 6 | from sqlalchemy import ForeignKey 7 | from sqlalchemy.orm import Mapped, mapped_column, relationship, validates 8 | 9 | from stalker.log import get_logger 10 | from stalker.models.mixins import CodeMixin 11 | from stalker.models.task import Task 12 | 13 | if TYPE_CHECKING: # pragma: no cover 14 | from stalker.models.shot import Shot 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | class Scene(Task, CodeMixin): 20 | """Stores data about Scenes. 21 | 22 | Scenes are grouping the Shots according to their view to the world, that is 23 | shots taking place in the same set configuration can be grouped together by 24 | using Scenes. 25 | 26 | You cannot replace :class:`.Sequence` s with Scenes, because Scene 27 | instances doesn't have some key features that :class:`.Sequence` s have. 28 | 29 | A Scene needs to be tied to a :class:`.Project` 30 | instance, so it is not possible to create a Scene without a one. 31 | """ 32 | 33 | __auto_name__ = False 34 | __tablename__ = "Scenes" 35 | __mapper_args__ = {"polymorphic_identity": "Scene"} 36 | scene_id: Mapped[int] = mapped_column( 37 | "id", 38 | ForeignKey("Tasks.id"), 39 | primary_key=True, 40 | ) 41 | 42 | shots: Mapped[Optional[List["Shot"]]] = relationship( 43 | primaryjoin="Shots.c.scene_id==Scenes.c.id", 44 | back_populates="scene", 45 | doc="""The :class:`.Shot` s that is related with this Scene. 46 | 47 | It is a list of :class:`.Shot` instances. 48 | """, 49 | ) 50 | 51 | def __init__(self, shots: Optional[List["Shot"]] = None, **kwargs: Dict[str, Any]): 52 | super(Scene, self).__init__(**kwargs) 53 | 54 | # call the mixin __init__ methods 55 | CodeMixin.__init__(self, **kwargs) 56 | 57 | if shots is None: 58 | shots = [] 59 | 60 | self.shots = shots 61 | 62 | @validates("shots") 63 | def _validate_shots(self, key: str, shot: "Shot") -> "Shot": 64 | """Validate the given shot value. 65 | 66 | Args: 67 | key (str): The name of the validated column. 68 | shot (Shot): The shot instance. 69 | 70 | Raises: 71 | TypeError: If the shot is not a Shot instance. 72 | 73 | Returns: 74 | Shot: Return the validated Shot instance. 75 | """ 76 | from stalker.models.shot import Shot 77 | 78 | if not isinstance(shot, Shot): 79 | raise TypeError( 80 | f"{self.__class__.__name__}.shots should only contain " 81 | "instances of stalker.models.shot.Shot, " 82 | f"not {shot.__class__.__name__}: '{shot}'" 83 | ) 84 | return shot 85 | 86 | def __eq__(self, other: Any) -> bool: 87 | """Check the equality with the other object. 88 | 89 | Args: 90 | other (Any): The other object. 91 | 92 | Returns: 93 | bool: True if the other object is equal to this object. 94 | """ 95 | return isinstance(other, Scene) and super(Scene, self).__eq__(other) 96 | 97 | def __hash__(self) -> int: 98 | """Return the hash value of this instance. 99 | 100 | Because the __eq__ is overridden the __hash__ also needs to be overridden. 101 | 102 | Returns: 103 | int: The hash value. 104 | """ 105 | return super(Scene, self).__hash__() 106 | -------------------------------------------------------------------------------- /src/stalker/models/sequence.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Sequence related function and classes are situated here.""" 3 | 4 | from typing import Any, Dict, List, Optional, TYPE_CHECKING 5 | 6 | from sqlalchemy import ForeignKey 7 | from sqlalchemy.orm import Mapped, mapped_column, relationship, validates 8 | 9 | from stalker.log import get_logger 10 | from stalker.models.mixins import CodeMixin, ReferenceMixin 11 | from stalker.models.task import Task 12 | 13 | if TYPE_CHECKING: # pragma: no cover 14 | from stalker.models.shot import Shot 15 | 16 | logger = get_logger(__name__) 17 | 18 | 19 | class Sequence(Task, CodeMixin): 20 | """Stores data about Sequences. 21 | 22 | Sequences are a way of grouping the Shots according to their temporal 23 | position to each other. 24 | 25 | **Initialization** 26 | 27 | .. warning:: 28 | 29 | .. deprecated:: 0.2.0 30 | 31 | Sequences do not have a lead anymore. Use the :class:`.Task.responsible` 32 | attribute of the super (:class:`.Task`). 33 | """ 34 | 35 | __auto_name__ = False 36 | __tablename__ = "Sequences" 37 | __mapper_args__ = {"polymorphic_identity": "Sequence"} 38 | sequence_id: Mapped[int] = mapped_column( 39 | "id", 40 | ForeignKey("Tasks.id"), 41 | primary_key=True, 42 | ) 43 | 44 | shots: Mapped[Optional[List["Shot"]]] = relationship( 45 | primaryjoin="Shots.c.sequence_id==Sequences.c.id", 46 | back_populates="sequence", 47 | doc="""The :class:`.Shot` s assigned to this Sequence. 48 | 49 | It is a list of :class:`.Shot` instances. 50 | """, 51 | ) 52 | 53 | def __init__(self, **kwargs: Dict[str, Any]) -> None: 54 | super(Sequence, self).__init__(**kwargs) 55 | 56 | # call the mixin __init__ methods 57 | ReferenceMixin.__init__(self, **kwargs) 58 | CodeMixin.__init__(self, **kwargs) 59 | self.shots = [] 60 | 61 | @validates("shots") 62 | def _validate_shots(self, key: str, shot: "Shot") -> "Shot": 63 | """Validate the given shot value. 64 | 65 | Args: 66 | key (str): The name of the validated column. 67 | shot (Shot): The Shot instance to validate. 68 | 69 | Raises: 70 | TypeError: If the given shot is not a Shot instance. 71 | 72 | Returns: 73 | Shot: The validated shot value. 74 | """ 75 | from stalker.models.shot import Shot 76 | 77 | if not isinstance(shot, Shot): 78 | raise TypeError( 79 | f"{self.__class__.__name__}.shots should only contain " 80 | "instances of stalker.models.shot.Shot, " 81 | f"not {shot.__class__.__name__}: '{shot}'" 82 | ) 83 | return shot 84 | 85 | def __eq__(self, other: Any) -> bool: 86 | """Check the equality. 87 | 88 | Args: 89 | other (Any): The other object. 90 | 91 | Returns: 92 | bool: True if the other object is a Sequence instance and has the same 93 | attributes. 94 | """ 95 | return isinstance(other, Sequence) and super(Sequence, self).__eq__(other) 96 | 97 | def __hash__(self) -> int: 98 | """Return the hash value of this instance. 99 | 100 | Because the __eq__ is overridden the __hash__ also needs to be overridden. 101 | 102 | Returns: 103 | int: The hash value. 104 | """ 105 | return super(Sequence, self).__hash__() 106 | -------------------------------------------------------------------------------- /src/stalker/models/tag.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tag related functions and classes are situated here.""" 3 | from typing import Any, Dict 4 | 5 | from sqlalchemy import ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column 7 | 8 | from stalker.log import get_logger 9 | from stalker.models.entity import SimpleEntity 10 | 11 | logger = get_logger(__name__) 12 | 13 | 14 | class Tag(SimpleEntity): 15 | """Use it to create tags for any object available in SOM. 16 | 17 | Doesn't have any other attribute than what is inherited from 18 | :class:`.SimpleEntity` 19 | """ 20 | 21 | __auto_name__ = False 22 | __tablename__ = "Tags" 23 | __mapper_args__ = {"polymorphic_identity": "Tag"} 24 | tag_id: Mapped[int] = mapped_column( 25 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 26 | ) 27 | 28 | def __init__(self, **kwargs: Dict[str, Any]) -> None: 29 | super(Tag, self).__init__(**kwargs) 30 | 31 | def __eq__(self, other: Any) -> bool: 32 | """Check the equality. 33 | 34 | Args: 35 | other (Any): The other object. 36 | 37 | Returns: 38 | bool: True if the other object is a Tag instance and has the same 39 | attributes. 40 | """ 41 | return super(Tag, self).__eq__(other) and isinstance(other, Tag) 42 | 43 | def __hash__(self) -> int: 44 | """Return the hash value for this Tag instance. 45 | 46 | Returns: 47 | int: The hash of this Tag. 48 | """ 49 | return super(Tag, self).__hash__() 50 | -------------------------------------------------------------------------------- /src/stalker/models/variant.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Variant related functions and classes are situated here.""" 3 | 4 | from sqlalchemy import ForeignKey 5 | from sqlalchemy.orm import Mapped, mapped_column 6 | 7 | from stalker.models.task import Task 8 | 9 | 10 | class Variant(Task): 11 | """A Task derivative to keep track of Variants in a Task hierarchy. 12 | 13 | The basic reason to have the Variant class is to upgrade the variants, 14 | into a Task derivative so that it is possible to create dependencies 15 | between different variants and being able to review them individually. 16 | 17 | You see, in previous versions of Stalker, the variants were handled as a 18 | part of the Version instances with a str attribute. The down side of that 19 | design was not being able to distinguish any reviews per variant. 20 | 21 | So, when a Model task is approved, all its variant approved all together, 22 | even if one of the variants were still getting worked on. 23 | 24 | The new design prevents that and gives the variant the level of attention 25 | they deserved. 26 | 27 | Variants doesn't introduce any new arguments or attributes. They are just 28 | initialized like any other Tasks. 29 | """ 30 | 31 | __tablename__ = "Variants" 32 | __mapper_args__ = {"polymorphic_identity": "Variant"} 33 | variant_id: Mapped[int] = mapped_column( 34 | "id", 35 | ForeignKey("Tasks.id"), 36 | primary_key=True, 37 | ) 38 | -------------------------------------------------------------------------------- /src/stalker/models/wiki.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Wiki related functions and classes are situated here.""" 3 | from typing import Any, Dict, Optional, TYPE_CHECKING, Union 4 | 5 | from sqlalchemy import ForeignKey, Text 6 | from sqlalchemy.orm import Mapped, mapped_column, validates 7 | 8 | from stalker import Entity, ProjectMixin 9 | 10 | if TYPE_CHECKING: # pragma: no cover 11 | from stalker.models.project import Project 12 | 13 | 14 | class Page(Entity, ProjectMixin): 15 | """A simple Wiki page implementation. 16 | 17 | Wiki in Stalker are managed per Project. That is, all Wiki pages are 18 | related to a Project. 19 | 20 | Stalker wiki pages are very simple in terms of data it holds. It has only 21 | one :attr:`.title` and one :attr:`.content` an some usual audit info coming 22 | from :class:`.SimpleEntity` and a :attr:`.project` coming from 23 | :class:`.ProjectMixin`. 24 | 25 | Args: 26 | title (str): The title of this Page. 27 | content (str): The content of this page. Can contain any kind of string 28 | literals including HTML tags etc. 29 | """ 30 | 31 | __auto_name__ = True 32 | __tablename__ = "Pages" 33 | __mapper_args__ = {"polymorphic_identity": "Page"} 34 | page_id: Mapped[int] = mapped_column( 35 | "id", 36 | ForeignKey("Entities.id"), 37 | primary_key=True, 38 | ) 39 | 40 | title: Mapped[Optional[str]] = mapped_column(Text) 41 | content: Mapped[Optional[str]] = mapped_column(Text) 42 | 43 | def __init__( 44 | self, 45 | title: str = "", 46 | content: str = "", 47 | project: Optional["Project"] = None, 48 | **kwargs: Dict[str, Any], 49 | ) -> None: 50 | kwargs["project"] = project 51 | super(Page, self).__init__(**kwargs) 52 | ProjectMixin.__init__(self, **kwargs) 53 | 54 | self.title = title 55 | self.content = content 56 | 57 | @validates("title") 58 | def _validate_title(self, key: str, title: str) -> str: 59 | """Validate the given title value. 60 | 61 | Args: 62 | key (str): The name of the validated column. 63 | title (str): The title value to be validated. 64 | 65 | Raises: 66 | TypeError: If the given title is not a string. 67 | ValueError: If the title is an empty string. 68 | 69 | Returns: 70 | str: The validated title value. 71 | """ 72 | if not isinstance(title, str): 73 | raise TypeError( 74 | "{}.title should be a string, not {}: '{}'".format( 75 | self.__class__.__name__, title.__class__.__name__, title 76 | ) 77 | ) 78 | 79 | if not title: 80 | raise ValueError(f"{self.__class__.__name__}.title cannot be empty") 81 | 82 | return title 83 | 84 | @validates("content") 85 | def _validate_content(self, key: str, content: Union[None, str]) -> str: 86 | """Validate the given content value. 87 | 88 | Args: 89 | key (str): The name of the validated column. 90 | content (Union[None, str]): The content value to be validated. 91 | 92 | Raises: 93 | TypeError: If the content is not None and not str. 94 | 95 | Returns: 96 | str: The validated content value. 97 | """ 98 | content = "" if content is None else content 99 | if not isinstance(content, str): 100 | raise TypeError( 101 | "{}.content should be a string, not {}: '{}'".format( 102 | self.__class__.__name__, content.__class__.__name__, content 103 | ) 104 | ) 105 | return content 106 | -------------------------------------------------------------------------------- /src/stalker/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/src/stalker/py.typed -------------------------------------------------------------------------------- /src/stalker/version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Provides functionality to parse the version number from the VERSION file.""" 3 | import os 4 | from typing import Union 5 | 6 | VERSION: Union[None, str] = None 7 | VERSION_FILE: str = os.path.join(os.path.dirname(__file__), "VERSION") 8 | if os.path.isfile(VERSION_FILE): 9 | with open(VERSION_FILE, "r") as f: 10 | VERSION = f.read().strip() 11 | __version__ = VERSION or "0.0.0" 12 | """str: The version of the package.""" 13 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/tests/__init__.py -------------------------------------------------------------------------------- /tests/benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/tests/benchmarks/__init__.py -------------------------------------------------------------------------------- /tests/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/tests/config/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Configure tests.""" 3 | import datetime 4 | import logging 5 | import os 6 | from subprocess import CalledProcessError 7 | 8 | import pytest 9 | 10 | from sqlalchemy.pool import NullPool 11 | 12 | import stalker 13 | import stalker.db.setup 14 | from stalker import db, defaults, log, User 15 | from stalker.config import Config 16 | from stalker.db.session import DBSession 17 | 18 | from tests.utils import create_random_db, tear_down_db 19 | 20 | logger = logging.getLogger(__name__) 21 | log.register_logger(logger) 22 | log.set_level(logging.DEBUG) 23 | 24 | HERE = os.path.dirname(__file__) 25 | 26 | 27 | @pytest.fixture(scope="function") 28 | def setup_sqlite3(): 29 | """Set up in memory SQLite3 database for tests.""" 30 | try: 31 | os.environ.pop(Config.env_key) 32 | except KeyError: 33 | # already removed 34 | pass 35 | 36 | # regenerate the defaults 37 | stalker.defaults.config_values = stalker.defaults.default_config_values.copy() 38 | stalker.defaults["timing_resolution"] = datetime.timedelta(hours=1) 39 | 40 | # Enable Debug logging 41 | log.set_level(logging.DEBUG) 42 | yield 43 | tear_down_db({}) 44 | 45 | 46 | @pytest.fixture 47 | def get_data_file(request): 48 | """Request a specific datafile. 49 | 50 | Args: 51 | request: pytest.request object. 52 | 53 | Returns: 54 | str: Desired data file path. 55 | """ 56 | if isinstance(request.param, str): 57 | return os.path.join(HERE, "data", request.param) 58 | elif isinstance(request.param, list): 59 | output = [] 60 | for path in request.param: 61 | output.append(os.path.join(HERE, "data", path)) 62 | return output 63 | 64 | 65 | @pytest.fixture(scope="function") 66 | def setup_postgresql_db(): 67 | """Set up Postgresql database. 68 | 69 | Yields: 70 | dict: Test data storage. 71 | """ 72 | data = {"config": {}, "database_url": None} 73 | 74 | # create a new database for this test only 75 | while True: 76 | try: 77 | data["database_url"] = create_random_db() 78 | except CalledProcessError: 79 | # in very rare cases the create_random_db generates an already 80 | # existing database name 81 | # call it again 82 | pass 83 | else: 84 | break 85 | 86 | # update the config 87 | data["config"]["sqlalchemy.url"] = data["database_url"] 88 | data["config"]["sqlalchemy.poolclass"] = NullPool 89 | 90 | try: 91 | os.environ.pop(Config.env_key) 92 | except KeyError: 93 | # already removed 94 | pass 95 | 96 | # regenerate the defaults 97 | stalker.defaults.config_values = stalker.defaults.default_config_values.copy() 98 | stalker.defaults["timing_resolution"] = datetime.timedelta(hours=1) 99 | 100 | # init database 101 | # remove anything beforehand 102 | stalker.db.setup.setup(data["config"]) 103 | stalker.db.setup.init() 104 | 105 | yield data 106 | tear_down_db(data) 107 | -------------------------------------------------------------------------------- /tests/data/project_to_tjp_output_formatted: -------------------------------------------------------------------------------- 1 | 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 } } -------------------------------------------------------------------------------- /tests/db/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/tests/db/__init__.py -------------------------------------------------------------------------------- /tests/db/test_dbsession.py: -------------------------------------------------------------------------------- 1 | from stalker import User 2 | from stalker.db.session import DBSession, ExtendedScopedSession 3 | 4 | 5 | def test_dbsession_save_method_is_correctly_created(setup_postgresql_db): 6 | """DBSession is correctly created from ExtendedScopedSession class.""" 7 | assert isinstance(DBSession, ExtendedScopedSession) 8 | 9 | 10 | def test_dbsession_save_method_is_working_as_expected_for_single_entity( 11 | setup_postgresql_db, 12 | ): 13 | """DBSession.save() method is working as expected for single entity.""" 14 | test_user = User( 15 | name="Test User", login="tuser", email="tuser@gmail.com", password="12345" 16 | ) 17 | DBSession.save(test_user) 18 | 19 | del test_user 20 | test_user_db = User.query.filter(User.name == "Test User").first() 21 | assert test_user_db is not None 22 | 23 | 24 | def test_dbsession_save_method_is_working_as_expected_for_multiple_entity( 25 | setup_postgresql_db, 26 | ): 27 | """DBSession.save() method is working as expected for single entity.""" 28 | test_user1 = User( 29 | name="Test User 1", 30 | login="tuser1", 31 | email="tuser1@gmail.com", 32 | password="12345", 33 | ) 34 | test_user2 = User( 35 | name="Test User 2", 36 | login="tuser2", 37 | email="tuser2@gmail.com", 38 | password="12345", 39 | ) 40 | 41 | DBSession.save([test_user1, test_user2]) 42 | 43 | del test_user1 44 | del test_user2 45 | test_user1_db = User.query.filter(User.name == "Test User 1").first() 46 | test_user2_db = User.query.filter(User.name == "Test User 2").first() 47 | assert test_user1_db is not None 48 | assert test_user2_db is not None 49 | 50 | 51 | def test_dbsession_save_method_is_working_as_expected_for_no_entry(setup_postgresql_db): 52 | """DBSession.save() method is working as expected with no parameters.""" 53 | test_user = User( 54 | name="Test User", login="tuser", email="tuser@gmail.com", password="12345" 55 | ) 56 | DBSession.add(test_user) 57 | DBSession.save() 58 | 59 | del test_user 60 | test_user_db = User.query.filter(User.name == "Test User").first() 61 | assert test_user_db is not None 62 | -------------------------------------------------------------------------------- /tests/db/test_types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import pytest 4 | 5 | from sqlalchemy import Column, ForeignKey, Integer 6 | 7 | from stalker.db.setup import init, setup 8 | from stalker.db.session import DBSession 9 | from stalker.db.types import GenericJSON 10 | from stalker.models.entity import Entity 11 | 12 | 13 | @pytest.fixture(scope="function") 14 | def setup_db(setup_sqlite3): 15 | """setup test db.""" 16 | 17 | class MyEntityClass(Entity): 18 | __tablename__ = "MyEntityClasses" 19 | __table_args__ = { 20 | "extend_existing": True, 21 | } 22 | __mapper_args__ = { 23 | "polymorphic_identity": "MyEntityClass", 24 | } 25 | my_entity_id = Column( 26 | "id", Integer, ForeignKey("Entities.id"), primary_key=True 27 | ) 28 | data = Column(GenericJSON) 29 | 30 | # setup and initialize db 31 | setup() 32 | init() 33 | 34 | yield MyEntityClass 35 | 36 | 37 | def test_json_encoded_dict_with_generic_data_stored(setup_db): 38 | """JSONEncodedDict with generic data.""" 39 | MyEntityClass = setup_db 40 | 41 | my_entity = MyEntityClass() 42 | my_entity.data = { 43 | "some key": "and this is the value", 44 | } 45 | DBSession.add(my_entity) 46 | DBSession.commit() 47 | 48 | 49 | def test_json_encoded_dict_with_generic_data_none_data_stored(setup_db): 50 | """JSONEncodedDict with generic data.""" 51 | MyEntityClass = setup_db 52 | 53 | my_entity = MyEntityClass() 54 | my_entity.data = None 55 | DBSession.add(my_entity) 56 | DBSession.commit() 57 | 58 | 59 | def test_json_encoded_dict_with_generic_data_retrieved(setup_db): 60 | """JSONEncodedDict with generic data.""" 61 | MyEntityClass = setup_db 62 | 63 | test_data = { 64 | "some key": "and this is the value", 65 | } 66 | 67 | my_entity = MyEntityClass() 68 | my_entity.data = test_data 69 | DBSession.add(my_entity) 70 | DBSession.commit() 71 | 72 | del my_entity 73 | 74 | retrieved_data = MyEntityClass.query.first() 75 | assert retrieved_data.data == test_data 76 | -------------------------------------------------------------------------------- /tests/mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/tests/mixins/__init__.py -------------------------------------------------------------------------------- /tests/mixins/test_acl_mixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ACLMixin related tests.""" 3 | 4 | import pytest 5 | 6 | from sqlalchemy import Column, Integer 7 | from sqlalchemy.orm import Mapped, mapped_column 8 | 9 | from stalker import ACLMixin, Permission 10 | from stalker.db.declarative import Base 11 | 12 | 13 | class TestClassForACL(Base, ACLMixin): 14 | """A class for testing ACLMixing.""" 15 | 16 | __tablename__ = "TestClassForACLs" 17 | id: Mapped[int] = mapped_column(primary_key=True) 18 | 19 | def __init__(self): 20 | super(TestClassForACL, self).__init__() 21 | self.name = None 22 | 23 | 24 | @pytest.fixture(scope="function") 25 | def acl_mixin_test_setup(): 26 | """stalker.models.mixins.ACLMixin class. 27 | 28 | Returns: 29 | dict: Test data. 30 | """ 31 | data = dict() 32 | # create permissions 33 | data["test_perm1"] = Permission( 34 | access="Allow", action="Create", class_name="Something" 35 | ) 36 | data["test_instance"] = TestClassForACL() 37 | data["test_instance"].name = "Test" 38 | data["test_instance"].permissions.append(data["test_perm1"]) 39 | return data 40 | 41 | 42 | def test_permission_attribute_accept_permission_instances_only(acl_mixin_test_setup): 43 | """permissions attribute accepts only Permission instances.""" 44 | data = acl_mixin_test_setup 45 | with pytest.raises(TypeError) as cm: 46 | data["test_instance"].permissions = [234] 47 | 48 | assert str(cm.value) == ( 49 | "TestClassForACL.permissions should be all instances of " 50 | "stalker.models.auth.Permission, not int: '234'" 51 | ) 52 | 53 | 54 | def test_permission_attribute_is_working_as_expected(acl_mixin_test_setup): 55 | """permissions attribute is working as expected.""" 56 | data = acl_mixin_test_setup 57 | assert data["test_instance"].permissions == [data["test_perm1"]] 58 | 59 | 60 | def test_acl_property_returns_a_list(acl_mixin_test_setup): 61 | """__acl__ property returns a list.""" 62 | data = acl_mixin_test_setup 63 | assert isinstance(data["test_instance"].__acl__, list) 64 | 65 | 66 | def test_acl_property_returns_a_proper_ACL_list(acl_mixin_test_setup): 67 | """__acl__ property is a list of ACLs according to the given permissions.""" 68 | data = acl_mixin_test_setup 69 | assert data["test_instance"].__acl__ == [ 70 | ("Allow", "TestClassForACL:Test", "Create_Something") 71 | ] 72 | -------------------------------------------------------------------------------- /tests/mixins/test_amount_mixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """AmountMixin related tests.""" 3 | 4 | import pytest 5 | 6 | from sqlalchemy import ForeignKey, Integer 7 | from sqlalchemy.orm import Mapped, mapped_column 8 | 9 | from stalker import AmountMixin, SimpleEntity 10 | 11 | 12 | class AmountMixinFooMixedInClass(SimpleEntity, AmountMixin): 13 | """A class which derives from another which has and __init__ already.""" 14 | 15 | __tablename__ = "AmountMixinFooMixedInClasses" 16 | __mapper_args__ = {"polymorphic_identity": "AmountMixinFooMixedInClass"} 17 | amountMixinFooMixedInClass_id: Mapped[int] = mapped_column( 18 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 19 | ) 20 | __id_column__ = "amountMixinFooMixedInClass_id" 21 | 22 | def __init__(self, **kwargs): 23 | super(AmountMixinFooMixedInClass, self).__init__(**kwargs) 24 | AmountMixin.__init__(self, **kwargs) 25 | 26 | 27 | def test_mixed_in_class_initialization(): 28 | """init() is working as expected.""" 29 | a = AmountMixinFooMixedInClass(amount=1500) 30 | assert isinstance(a, AmountMixinFooMixedInClass) 31 | assert a.amount == 1500 32 | 33 | 34 | def test_amount_argument_is_skipped(): 35 | """amount attribute will be 0 if the amount argument is skipped.""" 36 | entry = AmountMixinFooMixedInClass() 37 | assert entry.amount == 0.0 38 | 39 | 40 | def test_amount_argument_is_set_to_none(): 41 | """amount attribute will be 0 if the amount argument is None.""" 42 | entry = AmountMixinFooMixedInClass(amount=None) 43 | assert entry.amount == 0.0 44 | 45 | 46 | def test_amount_attribute_is_set_to_none(): 47 | """amount attribute will be set to 0 if it is set to None.""" 48 | entry = AmountMixinFooMixedInClass(amount=10.0) 49 | assert entry.amount == 10.0 50 | entry.amount = None 51 | assert entry.amount == 0.0 52 | 53 | 54 | def test_amount_argument_is_not_a_number(): 55 | """TypeError will be raised if the amount argument is not a number.""" 56 | with pytest.raises(TypeError) as cm: 57 | AmountMixinFooMixedInClass(amount="some string") 58 | 59 | assert str(cm.value) == ( 60 | "AmountMixinFooMixedInClass.amount should be a number, not str: 'some string'" 61 | ) 62 | 63 | 64 | def test_amount_attribute_is_not_a_number(): 65 | """TypeError will be raised if amount attribute is not a number.""" 66 | entry = AmountMixinFooMixedInClass(amount=10) 67 | with pytest.raises(TypeError) as cm: 68 | entry.amount = "some string" 69 | 70 | assert str(cm.value) == ( 71 | "AmountMixinFooMixedInClass.amount should be a number, not str: 'some string'" 72 | ) 73 | 74 | 75 | def test_amount_argument_is_working_as_expected(): 76 | """amount argument value is correctly passed to the amount attribute.""" 77 | entry = AmountMixinFooMixedInClass(amount=10) 78 | assert entry.amount == 10.0 79 | 80 | 81 | def test_amount_attribute_is_working_as_expected(): 82 | """amount attribute is working as expected.""" 83 | entry = AmountMixinFooMixedInClass(amount=10) 84 | test_value = 5.0 85 | assert entry.amount != test_value 86 | entry.amount = test_value 87 | assert entry.amount == test_value 88 | -------------------------------------------------------------------------------- /tests/mixins/test_declarative_project_mixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ProjectMixin related tests.""" 3 | import pytest 4 | 5 | from sqlalchemy import Column, ForeignKey, Integer 6 | from sqlalchemy.orm import Mapped, mapped_column 7 | 8 | from stalker import ( 9 | Project, 10 | ProjectMixin, 11 | Repository, 12 | SimpleEntity, 13 | Status, 14 | StatusList, 15 | Type, 16 | ) 17 | 18 | 19 | class DeclProjMixA(SimpleEntity, ProjectMixin): 20 | """A class for testing ProjectMixin.""" 21 | 22 | __tablename__ = "DeclProjMixAs" 23 | __mapper_args__ = {"polymorphic_identity": "DeclProjMixA"} 24 | a_id: Mapped[int] = mapped_column( 25 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 26 | ) 27 | 28 | def __init__(self, **kwargs): 29 | super(DeclProjMixA, self).__init__(**kwargs) 30 | ProjectMixin.__init__(self, **kwargs) 31 | 32 | 33 | class DeclProjMixB(SimpleEntity, ProjectMixin): 34 | """A class for testing ProjectMixin.""" 35 | 36 | __tablename__ = "DeclProjMixBs" 37 | __mapper_args__ = {"polymorphic_identity": "DeclProjMixB"} 38 | b_id: Mapped[int] = mapped_column( 39 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 40 | ) 41 | 42 | def __init__(self, **kwargs): 43 | super(DeclProjMixB, self).__init__(**kwargs) 44 | ProjectMixin.__init__(self, **kwargs) 45 | 46 | 47 | @pytest.fixture(scope="function") 48 | def setup_project_mixin_tester(): 49 | """Set up the tests for ProjectMixin. 50 | 51 | Returns: 52 | dict: Test data. 53 | """ 54 | data = dict() 55 | data["test_stat1"] = Status(name="On Hold", code="OH") 56 | data["test_stat2"] = Status(name="Work In Progress", code="WIP") 57 | data["test_stat3"] = Status(name="Approved", code="APP") 58 | data["test_status_list_1"] = StatusList( 59 | name="A Statuses", 60 | statuses=[data["test_stat1"], data["test_stat3"]], 61 | target_entity_type=DeclProjMixA, 62 | ) 63 | 64 | data["test_status_list_2"] = StatusList( 65 | name="B Statuses", 66 | statuses=[data["test_stat2"], data["test_stat3"]], 67 | target_entity_type=DeclProjMixB, 68 | ) 69 | 70 | data["test_project_statuses"] = StatusList( 71 | name="Project Statuses", 72 | statuses=[data["test_stat2"], data["test_stat3"]], 73 | target_entity_type="Project", 74 | ) 75 | 76 | data["test_project_type"] = Type( 77 | name="Test Project Type", 78 | code="testproj", 79 | target_entity_type="Project", 80 | ) 81 | 82 | data["test_repository"] = Repository( 83 | name="Test Repo", 84 | code="TR", 85 | ) 86 | 87 | data["test_project"] = Project( 88 | name="Test Project", 89 | code="tp", 90 | type=data["test_project_type"], 91 | status_list=data["test_project_statuses"], 92 | repository=data["test_repository"], 93 | ) 94 | 95 | data["kwargs"] = { 96 | "name": "ozgur", 97 | "status_list": data["test_status_list_1"], 98 | "project": data["test_project"], 99 | } 100 | 101 | data["test_a_obj"] = DeclProjMixA(**data["kwargs"]) 102 | return data 103 | 104 | 105 | def test_project_attribute_is_working_as_expected(setup_project_mixin_tester): 106 | """project attribute is working as expected.""" 107 | data = setup_project_mixin_tester 108 | assert data["test_a_obj"].project == data["test_project"] 109 | -------------------------------------------------------------------------------- /tests/mixins/test_declarative_reference_mixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ReferenceMixin related tests.""" 3 | from sqlalchemy import ForeignKey 4 | from sqlalchemy.orm import Mapped, mapped_column 5 | 6 | from stalker import File, SimpleEntity 7 | from stalker.models.mixins import ReferenceMixin 8 | 9 | 10 | class DeclRefMixA(SimpleEntity, ReferenceMixin): 11 | """A test class for testing ReferenceMixin.""" 12 | 13 | __tablename__ = "DeclRefMixAs" 14 | __mapper_args__ = {"polymorphic_identity": "DeclRefMixA"} 15 | a_id: Mapped[int] = mapped_column( 16 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 17 | ) 18 | 19 | def __init__(self, **kwargs): 20 | super(DeclRefMixA, self).__init__(**kwargs) 21 | ReferenceMixin.__init__(self, **kwargs) 22 | 23 | 24 | class DeclRefMixB(SimpleEntity, ReferenceMixin): 25 | """A test class for testing ReferenceMixin.""" 26 | 27 | __tablename__ = "RefMixBs" 28 | __mapper_args__ = {"polymorphic_identity": "DeclRefMixB"} 29 | b_id: Mapped[int] = mapped_column( 30 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 31 | ) 32 | 33 | def __init__(self, **kwargs): 34 | super(DeclRefMixB, self).__init__(**kwargs) 35 | ReferenceMixin.__init__(self, **kwargs) 36 | 37 | 38 | def test_reference_mixin_setup(): 39 | """ReferenceMixin setup.""" 40 | a_ins = DeclRefMixA(name="ozgur") 41 | b_ins = DeclRefMixB(name="bozgur") 42 | 43 | new_file1 = File(name="test file 1", full_path="none") 44 | new_file2 = File(name="test file 2", full_path="no path") 45 | 46 | a_ins.references.append(new_file1) 47 | b_ins.references.append(new_file2) 48 | 49 | assert new_file1 in a_ins.references 50 | assert new_file2 in b_ins.references 51 | -------------------------------------------------------------------------------- /tests/mixins/test_declarative_schedule_mixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """DateRangeMixin related tests.""" 3 | 4 | import datetime 5 | import logging 6 | 7 | import pytest 8 | 9 | import pytz 10 | 11 | from sqlalchemy import ForeignKey 12 | from sqlalchemy.orm import Mapped, mapped_column 13 | 14 | from stalker import log 15 | from stalker.models.entity import SimpleEntity 16 | from stalker.models.mixins import DateRangeMixin 17 | 18 | 19 | logger = log.get_logger(__name__) 20 | log.set_level(logging.DEBUG) 21 | 22 | 23 | class DeclSchedMixA(SimpleEntity, DateRangeMixin): 24 | """A class for testing DateRangeMixin.""" 25 | 26 | __tablename__ = "DeclSchedMixAs" 27 | __mapper_args__ = {"polymorphic_identity": "DeclSchedMixA"} 28 | a_id: Mapped[int] = mapped_column( 29 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 30 | ) 31 | 32 | def __init__(self, **kwargs): 33 | super(DeclSchedMixA, self).__init__(**kwargs) 34 | DateRangeMixin.__init__(self, **kwargs) 35 | 36 | 37 | class DeclSchedMixB(SimpleEntity, DateRangeMixin): 38 | """A class for testing DateRangeMixin.""" 39 | 40 | __tablename__ = "DeclSchedMixBs" 41 | __mapper_args__ = {"polymorphic_identity": "DeclSchedMixB"} 42 | b_id: Mapped[int] = mapped_column( 43 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 44 | ) 45 | 46 | def __init__(self, **kwargs): 47 | super(DeclSchedMixB, self).__init__(**kwargs) 48 | DateRangeMixin.__init__(self, **kwargs) 49 | 50 | 51 | @pytest.fixture(scope="function") 52 | def setup_schedule_mixin_tester(): 53 | """Set up the tests for DateRangeMixin setup. 54 | 55 | Returns: 56 | dict: Test data. 57 | """ 58 | data = dict() 59 | data["kwargs"] = { 60 | "name": "ozgur", 61 | "start": datetime.datetime(2013, 3, 20, 4, 0, tzinfo=pytz.utc), 62 | "end": datetime.datetime(2013, 3, 30, 4, 0, tzinfo=pytz.utc), 63 | "duration": datetime.timedelta(10), 64 | } 65 | return data 66 | 67 | 68 | def test_mixin_setup_is_working_as_expected(setup_schedule_mixin_tester): 69 | """Mixin setup is working as expected.""" 70 | data = setup_schedule_mixin_tester 71 | new_a = DeclSchedMixA(**data["kwargs"]) # should not create any problem 72 | assert new_a.start == data["kwargs"]["start"] 73 | assert new_a.end == data["kwargs"]["end"] 74 | assert new_a.duration == data["kwargs"]["duration"] 75 | 76 | logger.debug("----------------------------") 77 | logger.debug(new_a.start) 78 | logger.debug(new_a.end) 79 | logger.debug(new_a.duration) 80 | 81 | # try to change the start and check if the duration is also updated 82 | new_a.start = datetime.datetime(2013, 3, 30, 10, 0, tzinfo=pytz.utc) 83 | 84 | assert new_a.start == datetime.datetime(2013, 3, 30, 10, 0, tzinfo=pytz.utc) 85 | 86 | assert new_a.end == datetime.datetime(2013, 4, 9, 10, 0, tzinfo=pytz.utc) 87 | 88 | assert new_a.duration == datetime.timedelta(10) 89 | 90 | a_start = new_a.start 91 | a_end = new_a.end 92 | a_duration = new_a.duration 93 | 94 | # now check the start, end and duration 95 | logger.debug("----------------------------") 96 | logger.debug(new_a.start) 97 | logger.debug(new_a.end) 98 | logger.debug(new_a.duration) 99 | 100 | # create a new class 101 | new_b = DeclSchedMixB(**data["kwargs"]) 102 | # now check the start, end and duration 103 | assert new_b.start == data["kwargs"]["start"] 104 | assert new_b.end == data["kwargs"]["end"] 105 | assert new_b.duration == data["kwargs"]["duration"] 106 | 107 | logger.debug("----------------------------") 108 | logger.debug(new_b.start) 109 | logger.debug(new_b.end) 110 | logger.debug(new_b.duration) 111 | 112 | # now check the start, end and duration of A 113 | logger.debug("----------------------------") 114 | logger.debug(new_a.start) 115 | logger.debug(new_a.end) 116 | logger.debug(new_a.duration) 117 | assert new_a.start == a_start 118 | assert new_a.end == a_end 119 | assert new_a.duration == a_duration 120 | -------------------------------------------------------------------------------- /tests/mixins/test_declarative_status_mixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """StatusMixin class related tests.""" 3 | import pytest 4 | 5 | from sqlalchemy import Column, ForeignKey, Integer 6 | from sqlalchemy.orm import Mapped, mapped_column 7 | 8 | from stalker import SimpleEntity, Status, StatusList, StatusMixin 9 | 10 | 11 | class DeclStatMixA(SimpleEntity, StatusMixin): 12 | """A class for testing StatusMixin.""" 13 | 14 | __tablename__ = "DeclStatMixAs" 15 | __mapper_args__ = {"polymorphic_identity": "DeclStatMixA"} 16 | declStatMixAs_id: Mapped[int] = mapped_column( 17 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 18 | ) 19 | 20 | def __init__(self, **kwargs): 21 | super(DeclStatMixA, self).__init__(**kwargs) 22 | StatusMixin.__init__(self, **kwargs) 23 | 24 | 25 | class DeclStatMixB(SimpleEntity, StatusMixin): 26 | """A class for testing StatusMixin.""" 27 | 28 | __tablename__ = "DeclStatMixBs" 29 | __mapper_args__ = {"polymorphic_identity": "DeclStatMixB"} 30 | b_id: Mapped[int] = mapped_column( 31 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 32 | ) 33 | 34 | def __init__(self, **kwargs): 35 | super(DeclStatMixB, self).__init__(**kwargs) 36 | StatusMixin.__init__(self, **kwargs) 37 | 38 | 39 | @pytest.fixture(scope="function") 40 | def setup_status_mixin_tester(): 41 | """Set up the tests for StatusMixin. 42 | 43 | Returns: 44 | dict: Test data. 45 | """ 46 | data = dict() 47 | data["test_stat1"] = Status(name="On Hold", code="OH") 48 | data["test_stat2"] = Status(name="Work In Progress", code="WIP") 49 | data["test_stat3"] = Status(name="Approved", code="APP") 50 | data["test_a_statusList"] = StatusList( 51 | name="A Statuses", 52 | statuses=[data["test_stat1"], data["test_stat3"]], 53 | target_entity_type="DeclStatMixA", 54 | ) 55 | data["test_b_statusList"] = StatusList( 56 | name="B Statuses", 57 | statuses=[data["test_stat2"], data["test_stat3"]], 58 | target_entity_type="DeclStatMixB", 59 | ) 60 | data["kwargs"] = {"name": "ozgur", "status_list": data["test_a_statusList"]} 61 | return data 62 | 63 | 64 | def test_status_list_argument_not_set(setup_status_mixin_tester): 65 | """TypeError will be raised if the status_list argument is not set.""" 66 | data = setup_status_mixin_tester 67 | data["kwargs"].pop("status_list") 68 | with pytest.raises(TypeError) as cm: 69 | DeclStatMixA(**data["kwargs"]) 70 | assert ( 71 | str(cm.value) == "DeclStatMixA instances cannot be initialized without a " 72 | "stalker.models.status.StatusList instance, please pass a suitable StatusList " 73 | "(StatusList.target_entity_type=DeclStatMixA) with the 'status_list' argument" 74 | ) 75 | 76 | 77 | def test_status_list_argument_is_not_correct(setup_status_mixin_tester): 78 | """TypeError is raised if status_list argument is not a StatusList.""" 79 | data = setup_status_mixin_tester 80 | data["kwargs"]["status_list"] = data["test_b_statusList"] 81 | with pytest.raises(TypeError) as cm: 82 | DeclStatMixA(**data["kwargs"]) 83 | assert ( 84 | str(cm.value) == "The given StatusLists' target_entity_type is DeclStatMixB, " 85 | "whereas the entity_type of this object is DeclStatMixA" 86 | ) 87 | 88 | 89 | def test_status_list_working_as_expected(setup_status_mixin_tester): 90 | """status_list attribute is working as expected.""" 91 | data = setup_status_mixin_tester 92 | new_a_ins = DeclStatMixA(name="Ozgur", status_list=data["test_a_statusList"]) 93 | assert data["test_stat1"] in new_a_ins.status_list 94 | assert data["test_stat2"] not in new_a_ins.status_list 95 | assert data["test_stat3"] in new_a_ins.status_list 96 | -------------------------------------------------------------------------------- /tests/mixins/test_unit_mixin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """UnitMixin class related tests.""" 3 | 4 | import pytest 5 | 6 | from sqlalchemy import ForeignKey 7 | from sqlalchemy.orm import Mapped, mapped_column 8 | 9 | from stalker import SimpleEntity, UnitMixin 10 | 11 | 12 | class UnitMixinFooMixedInClass(SimpleEntity, UnitMixin): 13 | """A class which derives from another which has and __init__ already.""" 14 | 15 | __tablename__ = "UnitMixinFooMixedInClasses" 16 | __mapper_args__ = {"polymorphic_identity": "UnitMixinFooMixedInClass"} 17 | unitMixinFooMixedInClass_id: Mapped[int] = mapped_column( 18 | "id", ForeignKey("SimpleEntities.id"), primary_key=True 19 | ) 20 | __id_column__ = "unitMixinFooMixedInClass_id" 21 | 22 | def __init__(self, **kwargs): 23 | super(UnitMixinFooMixedInClass, self).__init__(**kwargs) 24 | UnitMixin.__init__(self, **kwargs) 25 | 26 | 27 | def test_mixed_in_class_initialization(): 28 | """init is working as expected.""" 29 | a = UnitMixinFooMixedInClass(unit="TRY") 30 | assert isinstance(a, UnitMixinFooMixedInClass) 31 | assert a.unit == "TRY" 32 | 33 | 34 | def test_unit_argument_is_skipped(): 35 | """unit attribute is an empty string if the unit argument is skipped.""" 36 | g = UnitMixinFooMixedInClass() 37 | assert g.unit == "" 38 | 39 | 40 | def test_unit_argument_is_none(): 41 | """unit attribute will be an empty string if the unit argument is None.""" 42 | g = UnitMixinFooMixedInClass(unit=None) 43 | assert g.unit == "" 44 | 45 | 46 | def test_unit_attribute_is_set_to_none(): 47 | """unit attribute will be an empty string if it is set to None.""" 48 | g = UnitMixinFooMixedInClass(unit="TRY") 49 | assert g.unit != "" 50 | g.unit = None 51 | assert g.unit == "" 52 | 53 | 54 | def test_unit_argument_is_not_a_string(): 55 | """TypeError is raised if the unit argument is not a str.""" 56 | with pytest.raises(TypeError) as cm: 57 | UnitMixinFooMixedInClass(unit=1234) 58 | 59 | assert str(cm.value) == ( 60 | "UnitMixinFooMixedInClass.unit should be a string, not int: '1234'" 61 | ) 62 | 63 | 64 | def test_unit_attribute_is_not_a_string(): 65 | """TypeError is raised if the unit attribute is set to non-str.""" 66 | g = UnitMixinFooMixedInClass(unit="TRY") 67 | with pytest.raises(TypeError) as cm: 68 | g.unit = 2342 69 | 70 | assert str(cm.value) == ( 71 | "UnitMixinFooMixedInClass.unit should be a string, not int: '2342'" 72 | ) 73 | 74 | 75 | def test_unit_argument_is_working_as_expected(): 76 | """unit arg value is passed to the unit attribute.""" 77 | test_value = "this is my unit" 78 | g = UnitMixinFooMixedInClass(unit=test_value) 79 | assert g.unit == test_value 80 | 81 | 82 | def test_unit_attribute_is_working_as_expected(): 83 | """unit attribute value can be changed.""" 84 | test_value = "this is my unit" 85 | g = UnitMixinFooMixedInClass(unit="TRY") 86 | assert g.unit != test_value 87 | g.unit = test_value 88 | assert g.unit == test_value 89 | -------------------------------------------------------------------------------- /tests/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eoyilmaz/stalker/86ecd55d9c77225068c5c864ba08cd09f3375d03/tests/models/__init__.py -------------------------------------------------------------------------------- /tests/models/test_client_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for the ClientUser class.""" 3 | 4 | import pytest 5 | 6 | from stalker import Client, ClientUser, User 7 | 8 | 9 | def test_role_argument_is_not_a_role_instance(): 10 | """TypeError will be raised when the role argument is not a Role instance.""" 11 | with pytest.raises(TypeError) as cm: 12 | ClientUser( 13 | client=Client(name="Test Client"), 14 | user=User( 15 | name="Test User", login="tuser", email="u@u.com", password="secret" 16 | ), 17 | role="not a role instance", 18 | ) 19 | 20 | assert str(cm.value) == ( 21 | "ClientUser.role should be a stalker.models.auth.Role instance, " 22 | "not str: 'not a role instance'" 23 | ) 24 | -------------------------------------------------------------------------------- /tests/models/test_department_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for the DepartmentUser class.""" 3 | 4 | import pytest 5 | 6 | from stalker import Department, DepartmentUser, User 7 | 8 | 9 | def test_role_argument_is_not_a_role_instance(): 10 | """TypeError will be raised when the role argument is not a Role instance.""" 11 | with pytest.raises(TypeError) as cm: 12 | DepartmentUser( 13 | department=Department(name="Test Department"), 14 | user=User( 15 | name="Test User", login="tuser", email="u@u.com", password="secret" 16 | ), 17 | role="not a role instance", 18 | ) 19 | 20 | assert str(cm.value) == ( 21 | "DepartmentUser.role should be a stalker.models.auth.Role instance, " 22 | "not str: 'not a role instance'" 23 | ) 24 | -------------------------------------------------------------------------------- /tests/models/test_generic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests utility functions.""" 3 | 4 | import datetime 5 | 6 | import pytest 7 | 8 | import pytz 9 | 10 | from stalker.utils import make_plural, utc_to_local, local_to_utc 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "test_value,expected", 15 | [ 16 | ("asset", "assets"), 17 | ("client", "clients"), 18 | ("department", "departments"), 19 | ("entity", "entities"), 20 | ("template", "templates"), 21 | ("group", "groups"), 22 | ("format", "formats"), 23 | ("file", "files"), 24 | ("session", "sessions"), 25 | ("note", "notes"), 26 | ("permission", "permissions"), 27 | ("project", "projects"), 28 | ("repository", "repositories"), 29 | ("review", "reviews"), 30 | ("scene", "scenes"), 31 | ("sequence", "sequences"), 32 | ("shot", "shots"), 33 | ("status", "statuses"), 34 | ("list", "lists"), 35 | ("structure", "structures"), 36 | ("studio", "studios"), 37 | ("tag", "tags"), 38 | ("task", "tasks"), 39 | ("dependency", "dependencies"), 40 | ("type", "types"), 41 | ("bench", "benches"), 42 | ("thief", "thieves"), 43 | ], 44 | ) 45 | def test_make_plural_is_working_as_expected(test_value, expected): 46 | """make_plural() is working as expected.""" 47 | assert expected == make_plural(test_value) 48 | 49 | 50 | def test_utc_to_local_is_working_as_expected(): 51 | """utc_to_local() is working as expected.""" 52 | local_now = datetime.datetime.now() 53 | utc_now = datetime.datetime.now(pytz.utc) 54 | 55 | utc_without_tz = datetime.datetime( 56 | utc_now.year, 57 | utc_now.month, 58 | utc_now.day, 59 | utc_now.hour, 60 | utc_now.minute, 61 | ) 62 | local_from_utc = utc_to_local(utc_without_tz) 63 | 64 | assert local_from_utc.year == local_now.year 65 | assert local_from_utc.month == local_now.month 66 | assert local_from_utc.day == local_now.day 67 | assert local_from_utc.hour == local_now.hour 68 | assert local_from_utc.minute == local_now.minute 69 | 70 | 71 | def test_local_to_utc_is_working_as_expected(): 72 | """local_to_utc() is working as expected.""" 73 | local_now = datetime.datetime.now() 74 | utc_now = datetime.datetime.now(pytz.utc) 75 | 76 | utc_without_tz = datetime.datetime( 77 | utc_now.year, 78 | utc_now.month, 79 | utc_now.day, 80 | utc_now.hour, 81 | utc_now.minute, 82 | ) 83 | utc_from_local = local_to_utc(local_now) 84 | 85 | assert utc_from_local.year == utc_without_tz.year 86 | assert utc_from_local.month == utc_without_tz.month 87 | assert utc_from_local.day == utc_without_tz.day 88 | assert utc_from_local.hour == utc_without_tz.hour 89 | assert utc_from_local.minute == utc_without_tz.minute 90 | -------------------------------------------------------------------------------- /tests/models/test_message.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests related to the Message class.""" 3 | 4 | from stalker import Message, Status, StatusList 5 | 6 | 7 | def test_message_instance_creation(): 8 | """message instance creation.""" 9 | status_unread = Status(name="Unread", code="UR") 10 | status_read = Status(name="Read", code="READ") 11 | status_replied = Status(name="Replied", code="REP") 12 | 13 | message_status_list = StatusList( 14 | name="Message Statuses", 15 | statuses=[status_unread, status_read, status_replied], 16 | target_entity_type="Message", 17 | ) 18 | 19 | new_message = Message( 20 | description="This is a test message", status_list=message_status_list 21 | ) 22 | assert new_message.description == "This is a test message" 23 | -------------------------------------------------------------------------------- /tests/models/test_project_client.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests related to the ProjectClient class.""" 3 | 4 | import pytest 5 | 6 | from stalker import Client, Project, ProjectClient, Repository, Role, Status, User 7 | 8 | 9 | @pytest.fixture(scope="function") 10 | def setup_project_client_db_test(setup_postgresql_db): 11 | """Set the test up ProjectClient class tests with a DB.""" 12 | data = dict() 13 | data["test_repo"] = Repository(name="Test Repo", code="TR") 14 | data["status_new"] = Status(name="New", code="NEW") 15 | data["status_wip"] = Status(name="Work In Progress", code="WIP") 16 | data["status_cmpl"] = Status(name="Completed", code="CMPL") 17 | 18 | data["test_user1"] = User( 19 | name="Test User 1", 20 | login="testuser1", 21 | email="testuser1@users.com", 22 | password="secret", 23 | ) 24 | 25 | data["test_client"] = Client(name="Test Client") 26 | 27 | data["test_project"] = Project( 28 | name="Test Project 1", 29 | code="TP1", 30 | repositories=[data["test_repo"]], 31 | ) 32 | 33 | data["test_role"] = Role(name="Test Client") 34 | return data 35 | 36 | 37 | def test_project_client_creation(setup_project_client_db_test): 38 | """Project client creation.""" 39 | data = setup_project_client_db_test 40 | ProjectClient( 41 | project=data["test_project"], client=data["test_client"], role=data["test_role"] 42 | ) 43 | 44 | assert data["test_client"] in data["test_project"].clients 45 | 46 | 47 | def test_role_argument_is_not_a_role_instance(setup_project_client_db_test): 48 | """TypeError will be raised when the role argument is not a Role instance.""" 49 | data = setup_project_client_db_test 50 | with pytest.raises(TypeError) as cm: 51 | ProjectClient( 52 | project=data["test_project"], 53 | client=data["test_client"], 54 | role="not a role instance", 55 | ) 56 | 57 | assert str(cm.value) == ( 58 | "ProjectClient.role should be a stalker.models.auth.Role " 59 | "instance, not str: 'not a role instance'" 60 | ) 61 | -------------------------------------------------------------------------------- /tests/models/test_project_user.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests related to the ProjectUser class.""" 3 | 4 | import pytest 5 | 6 | from stalker import Project, ProjectUser, Repository, Role, User 7 | from stalker.db.session import DBSession 8 | 9 | 10 | @pytest.fixture(scope="function") 11 | def setup_project_user_db_tests(setup_postgresql_db): 12 | """Set up the tests database and data for the ProjectUser class related tests.""" 13 | data = dict() 14 | data["test_repo"] = Repository(name="Test Repo", code="TR") 15 | 16 | DBSession.add(data["test_repo"]) 17 | DBSession.commit() 18 | 19 | data["test_user1"] = User( 20 | name="Test User 1", 21 | login="testuser1", 22 | email="testuser1@users.com", 23 | password="secret", 24 | ) 25 | DBSession.add(data["test_user1"]) 26 | 27 | data["test_project"] = Project( 28 | name="Test Project 1", 29 | code="TP1", 30 | repositories=[data["test_repo"]], 31 | ) 32 | DBSession.add(data["test_project"]) 33 | 34 | data["test_role"] = Role(name="Test User") 35 | DBSession.add(data["test_role"]) 36 | DBSession.commit() 37 | return data 38 | 39 | 40 | def test_project_user_creation(setup_project_user_db_tests): 41 | """project user creation.""" 42 | data = setup_project_user_db_tests 43 | puser = ProjectUser( 44 | project=data["test_project"], user=data["test_user1"], role=data["test_role"] 45 | ) 46 | DBSession.save(puser) 47 | assert data["test_user1"] in data["test_project"].users 48 | 49 | 50 | def test_role_argument_is_not_a_role_instance(setup_project_user_db_tests): 51 | """TypeError will be raised if the role argument is not a Role instance.""" 52 | data = setup_project_user_db_tests 53 | with pytest.raises(TypeError) as cm: 54 | ProjectUser( 55 | project=data["test_project"], 56 | user=data["test_user1"], 57 | role="not a role instance", 58 | ) 59 | 60 | assert str(cm.value) == ( 61 | "ProjectUser.role should be a stalker.models.auth.Role instance, " 62 | "not str: 'not a role instance'" 63 | ) 64 | 65 | 66 | def test_rate_attribute_is_copied_from_user(setup_project_user_db_tests): 67 | """rate attribute value is copied from the user on init.""" 68 | data = setup_project_user_db_tests 69 | data["test_user1"].rate = 100.0 70 | project_user1 = ProjectUser( 71 | project=data["test_project"], user=data["test_user1"], role=data["test_role"] 72 | ) 73 | assert data["test_user1"].rate == project_user1.rate 74 | 75 | 76 | def test_rate_attribute_initialization_through_user(setup_project_user_db_tests): 77 | """rate attribute initialization through ``user.projects`` attribute.""" 78 | data = setup_project_user_db_tests 79 | data["test_user1"].rate = 102.0 80 | data["test_user1"].projects = [data["test_project"]] 81 | assert data["test_project"].user_role[0].rate == data["test_user1"].rate 82 | 83 | 84 | def test_rate_attribute_initialization_through_project(setup_project_user_db_tests): 85 | """rate attribute initialization through ``project.users`` attribute.""" 86 | data = setup_project_user_db_tests 87 | data["test_user1"].rate = 102.0 88 | data["test_project"].users = [data["test_user1"]] 89 | 90 | assert data["test_project"].user_role[0].rate == data["test_user1"].rate 91 | -------------------------------------------------------------------------------- /tests/models/test_role.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for the Role class.""" 3 | 4 | from stalker import Role 5 | 6 | 7 | def test_role_class_generic(): 8 | """creation of a Role instance.""" 9 | r = Role(name="Lead") 10 | assert isinstance(r, Role) 11 | assert r.name == "Lead" 12 | -------------------------------------------------------------------------------- /tests/models/test_schedulers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for the SchedulerBase class.""" 3 | 4 | import pytest 5 | 6 | from stalker import SchedulerBase, Studio 7 | 8 | 9 | @pytest.fixture(scope="function") 10 | def setup_scheduler_base_tests(): 11 | """Set up the tests for stalker.models.scheduler.SchedulerBase class.""" 12 | data = dict() 13 | data["test_studio"] = Studio(name="Test Studio") 14 | data["kwargs"] = {"studio": data["test_studio"]} 15 | data["test_scheduler_base"] = SchedulerBase(**data["kwargs"]) 16 | return data 17 | 18 | 19 | def test_studio_argument_is_skipped(setup_scheduler_base_tests): 20 | """studio attribute None if the studio argument is skipped.""" 21 | data = setup_scheduler_base_tests 22 | data["kwargs"].pop("studio") 23 | new_scheduler_base = SchedulerBase(**data["kwargs"]) 24 | assert new_scheduler_base.studio is None 25 | 26 | 27 | def test_studio_argument_is_none(setup_scheduler_base_tests): 28 | """studio attribute None if the studio argument is None.""" 29 | data = setup_scheduler_base_tests 30 | data["kwargs"]["studio"] = None 31 | new_scheduler_base = SchedulerBase(**data["kwargs"]) 32 | assert new_scheduler_base.studio is None 33 | 34 | 35 | def test_studio_attribute_is_none(setup_scheduler_base_tests): 36 | """studio argument can be set to None.""" 37 | data = setup_scheduler_base_tests 38 | data["test_scheduler_base"].studio = None 39 | assert data["test_scheduler_base"].studio is None 40 | 41 | 42 | def test_studio_argument_is_not_a_studio_instance(setup_scheduler_base_tests): 43 | """TypeError raised if the studio argument is not Studio instance.""" 44 | data = setup_scheduler_base_tests 45 | data["kwargs"]["studio"] = "not a studio instance" 46 | with pytest.raises(TypeError) as cm: 47 | SchedulerBase(**data["kwargs"]) 48 | 49 | assert ( 50 | str(cm.value) == "SchedulerBase.studio should be an instance of " 51 | "stalker.models.studio.Studio, not str: 'not a studio instance'" 52 | ) 53 | 54 | 55 | def test_studio_attribute_is_not_a_studio_instance(setup_scheduler_base_tests): 56 | """TypeError raised if the studio attr is not a Studio instance.""" 57 | data = setup_scheduler_base_tests 58 | with pytest.raises(TypeError) as cm: 59 | data["test_scheduler_base"].studio = "not a studio instance" 60 | 61 | assert ( 62 | str(cm.value) == "SchedulerBase.studio should be an instance of " 63 | "stalker.models.studio.Studio, not str: 'not a studio instance'" 64 | ) 65 | 66 | 67 | def test_studio_argument_is_working_as_expected(setup_scheduler_base_tests): 68 | """studio argument value is correctly passed to the studio attribute.""" 69 | data = setup_scheduler_base_tests 70 | assert data["test_scheduler_base"].studio == data["kwargs"]["studio"] 71 | 72 | 73 | def test_studio_attribute_is_working_as_expected(setup_scheduler_base_tests): 74 | """studio attribute is working as expected.""" 75 | data = setup_scheduler_base_tests 76 | new_studio = Studio(name="Test Studio 2") 77 | data["test_scheduler_base"].studio = new_studio 78 | assert data["test_scheduler_base"].studio == new_studio 79 | 80 | 81 | def test_schedule_method_will_raise_not_implemented_error(): 82 | """schedule() method will raise a NotImplementedError.""" 83 | base = SchedulerBase() 84 | with pytest.raises(NotImplementedError) as cm: 85 | base.schedule() 86 | 87 | assert str(cm.value) == "" 88 | -------------------------------------------------------------------------------- /tests/models/test_status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for the Status class.""" 3 | 4 | import pytest 5 | 6 | from stalker import Entity, Status 7 | 8 | 9 | @pytest.fixture(scope="function") 10 | def setup_status_tests(): 11 | """Set up tests for the stalker.models.status.Status class.""" 12 | data = dict() 13 | data["kwargs"] = { 14 | "name": "Complete", 15 | "description": "use this if the object is complete", 16 | "code": "CMPLT", 17 | } 18 | 19 | # create an entity object with same kwargs for __eq__ and __ne__ tests 20 | # (it should return False for __eq__ and True for __ne__ for same 21 | # kwargs) 22 | data["entity1"] = Entity(**data["kwargs"]) 23 | return data 24 | 25 | 26 | def test___auto_name__class_attribute_is_set_to_false(): 27 | """__auto_name__ class attribute is set to False for Status class.""" 28 | assert Status.__auto_name__ is False 29 | 30 | 31 | def test_equality(setup_status_tests): 32 | """equality of two statuses.""" 33 | data = setup_status_tests 34 | status1 = Status(**data["kwargs"]) 35 | status2 = Status(**data["kwargs"]) 36 | 37 | data["kwargs"]["name"] = "Work In Progress" 38 | data["kwargs"]["description"] = "use this if the object is still in progress" 39 | data["kwargs"]["code"] = "WIP" 40 | 41 | status3 = Status(**data["kwargs"]) 42 | 43 | assert status1 == status2 44 | assert not status1 == status3 45 | assert not status1 == data["entity1"] 46 | 47 | 48 | def test_status_and_string_equality_in_status_name(setup_status_tests): 49 | """status can be compared with a string matching the Status.name.""" 50 | data = setup_status_tests 51 | a_status = Status(**data["kwargs"]) 52 | assert a_status == data["kwargs"]["name"] 53 | assert a_status == data["kwargs"]["name"].lower() 54 | assert a_status == data["kwargs"]["name"].upper() 55 | assert a_status != "another name" 56 | 57 | 58 | def test_status_and_string_equality_in_status_code(setup_status_tests): 59 | """status can be compared with a string matching the Status.code.""" 60 | data = setup_status_tests 61 | a_status = Status(**data["kwargs"]) 62 | assert a_status == data["kwargs"]["code"] 63 | assert a_status == data["kwargs"]["code"].lower() 64 | assert a_status == data["kwargs"]["code"].upper() 65 | 66 | 67 | def test_inequality(setup_status_tests): 68 | """inequality of two statuses.""" 69 | data = setup_status_tests 70 | status1 = Status(**data["kwargs"]) 71 | status2 = Status(**data["kwargs"]) 72 | data["kwargs"]["name"] = "Work In Progress" 73 | data["kwargs"]["description"] = "use this if the object is still in progress" 74 | data["kwargs"]["code"] = "WIP" 75 | 76 | status3 = Status(**data["kwargs"]) 77 | 78 | assert not status1 != status2 79 | assert status1 != status3 80 | assert status1 != data["entity1"] 81 | 82 | 83 | def test_status_and_string_inequality_in_status_name(setup_status_tests): 84 | """status can be compared with a string.""" 85 | data = setup_status_tests 86 | a_status = Status(**data["kwargs"]) 87 | assert not a_status != data["kwargs"]["name"] 88 | assert not a_status != data["kwargs"]["name"].lower() 89 | assert not a_status != data["kwargs"]["name"].upper() 90 | assert a_status != "another name" 91 | 92 | 93 | def test_status_and_string_inequality_in_status_code(setup_status_tests): 94 | """status can be compared with a string.""" 95 | data = setup_status_tests 96 | a_status = Status(**data["kwargs"]) 97 | assert not a_status != data["kwargs"]["code"] 98 | assert not a_status != data["kwargs"]["code"].lower() 99 | assert not a_status != data["kwargs"]["code"].upper() 100 | 101 | 102 | def test__hash__is_working_as_expected(setup_status_tests): 103 | """__hash__ is working as expected.""" 104 | data = setup_status_tests 105 | data["test_status"] = Status(**data["kwargs"]) 106 | result = hash(data["test_status"]) 107 | assert isinstance(result, int) 108 | assert result == data["test_status"].__hash__() 109 | -------------------------------------------------------------------------------- /tests/models/test_tag.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for the Tag class.""" 3 | 4 | from stalker import Tag, SimpleEntity 5 | 6 | 7 | def test___auto_name__class_attribute_is_set_to_false(): 8 | """__auto_name__ class attribute is set to False for Tag class.""" 9 | assert Tag.__auto_name__ is False 10 | 11 | 12 | def test_tag_init(): 13 | """tag inits as expected.""" 14 | # this should work without any error 15 | tag = Tag(name="a test tag", description="this is a test tag") 16 | assert isinstance(tag, Tag) 17 | 18 | 19 | def test_equality(): 20 | """equality of two Tags.""" 21 | kwargs = dict(name="a test tag", description="this is a test tag") 22 | 23 | simple_entity = SimpleEntity(**kwargs) 24 | 25 | a_tag_object1 = Tag(**kwargs) 26 | a_tag_object2 = Tag(**kwargs) 27 | 28 | kwargs["name"] = "a new test Tag" 29 | kwargs["description"] = "this is a new test Tag" 30 | 31 | a_tag_object3 = Tag(**kwargs) 32 | 33 | assert a_tag_object1 == a_tag_object2 34 | assert not a_tag_object1 == a_tag_object3 35 | assert not a_tag_object1 == simple_entity 36 | 37 | 38 | def test_inequality(): 39 | """inequality of two Tags.""" 40 | kwargs = dict(name="a test tag", description="this is a test tag") 41 | 42 | simple_entity = SimpleEntity(**kwargs) 43 | 44 | a_tag_object1 = Tag(**kwargs) 45 | a_tag_object2 = Tag(**kwargs) 46 | 47 | kwargs["name"] = "a new test Tag" 48 | kwargs["description"] = "this is a new test Tag" 49 | 50 | a_tag_object3 = Tag(**kwargs) 51 | 52 | assert not a_tag_object1 != a_tag_object2 53 | assert a_tag_object1 != a_tag_object3 54 | assert a_tag_object1 != simple_entity 55 | 56 | 57 | def test_plural_class_name(): 58 | """plural name of Tag class.""" 59 | kwargs = dict(name="a test tag", description="this is a test tag") 60 | test_tag = Tag(**kwargs) 61 | assert test_tag.plural_class_name == "Tags" 62 | 63 | 64 | def test__hash__is_working_as_expected(): 65 | """__hash__ is working as expected.""" 66 | kwargs = dict(name="a test tag", description="this is a test tag") 67 | test_tag = Tag(**kwargs) 68 | result = hash(test_tag) 69 | assert isinstance(result, int) 70 | assert result == test_tag.__hash__() 71 | -------------------------------------------------------------------------------- /tests/test_exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """Tests for the exceptions module.""" 3 | import pytest 4 | 5 | from stalker.exceptions import ( 6 | CircularDependencyError, 7 | DependencyViolationError, 8 | LoginError, 9 | OverBookedError, 10 | StatusError, 11 | ) 12 | 13 | 14 | def test_login_error_is_working_as_expected(): 15 | """LoginError is working as expected.""" 16 | test_message = "testing LoginError" 17 | with pytest.raises(LoginError) as cm: 18 | raise LoginError(test_message) 19 | 20 | assert str(cm.value) == test_message 21 | 22 | 23 | def test_circular_dependency_error_is_working_as_expected(): 24 | """CircularDependencyError is working as expected.""" 25 | test_message = "testing CircularDependencyError" 26 | with pytest.raises(CircularDependencyError) as cm: 27 | raise CircularDependencyError(test_message) 28 | 29 | assert str(cm.value) == test_message 30 | 31 | 32 | def test_over_booked_error_is_working_as_expected(): 33 | """OverBookedError is working as expected.""" 34 | test_message = "testing OverBookedError" 35 | with pytest.raises(OverBookedError) as cm: 36 | raise OverBookedError(test_message) 37 | 38 | assert str(cm.value) == test_message 39 | 40 | 41 | def test_status_error_is_working_as_expected(): 42 | """StatusError is working as expected.""" 43 | test_message = "testing StatusError" 44 | with pytest.raises(StatusError) as cm: 45 | raise StatusError(test_message) 46 | 47 | assert str(cm.value) == test_message 48 | 49 | 50 | def test_dependency_violation_error_is_working_as_expected(): 51 | """DependencyViolationError is working as expected.""" 52 | test_message = "testing DependencyViolationError" 53 | with pytest.raises(DependencyViolationError) as cm: 54 | raise DependencyViolationError(test_message) 55 | 56 | assert str(cm.value) == test_message 57 | -------------------------------------------------------------------------------- /tests/test_testing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """stalker.testing module.""" 3 | 4 | import pytest 5 | from tests.utils import get_server_details_from_url 6 | 7 | 8 | @pytest.mark.parametrize( 9 | "url,expected", 10 | [ 11 | ( 12 | "postgresql://postgres:postgres@localhost:5432/stalker_test_e0b9bc6a", 13 | { 14 | "dialect": "postgresql", 15 | "username": "postgres", 16 | "password": "postgres", 17 | "hostname": "localhost", 18 | "port": "5432", 19 | "database_name": "stalker_test_e0b9bc6a", 20 | }, 21 | ), 22 | ( 23 | "postgresql://postgres:postgres@localhost/stalker_test_e0b9bc6a", 24 | { 25 | "dialect": "postgresql", 26 | "username": "postgres", 27 | "password": "postgres", 28 | "hostname": "localhost", 29 | "port": "", 30 | "database_name": "stalker_test_e0b9bc6a", 31 | }, 32 | ), 33 | ], 34 | ) 35 | def test_get_server_details_from_url(url, expected): 36 | """get_server_details_from_url.""" 37 | assert get_server_details_from_url(url) == expected 38 | -------------------------------------------------------------------------------- /tests/test_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | # Local Imports 5 | import stalker 6 | from stalker import version 7 | 8 | 9 | def test_version_number_is_correct(): 10 | """version.VERSION is correct.""" 11 | version_file_path = os.path.join(os.path.dirname(stalker.__file__), "VERSION") 12 | with open(version_file_path) as f: 13 | expected_version = f.read().strip() 14 | assert expected_version == version.__version__ 15 | 16 | 17 | def test_version_number_as_a_module_level_variable(): 18 | """stalker.__version__ exists and value is correct.""" 19 | assert version.__version__ == stalker.__version__ 20 | -------------------------------------------------------------------------------- /whitelist.txt: -------------------------------------------------------------------------------- 1 | autoflush 2 | autogenerate 3 | BFS 4 | CMPL 5 | CodeMixin 6 | CRUDL 7 | csv 8 | DBSession 9 | DDL 10 | DEFERRABLE 11 | DFS 12 | DREV 13 | expandvars 14 | fetchall 15 | FilenameTemplates 16 | formatter 17 | HREV 18 | Lite3 19 | macOS 20 | minallocated 21 | mixin 22 | Mixins 23 | myapp 24 | mymodel 25 | normpath 26 | nullable 27 | num 28 | OH 29 | onend 30 | onstart 31 | oy 32 | oyProjectManager 33 | Postgre 34 | PostgreSQL 35 | preliminarily 36 | PREV 37 | repo 38 | RREV 39 | RTS 40 | sessionmaker 41 | SOM 42 | sqlalchemy 43 | SQLite3 44 | StatusList 45 | STOP 46 | tablename 47 | Taskable 48 | TimeLog 49 | TJ 50 | tj3 51 | tjp 52 | UniqueConstraint 53 | unmanaged 54 | WFD 55 | WIP 56 | WorkingHours --------------------------------------------------------------------------------