From ebbccd1a5da3db49abcc06e6487dfd11994e5396 Mon Sep 17 00:00:00 2001 From: Ron Date: Fri, 17 Nov 2023 19:14:41 +0100 Subject: [PATCH] v9.2.0 (#11) --- .ambient-package-update/metadata.py | 65 ++--- .coveragerc | 12 +- .github/workflows/ci.yml | 61 ++++- .pre-commit-config.yaml | 18 +- .readthedocs.yaml | 31 +++ .readthedocs.yml | 24 -- CHANGES.md | 15 +- README.md | 19 +- ambient_toolbox/__init__.py | 2 +- ambient_toolbox/admin/model_admins/classes.py | 10 +- ambient_toolbox/admin/model_admins/inlines.py | 2 +- ambient_toolbox/admin/model_admins/mixins.py | 16 +- ambient_toolbox/admin/views/forms.py | 16 +- ambient_toolbox/admin/views/mixins.py | 32 +-- ambient_toolbox/apps.py | 4 +- ambient_toolbox/context_processors.py | 2 +- ambient_toolbox/drf/fields.py | 2 +- ambient_toolbox/drf/serializers.py | 6 +- ambient_toolbox/drf/tests.py | 6 +- ambient_toolbox/gitlab/coverage.py | 195 +++++++-------- ambient_toolbox/graphql/forms/mutations.py | 4 +- ambient_toolbox/graphql/schemes/mutations.py | 8 +- ambient_toolbox/graphql/sentry/utils.py | 2 +- ambient_toolbox/graphql/tests/base_test.py | 16 +- .../mail/backends/whitelist_smtp.py | 10 +- .../commands/install_permission_fixtures.py | 10 +- ambient_toolbox/managers.py | 14 +- ambient_toolbox/middleware/current_request.py | 6 +- ambient_toolbox/middleware/current_user.py | 2 +- ambient_toolbox/mixins/bleacher.py | 40 ++-- ambient_toolbox/models.py | 2 +- .../permissions/fixtures/helpers.py | 8 +- .../permissions/fixtures/services.py | 4 +- ambient_toolbox/sentry/helpers.py | 16 +- ambient_toolbox/services/custom_scrubber.py | 36 +-- ambient_toolbox/templatetags/ai_email_tags.py | 4 +- ambient_toolbox/templatetags/ai_file_tags.py | 2 +- .../templatetags/ai_number_tags.py | 30 +-- .../templatetags/ai_object_tags.py | 4 +- .../templatetags/ai_string_tags.py | 4 +- ambient_toolbox/tests/mixins.py | 19 +- .../tests/structure_validator/settings.py | 4 +- .../test_structure_validator.py | 18 +- ambient_toolbox/utils/date.py | 8 +- ambient_toolbox/utils/file.py | 12 +- ambient_toolbox/utils/log_whodid.py | 4 +- ambient_toolbox/utils/model.py | 4 +- ambient_toolbox/utils/named_tuple.py | 6 +- ambient_toolbox/utils/string.py | 22 +- ambient_toolbox/view_layer/form_mixins.py | 12 +- ambient_toolbox/view_layer/formset_mixins.py | 2 +- .../view_layer/formset_view_mixin.py | 10 +- ambient_toolbox/view_layer/htmx_mixins.py | 6 +- ambient_toolbox/view_layer/mixins.py | 6 +- ambient_toolbox/view_layer/tests/mixins.py | 16 +- ambient_toolbox/view_layer/views.py | 6 +- docs/conf.py | 50 ++-- docs/features/gitlab.md | 8 +- manage.py | 4 +- pyproject.toml | 50 ++-- scripts/unix/install_requirements.sh | 3 + scripts/windows/install_requirements.ps1 | 3 + settings.py | 72 +++--- setup.cfg | 15 ++ testapp/api/serializers.py | 4 +- testapp/api/urls.py | 2 +- testapp/forms.py | 2 +- testapp/migrations/0001_initial.py | 4 +- testapp/models.py | 10 +- testapp/permissions.py | 8 +- testapp/urls.py | 6 +- .../test_admin_common_info_mixin.py | 16 +- .../test_admin_create_form_mixin.py | 2 +- .../test_admin_no_inlines_for_create_mixin.py | 2 +- .../test_admin_request_in_form_mixin.py | 2 +- .../admin/model_admin_mixins/test_classes.py | 127 ++++++++++ ...t_deactivatable_change_view_admin_mixin.py | 16 +- .../test_fetch_object_mixin.py | 6 +- .../test_fetch_parent_object_inline_mixin.py | 6 +- .../test_test_structure_validator.py | 103 ++++---- tests/drf/test_fields.py | 54 +++-- .../{validation.py => test_validation.py} | 6 +- .../permissions/fixtures/test_declarations.py | 22 +- tests/permissions/fixtures/test_helpers.py | 10 +- .../fixtures/test_services_setup_service.py | 42 ++-- ...est_install_permission_fixtures_command.py | 23 +- tests/sentry/mock_data.py | 222 +++++++++--------- tests/sentry/test_sentry_helper.py | 8 +- tests/templatetags/test_ai_number_tags.py | 23 +- tests/test_admin_forms.py | 9 +- tests/test_admin_inlines.py | 4 +- tests/test_admin_model_admins_classes.py | 71 ------ tests/test_admin_view_mixins.py | 36 +-- tests/test_context_manager.py | 28 +-- tests/test_managers.py | 73 +++++- tests/test_middleware.py | 14 +- tests/test_models.py | 27 ++- tests/test_rest_api_mixins.py | 16 +- tests/test_scrubbing_service.py | 9 +- tests/test_utils_date.py | 54 ++--- tests/test_utils_file.py | 24 +- tests/test_utils_model.py | 21 +- tests/test_utils_named_tuple.py | 54 +++-- tests/test_utils_string.py | 86 +++---- .../mixins/test_django_message_framework.py | 8 +- tests/tests/mixins/test_mixins.py | 57 +++++ .../mixins/{models.py => test_models.py} | 3 - .../mixins/test_request_provider_mixin.py | 16 +- tests/tests/test_mail_backends.py | 42 +++- tests/view_layer/test_formset_mixins.py | 6 +- tests/view_layer/test_htmx_response_mixin.py | 22 +- tests/view_layer/test_meta_mixins.py | 35 ++- tests/view_layer/test_views.py | 8 +- 113 files changed, 1509 insertions(+), 1060 deletions(-) create mode 100644 .readthedocs.yaml delete mode 100644 .readthedocs.yml create mode 100644 scripts/unix/install_requirements.sh create mode 100644 scripts/windows/install_requirements.ps1 create mode 100644 tests/admin/model_admin_mixins/test_classes.py rename tests/mixins/{validation.py => test_validation.py} (62%) delete mode 100644 tests/test_admin_model_admins_classes.py create mode 100644 tests/tests/mixins/test_mixins.py rename tests/tests/mixins/{models.py => test_models.py} (63%) diff --git a/.ambient-package-update/metadata.py b/.ambient-package-update/metadata.py index 5cd3f6f..0ecb774 100644 --- a/.ambient-package-update/metadata.py +++ b/.ambient-package-update/metadata.py @@ -1,23 +1,29 @@ from ambient_package_update.metadata.author import PackageAuthor -from ambient_package_update.metadata.constants import DEV_DEPENDENCIES, LICENSE_MIT +from ambient_package_update.metadata.constants import ( + DEV_DEPENDENCIES, + LICENSE_MIT, + SUPPORTED_DJANGO_VERSIONS, + SUPPORTED_PYTHON_VERSIONS, +) from ambient_package_update.metadata.package import PackageMetadata from ambient_package_update.metadata.readme import ReadmeContent from ambient_package_update.metadata.ruff_ignored_inspection import RuffIgnoredInspection METADATA = PackageMetadata( - package_name='ambient_toolbox', - company='Ambient Innovation: GmbH', + package_name="ambient_toolbox", + company="Ambient Innovation: GmbH", authors=[ PackageAuthor( - name='Ambient Digital', - email='hello@ambient.digital', + name="Ambient Digital", + email="hello@ambient.digital", ), ], - development_status='5 - Production/Stable', + min_coverage=96.52, + development_status="5 - Production/Stable", license=LICENSE_MIT, license_year=2012, readme_content=ReadmeContent( - tagline='Python toolbox of Ambient Digital containing an abundance of useful tools and gadgets.', + tagline="Python toolbox of Ambient Digital containing an abundance of useful tools and gadgets.", content="""## Features * Useful classes and mixins for Django admin @@ -46,35 +52,40 @@ """, ), dependencies=[ - 'Django>=3.2.20', - 'bleach>=1.4,<6', - 'python-dateutil>=2.5.3', + "Django>=3.2.20", + "bleach>=1.4,<6", + "python-dateutil>=2.5.3", ], + supported_django_versions=SUPPORTED_DJANGO_VERSIONS, + supported_python_versions=SUPPORTED_PYTHON_VERSIONS, + has_migrations=True, optional_dependencies={ - 'dev': [ + "dev": [ *DEV_DEPENDENCIES, - 'gevent~=22.10', + "gevent~=23.9", ], - 'drf': [ - 'djangorestframework>=3.8.2', + "drf": [ + "djangorestframework>=3.8.2", ], - 'graphql': [ - 'graphene-django>=2.2.0', - 'django-graphql-jwt>=0.2.1', + "graphql": [ + "graphene-django>=2.2.0", + "django-graphql-jwt>=0.2.1", ], - 'sentry': [ - 'sentry-sdk>=1.19.1', + "sentry": [ + "sentry-sdk>=1.19.1", ], - 'view-layer': [ - 'django-crispy-forms>=1.4.0', + "view-layer": [ + "django-crispy-forms>=1.4.0", ], }, ruff_ignore_list=[ - RuffIgnoredInspection(key='N999', comment="Project name contains underscore, not fixable"), - RuffIgnoredInspection(key='A003', comment="Django attributes shadow python builtins"), - RuffIgnoredInspection(key='DJ001', comment="Django model text-based fields shouldn't be nullable"), - RuffIgnoredInspection(key='B905', comment="Can be enabled when Python <=3.9 support is dropped"), - RuffIgnoredInspection(key='DTZ001', comment="TODO will affect \"tz_today()\" method"), - RuffIgnoredInspection(key='DTZ005', comment="TODO will affect \"tz_today()\" method"), + RuffIgnoredInspection(key="N999", comment="Project name contains underscore, not fixable"), + RuffIgnoredInspection(key="A003", comment="Django attributes shadow python builtins"), + RuffIgnoredInspection(key="DJ001", comment="Django model text-based fields shouldn't be nullable"), + RuffIgnoredInspection(key="B905", comment="Can be enabled when Python <=3.9 support is dropped"), + RuffIgnoredInspection(key="DTZ001", comment='TODO will affect "tz_today()" method'), + RuffIgnoredInspection(key="DTZ005", comment='TODO will affect "tz_today()" method'), + RuffIgnoredInspection(key="TD002", comment="Missing author in TODO"), + RuffIgnoredInspection(key="TD003", comment="Missing issue link on the line following this TODO"), ], ) diff --git a/.coveragerc b/.coveragerc index c69ce5c..0880333 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,5 +3,15 @@ omit = setup.py, *_test.py, tests.py, - *tests*, + testapp/* + tests/*, conftest.py + +[report] +precision = 2 +show_missing = True +exclude_lines = + # Don't complain if tests don't hit defensive assertion code: + raise NotImplementedError + # Ignore type checking conditions + if TYPE_CHECKING: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 930c6d6..25f1227 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,10 +8,10 @@ jobs: linting: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.12 - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.12" @@ -21,13 +21,31 @@ jobs: - name: Run pre-commit hooks run: pre-commit run --all-files --hook-stage push - build: + + validate_migrations: + name: Validate migrations + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: python -m pip install -U pip-tools && pip-compile --extra dev,drf,graphql,sentry,view-layer, -o requirements.txt pyproject.toml --resolver=backtracking && pip-sync + + - name: Validate migration integrity + run: python manage.py makemigrations --check --dry-run + + + tests: name: Python ${{ matrix.python-version }}, django ${{ matrix.django-version }} runs-on: ubuntu-22.04 strategy: matrix: - python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] - django-version: [32, 41, 42] + python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', ] + django-version: ['32', '41', '42', ] exclude: - python-version: '3.12' @@ -38,9 +56,9 @@ jobs: django-version: 32 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: setup python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Install tox @@ -49,3 +67,32 @@ jobs: env: TOXENV: django${{ matrix.django-version }} run: tox + - name: Upload coverage data + uses: actions/upload-artifact@v3 + with: + name: coverage-data + path: '.coverage*' + + coverage: + name: Coverage + runs-on: ubuntu-22.04 + needs: tests + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install dependencies + run: python -m pip install --upgrade coverage[toml] + + - name: Download data + uses: actions/download-artifact@v3 + with: + name: coverage-data + + - name: Combine coverage and fail if it's <100% + run: | + python -m coverage html --skip-covered --skip-empty + python -m coverage report --fail-under=96.62 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0fd048d..2f451e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,21 +2,17 @@ # https://pre-commit.com/ repos: - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 23.9.1 - hooks: - - id: black - args: [ --check, --diff, --config, ./pyproject.toml ] - stages: [ push ] - - - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.292' + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.4 hooks: + # Run the Ruff linter. - id: ruff - args: [ --fix, --exit-non-zero-on-fix ] + args: [--fix, --exit-non-zero-on-fix] + # Run the Ruff formatter. + - id: ruff-format - repo: https://github.com/asottile/pyupgrade - rev: v3.14.0 + rev: v3.15.0 hooks: - id: pyupgrade args: [ --py38-plus ] diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..5e5edaa --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,31 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - method: pip + path: . + extra_requirements: + - dev diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index cc9b53c..0000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,24 +0,0 @@ -# .readthedocs.yml -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -# Required -version: 2 - -# Set the version of Python and other tools you might need -build: - os: ubuntu-22.04 - tools: - python: "3.11" - -# Build documentation in the docs/ directory with Sphinx -sphinx: - configuration: docs/conf.py - -# Optionally declare the Python requirements required to build your docs -python: - install: - - method: pip - path: . - extra_requirements: - - dev \ No newline at end of file diff --git a/CHANGES.md b/CHANGES.md index 3096e0d..8dc6100 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,14 @@ # Changelog +**9.2.0** (2023-11-17) + * Added migration docs to Readme + * Added migration check to GitHub actions + * Switched formatter from `black` to `ruff` + * Enforcing minimum coverage in CI/CD pipeline + * Further updates from ambient updater package + * Updated GitHub action versions + * Fixes typos + **9.1.5** (2023-10-31) * Coverage service fixed crash on zero pipelines * Added default `develop` for `GITLAB_CI_COVERAGE_TARGET_BRANCH` in GitLab coverage service @@ -134,7 +143,7 @@ * Set version border for dependency "bleach" because of breaking changes **6.9.0** (2023-01-18) - * Added "GITLAB_CI_DISABLE_COVERAGE" flag to coverage script for Gitlab pipeline + * Added "GITLAB_CI_DISABLE_COVERAGE" flag to coverage script for GitLab pipeline * Fixed typo in the docs **6.8.4** (2023-01-11) @@ -263,10 +272,10 @@ * Added a view which allows logging errors to Sentry normally while using Graphene. **5.12.1** (2022-01-31) - * Fixed bug in Gitlab code coverage compare service documentation + * Fixed bug in GitLab code coverage compare service documentation **5.12.0** (2022-01-28) - * Added Gitlab code coverage compare service `CoverageService` with documentation + * Added GitLab code coverage compare service `CoverageService` with documentation **5.11.1** (2022-01-24) * Added docs for `ToggleView` diff --git a/README.md b/README.md index e119afb..01f4991 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ [![PyPI release](https://img.shields.io/pypi/v/ambient-toolbox.svg)](https://pypi.org/project/ambient-toolbox/) [![Downloads](https://static.pepy.tech/badge/ambient-toolbox)](https://pepy.tech/project/ambient-toolbox) +[![Coverage](https://img.shields.io/badge/Coverage-96.62%25-success)](https://github.com/ambient-innovation/ambient-toolbox/actions?workflow=CI) [![Linting](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) -[![Coding Style](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) +[![Coding Style](https://img.shields.io/badge/code%20style-Ruff-000000.svg)](https://github.com/astral-sh/ruff) [![Documentation Status](https://readthedocs.org/projects/ambient-toolbox/badge/?version=latest)](https://ambient-toolbox.readthedocs.io/en/latest/?badge=latest) Python toolbox of Ambient Digital containing an abundance of useful tools and gadgets. @@ -59,12 +60,20 @@ The migration is really simple, just: ```` +- Apply migrations by running: + + `python ./manage.py migrate` + + + + + ## Contribute ### Setup package for development - Create a Python virtualenv and activate it -- Install "pip-tools" with `pip install pip-tools` +- Install "pip-tools" with `pip install -U pip-tools` - Compile the requirements with `pip-compile --extra dev,drf,graphql,sentry,view-layer, -o requirements.txt pyproject.toml --resolver=backtracking` - Sync the dependencies with your virtualenv with `pip-sync` @@ -83,6 +92,12 @@ The migration is really simple, just: pytest --ds settings tests ```` +- Check coverage + ```` + coverage run -m pytest --ds settings tests + coverage report -m + ```` + ### Git hooks (via pre-commit) We use pre-push hooks to ensure that only linted code reaches our remote repository and pipelines aren't triggered in diff --git a/ambient_toolbox/__init__.py b/ambient_toolbox/__init__.py index 8cae47b..5c5d12f 100644 --- a/ambient_toolbox/__init__.py +++ b/ambient_toolbox/__init__.py @@ -1,3 +1,3 @@ """Python toolbox of Ambient Digital containing an abundance of useful tools and gadgets.""" -__version__ = '9.1.5' +__version__ = "9.2.0" diff --git a/ambient_toolbox/admin/model_admins/classes.py b/ambient_toolbox/admin/model_admins/classes.py index 2516272..eebd983 100644 --- a/ambient_toolbox/admin/model_admins/classes.py +++ b/ambient_toolbox/admin/model_admins/classes.py @@ -14,10 +14,10 @@ def get_readonly_fields(self, request, obj=None): ] return self.readonly_fields - def changeform_view(self, request, object_id=None, form_url='', extra_context=None): + def changeform_view(self, request, object_id=None, form_url="", extra_context=None): extra_context = extra_context or {} - extra_context['show_save_and_continue'] = False - extra_context['show_save'] = False + extra_context["show_save_and_continue"] = False + extra_context["show_save"] = False return super().changeform_view(request, object_id, extra_context=extra_context) def has_add_permission(self, request): @@ -39,8 +39,8 @@ class EditableOnlyAdmin(admin.ModelAdmin): def get_actions(self, request): # Disable delete actions = super().get_actions(request) - if 'delete_selected' in actions: - del actions['delete_selected'] + if "delete_selected" in actions: + del actions["delete_selected"] return actions def has_add_permission(self, request): diff --git a/ambient_toolbox/admin/model_admins/inlines.py b/ambient_toolbox/admin/model_admins/inlines.py index c50a276..e1b7d4e 100644 --- a/ambient_toolbox/admin/model_admins/inlines.py +++ b/ambient_toolbox/admin/model_admins/inlines.py @@ -25,5 +25,5 @@ def get_readonly_fields(self, request, obj=None): + [field.name for field in self.opts.local_many_to_many] ) ) - result.remove('id') + result.remove("id") return result diff --git a/ambient_toolbox/admin/model_admins/mixins.py b/ambient_toolbox/admin/model_admins/mixins.py index 537f5e4..37af0d3 100644 --- a/ambient_toolbox/admin/model_admins/mixins.py +++ b/ambient_toolbox/admin/model_admins/mixins.py @@ -16,7 +16,7 @@ class AdminCreateFormMixin: def get_form(self, request, obj=None, **kwargs): defaults = {} if obj is None: - defaults['form'] = self.add_form + defaults["form"] = self.add_form defaults.update(kwargs) return super().get_form(request, obj, **defaults) @@ -58,7 +58,7 @@ def _resolve_url(request): def get_parent_object_from_request(self, request): resolved = self._resolve_url(request) if resolved.kwargs: - return self.parent_model.objects.get(pk=resolved.kwargs.get('object_id', None)) + return self.parent_model.objects.get(pk=resolved.kwargs.get("object_id", None)) return None def get_formset(self, request, obj=None, **kwargs): @@ -75,7 +75,7 @@ class FetchObjectMixin: def get_object_from_request(self, request): resolved = resolve(request.path_info) if resolved.kwargs: - return self.model.objects.get(pk=resolved.kwargs.get('object_id', None)) + return self.model.objects.get(pk=resolved.kwargs.get("object_id", None)) return None @@ -90,10 +90,10 @@ def get_readonly_fields(self, request, obj=None): Set the fields CommonInfo handles to readonly to avoid users fiddling around with them. """ return super().get_readonly_fields(request, obj) + ( - 'created_by', - 'lastmodified_by', - 'created_at', - 'lastmodified_at', + "created_by", + "lastmodified_by", + "created_at", + "lastmodified_at", ) def get_user_obj(self, request) -> Optional[AbstractUser]: @@ -145,7 +145,7 @@ def change_view(self, request, *args, **kwargs): else: opts = self.model._meta url = reverse( - 'admin:{app}_{model}_changelist'.format( + "admin:{app}_{model}_changelist".format( app=opts.app_label, model=opts.model_name, ) diff --git a/ambient_toolbox/admin/views/forms.py b/ambient_toolbox/admin/views/forms.py index d881b39..d4e01a0 100644 --- a/ambient_toolbox/admin/views/forms.py +++ b/ambient_toolbox/admin/views/forms.py @@ -9,24 +9,24 @@ class AdminCrispyForm(forms.Form): Base crispy form to be used in custom views within the django admin. """ - section_title = _('No title defined') - button_label = _('Save') + section_title = _("No title defined") + button_label = _("Save") def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Build fieldset - fieldset_list = [''] + fieldset_list = [""] for field in self.fields: - fieldset_list.append(Div(field, css_class='form-row field-name')) + fieldset_list.append(Div(field, css_class="form-row field-name")) # noqa: PERF401 # Crispy self.helper = FormHelper() - self.helper.form_method = 'post' - self.helper.add_input(Submit('submit', self.button_label, css_class="button btn-primary")) + self.helper.form_method = "post" + self.helper.add_input(Submit("submit", self.button_label, css_class="button btn-primary")) self.helper.layout = Layout( Div( - Div(HTML(f'

{self.section_title}

'), Fieldset(*fieldset_list), css_class='module aligned'), - css_class='custom-form', + Div(HTML(f"

{self.section_title}

"), Fieldset(*fieldset_list), css_class="module aligned"), + css_class="custom-form", ), ) diff --git a/ambient_toolbox/admin/views/mixins.py b/ambient_toolbox/admin/views/mixins.py index 6581455..59fbd1f 100644 --- a/ambient_toolbox/admin/views/mixins.py +++ b/ambient_toolbox/admin/views/mixins.py @@ -8,7 +8,7 @@ class AdminViewMixin: """ model = None - admin_page_title = '' + admin_page_title = "" def has_view_permission(self, user, **kwargs) -> bool: """ @@ -39,23 +39,23 @@ def get_context_data(self, **kwargs): admin_site = self.get_admin_site() context.update( { - 'site_header': admin_site.site_header, - 'site_title': admin_site.site_title, - 'title': self.admin_page_title, - 'name': self.admin_page_title, - 'original': self.admin_page_title, - 'is_nav_sidebar_enabled': True, - 'available_apps': admin.site.get_app_list(self.request), - 'opts': { - 'app_label': self.model._meta.app_label, - 'verbose_name': self.model._meta.verbose_name, - 'verbose_name_plural': self.model._meta.verbose_name_plural, - 'model_name': self.model._meta.model_name, - 'app_config': { - 'verbose_name': self.model._meta.app_config.verbose_name, + "site_header": admin_site.site_header, + "site_title": admin_site.site_title, + "title": self.admin_page_title, + "name": self.admin_page_title, + "original": self.admin_page_title, + "is_nav_sidebar_enabled": True, + "available_apps": admin.site.get_app_list(self.request), + "opts": { + "app_label": self.model._meta.app_label, + "verbose_name": self.model._meta.verbose_name, + "verbose_name_plural": self.model._meta.verbose_name_plural, + "model_name": self.model._meta.model_name, + "app_config": { + "verbose_name": self.model._meta.app_config.verbose_name, }, }, - 'has_permission': admin_site.has_permission(request=self.request), + "has_permission": admin_site.has_permission(request=self.request), } ) return context diff --git a/ambient_toolbox/apps.py b/ambient_toolbox/apps.py index e84b645..b36e674 100644 --- a/ambient_toolbox/apps.py +++ b/ambient_toolbox/apps.py @@ -3,5 +3,5 @@ class AmbientToolboxConfig(AppConfig): - name = 'ambient_toolbox' - verbose_name = _('Ambient Toolbox') + name = "ambient_toolbox" + verbose_name = _("Ambient Toolbox") diff --git a/ambient_toolbox/context_processors.py b/ambient_toolbox/context_processors.py index 4b1652e..75770b9 100644 --- a/ambient_toolbox/context_processors.py +++ b/ambient_toolbox/context_processors.py @@ -2,4 +2,4 @@ def server_settings(request): - return {'DEBUG_MODE': settings.DEBUG, 'SERVER_URL': settings.SERVER_URL} + return {"DEBUG_MODE": settings.DEBUG, "SERVER_URL": settings.SERVER_URL} diff --git a/ambient_toolbox/drf/fields.py b/ambient_toolbox/drf/fields.py index 1fcab78..f9004f2 100644 --- a/ambient_toolbox/drf/fields.py +++ b/ambient_toolbox/drf/fields.py @@ -17,7 +17,7 @@ def to_representation(self, value): # parent serializer. # Explanation: With `many=True` DRF creates an intermediate `ListSerializer`. It has `many=True`, so we'll end # up in the first if-case. If we do not use `many=True`, the "many" attribute is not set. - if getattr(self.parent, 'many', False): + if getattr(self.parent, "many", False): parent = self.parent.parent else: parent = self.parent diff --git a/ambient_toolbox/drf/serializers.py b/ambient_toolbox/drf/serializers.py index 778cba1..c3aa9b8 100644 --- a/ambient_toolbox/drf/serializers.py +++ b/ambient_toolbox/drf/serializers.py @@ -23,12 +23,12 @@ class CommonInfoSerializer(BaseModelSerializer): def validate(self, data): data = super().validate(data) - request = self.context.get('request', None) + request = self.context.get("request", None) if request.user: - data['lastmodified_by'] = request.user + data["lastmodified_by"] = request.user if not self.instance: # If this is a new instance, set created_by - data['created_by'] = request.user + data["created_by"] = request.user return data diff --git a/ambient_toolbox/drf/tests.py b/ambient_toolbox/drf/tests.py index 4c6bbff..6b3a3c9 100644 --- a/ambient_toolbox/drf/tests.py +++ b/ambient_toolbox/drf/tests.py @@ -40,17 +40,17 @@ def validate_authentication_required(self, *, url: str, method: str, view: str): self.assertIn(response.status_code, [status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN]) - def execute_request( + def execute_request( # noqa: PLR0913 self, *, url, view_kwargs=None, - method='get', + method="get", data=None, view_class=None, user=None, viewset_kwargs=None, - data_format='json', + data_format="json", ) -> Response: """ Helper method which wraps all relevant setup to execute a request to the backends api diff --git a/ambient_toolbox/gitlab/coverage.py b/ambient_toolbox/gitlab/coverage.py index 1b579ae..83a928e 100644 --- a/ambient_toolbox/gitlab/coverage.py +++ b/ambient_toolbox/gitlab/coverage.py @@ -4,6 +4,7 @@ import subprocess import sys from difflib import ndiff +from http import HTTPStatus import httpx @@ -17,41 +18,41 @@ def __init__(self) -> None: super().__init__() # Get ENV variables - self.current_pipeline_id: int = int(os.environ.get('CI_PIPELINE_ID')) - self.base_api_url: str = os.environ.get('CI_API_V4_URL') - self.current_branch: str = os.environ.get('CI_COMMIT_REF_NAME') - self.token: str = os.environ.get('GITLAB_CI_COVERAGE_PIPELINE_TOKEN') - self.project_id: int = int(os.environ.get('CI_PROJECT_ID')) - self.job_name: str = os.environ.get('CI_COVERAGE_JOB_NAME', '') - self.target_branch: str = os.environ.get('GITLAB_CI_COVERAGE_TARGET_BRANCH', 'develop') + self.current_pipeline_id: int = int(os.environ.get("CI_PIPELINE_ID")) + self.base_api_url: str = os.environ.get("CI_API_V4_URL") + self.current_branch: str = os.environ.get("CI_COMMIT_REF_NAME") + self.token: str = os.environ.get("GITLAB_CI_COVERAGE_PIPELINE_TOKEN") + self.project_id: int = int(os.environ.get("CI_PROJECT_ID")) + self.job_name: str = os.environ.get("CI_COVERAGE_JOB_NAME", "") + self.target_branch: str = os.environ.get("GITLAB_CI_COVERAGE_TARGET_BRANCH", "develop") self.pipelines_url = ( - f'{self.base_api_url}/projects/{self.project_id}/pipelines?ref={self.target_branch}&status=success' + f"{self.base_api_url}/projects/{self.project_id}/pipelines?ref={self.target_branch}&status=success" ) - self.pipelines_url_with_token = f'{self.pipelines_url}&private_token={self.token}' + self.pipelines_url_with_token = f"{self.pipelines_url}&private_token={self.token}" - self.disable_coverage: bool = os.environ.get('GITLAB_CI_DISABLE_COVERAGE', False) + self.disable_coverage: bool = os.environ.get("GITLAB_CI_DISABLE_COVERAGE", False) def get_latest_target_branch_commit_sha(self) -> str: """ Get the latest commit which is in the current branch and the target compare branch. """ result = subprocess.run( - ['git', 'merge-base', '--fork-point', f'origin/{self.target_branch}'], stdout=subprocess.PIPE + ["git", "merge-base", "--fork-point", f"origin/{self.target_branch}"], stdout=subprocess.PIPE, check=True ) return result.stdout.decode("utf-8").strip() def get_pipeline_id_by_commit_sha(self, sha: str) -> int | None: - pipeline_url = f'{self.pipelines_url_with_token}&sha={sha}' + pipeline_url = f"{self.pipelines_url_with_token}&sha={sha}" response = httpx.get(pipeline_url) status_code = response.status_code - if status_code == 200: + if status_code == HTTPStatus.OK: pipelines = json.loads(response.content) if pipelines and len(pipelines) > 0: - return pipelines[0].get('id', None) + return pipelines[0].get("id", None) else: - print('\n### ERROR: No pipelines found for SHA1 ###\n') - print(f'Pipeline URL: {self.pipelines_url}&sha={sha}') + print("\n### ERROR: No pipelines found for SHA1 ###\n") + print(f"Pipeline URL: {self.pipelines_url}&sha={sha}") print(response.content) return None @@ -59,39 +60,39 @@ def get_coverage_from_pipeline(self, pipeline_id: int, job_name: str) -> (float, """ Get coverage from given pipeline (by id) """ - jobs_url = f'{self.base_api_url}/projects/{self.project_id}/pipelines/{pipeline_id}/jobs' - jobs_with_token_url = f'{jobs_url}?private_token={self.token}' + jobs_url = f"{self.base_api_url}/projects/{self.project_id}/pipelines/{pipeline_id}/jobs" + jobs_with_token_url = f"{jobs_url}?private_token={self.token}" - print(f'Jobs-API-URL: {jobs_url}') + print(f"Jobs-API-URL: {jobs_url}") jobs_response = httpx.get(jobs_with_token_url) jobs_status_code = jobs_response.status_code - if jobs_status_code != 200: - raise ConnectionError(f'Call to jobs api endpoint failed with status code {jobs_status_code}') + if jobs_status_code != HTTPStatus.OK: + raise ConnectionError(f"Call to jobs api endpoint failed with status code {jobs_status_code}") jobs = json.loads(jobs_response.content) coverages = { - job['name']: {'id': job['id'], 'coverage': float(job['coverage']), 'web_url': job['web_url']} + job["name"]: {"id": job["id"], "coverage": float(job["coverage"]), "web_url": job["web_url"]} for job in jobs - if job.get('coverage') + if job.get("coverage") } - pipeline_url = f'{self.base_api_url}/projects/{self.project_id}/pipelines/{pipeline_id}' - pipeline_with_token_url = f'{pipeline_url}?private_token={self.token}' + pipeline_url = f"{self.base_api_url}/projects/{self.project_id}/pipelines/{pipeline_id}" + pipeline_with_token_url = f"{pipeline_url}?private_token={self.token}" pipeline_response = httpx.get(pipeline_with_token_url) pipeline_status_code = pipeline_response.status_code - if pipeline_status_code != 200: - raise ConnectionError(f'Call to pipeline api endpoint failed with status code {pipeline_status_code}') + if pipeline_status_code != HTTPStatus.OK: + raise ConnectionError(f"Call to pipeline api endpoint failed with status code {pipeline_status_code}") pipeline = json.loads(pipeline_response.content) - coverages_total = float(pipeline['coverage'] if pipeline['coverage'] else 0.0) - print(f'Pipeline-API-URL: {pipeline_url}') + coverages_total = float(pipeline["coverage"] if pipeline["coverage"] else 0.0) + print(f"Pipeline-API-URL: {pipeline_url}") print(f'Pipeline-URL: {pipeline["web_url"]}') - if job_name == '': + if job_name == "": print( - '\033[91mATTN: No CI_COVERAGE_JOB_NAME provided, using Total Coverage and skipping Coverage Diff\033[0m' + "\033[91mATTN: No CI_COVERAGE_JOB_NAME provided, using Total Coverage and skipping Coverage Diff\033[0m" ) return coverages_total, coverages_total, None @@ -100,23 +101,23 @@ def get_coverage_from_pipeline(self, pipeline_id: int, job_name: str) -> (float, print(f'Job-URL: {coverage_job["web_url"]}') job_url = f'{self.base_api_url}/projects/{self.project_id}/jobs/{coverage_job["id"]}/trace' - job_with_token_url = f'{job_url}?private_token={self.token}' + job_with_token_url = f"{job_url}?private_token={self.token}" job_response = httpx.get(job_with_token_url) job_status_code = job_response.status_code - if job_status_code != 200: - raise ConnectionError(f'Call to job api endpoint failed with status code {job_status_code}') + if job_status_code != HTTPStatus.OK: + raise ConnectionError(f"Call to job api endpoint failed with status code {job_status_code}") - print(f'Job-Log-URL: {job_url}') + print(f"Job-Log-URL: {job_url}") job_log = re.search( - r'Name\s+Stmts\s+Miss\s+Branch\s+BrPart\s+Cover\s+Missing.*files skipped due to complete coverage\.', - job_response.content.decode('utf-8'), + r"Name\s+Stmts\s+Miss\s+Branch\s+BrPart\s+Cover\s+Missing.*files skipped due to complete coverage\.", + job_response.content.decode("utf-8"), re.DOTALL | re.MULTILINE, ) # print(job_log.group()) - return coverage_job['coverage'] if coverage_job else 0.0, coverages_total, job_log.group() + return coverage_job["coverage"] if coverage_job else 0.0, coverages_total, job_log.group() @staticmethod def color_text(sign: int, prefix: str, target: float, current: float, diff: float): @@ -134,9 +135,9 @@ def color_text(sign: int, prefix: str, target: float, current: float, diff: floa :return: fully assembled and colored summary text """ change = { - -1: {'text': 'dropped', 'color': '\033[91m'}, - 0: {'text': 'unchanged', 'color': ''}, - 1: {'text': 'climbed', 'color': '\033[92m'}, + -1: {"text": "dropped", "color": "\033[91m"}, + 0: {"text": "unchanged", "color": ""}, + 1: {"text": "climbed", "color": "\033[92m"}, } return ( f'{change[sign]["color"]} {prefix} {change[sign]["text"]} ' @@ -149,31 +150,31 @@ def print_diff(target_job_log, current_job_log): Print a diff between the coverage reports of Current and Target branch """ diff = ndiff(target_job_log.splitlines(keepends=True), current_job_log.splitlines(keepends=True)) - print('\n############################## Coverage Diff ##############################') - print('# \033[91m- Target Branch\033[0m #') - print('# \033[92m+ Current Branch\033[0m #') - print('###########################################################################') + print("\n############################## Coverage Diff ##############################") + print("# \033[91m- Target Branch\033[0m #") + print("# \033[92m+ Current Branch\033[0m #") + print("###########################################################################") for _idx, line in enumerate(diff): match = re.match( - r'^\s*-+\s*$|' - r'^\s*Name\s+Stmts\s+Miss\s+Branch\s+BrPart\s+Cover\s+Missing|' # first line of the report - r'^.*files skipped due to complete coverage.$|' # Final line of the report - r'^[+\-?]', # Line starts with +,-,$ to indicate changes + r"^\s*-+\s*$|" + r"^\s*Name\s+Stmts\s+Miss\s+Branch\s+BrPart\s+Cover\s+Missing|" # first line of the report + r"^.*files skipped due to complete coverage.$|" # Final line of the report + r"^[+\-?]", # Line starts with +,-,$ to indicate changes line, ) if match: - if line[0] == '-': - print('\033[91m', end="") - if line[0] == '+': - print('\033[92m', end="") + if line[0] == "-": + print("\033[91m", end="") + if line[0] == "+": + print("\033[92m", end="") print(line, end="") - if line[0] in ['+', '-']: - print('\033[0m', end="") + if line[0] in ["+", "-"]: + print("\033[0m", end="") - print('\n###########################################################################') - print('# \033[91m- Target Branch\033[0m #') - print('# \033[92m+ Current Branch\033[0m #') - print('############################## Coverage Diff ##############################\n') + print("\n###########################################################################") + print("# \033[91m- Target Branch\033[0m #") + print("# \033[92m+ Current Branch\033[0m #") + print("############################## Coverage Diff ##############################\n") def process(self): """ @@ -183,58 +184,58 @@ def process(self): # Check, if coverage is supposed to run. If not, inform the user and return early. if self.disable_coverage: - print('Coverage was skipped!') + print("Coverage was skipped!") sys.exit(0) - print('\n###########################################################################\n') - print('DEBUG INFO:') + print("\n###########################################################################\n") + print("DEBUG INFO:") # Get the latest commit SHA which is also in develop commit_sha = self.get_latest_target_branch_commit_sha() # Try to find the latest successful pipeline for "TARGET_BRANCH" where our SHA was in - print('Trying base branch for comparison.') + print("Trying base branch for comparison.") target_pipeline_id = None if commit_sha: print(f'Found latest target branch commit SHA "{commit_sha}".') target_pipeline_id = self.get_pipeline_id_by_commit_sha(commit_sha) - print(f'Target branch pipeline ID identified: {target_pipeline_id}.') + print(f"Target branch pipeline ID identified: {target_pipeline_id}.") # Get target pipeline id (from develop branch) if we were not successful the first time if not target_pipeline_id: print("Didn't work. Using default branch for comparison.") response = httpx.get(self.pipelines_url_with_token) status_code = response.status_code - print(f'Pipelines-API-URL: {self.pipelines_url}') + print(f"Pipelines-API-URL: {self.pipelines_url}") # Ensure call did not go sideways - if status_code != 200: - raise ConnectionError(f'Call to global pipeline api endpoint failed with status code {status_code}') + if status_code != HTTPStatus.OK: + raise ConnectionError(f"Call to global pipeline api endpoint failed with status code {status_code}") # Read target pipeline ID from content try: - target_pipeline_id = json.loads(response.content)[0]['id'] + target_pipeline_id = json.loads(response.content)[0]["id"] except IndexError: # This happens when there are zero `target_branch` pipelines target_pipeline_id = 0 - print(f'Default branch pipeline ID identified: {target_pipeline_id}.') + print(f"Default branch pipeline ID identified: {target_pipeline_id}.") # Get coverage from target pipeline - print(f'Target Pipeline ID: {target_pipeline_id}') + print(f"Target Pipeline ID: {target_pipeline_id}") target_job_coverage, target_total_coverage, target_job_log = self.get_coverage_from_pipeline( target_pipeline_id, self.job_name ) # Get coverage from this pipeline - print(f'Current pipeline ID: {self.current_pipeline_id}') + print(f"Current pipeline ID: {self.current_pipeline_id}") current_job_coverage, current_total_coverage, current_job_log = self.get_coverage_from_pipeline( self.current_pipeline_id, self.job_name ) if target_job_log is None or current_job_log is None: - print('\n\n\033[91m***************************************************************************\033[0m') - print(' \033[91m**/!\\** Coverage log not found. Skipping diff. **/!\\**\033[0m ') - print('\033[91m***************************************************************************\033[0m\n\n') + print("\n\n\033[91m***************************************************************************\033[0m") + print(" \033[91m**/!\\** Coverage log not found. Skipping diff. **/!\\**\033[0m ") + print("\033[91m***************************************************************************\033[0m\n\n") else: self.print_diff(target_job_log, current_job_log) @@ -249,41 +250,41 @@ def process(self): diff_total_coverage = current_total_coverage - target_total_coverage coverage = { - 'total': { - 'target': target_total_coverage, - 'current': current_total_coverage, - 'sign': sign_total_coverage, - 'diff': diff_total_coverage, - 'prefix': 'Total coverage', + "total": { + "target": target_total_coverage, + "current": current_total_coverage, + "sign": sign_total_coverage, + "diff": diff_total_coverage, + "prefix": "Total coverage", }, - 'job': { - 'target': target_job_coverage, - 'current': current_job_coverage, - 'sign': sign_job_coverage, - 'diff': diff_job_coverage, - 'prefix': 'Job coverage', + "job": { + "target": target_job_coverage, + "current": current_job_coverage, + "sign": sign_job_coverage, + "diff": diff_job_coverage, + "prefix": "Job coverage", }, } # Print results print( self.color_text( - coverage['total']['sign'], - coverage['total']['prefix'], - coverage['total']['target'], - coverage['total']['current'], - coverage['total']['diff'], + coverage["total"]["sign"], + coverage["total"]["prefix"], + coverage["total"]["target"], + coverage["total"]["current"], + coverage["total"]["diff"], ) ) print( self.color_text( - coverage['job']['sign'], - coverage['job']['prefix'], - coverage['job']['target'], - coverage['job']['current'], - coverage['job']['diff'], + coverage["job"]["sign"], + coverage["job"]["prefix"], + coverage["job"]["target"], + coverage["job"]["current"], + coverage["job"]["diff"], ) ) - if coverage['job']['sign'] == -1: + if coverage["job"]["sign"] == -1: sys.exit(1) diff --git a/ambient_toolbox/graphql/forms/mutations.py b/ambient_toolbox/graphql/forms/mutations.py index 42211e3..c667672 100644 --- a/ambient_toolbox/graphql/forms/mutations.py +++ b/ambient_toolbox/graphql/forms/mutations.py @@ -30,7 +30,7 @@ def on_resolve(payload): result = cls.mutate_and_get_payload(root, info, **input) if result.errors: - err_msg = '' + err_msg = "" for err in result.errors: err_msg += f"Field '{err.field}': {err.messages[0]} " @@ -42,7 +42,7 @@ def on_resolve(payload): return on_resolve(result) -@method_decorator(login_required, name='perform_mutate') +@method_decorator(login_required, name="perform_mutate") class LoginRequiredDjangoModelFormMutation(DjangoValidatedModelFormMutation): """ Ensures that you need to be logged in with GraphQL JWT (json web token) authentication diff --git a/ambient_toolbox/graphql/schemes/mutations.py b/ambient_toolbox/graphql/schemes/mutations.py index 983cef8..350de85 100644 --- a/ambient_toolbox/graphql/schemes/mutations.py +++ b/ambient_toolbox/graphql/schemes/mutations.py @@ -21,7 +21,7 @@ class Input: @classmethod def __init_subclass_with_meta__(cls, resolver=None, output=None, arguments=None, _meta=None, model=None, **options): if not model: - raise AttributeError('DeleteMutation needs a valid model to be set.') + raise AttributeError("DeleteMutation needs a valid model to be set.") super().__init_subclass_with_meta__(resolver, output, arguments, _meta, **options) cls.model = model @@ -45,10 +45,10 @@ def mutate_and_get_payload(cls, root, info, **input_data): Ensure custom validation, fetch object and delete it afterwards. """ if not cls.validate(info.context): - raise GraphQLError('Delete method not allowed.') + raise GraphQLError("Delete method not allowed.") # Get object id - object_id = int(input_data.get('id', None)) + object_id = int(input_data.get("id", None)) # Find and delete object obj = cls.get_queryset(info.context).get(pk=object_id) @@ -58,7 +58,7 @@ def mutate_and_get_payload(cls, root, info, **input_data): return DeleteMutation() -@method_decorator(login_required, name='mutate_and_get_payload') +@method_decorator(login_required, name="mutate_and_get_payload") class LoginRequiredDeleteMutation(DeleteMutation): """ Deletes an object from the database. diff --git a/ambient_toolbox/graphql/sentry/utils.py b/ambient_toolbox/graphql/sentry/utils.py index 3a0290d..73b20ef 100644 --- a/ambient_toolbox/graphql/sentry/utils.py +++ b/ambient_toolbox/graphql/sentry/utils.py @@ -9,4 +9,4 @@ def ignore_graphene_logger(): Test for: * sentry_sdk >= 0.13.0 """ - ignore_logger('graphql.execution.utils') + ignore_logger("graphql.execution.utils") diff --git a/ambient_toolbox/graphql/tests/base_test.py b/ambient_toolbox/graphql/tests/base_test.py index 9c7a293..5a7ce03 100644 --- a/ambient_toolbox/graphql/tests/base_test.py +++ b/ambient_toolbox/graphql/tests/base_test.py @@ -9,7 +9,7 @@ class GraphQLTestCase(TestCase): """ # URL to graphql endpoint - GRAPHQL_URL = '/graphql/' + GRAPHQL_URL = "/graphql/" # Here you need to set your graphql schema for the tests GRAPHQL_SCHEMA = None @@ -18,7 +18,7 @@ def setUpClass(cls): super().setUpClass() if not cls.GRAPHQL_SCHEMA: - raise AttributeError('Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase.') + raise AttributeError("Variable GRAPHQL_SCHEMA not defined in GraphQLTestCase.") cls._client = Client(cls.GRAPHQL_SCHEMA) @@ -31,13 +31,13 @@ def query(self, query: str, op_name: str = None, input_data: dict = None): :return: Response object from client """ - body = {'query': query} + body = {"query": query} if op_name: - body['operation_name'] = op_name + body["operation_name"] = op_name if input_data: - body['variables'] = {'input': input_data} + body["variables"] = {"input": input_data} - resp = self._client.post(self.GRAPHQL_URL, json.dumps(body), content_type='application/json') + resp = self._client.post(self.GRAPHQL_URL, json.dumps(body), content_type="application/json") return resp def assertResponseNoErrors(self, resp): # noqa: N802 @@ -48,7 +48,7 @@ def assertResponseNoErrors(self, resp): # noqa: N802 """ content = json.loads(resp.content) self.assertEqual(resp.status_code, 200) - self.assertNotIn('errors', list(content.keys())) + self.assertNotIn("errors", list(content.keys())) def assertResponseHasErrors(self, resp): # noqa: N802 """ @@ -56,4 +56,4 @@ def assertResponseHasErrors(self, resp): # noqa: N802 :resp HttpResponse: Response """ content = json.loads(resp.content) - self.assertIn('errors', list(content.keys())) + self.assertIn("errors", list(content.keys())) diff --git a/ambient_toolbox/mail/backends/whitelist_smtp.py b/ambient_toolbox/mail/backends/whitelist_smtp.py index 29c1755..e55860c 100644 --- a/ambient_toolbox/mail/backends/whitelist_smtp.py +++ b/ambient_toolbox/mail/backends/whitelist_smtp.py @@ -23,7 +23,7 @@ def get_domain_whitelist() -> list: Getter for configuration variable from the settings. Will return a list of domains: ['ambient.digital', 'ambient.digital'] """ - return getattr(settings, 'EMAIL_BACKEND_DOMAIN_WHITELIST', []) + return getattr(settings, "EMAIL_BACKEND_DOMAIN_WHITELIST", []) @staticmethod def get_email_regex(): @@ -31,8 +31,8 @@ def get_email_regex(): Getter for configuration variable from the settings. Will return a RegEX to match email whitelisted domains. """ - return r'^[\w\-\.]+@(%s)$' % '|'.join(x for x in WhitelistEmailBackend.get_domain_whitelist()).replace( - '.', r'\.' + return r"^[\w\-\.]+@(%s)$" % "|".join(x for x in WhitelistEmailBackend.get_domain_whitelist()).replace( + ".", r"\." ) @staticmethod @@ -56,7 +56,7 @@ def whitify_mail_addresses(mail_address_list: list) -> list: allowed_recipients.append(to) elif WhitelistEmailBackend.get_backend_redirect_address(): # Send not allowed emails to the configured redirect address (with CATCHALL) - allowed_recipients.append(WhitelistEmailBackend.get_backend_redirect_address() % to.replace('@', '_')) + allowed_recipients.append(WhitelistEmailBackend.get_backend_redirect_address() % to.replace("@", "_")) return allowed_recipients def _process_recipients(self, email_messages): @@ -71,7 +71,7 @@ def _process_recipients(self, email_messages): def send_messages(self, email_messages): """ Checks if email-recipients are in allowed domains and cancels if not. - Uses regular smtp-sending afterwards. + Uses regular smtp-sending afterward. """ email_messages = self._process_recipients(email_messages) return super().send_messages(email_messages) diff --git a/ambient_toolbox/management/commands/install_permission_fixtures.py b/ambient_toolbox/management/commands/install_permission_fixtures.py index 8c3b2c7..7038619 100644 --- a/ambient_toolbox/management/commands/install_permission_fixtures.py +++ b/ambient_toolbox/management/commands/install_permission_fixtures.py @@ -18,14 +18,14 @@ def add_arguments(self, parser): ) def handle(self, *args, **options): - dry_run = options.get('dry_run') + dry_run = options.get("dry_run") if dry_run: print('Starting in "dry-run" mode...') try: fixture_declaration_list = settings.GROUP_PERMISSION_FIXTURES except AttributeError: - print('No fixtures found in Django settings.') + print("No fixtures found in Django settings.") fixture_declaration_list = [] for declaration_path in fixture_declaration_list: @@ -34,11 +34,11 @@ def handle(self, *args, **options): assert isinstance( declaration_class, type(GroupPermissionDeclaration) - ), f"Could\'t load group declaration \"{declaration_path}\"." + ), f'Could\'t load group declaration "{declaration_path}".' print(f'> Installing permissions of group "{declaration_class.name}"...') service = PermissionSetupService(group_declaration=declaration_class, dry_run=dry_run) new_permissions, removed_permissions = service.process() - print(f'> Newly installed permissions: {new_permissions}') - print(f'> Removed permissions: {removed_permissions}\n') + print(f"> Newly installed permissions: {new_permissions}") + print(f"> Removed permissions: {removed_permissions}\n") diff --git a/ambient_toolbox/managers.py b/ambient_toolbox/managers.py index 47be914..739122f 100644 --- a/ambient_toolbox/managers.py +++ b/ambient_toolbox/managers.py @@ -10,13 +10,13 @@ class AbstractPermissionMixin: """ def visible_for(self, user): - raise NotImplementedError('Please implement this method') + raise NotImplementedError("Please implement this method") def editable_for(self, user): - raise NotImplementedError('Please implement this method') + raise NotImplementedError("Please implement this method") def deletable_for(self, user): - raise NotImplementedError('Please implement this method') + raise NotImplementedError("Please implement this method") class AbstractUserSpecificQuerySet(QuerySet, AbstractPermissionMixin): @@ -28,20 +28,20 @@ def default(self, user): return self def visible_for(self, user): - raise NotImplementedError('Please implement this method') + raise NotImplementedError("Please implement this method") def editable_for(self, user): - raise NotImplementedError('Please implement this method') + raise NotImplementedError("Please implement this method") def deletable_for(self, user): - raise NotImplementedError('Please implement this method') + raise NotImplementedError("Please implement this method") class AbstractUserSpecificManager(Manager, AbstractPermissionMixin): """ The UserSpecificQuerySet has a method 'as_manger', which can be used for creating a default manager, which inherits all methods of the queryset and invokes the respective method of it's queryset, respectively. - If the manager has to be declared separately for some reasons, all queryset methods, have to be declared twice, + If the manager has to be declared separately for some reason, all queryset methods, have to be declared twice, once in the QuerySet, once in the manager class. For consistency reasons, both inherit from the same mixin, to ensure the equality of the method's names. """ diff --git a/ambient_toolbox/middleware/current_request.py b/ambient_toolbox/middleware/current_request.py index ce18c21..8bfe969 100644 --- a/ambient_toolbox/middleware/current_request.py +++ b/ambient_toolbox/middleware/current_request.py @@ -4,7 +4,7 @@ if TYPE_CHECKING: from django.http import HttpRequest, HttpResponse -_request_cv: ContextVar[Optional['HttpRequest']] = ContextVar('request', default=None) +_request_cv: ContextVar[Optional["HttpRequest"]] = ContextVar("request", default=None) class CurrentRequestMiddleware: @@ -12,10 +12,10 @@ class CurrentRequestMiddleware: Middleware which stores the current request in a thread-safe manner. """ - def __init__(self, get_response: Callable[['HttpRequest'], 'HttpResponse']): + def __init__(self, get_response: Callable[["HttpRequest"], "HttpResponse"]): self.get_response = get_response - def __call__(self, request: 'HttpRequest') -> 'HttpResponse': + def __call__(self, request: "HttpRequest") -> "HttpResponse": token = _request_cv.set(request) response = self.get_response(request) _request_cv.reset(token) diff --git a/ambient_toolbox/middleware/current_user.py b/ambient_toolbox/middleware/current_user.py index afbe8fd..277d309 100644 --- a/ambient_toolbox/middleware/current_user.py +++ b/ambient_toolbox/middleware/current_user.py @@ -19,7 +19,7 @@ class CurrentUserMiddleware(CurrentRequestMiddleware): # of one of the next major releases, then fully dropping support for # CurrentUserMiddleware. - def __init__(self, get_response: Callable[['HttpRequest'], 'HttpResponse']): + def __init__(self, get_response: Callable[["HttpRequest"], "HttpResponse"]): warnings.warn( "CurrentUserMiddleware is deprecated. Use CurrentRequestMiddleware instead.", category=DeprecationWarning, diff --git a/ambient_toolbox/mixins/bleacher.py b/ambient_toolbox/mixins/bleacher.py index bd8dcd7..021e814 100644 --- a/ambient_toolbox/mixins/bleacher.py +++ b/ambient_toolbox/mixins/bleacher.py @@ -32,35 +32,35 @@ class BleacherMixin: BLEACH_FIELD_LIST = [] DEFAULT_ALLOWED_ATTRIBUTES = { - '*': ['class', 'style', 'id'], - 'a': ['href', 'rel'], - 'img': ['alt', 'src'], + "*": ["class", "style", "id"], + "a": ["href", "rel"], + "img": ["alt", "src"], } DEFAULT_ALLOWED_TAGS = bleach.ALLOWED_TAGS + [ - 'span', - 'p', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'img', - 'div', - 'u', - 'br', - 'blockquote', + "span", + "p", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "img", + "div", + "u", + "br", + "blockquote", ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields_to_bleach = getattr(self, 'BLEACH_FIELD_LIST', []) - self.allowed_tags = getattr(self, 'ALLOWED_TAGS', self.DEFAULT_ALLOWED_TAGS) - self.allowed_attributes = getattr(self, 'ALLOWED_ATTRIBUTES', self.DEFAULT_ALLOWED_ATTRIBUTES) + self.fields_to_bleach = getattr(self, "BLEACH_FIELD_LIST", []) + self.allowed_tags = getattr(self, "ALLOWED_TAGS", self.DEFAULT_ALLOWED_TAGS) + self.allowed_attributes = getattr(self, "ALLOWED_ATTRIBUTES", self.DEFAULT_ALLOWED_ATTRIBUTES) def _bleach_field(self, field_name): - str_to_bleach = getattr(self, field_name, '') + str_to_bleach = getattr(self, field_name, "") if str_to_bleach: cleaned_value = bleach.clean(str_to_bleach, tags=self.allowed_tags, attributes=self.allowed_attributes) setattr(self, field_name, cleaned_value) diff --git a/ambient_toolbox/models.py b/ambient_toolbox/models.py index 0106686..3c311e5 100644 --- a/ambient_toolbox/models.py +++ b/ambient_toolbox/models.py @@ -57,7 +57,7 @@ def save(self, force_insert=False, force_update=False, using=None, update_fields # Handle case that somebody only wants to update some fields if update_fields is not None and self.ALWAYS_UPDATE_FIELDS: - update_fields = {'lastmodified_at', 'lastmodified_by', 'created_at', 'created_by'}.union(update_fields) + update_fields = {"lastmodified_at", "lastmodified_by", "created_at", "created_by"}.union(update_fields) super().save( force_insert=force_insert, diff --git a/ambient_toolbox/permissions/fixtures/helpers.py b/ambient_toolbox/permissions/fixtures/helpers.py index fe69f28..f2e29c4 100644 --- a/ambient_toolbox/permissions/fixtures/helpers.py +++ b/ambient_toolbox/permissions/fixtures/helpers.py @@ -3,8 +3,8 @@ def generate_default_permissions(model_name: str) -> List[str]: return [ - f'add_{model_name}', - f'change_{model_name}', - f'delete_{model_name}', - f'view_{model_name}', + f"add_{model_name}", + f"change_{model_name}", + f"delete_{model_name}", + f"view_{model_name}", ] diff --git a/ambient_toolbox/permissions/fixtures/services.py b/ambient_toolbox/permissions/fixtures/services.py index 5a0eebb..60dc84a 100644 --- a/ambient_toolbox/permissions/fixtures/services.py +++ b/ambient_toolbox/permissions/fixtures/services.py @@ -48,7 +48,7 @@ def process(self) -> (List[Permission], List[Permission]): # Add permission object to list if new_permission in defined_permission_list: - raise ValueError(f'Permission {new_permission} declared twice.') + raise ValueError(f"Permission {new_permission} declared twice.") defined_permission_list.append(new_permission) # Check if permission is already set in the group @@ -58,7 +58,7 @@ def process(self) -> (List[Permission], List[Permission]): # Check which permissions were removed for the given group for existing_permission in group.permissions.all(): if existing_permission not in defined_permission_list: - removed_permissions.append(existing_permission) + removed_permissions.append(existing_permission) # noqa: PERF401 if not self.dry_run: # Persist changes on removed permissions diff --git a/ambient_toolbox/sentry/helpers.py b/ambient_toolbox/sentry/helpers.py index 45f0db7..c4c33c1 100644 --- a/ambient_toolbox/sentry/helpers.py +++ b/ambient_toolbox/sentry/helpers.py @@ -22,11 +22,11 @@ def __init__(self, denylist: Optional[List[str]] = None, standard_denylist: Opti self.denylist = [] if denylist is None else denylist self.standard_denylist = ( [ - 'username', - 'email', - 'ip_address', - 'serializer', - 'admin', + "username", + "email", + "ip_address", + "serializer", + "admin", ] if standard_denylist else [] @@ -49,15 +49,15 @@ def strip_sensitive_data_from_sentry_event(event, hint): Requires "sentry-sdk>=1.5.0" to work. """ try: - del event['user']['username'] + del event["user"]["username"] except KeyError: pass try: - del event['user']['email'] + del event["user"]["email"] except KeyError: pass try: - del event['user']['ip_address'] + del event["user"]["ip_address"] except KeyError: pass return event diff --git a/ambient_toolbox/services/custom_scrubber.py b/ambient_toolbox/services/custom_scrubber.py index 50b3dfd..a06d10c 100644 --- a/ambient_toolbox/services/custom_scrubber.py +++ b/ambient_toolbox/services/custom_scrubber.py @@ -13,9 +13,9 @@ class ScrubbingError(RuntimeError): class AbstractScrubbingService: - DEFAULT_USER_PASSWORD = 'Admin0404!' + DEFAULT_USER_PASSWORD = "Admin0404!" - # Overwritable values + # Over-writable values keep_session_data = False keep_scrubber_data = False keep_django_admin_log = False @@ -23,32 +23,32 @@ class AbstractScrubbingService: post_scrub_functions = [] def __init__(self): - self._logger = logging.getLogger('django_scrubber') + self._logger = logging.getLogger("django_scrubber") def _get_hashed_default_password(self): return make_password(self.DEFAULT_USER_PASSWORD) def _validation(self): if not settings.DEBUG: - self._logger.warning('Attention! Has to run in DEBUG mode!') + self._logger.warning("Attention! Has to run in DEBUG mode!") return False - if 'django_scrubber' not in settings.INSTALLED_APPS: - self._logger.warning('Attention! django-scrubber needs to be installed!') + if "django_scrubber" not in settings.INSTALLED_APPS: + self._logger.warning("Attention! django-scrubber needs to be installed!") return False - if 'django_scrubber' not in list(settings.LOGGING['loggers'].keys()): - self._logger.warning('Attention! Logging for django-scrubber is not activated!') + if "django_scrubber" not in list(settings.LOGGING["loggers"].keys()): + self._logger.warning("Attention! Logging for django-scrubber is not activated!") return True def process(self): - self._logger.info('Start scrubbing process...') + self._logger.info("Start scrubbing process...") - self._logger.info('Validating setup...') + self._logger.info("Validating setup...") if not self._validation(): - self._logger.warning('Aborting process!') - raise ScrubbingError('Scrubber settings validation failed') + self._logger.warning("Aborting process!") + raise ScrubbingError("Scrubber settings validation failed") # Custom pre-scrubbing for name in self.pre_scrub_functions: @@ -57,7 +57,7 @@ def process(self): method() self._logger.info('Scrubbing data with "scrub_data"...') - call_command('scrub_data') + call_command("scrub_data") # Custom post-scrubbing for name in self.post_scrub_functions: @@ -73,17 +73,17 @@ def process(self): if not self.keep_session_data: self._logger.info('Clearing data from "django_session" ...') # Sessions might contain private information and furthermore cannot be used anyway because we anonymised - # all the users. Therefore it is being cleared by default + # all the users. Therefore, it is being cleared by default Session.objects.all().delete() # Reset scrubber data to avoid huge db dumps if not self.keep_scrubber_data: self._logger.info('Clearing data from "django_scrubber_fakedata" ...') # We truncate and don't scrub because the table is huge and clearing on object-level might take a while. - # Furthermore can we avoid having a direct dependency to django-scrubber this way. - cursor = connections['default'].cursor() - cursor.execute('TRUNCATE TABLE django_scrubber_fakedata;') + # Furthermore, can we avoid having a direct dependency to django-scrubber this way. + cursor = connections["default"].cursor() + cursor.execute("TRUNCATE TABLE django_scrubber_fakedata;") - self._logger.info('Scrubbing finished!') + self._logger.info("Scrubbing finished!") return True diff --git a/ambient_toolbox/templatetags/ai_email_tags.py b/ambient_toolbox/templatetags/ai_email_tags.py index 303b6b2..13b6018 100644 --- a/ambient_toolbox/templatetags/ai_email_tags.py +++ b/ambient_toolbox/templatetags/ai_email_tags.py @@ -6,7 +6,7 @@ def obfuscate_string(value): - return ''.join([f'&#{str(ord(char)):s};' for char in value]) + return "".join([f"&#{str(ord(char)):s};" for char in value]) @register.filter @@ -25,4 +25,4 @@ def obfuscate_mailto(value, text=False): else: link_text = mail - return mark_safe('{:s}'.format(obfuscate_string('mailto:'), mail, link_text)) + return mark_safe('{:s}'.format(obfuscate_string("mailto:"), mail, link_text)) diff --git a/ambient_toolbox/templatetags/ai_file_tags.py b/ambient_toolbox/templatetags/ai_file_tags.py index 45c6f04..5a208eb 100644 --- a/ambient_toolbox/templatetags/ai_file_tags.py +++ b/ambient_toolbox/templatetags/ai_file_tags.py @@ -17,7 +17,7 @@ def filename(value, max_length=25): """ name = os.path.basename(value.url) if len(name) > max_length: - ext = name.split('.')[-1] + ext = name.split(".")[-1] name = f"{name[:max_length]}[..].{ext}" return name diff --git a/ambient_toolbox/templatetags/ai_number_tags.py b/ambient_toolbox/templatetags/ai_number_tags.py index 0e7daaa..bfc4fe1 100644 --- a/ambient_toolbox/templatetags/ai_number_tags.py +++ b/ambient_toolbox/templatetags/ai_number_tags.py @@ -3,14 +3,10 @@ register = template.Library() -@register.filter(name='multiply') +@register.filter(name="multiply") def multiply(value, arg): """ Multiplies the arg and the value - - :param value: - :param arg: - :return: """ if value: value = f"{value}" @@ -20,28 +16,20 @@ def multiply(value, arg): return None -@register.filter(name='subtract') +@register.filter(name="subtract") def subtract(value, arg): """ Subtracts the arg from the value - - :param value: - :param arg: - :return: """ value = value if value is not None else 0 - arg = value if arg is not None else 0 + arg = arg if arg is not None else 0 return int(value) - int(arg) -@register.filter(name='divide') +@register.filter(name="divide") def divide(value, arg): """ Divides the value by the arg - - :param value: - :param arg: - :return: """ if value: return value / arg @@ -49,15 +37,15 @@ def divide(value, arg): return None -@register.filter(name='to_int') +@register.filter(name="to_int") def to_int(value): """ Parses a string to int value - - :param value: - :return: """ - return int(value) if value else 0 + try: + return int(value) + except ValueError: + return 0 @register.filter(name="currency") diff --git a/ambient_toolbox/templatetags/ai_object_tags.py b/ambient_toolbox/templatetags/ai_object_tags.py index 3f95a27..75b5aea 100644 --- a/ambient_toolbox/templatetags/ai_object_tags.py +++ b/ambient_toolbox/templatetags/ai_object_tags.py @@ -12,9 +12,9 @@ def dict_key_lookup(the_dict, key): :param key: :return: str """ - return the_dict.get(key, '') + return the_dict.get(key, "") -@register.filter(name='label') +@register.filter(name="label") def label(value): return value.field.__class__.__name__ diff --git a/ambient_toolbox/templatetags/ai_string_tags.py b/ambient_toolbox/templatetags/ai_string_tags.py index 25f9f99..3227bcf 100644 --- a/ambient_toolbox/templatetags/ai_string_tags.py +++ b/ambient_toolbox/templatetags/ai_string_tags.py @@ -4,7 +4,7 @@ register = template.Library() -@register.filter(name='get_first_char') +@register.filter(name="get_first_char") def get_first_char(value): """ Returns the first char of the given string @@ -14,7 +14,7 @@ def get_first_char(value): return value[:1] -@register.filter(name='concat') +@register.filter(name="concat") def concat(obj, value: str) -> str: """ Concatenates the two given strings diff --git a/ambient_toolbox/tests/mixins.py b/ambient_toolbox/tests/mixins.py index daca417..17c8ebf 100644 --- a/ambient_toolbox/tests/mixins.py +++ b/ambient_toolbox/tests/mixins.py @@ -35,8 +35,8 @@ def _get_response(self, method, user, data, url_params=None, *args, **kwargs): factory = self.factory_class() req_kwargs = {} if data: - req_kwargs.update({'data': data}) - request = getattr(factory, method)('/', **req_kwargs) + req_kwargs.update({"data": data}) + request = getattr(factory, method)("/", **req_kwargs) # Annotate a request object with a session middleware = SessionMiddleware(get_response=HttpResponse(status=200)) @@ -55,15 +55,15 @@ def _get_response(self, method, user, data, url_params=None, *args, **kwargs): def get(self, user=None, data=None, url_params=None, *args, **kwargs): """Returns response for a GET request.""" - return self._get_response('get', user, data, url_params, *args, **kwargs) + return self._get_response("get", user, data, url_params, *args, **kwargs) def post(self, user=None, data=None, url_params=None, *args, **kwargs): """Returns response for a POST request.""" - return self._get_response('post', user, data, url_params, *args, **kwargs) + return self._get_response("post", user, data, url_params, *args, **kwargs) def delete(self, user=None, data=None, url_params=None, *args, **kwargs): """Returns response for a DELETE request.""" - return self._get_response('delete', user, data, url_params, *args, **kwargs) + return self._get_response("delete", user, data, url_params, *args, **kwargs) class RequestProviderMixin: @@ -75,23 +75,24 @@ class RequestProviderMixin: @staticmethod def get_request( - user: Union[AbstractBaseUser, AnonymousUser, None] = None, method: str = 'GET', url: Optional[str] = None + user: Union[AbstractBaseUser, AnonymousUser, None] = None, method: str = "GET", url: Optional[str] = None ): """ Creates and returns a django request. """ # Determine URL - url = url if url else '/' + url = url if url else "/" # Create test request factory = RequestFactory() request = factory.get(url) # Set user object if it is of a valid type - if user is None or isinstance(user, AbstractBaseUser) or isinstance(user, AnonymousUser): + # TODO: remove noqa when we drop older python support + if user is None or isinstance(user, AbstractBaseUser) or isinstance(user, AnonymousUser): # noqa: PLR1701 request.user = user else: - raise ValueError(_('Please pass a user object to RequestProviderMixin.')) + raise ValueError(_("Please pass a user object to RequestProviderMixin.")) # Annotate a request object with a session session_middleware = SessionMiddleware(get_response=HttpResponse(status=200)) diff --git a/ambient_toolbox/tests/structure_validator/settings.py b/ambient_toolbox/tests/structure_validator/settings.py index 315164a..7460e17 100644 --- a/ambient_toolbox/tests/structure_validator/settings.py +++ b/ambient_toolbox/tests/structure_validator/settings.py @@ -5,7 +5,7 @@ try: TEST_STRUCTURE_VALIDATOR_BASE_DIR = settings.BASE_DIR except AttributeError: - TEST_STRUCTURE_VALIDATOR_BASE_DIR = '' -TEST_STRUCTURE_VALIDATOR_BASE_APP_NAME = 'apps' + TEST_STRUCTURE_VALIDATOR_BASE_DIR = "" +TEST_STRUCTURE_VALIDATOR_BASE_APP_NAME = "apps" TEST_STRUCTURE_VALIDATOR_APP_LIST = settings.INSTALLED_APPS TEST_STRUCTURE_VALIDATOR_IGNORED_DIRECTORY_LIST = [] diff --git a/ambient_toolbox/tests/structure_validator/test_structure_validator.py b/ambient_toolbox/tests/structure_validator/test_structure_validator.py index 34f2a65..680f478 100644 --- a/ambient_toolbox/tests/structure_validator/test_structure_validator.py +++ b/ambient_toolbox/tests/structure_validator/test_structure_validator.py @@ -18,7 +18,7 @@ def __init__(self): @staticmethod def _get_file_whitelist() -> list: - default_whitelist = ['__init__'] + default_whitelist = ["__init__"] try: return default_whitelist + settings.TEST_STRUCTURE_VALIDATOR_FILE_WHITELIST except AttributeError: @@ -40,7 +40,7 @@ def _get_base_app_name() -> str: @staticmethod def _get_ignored_directory_list() -> list: - default_dir_list = ['__pycache__'] + default_dir_list = ["__pycache__"] try: return default_dir_list + settings.TEST_STRUCTURE_VALIDATOR_IGNORED_DIRECTORY_LIST except AttributeError: @@ -54,23 +54,23 @@ def _get_app_list() -> Union[list, tuple]: return toolbox_settings.TEST_STRUCTURE_VALIDATOR_APP_LIST def _check_missing_test_prefix(self, *, root: str, file: str, filename: str, extension: str) -> bool: - if extension == '.py' and not filename[0:5] == "test_" and filename not in self.file_whitelist: - file_path = f"{root}\\{file}".replace('\\', '/') + if extension == ".py" and not filename[0:5] == "test_" and filename not in self.file_whitelist: + file_path = f"{root}\\{file}".replace("\\", "/") self.issue_list.append(f'Python file without "test_" prefix found: {file_path!r}.') return False return True def _check_missing_init(self, *, root: str, init_found: bool, number_of_py_files: int) -> bool: if not init_found and number_of_py_files > 0: - path = root.replace('\\', '/') + path = root.replace("\\", "/") self.issue_list.append(f"__init__.py missing in {path!r}.") return False return True def _build_path_to_test_package(self, app: str) -> Path: - return self._get_base_dir() / Path(app.replace('.', '/')) / 'tests' + return self._get_base_dir() / Path(app.replace(".", "/")) / "tests" - def process(self) -> None: + def process(self) -> None: # noqa: PLR0912 backend_package = self._get_base_app_name() app_list = self._get_app_list() @@ -79,7 +79,7 @@ def process(self) -> None: continue app_path = self._build_path_to_test_package(app=app) for root, dirs, files in os.walk(app_path): - cleaned_root = root.replace('\\', '/') + cleaned_root = root.replace("\\", "/") print(f"Inspecting {cleaned_root!r}...") init_found = False number_of_py_files = 0 @@ -118,7 +118,7 @@ def process(self) -> None: print("=======================") if number_of_issues: - print(f'Checking test structure failed with {number_of_issues} issue(s).') + print(f"Checking test structure failed with {number_of_issues} issue(s).") sys.exit(1) else: print("0 issues detected. Yeah!") diff --git a/ambient_toolbox/utils/date.py b/ambient_toolbox/utils/date.py index fa8b193..62523c3 100644 --- a/ambient_toolbox/utils/date.py +++ b/ambient_toolbox/utils/date.py @@ -66,12 +66,12 @@ def first_day_of_month(source_date: datetime.date) -> datetime.date: def get_formatted_date_str(source_date: Union[datetime.date, datetime.datetime]) -> str: - return source_date.strftime('%d.%m.%Y') + return source_date.strftime("%d.%m.%Y") def get_time_from_seconds(seconds: int) -> str: if seconds < 0: - raise ValueError(_('Seconds must be positive.')) + raise ValueError(_("Seconds must be positive.")) hours = seconds // 3600 minutes = (seconds - (hours * 3600)) // 60 seconds = seconds - ((hours * 3600) + (minutes * 60)) @@ -93,7 +93,7 @@ def get_start_and_end_date_from_calendar_week(year: int, calendar_week: int) -> """ Returns the first and last day of a given calendar week """ - monday = datetime.datetime.strptime(f'{year}-{calendar_week}-1', "%Y-%W-%w").astimezone().date() + monday = datetime.datetime.strptime(f"{year}-{calendar_week}-1", "%Y-%W-%w").astimezone().date() return monday, monday + datetime.timedelta(days=6.9) @@ -124,7 +124,7 @@ def date_month_delta(start_date: datetime.date, end_date: datetime.date) -> floa """ # If `start_date` is greater, this logic doesn't make any sense if start_date > end_date: - raise NotImplementedError('Start date > end date') + raise NotImplementedError("Start date > end date") # Calculate date difference between dates date_diff = (end_date - start_date).days diff --git a/ambient_toolbox/utils/file.py b/ambient_toolbox/utils/file.py index de5a809..8dc0de0 100644 --- a/ambient_toolbox/utils/file.py +++ b/ambient_toolbox/utils/file.py @@ -5,17 +5,15 @@ def get_filename_without_ending(file_path: str) -> str: """ Returns the filename without extension - :param file_path: - :return: """ # if filename has file_path parts - if '/' in file_path: - filename = file_path.rsplit('/')[-1] + if "/" in file_path: + filename = file_path.rsplit("/")[-1] else: filename = file_path - return filename.rsplit('.', 1)[0] + return filename.rsplit(".", 1)[0] def crc(file_path: str) -> str: @@ -37,12 +35,12 @@ def md5_checksum(file_path: str) -> str: """ Returns the md5 checksum of the file from the given file_path. - See ``open`` for all the exceptins that can be raised. + See ``open`` for all the exceptions that can be raised. :param file_path: the file for which the MD5 hashsum should be calculated. :return: returns the MD5 of the file in hexadecimal format. """ - with open(file_path, 'rb') as fh: + with open(file_path, "rb") as fh: m = hashlib.md5() while True: data = fh.read(8192) diff --git a/ambient_toolbox/utils/log_whodid.py b/ambient_toolbox/utils/log_whodid.py index 0b0259e..7fd145f 100644 --- a/ambient_toolbox/utils/log_whodid.py +++ b/ambient_toolbox/utils/log_whodid.py @@ -5,8 +5,8 @@ def log_whodid(obj: models.Model, user) -> None: """ Stores the given user as creator or editor of the given object """ - if hasattr(obj, 'created_by') and obj.created_by is None: + if hasattr(obj, "created_by") and obj.created_by is None: obj.created_by = user - if hasattr(obj, 'lastmodified_by'): + if hasattr(obj, "lastmodified_by"): obj.lastmodified_by = user diff --git a/ambient_toolbox/utils/model.py b/ambient_toolbox/utils/model.py index 3917156..d1c57e3 100644 --- a/ambient_toolbox/utils/model.py +++ b/ambient_toolbox/utils/model.py @@ -12,7 +12,7 @@ def object_to_dict(obj, blacklisted_fields: list = None, include_id: bool = Fals # Add default django primary key to blacklist if not include_id: - blacklisted_fields.append('id') + blacklisted_fields.append("id") data = vars(obj) valid_data = {} @@ -22,7 +22,7 @@ def object_to_dict(obj, blacklisted_fields: list = None, include_id: bool = Fals if type(f) != ForeignKey: valid_fields.append(f.name) else: - valid_fields.append(f'{f.name}_id') + valid_fields.append(f"{f.name}_id") for key, value in list(data.items()): if key in valid_fields and key not in blacklisted_fields: diff --git a/ambient_toolbox/utils/named_tuple.py b/ambient_toolbox/utils/named_tuple.py index 824cdf9..4d6ca31 100644 --- a/ambient_toolbox/utils/named_tuple.py +++ b/ambient_toolbox/utils/named_tuple.py @@ -89,7 +89,7 @@ def get_name_by_value(self, input_value): def is_valid(self, selection): for val, _name, _desc in choices_tuple: - if val == selection or _name == selection or _desc == selection: + if selection in (val, _name, _desc): return True return False @@ -105,7 +105,7 @@ def get_value_from_tuple_by_key(choices: tuple, key) -> Any: try: return dict(choices)[key] except KeyError: - return '-' + return "-" def get_key_from_tuple_by_value(choices: tuple, value) -> Any: @@ -117,4 +117,4 @@ def get_key_from_tuple_by_value(choices: tuple, value) -> Any: try: return [x[0] for x in choices if x[1] == value][0] except IndexError: - return '-' + return "-" diff --git a/ambient_toolbox/utils/string.py b/ambient_toolbox/utils/string.py index df0dd3c..f81c667 100644 --- a/ambient_toolbox/utils/string.py +++ b/ambient_toolbox/utils/string.py @@ -21,22 +21,22 @@ def slugify_file_name(file_name: str, length: int = 40) -> str: Slugify the given file name """ name, ext = os.path.splitext(file_name) - name = smart_str(slugify(name).replace('-', '_')) + name = smart_str(slugify(name).replace("-", "_")) ext = smart_str(slugify(ext)) - result = '{}{}{}'.format(name[:length], "." if ext else "", ext) + result = "{}{}{}".format(name[:length], "." if ext else "", ext) return result -def smart_truncate(text: str, max_length: int = 100, suffix: str = '...') -> str: +def smart_truncate(text: Optional[str], max_length: int = 100, suffix: str = "...") -> str: """ Returns a string of at most `max_length` characters, cutting only at word-boundaries. If the string was truncated, `suffix` will be appended. - In comparison to Djangos default filter `truncatechars` this method does NOT break words and you + In comparison to Django's default filter `truncatechars` this method does NOT break words and you can choose a custom suffix. """ if text is None: - return '' + return "" # Return the string itself if length is smaller or equal to the limit if len(text) <= max_length: @@ -46,10 +46,10 @@ def smart_truncate(text: str, max_length: int = 100, suffix: str = '...') -> str value = text[:max_length] # Break into words and remove the last - words = value.split(' ')[:-1] + words = value.split(" ")[:-1] # Join the words and return - return ' '.join(words) + suffix + return " ".join(words) + suffix def float_to_string(value: Optional[float], replacement: str = "0,00") -> str: @@ -59,7 +59,7 @@ def float_to_string(value: Optional[float], replacement: str = "0,00") -> str: # todo thousand separator would be nice If the passed object is None, it will return `replacement`. """ - return ("%.2f" % value).replace('.', ',') if value is not None else replacement + return ("%.2f" % value).replace(".", ",") if value is not None else replacement def date_to_string(value: Optional[datetime.date], replacement: str = "-", str_format: str = "%d.%m.%Y") -> str: @@ -102,8 +102,8 @@ def encode_to_xml(text: str) -> str: Encodes ampersand, greater and lower characters in a given string to HTML-entities. """ text_str = str(text) - text_str = text_str.replace('&', '&') - text_str = text_str.replace('<', '<') - text_str = text_str.replace('>', '>') + text_str = text_str.replace("&", "&") + text_str = text_str.replace("<", "<") + text_str = text_str.replace(">", ">") return text_str diff --git a/ambient_toolbox/view_layer/form_mixins.py b/ambient_toolbox/view_layer/form_mixins.py index 5730e6c..e678564 100644 --- a/ambient_toolbox/view_layer/form_mixins.py +++ b/ambient_toolbox/view_layer/form_mixins.py @@ -11,12 +11,12 @@ class CrispyLayoutFormMixin: def __init__(self, *args, **kwargs): # Crispy self.helper = FormHelper() - self.helper.form_class = 'form-horizontal form-bordered form-row-stripped' - self.helper.form_method = 'post' - self.helper.add_input(Submit('submit_button', _('Save'))) + self.helper.form_class = "form-horizontal form-bordered form-row-stripped" + self.helper.form_method = "post" + self.helper.add_input(Submit("submit_button", _("Save"))) self.helper.form_tag = True - self.helper.label_class = 'col-md-3' - self.helper.field_class = 'col-md-9' - self.helper.label_size = ' col-md-offset-3' + self.helper.label_class = "col-md-3" + self.helper.field_class = "col-md-9" + self.helper.label_size = " col-md-offset-3" super().__init__(*args, **kwargs) diff --git a/ambient_toolbox/view_layer/formset_mixins.py b/ambient_toolbox/view_layer/formset_mixins.py index 89651f2..d5a7d82 100644 --- a/ambient_toolbox/view_layer/formset_mixins.py +++ b/ambient_toolbox/view_layer/formset_mixins.py @@ -7,6 +7,6 @@ def get_number_of_children(self): # Count all choices which are not being deleted right now no_choices = 0 for form in self.forms: - if getattr(form, 'cleaned_data', None) and not form.cleaned_data.get('DELETE'): + if getattr(form, "cleaned_data", None) and not form.cleaned_data.get("DELETE"): no_choices += 1 return no_choices diff --git a/ambient_toolbox/view_layer/formset_view_mixin.py b/ambient_toolbox/view_layer/formset_view_mixin.py index 704fbee..3eea19b 100644 --- a/ambient_toolbox/view_layer/formset_view_mixin.py +++ b/ambient_toolbox/view_layer/formset_view_mixin.py @@ -13,11 +13,11 @@ class _FormsetMixin: def get_formset_kwargs(self): # may be overridden or extended - return {'instance': self.object} + return {"instance": self.object} def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['formset'] = self.formset_class(**self.get_formset_kwargs()) + context["formset"] = self.formset_class(**self.get_formset_kwargs()) return context def form_valid(self, form, formset): @@ -28,7 +28,7 @@ def form_valid(self, form, formset): formset.instance = self.object formset.save() - if hasattr(self, 'additional_is_valid'): + if hasattr(self, "additional_is_valid"): self.additional_is_valid(form, formset) # Return response (a redirect) @@ -47,8 +47,8 @@ def post(self, request, *args, **kwargs): context = self.get_context_data() # Update form and formset variables - context['form'] = form - context['formset'] = formset + context["form"] = form + context["formset"] = formset # Pass all data to template return render(request, self.get_template_names(), context) diff --git a/ambient_toolbox/view_layer/htmx_mixins.py b/ambient_toolbox/view_layer/htmx_mixins.py index eb54081..2c791fd 100644 --- a/ambient_toolbox/view_layer/htmx_mixins.py +++ b/ambient_toolbox/view_layer/htmx_mixins.py @@ -22,13 +22,13 @@ def dispatch(self, request, *args, **kwargs): # Set redirect header if set if hx_redirect_url: - response['HX-Redirect'] = hx_redirect_url + response["HX-Redirect"] = hx_redirect_url # Set trigger header if set if isinstance(hx_trigger, dict): - response['HX-Trigger'] = json.dumps(hx_trigger) + response["HX-Trigger"] = json.dumps(hx_trigger) elif isinstance(hx_trigger, str): - response['HX-Trigger'] = hx_trigger + response["HX-Trigger"] = hx_trigger # Return augmented response return response diff --git a/ambient_toolbox/view_layer/mixins.py b/ambient_toolbox/view_layer/mixins.py index 586bf0c..bd8e693 100644 --- a/ambient_toolbox/view_layer/mixins.py +++ b/ambient_toolbox/view_layer/mixins.py @@ -11,14 +11,14 @@ class DjangoPermissionRequiredMixin: permission_list = None login_required = True - login_view_name = 'login-view' + login_view_name = "login-view" def __init__(self): super().__init__() if self.permission_list is None: raise RuntimeError( - _('Class-based view using DjangoPermissionRequiredMixin without defining a permission list.') + _("Class-based view using DjangoPermissionRequiredMixin without defining a permission list.") ) def get_login_url(self) -> str: @@ -54,7 +54,7 @@ def dispatch(self, request, *args, **kwargs): # Validate that user has all required permissions if not self.has_permissions(request.user): - return render(request, '403.html', status=403) + return render(request, "403.html", status=403) # If everything goes well, we'll continue to the view return super().dispatch(request, *args, **kwargs) diff --git a/ambient_toolbox/view_layer/tests/mixins.py b/ambient_toolbox/view_layer/tests/mixins.py index 49299e3..ab729ae 100644 --- a/ambient_toolbox/view_layer/tests/mixins.py +++ b/ambient_toolbox/view_layer/tests/mixins.py @@ -24,7 +24,7 @@ def setUpTestData(cls): @classmethod def get_test_user(cls): - return get_user_model().objects.create(username='test_user', email='test.user@ambient-toolbox.com') + return get_user_model().objects.create(username="test_user", email="test.user@ambient-toolbox.com") def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) @@ -33,7 +33,7 @@ def __init__(self, *args, **kwargs) -> None: raise TestSetupConfigurationError(_('BaseViewPermissionTestMixin used without setting a "view_class".')) def get_view_instance( - self, *, user: Union[AbstractBaseUser, AnonymousUser], kwargs: dict = None, method: str = 'GET' + self, *, user: Union[AbstractBaseUser, AnonymousUser], kwargs: dict = None, method: str = "GET" ): """ Creates an instance of the given view class and injects a valid request. @@ -49,8 +49,8 @@ def test_view_class_inherits_mixin(self): def test_permissions_are_equal(self): # Sanity checks - self.assertIsNotNone(self.permission_list, msg='Missing permission list declaration in test.') - self.assertIsNotNone(self.view_class.permission_list, msg='Missing permission list declaration in view.') + self.assertIsNotNone(self.permission_list, msg="Missing permission list declaration in test.") + self.assertIsNotNone(self.view_class.permission_list, msg="Missing permission list declaration in view.") # Assert same amount of permissions self.assertEqual(len(self.permission_list), len(self.view_class.permission_list)) @@ -65,11 +65,11 @@ def test_permissions_are_equal(self): def test_permissions_exist_in_database(self): for permission in self.permission_list: - if '.' not in permission: + if "." not in permission: raise TestSetupConfigurationError( f'View "{self.view_class}" contains ill-formatted permission ' f'"{permission}".' ) - app_label, codename = permission.split('.') + app_label, codename = permission.split(".") permission_qs = Permission.objects.filter(content_type__app_label=app_label, codename=codename) if not permission_qs.exists(): @@ -78,7 +78,7 @@ def test_permissions_exist_in_database(self): ) def test_passes_login_barrier_is_called(self): - with mock.patch.object(self.view_class, 'passes_login_barrier', return_value=False) as mock_method: + with mock.patch.object(self.view_class, "passes_login_barrier", return_value=False) as mock_method: view = self.get_view_instance(user=AnonymousUser()) response = view.dispatch(request=view.request, **view.kwargs) # If a user is not logged in, he'll be forwarded to the login view @@ -87,7 +87,7 @@ def test_passes_login_barrier_is_called(self): mock_method.assert_called_once() def test_has_permissions_is_called(self): - with mock.patch.object(self.view_class, 'has_permissions', return_value=False) as mock_method: + with mock.patch.object(self.view_class, "has_permissions", return_value=False) as mock_method: view = self.get_view_instance(user=self.user) response = view.dispatch(request=view.request, **view.kwargs) self.assertEqual(response.status_code, 403) diff --git a/ambient_toolbox/view_layer/views.py b/ambient_toolbox/view_layer/views.py index 844ffce..5805c58 100644 --- a/ambient_toolbox/view_layer/views.py +++ b/ambient_toolbox/view_layer/views.py @@ -29,7 +29,7 @@ class RequestInFormKwargsMixin: def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs.update({'request': self.request}) + kwargs.update({"request": self.request}) return kwargs @@ -41,7 +41,7 @@ class UserInFormKwargsMixin: def get_form_kwargs(self): kwargs = super().get_form_kwargs() - kwargs.update({'user': self.request.user}) + kwargs.update({"user": self.request.user}) return kwargs @@ -53,7 +53,7 @@ class ToggleView(SingleObjectMixin, generic.View): """ object = None - http_method_names = ('post',) + http_method_names = ("post",) def post(self, request, *args, **kwargs): raise NotImplementedError diff --git a/docs/conf.py b/docs/conf.py index b4b43b5..a38601e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,18 +16,18 @@ import django from django.conf import settings -sys.path.insert(0, os.path.abspath('..')) # so that we can access the "ambient_toolbox" package +sys.path.insert(0, os.path.abspath("..")) # so that we can access the "ambient_toolbox" package settings.configure( INSTALLED_APPS=[ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'ambient_toolbox', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "ambient_toolbox", ], - SECRET_KEY='ASDFjklö123456890', + SECRET_KEY="ASDFjklö123456890", ) django.setup() @@ -35,32 +35,32 @@ # -- Project information ----------------------------------------------------- -project = 'ambient-toolbox' -copyright = '2023, Ambient Innovation: GmbH' # noqa: A001 -author = 'Ambient Innovation: GmbH ' +project = "ambient-toolbox" +copyright = "2023, Ambient Innovation: GmbH" # noqa: A001 +author = "Ambient Innovation: GmbH " version = __version__ release = __version__ # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# extensions coming with Sphinx (named "sphinx.ext.*") or your custom # ones. extensions = [ - 'sphinx_rtd_theme', - 'sphinx.ext.autodoc', - 'm2r2', + "sphinx_rtd_theme", + "sphinx.ext.autodoc", + "m2r2", ] -source_suffix = ['.rst', '.md'] +source_suffix = [".rst", ".md"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # -- Options for HTML output ------------------------------------------------- @@ -68,17 +68,17 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -# html_theme = 'alabaster' -html_theme = 'sphinx_rtd_theme' +# html_theme = "alabaster" +html_theme = "sphinx_rtd_theme" html_theme_options = { - 'display_version': False, - 'style_external_links': False, + "display_version": False, + "style_external_links": False, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Set master doc file -master_doc = 'index' +master_doc = "index" diff --git a/docs/features/gitlab.md b/docs/features/gitlab.md index b860b72..519abb1 100644 --- a/docs/features/gitlab.md +++ b/docs/features/gitlab.md @@ -1,10 +1,10 @@ -# Gitlab +# GitLab ## Test coverage service ### Motivation -When using Gitlab, you can query your projects test coverage via the Gitlab API. This package contains a service which +When using GitLab, you can query your projects test coverage via the GitLab API. This package contains a service which you can utilise within your pipeline as follows. The script will try to get the last commit inside your branch which came from your default branch (usually "develop") @@ -31,7 +31,7 @@ service.process() ```yaml # POST-TEST STAGE check coverage: - image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:3.9 + image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/python:3.12 stage: posttest needs: - unittest @@ -48,7 +48,7 @@ check coverage: * Create an access token for your repo having `developer` role and `read_api` permission (Settings -> Access Tokens) -* Add two variables to your CI/CD inside your Gitlab repository (Settings -> CI/CD -> Variables). Insert the token from +* Add two variables to your CI/CD inside your GitLab repository (Settings -> CI/CD -> Variables). Insert the token from step 3 and define your default branch for comparison. Usually, this will be "develop". > GITLAB_CI_COVERAGE_PIPELINE_TOKEN = [token] diff --git a/manage.py b/manage.py index 1b243ab..9e94ce3 100644 --- a/manage.py +++ b/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") try: from django.core.management import execute_from_command_line except ImportError as exc: @@ -18,5 +18,5 @@ def main(): execute_from_command_line(sys.argv) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/pyproject.toml b/pyproject.toml index b4293cc..b6cf02b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,15 +42,15 @@ dev = [ 'freezegun~=1.2', 'pytest-django~=4.5', 'pytest-mock~=3.10', - 'pre-commit~=3.2', - 'black~=23.3', - 'Django~=3.2', + 'coverage~=7.3', + 'pre-commit~=3.5', + 'ruff~=0.1', 'sphinx==4.2.0', 'sphinx-rtd-theme==1.0.0', 'm2r2==0.3.1', 'mistune<2.0.0', - 'ambient-package-update~=23.10.1', - 'gevent~=22.10', + 'ambient-package-update~=23.11.1', + 'gevent~=23.9', ] drf = [ 'djangorestframework>=3.8.2', @@ -76,15 +76,6 @@ name = "ambient_toolbox" 'Bugtracker' = 'https://github.com/ambient-innovation/ambient-toolbox/issues' 'Changelog' = 'https://ambient-toolbox.readthedocs.io/en/latest/features/changelog.html' - -[tool.black] -# use force-exclude, so that black also applies exclude when run using pre-commit: https://github.com/psf/black/issues/395 -force-exclude = '''.*/migrations/.*''' -line-length = 120 -multi_line_output = 3 -skip-string-normalization = true -include_trailing_comma = true - [tool.ruff] select = [ "E", # pycodestyle errors @@ -96,6 +87,7 @@ select = [ "A", # flake8-builtins "DTZ", # flake8-datetimez "DJ", # flake8-django + "TD", # flake8-to-do "RUF100", # Removes unnecessary "#noqa" comments "YTT", # Avoid non-future-prove usages of "sys" # "FBT", # Protects you from the "boolean trap bug" @@ -103,8 +95,9 @@ select = [ "PIE", # Bunch of useful rules # "SIM", # Simplifies your code "INT", # Validates your gettext translation strings + "PERF", # PerfLint "PGH", # No all-purpose "# noqa" and eval validation - # "UP", # PyUpgrade + "PL", # PyLint ] ignore = [ 'N999', # Project name contains underscore, not fixable @@ -113,6 +106,8 @@ ignore = [ 'B905', # Can be enabled when Python <=3.9 support is dropped 'DTZ001', # TODO will affect "tz_today()" method 'DTZ005', # TODO will affect "tz_today()" method + 'TD002', # Missing author in TODO + 'TD003', # Missing issue link on the line following this TODO ] # Allow autofix for all enabled rules (when `--fix`) is provided. @@ -126,6 +121,7 @@ fixable = [ "A", # flake8-builtins "DTZ", # flake8-datetimez "DJ", # flake8-django + "TD", # flake8-to-do "RUF100", # Removes unnecessary "#noqa" comments "YTT", # Avoid non-future-prove usages of "sys" # "FBT", # Protects you from the "boolean trap bug" @@ -133,8 +129,9 @@ fixable = [ "PIE", # Bunch of useful rules # "SIM", # Simplifies your code "INT", # Validates your gettext translation strings + "PERF", # PerfLint "PGH", # No all-purpose "# noqa" and eval validation - # "UP", # PyUpgrade + "PL", # PyLint ] unfixable = [] @@ -171,6 +168,19 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" # Assume Python 3.12 target-version = "py312" +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + [tool.tox] legacy_tox_ini = """ [tox] @@ -180,12 +190,12 @@ isolated_build = True [testenv] # Django deprecation overview: https://www.djangoproject.com/download/ deps = - django32: Django>=3.2,<3.3 - django41: Django>=4.1,<4.2 - django42: Django>=4.2,<4.3 + django32: Django==3.2.* + django41: Django==4.1.* + django42: Django==4.2.* extras = dev,drf,graphql,sentry,view-layer, commands = - pytest --ds settings tests + coverage run -m pytest --ds settings tests [gh-actions] python = diff --git a/scripts/unix/install_requirements.sh b/scripts/unix/install_requirements.sh new file mode 100644 index 0000000..84a9910 --- /dev/null +++ b/scripts/unix/install_requirements.sh @@ -0,0 +1,3 @@ +pip install -U pip-tools +pip-compile --extra dev,drf,graphql,sentry,view-layer, -o requirements.txt pyproject.toml --resolver=backtracking +pip-sync diff --git a/scripts/windows/install_requirements.ps1 b/scripts/windows/install_requirements.ps1 new file mode 100644 index 0000000..84a9910 --- /dev/null +++ b/scripts/windows/install_requirements.ps1 @@ -0,0 +1,3 @@ +pip install -U pip-tools +pip-compile --extra dev,drf,graphql,sentry,view-layer, -o requirements.txt pyproject.toml --resolver=backtracking +pip-sync diff --git a/settings.py b/settings.py index 116bd94..5b55e87 100644 --- a/settings.py +++ b/settings.py @@ -3,64 +3,66 @@ BASE_PATH = Path(__file__).resolve(strict=True).parent INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'testapp', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "testapp", ) DEBUG = False -ALLOWED_HOSTS = ['localhost:8000'] +ALLOWED_HOSTS = ["localhost:8000"] -SECRET_KEY = 'ASDFjklö123456890' +SECRET_KEY = "ASDFjklö123456890" # Routing -ROOT_URLCONF = 'testapp.urls' +ROOT_URLCONF = "testapp.urls" -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +STATIC_URL = "/static/" + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'db.sqlite', + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": "db.sqlite", } } TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': ['templates'], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ], - 'debug': True, + "debug": True, }, }, ] MIDDLEWARE = ( - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ) # Mail backend -EMAIL_BACKEND_DOMAIN_WHITELIST = '' -EMAIL_BACKEND_REDIRECT_ADDRESS = '' +EMAIL_BACKEND_DOMAIN_WHITELIST = "" +EMAIL_BACKEND_REDIRECT_ADDRESS = "" -TIME_ZONE = 'UTC' +TIME_ZONE = "UTC" -LOCALE_PATHS = [str(BASE_PATH) + '/ambient_toolbox/locale'] +LOCALE_PATHS = [str(BASE_PATH) + "/ambient_toolbox/locale"] diff --git a/setup.cfg b/setup.cfg index ef803ed..6da4c9b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,3 +1,18 @@ [metadata] description-file = README.md license-file = LICENSE.md + +[coverage:run] +branch = True +parallel = True +source = + ambient_toolbox + tests + +[coverage:paths] +source = + ambient_toolbox + .tox/**/site-packages + +[coverage:report] +show_missing = True diff --git a/testapp/api/serializers.py b/testapp/api/serializers.py index 9e01499..7a18592 100644 --- a/testapp/api/serializers.py +++ b/testapp/api/serializers.py @@ -7,6 +7,6 @@ class MySingleSignalModelSerializer(serializers.ModelSerializer): class Meta: model = MySingleSignalModel fields = [ - 'id', - 'value', + "id", + "value", ] diff --git a/testapp/api/urls.py b/testapp/api/urls.py index 5d2a516..24c0724 100644 --- a/testapp/api/urls.py +++ b/testapp/api/urls.py @@ -3,4 +3,4 @@ from testapp.api.views import MySingleSignalModelViewSet model_router = DefaultRouter() -model_router.register(r'my-single-signal-model', MySingleSignalModelViewSet, basename='my-single-signal-model') +model_router.register(r"my-single-signal-model", MySingleSignalModelViewSet, basename="my-single-signal-model") diff --git a/testapp/forms.py b/testapp/forms.py index 1d5eead..96ddefc 100644 --- a/testapp/forms.py +++ b/testapp/forms.py @@ -6,4 +6,4 @@ class CommonInfoBasedModelTestForm(forms.ModelForm): class Meta: model = CommonInfoBasedModel - fields = ('value',) + fields = ("value",) diff --git a/testapp/migrations/0001_initial.py b/testapp/migrations/0001_initial.py index 74f4923..925691c 100644 --- a/testapp/migrations/0001_initial.py +++ b/testapp/migrations/0001_initial.py @@ -130,7 +130,7 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='testapp_commoninfobasedmodel_created', + related_name='%(app_label)s_%(class)s_created', to=settings.AUTH_USER_MODEL, verbose_name='Created by', ), @@ -141,7 +141,7 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='testapp_commoninfobasedmodel_lastmodified', + related_name='%(app_label)s_%(class)s_lastmodified', to=settings.AUTH_USER_MODEL, verbose_name='Last modified by', ), diff --git a/testapp/models.py b/testapp/models.py index 36bc0ad..324a97a 100644 --- a/testapp/models.py +++ b/testapp/models.py @@ -22,7 +22,7 @@ def __str__(self): class ForeignKeyRelatedModel(models.Model): single_signal = models.ForeignKey( - MySingleSignalModel, on_delete=models.CASCADE, related_name='foreign_key_related_models' + MySingleSignalModel, on_delete=models.CASCADE, related_name="foreign_key_related_models" ) objects = GloballyVisibleQuerySet.as_manager() @@ -43,7 +43,7 @@ def __str__(self): return str(self.value) -@receiver(pre_save, sender=MyMultipleSignalModel, dispatch_uid='test.mysinglesignalmodel.increase_value_with_uuid') +@receiver(pre_save, sender=MyMultipleSignalModel, dispatch_uid="test.mysinglesignalmodel.increase_value_with_uuid") def increase_value_with_dispatch_uid(sender, instance, **kwargs): instance.value += 1 @@ -51,7 +51,7 @@ def increase_value_with_dispatch_uid(sender, instance, **kwargs): @receiver(post_save, sender=MyMultipleSignalModel) def send_email(sender, instance, **kwargs): msg = EmailMultiAlternatives( - 'Test Mail', 'I am content', from_email='test@example.com', to=['random.dude@example.com'] + "Test Mail", "I am content", from_email="test@example.com", to=["random.dude@example.com"] ) msg.send() @@ -75,14 +75,14 @@ def __str__(self): class ModelWithFkToSelf(models.Model): - parent = models.ForeignKey('self', blank=True, null=True, related_name='children', on_delete=models.CASCADE) + parent = models.ForeignKey("self", blank=True, null=True, related_name="children", on_delete=models.CASCADE) def __str__(self): return str(self.id) class ModelWithOneToOneToSelf(models.Model): - peer = models.OneToOneField('self', blank=True, null=True, related_name='related_peer', on_delete=models.CASCADE) + peer = models.OneToOneField("self", blank=True, null=True, related_name="related_peer", on_delete=models.CASCADE) def __str__(self): return str(self.id) diff --git a/testapp/permissions.py b/testapp/permissions.py index e83147c..a465a20 100644 --- a/testapp/permissions.py +++ b/testapp/permissions.py @@ -2,11 +2,11 @@ class TestGroupDeclaration(GroupPermissionDeclaration): - name = ('group_1',) + name = ("group_1",) permission_list = [ PermissionModelDeclaration( - app_label='testapp', - codename_list=['view_mysinglesignalmodel'], - model='mysinglesignalmodel', + app_label="testapp", + codename_list=["view_mysinglesignalmodel"], + model="mysinglesignalmodel", ), ] diff --git a/testapp/urls.py b/testapp/urls.py index b24d26b..ca9e2c7 100644 --- a/testapp/urls.py +++ b/testapp/urls.py @@ -10,9 +10,9 @@ urlpatterns = [ # django Admin - path('admin/', admin.site.urls), + path("admin/", admin.site.urls), # REST Viewsets - path('api/v1/', include(router.urls)), + path("api/v1/", include(router.urls)), # Custom login view - path("other/login/", TemplateView.as_view(template_name="testapp/test_template.html"), name='other-login-view'), + path("other/login/", TemplateView.as_view(template_name="testapp/test_template.html"), name="other-login-view"), ] diff --git a/tests/admin/model_admin_mixins/test_admin_common_info_mixin.py b/tests/admin/model_admin_mixins/test_admin_common_info_mixin.py index c27585d..bc5dff9 100644 --- a/tests/admin/model_admin_mixins/test_admin_common_info_mixin.py +++ b/tests/admin/model_admin_mixins/test_admin_common_info_mixin.py @@ -13,7 +13,7 @@ class CommonInfoBasedModelForm(forms.ModelForm): class Meta: model = CommonInfoBasedModel - fields = ('value',) + fields = ("value",) class TestCommonInfoAdminMixinAdmin(CommonInfoAdminMixin, admin.ModelAdmin): @@ -25,7 +25,7 @@ class CommonInfoAdminMixinTest(RequestProviderMixin, TestCase): def setUpTestData(cls): super().setUpTestData() - cls.user = User.objects.create(username='my_user') + cls.user = User.objects.create(username="my_user") cls.request = cls.get_request(cls.user) admin.site.register(CommonInfoBasedModel, TestCommonInfoAdminMixinAdmin) @@ -39,11 +39,11 @@ def tearDownClass(cls): def test_readonly_fields_are_set(self): model_admin = TestCommonInfoAdminMixinAdmin(model=CommonInfoBasedModel, admin_site=admin.site) - self.assertIn('created_by', model_admin.get_readonly_fields(self.request)) - self.assertIn('created_at', model_admin.get_readonly_fields(self.request)) + self.assertIn("created_by", model_admin.get_readonly_fields(self.request)) + self.assertIn("created_at", model_admin.get_readonly_fields(self.request)) - self.assertIn('lastmodified_by', model_admin.get_readonly_fields(self.request)) - self.assertIn('lastmodified_at', model_admin.get_readonly_fields(self.request)) + self.assertIn("lastmodified_by", model_admin.get_readonly_fields(self.request)) + self.assertIn("lastmodified_at", model_admin.get_readonly_fields(self.request)) def test_get_user_obj_regular(self): model_admin = TestCommonInfoAdminMixinAdmin(model=CommonInfoBasedModel, admin_site=admin.site) @@ -61,8 +61,8 @@ def test_created_by_is_set_on_creation(self): def test_created_by_is_not_altered_on_update(self): model_admin = TestCommonInfoAdminMixinAdmin(model=CommonInfoBasedModel, admin_site=admin.site) - other_user = User.objects.create(username='other_user') - with mock.patch.object(CommonInfoBasedModel, 'get_current_user', return_value=other_user): + other_user = User.objects.create(username="other_user") + with mock.patch.object(CommonInfoBasedModel, "get_current_user", return_value=other_user): obj = CommonInfoBasedModel.objects.create(value=1, created_by=other_user, lastmodified_by=other_user) form = CommonInfoBasedModelForm(instance=obj) diff --git a/tests/admin/model_admin_mixins/test_admin_create_form_mixin.py b/tests/admin/model_admin_mixins/test_admin_create_form_mixin.py index 368262c..0c7a44b 100644 --- a/tests/admin/model_admin_mixins/test_admin_create_form_mixin.py +++ b/tests/admin/model_admin_mixins/test_admin_create_form_mixin.py @@ -22,7 +22,7 @@ class AdminCreateFormMixinTest(RequestProviderMixin, TestCase): def setUpTestData(cls): super().setUpTestData() - cls.super_user = User.objects.create(username='super_user', is_superuser=True) + cls.super_user = User.objects.create(username="super_user", is_superuser=True) admin.site.register(ForeignKeyRelatedModel, TestAdminCreateFormMixinAdmin) diff --git a/tests/admin/model_admin_mixins/test_admin_no_inlines_for_create_mixin.py b/tests/admin/model_admin_mixins/test_admin_no_inlines_for_create_mixin.py index a249ffa..0505441 100644 --- a/tests/admin/model_admin_mixins/test_admin_no_inlines_for_create_mixin.py +++ b/tests/admin/model_admin_mixins/test_admin_no_inlines_for_create_mixin.py @@ -20,7 +20,7 @@ class AdminNoInlinesForCreateMixinTest(RequestProviderMixin, TestCase): def setUpTestData(cls): super().setUpTestData() - cls.super_user = User.objects.create(username='super_user', is_superuser=True) + cls.super_user = User.objects.create(username="super_user", is_superuser=True) admin.site.register(MySingleSignalModel, TestAdminNoInlinesForCreateMixinAdmin) diff --git a/tests/admin/model_admin_mixins/test_admin_request_in_form_mixin.py b/tests/admin/model_admin_mixins/test_admin_request_in_form_mixin.py index 4a18390..15d76f1 100644 --- a/tests/admin/model_admin_mixins/test_admin_request_in_form_mixin.py +++ b/tests/admin/model_admin_mixins/test_admin_request_in_form_mixin.py @@ -17,7 +17,7 @@ class AdminRequestInFormMixinTest(RequestProviderMixin, TestCase): def setUpTestData(cls): super().setUpTestData() - cls.super_user = User.objects.create(username='super_user', is_superuser=True) + cls.super_user = User.objects.create(username="super_user", is_superuser=True) admin.site.register(MySingleSignalModel, TestAdminRequestInFormMixinAdmin) diff --git a/tests/admin/model_admin_mixins/test_classes.py b/tests/admin/model_admin_mixins/test_classes.py new file mode 100644 index 0000000..87395f5 --- /dev/null +++ b/tests/admin/model_admin_mixins/test_classes.py @@ -0,0 +1,127 @@ +from unittest import mock + +from django.contrib import admin +from django.contrib.admin import AdminSite, ModelAdmin +from django.contrib.auth.models import User +from django.core.handlers.wsgi import WSGIRequest +from django.test import RequestFactory, TestCase + +from ambient_toolbox.admin.model_admins.classes import EditableOnlyAdmin, ReadOnlyAdmin +from ambient_toolbox.tests.mixins import RequestProviderMixin +from testapp.models import MyMultipleSignalModel, MySingleSignalModel + + +class TestReadOnlyAdmin(ReadOnlyAdmin): + model = MySingleSignalModel + + +class TestEditableOnlyAdmin(EditableOnlyAdmin): + model = MySingleSignalModel + + +class AdminClassesTest(RequestProviderMixin, TestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + cls.super_user = User.objects.create(username="super_user", is_superuser=True) + + admin.site.register(MySingleSignalModel, TestReadOnlyAdmin) + admin.site.register(MyMultipleSignalModel, TestEditableOnlyAdmin) + + @classmethod + def tearDownClass(cls): + super().tearDownClass() + + admin.site.unregister(MySingleSignalModel) + admin.site.unregister(MyMultipleSignalModel) + + def test_read_only_admin_all_fields_readonly(self): + obj = MySingleSignalModel(value=1) + + admin_class = TestReadOnlyAdmin(model=obj, admin_site=admin.site) + readonly_fields = admin_class.get_readonly_fields(request=self.get_request(), obj=obj) + + self.assertEqual(len(readonly_fields), 2) + self.assertIn("id", readonly_fields) + self.assertIn("value", readonly_fields) + + def test_read_only_admin_no_change_permissions(self): + admin_class = TestReadOnlyAdmin(model=MySingleSignalModel, admin_site=admin.site) + + request = self.get_request(self.super_user) + + self.assertFalse(admin_class.has_add_permission(request)) + self.assertFalse(admin_class.has_change_permission(request)) + self.assertFalse(admin_class.has_delete_permission(request)) + + def test_editable_only_admin_delete_action_removed(self): + obj = MyMultipleSignalModel(value=1) + admin_class = TestEditableOnlyAdmin(model=obj, admin_site=admin.site) + + request = self.get_request(self.super_user) + actions = admin_class.get_actions(request=request) + + self.assertNotIn("delete_selected", actions) + + def test_editable_only_admin_no_change_permissions(self): + admin_class = TestEditableOnlyAdmin(model=MyMultipleSignalModel, admin_site=admin.site) + + request = self.get_request(self.super_user) + + self.assertTrue(admin_class.has_change_permission(request)) + + self.assertFalse(admin_class.has_add_permission(request)) + self.assertFalse(admin_class.has_delete_permission(request)) + + +class ReadOnlyAdminTest(TestCase): + user: User + request: WSGIRequest + + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user(username="testuser", password="testpassword", is_superuser=True) + + factory = RequestFactory() + cls.request = factory.get(f"/admin/auth/user/{cls.user.id}/change/") + cls.request.user = cls.user + + def test_changeform_view_regular(self): + model_admin = ReadOnlyAdmin(User, AdminSite()) + response = model_admin.changeform_view(self.request, str(self.user.id)) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, "Save and continue editing") + self.assertNotContains(response, "Save") + + def test_has_add_permission_regular(self): + model_admin = ReadOnlyAdmin(User, AdminSite()) + self.assertFalse(model_admin.has_add_permission(self.request)) + + def test_has_change_permission_regular(self): + model_admin = ReadOnlyAdmin(User, AdminSite()) + self.assertFalse(model_admin.has_change_permission(self.request)) + + def test_has_delete_permission_regular(self): + model_admin = ReadOnlyAdmin(User, AdminSite()) + self.assertFalse(model_admin.has_delete_permission(self.request)) + + +class EditableOnlyAdminTest(TestCase): + user: User + request: WSGIRequest + + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user(username="testuser", password="testpassword", is_superuser=True) + + factory = RequestFactory() + cls.request = factory.get(f"/admin/auth/user/{cls.user.id}/change/") + cls.request.user = cls.user + + @mock.patch.object(ModelAdmin, "get_actions", return_value={"delete_selected": 1}) + def test_get_actions_regular(self, *args): + model_admin = EditableOnlyAdmin(User, AdminSite()) + actions = model_admin.get_actions(self.request) + self.assertIsInstance(actions, dict) + self.assertNotIn("delete_selected", actions) diff --git a/tests/admin/model_admin_mixins/test_deactivatable_change_view_admin_mixin.py b/tests/admin/model_admin_mixins/test_deactivatable_change_view_admin_mixin.py index 1cdc7bb..0095d1d 100644 --- a/tests/admin/model_admin_mixins/test_deactivatable_change_view_admin_mixin.py +++ b/tests/admin/model_admin_mixins/test_deactivatable_change_view_admin_mixin.py @@ -33,14 +33,14 @@ def test_can_see_change_view_negative_flag(self): def test_get_list_display_links_can_see_method_called(self): admin_cls = TestAdmin(admin_site=None, model=User) - with mock.patch.object(admin_cls, 'can_see_change_view', return_value=True) as mock_method: - admin_cls.get_list_display_links(request=self.get_request(user=self.user), list_display=('first_name',)) + with mock.patch.object(admin_cls, "can_see_change_view", return_value=True) as mock_method: + admin_cls.get_list_display_links(request=self.get_request(user=self.user), list_display=("first_name",)) mock_method.assert_called_once() def test_get_list_display_links_can_see_method_positive_flag(self): admin_cls = TestAdmin(admin_site=None, model=User) - field_tuple = ('first_name',) + field_tuple = ("first_name",) self.assertEqual( list(field_tuple), admin_cls.get_list_display_links(request=self.get_request(user=self.user), list_display=field_tuple), @@ -50,13 +50,13 @@ def test_get_list_display_links_can_see_method_negative_flag(self): admin_cls = TestAdmin(admin_site=None, model=User) admin_cls.enable_change_view = False self.assertIsNone( - admin_cls.get_list_display_links(request=self.get_request(user=self.user), list_display=('first_name',)) + admin_cls.get_list_display_links(request=self.get_request(user=self.user), list_display=("first_name",)) ) def test_change_view_can_see_method_called_because_of_positive_flag(self): admin_cls = TestAdmin(admin_site=None, model=User) - with mock.patch.object(admin_cls, 'can_see_change_view', return_value=True) as mocked_can_see_method: - with mock.patch('django.contrib.admin.ModelAdmin.change_view') as mocked_base_change_view: + with mock.patch.object(admin_cls, "can_see_change_view", return_value=True) as mocked_can_see_method: + with mock.patch("django.contrib.admin.ModelAdmin.change_view") as mocked_base_change_view: admin_cls.change_view(request=self.get_request(user=self.super_user), object_id=str(self.user.id)) mocked_can_see_method.assert_called_once() @@ -64,8 +64,8 @@ def test_change_view_can_see_method_called_because_of_positive_flag(self): def test_change_view_can_see_method_not_called_because_of_negative_flag(self): admin_cls = TestAdmin(admin_site=None, model=User) - with mock.patch.object(admin_cls, 'can_see_change_view', return_value=False) as mocked_can_see_method: - with mock.patch('django.contrib.admin.ModelAdmin.change_view') as mocked_base_change_view: + with mock.patch.object(admin_cls, "can_see_change_view", return_value=False) as mocked_can_see_method: + with mock.patch("django.contrib.admin.ModelAdmin.change_view") as mocked_base_change_view: admin_cls.change_view(request=self.get_request(user=self.super_user), object_id=str(self.user.id)) mocked_can_see_method.assert_called_once() diff --git a/tests/admin/model_admin_mixins/test_fetch_object_mixin.py b/tests/admin/model_admin_mixins/test_fetch_object_mixin.py index 8d19756..2b919cc 100644 --- a/tests/admin/model_admin_mixins/test_fetch_object_mixin.py +++ b/tests/admin/model_admin_mixins/test_fetch_object_mixin.py @@ -22,7 +22,7 @@ class FetchObjectMixinTest(RequestProviderMixin, TestCase): def setUpTestData(cls): super().setUpTestData() - cls.super_user = User.objects.create(username='super_user', is_superuser=True) + cls.super_user = User.objects.create(username="super_user", is_superuser=True) admin.site.register(MySingleSignalModel, TestFetchObjectMixinAdmin) @@ -39,8 +39,8 @@ def test_model_is_set(self): request = self.get_request(self.super_user) return_obj = MockResolverResponse() - return_obj.kwargs = {'object_id': obj.id} - with mock.patch('ambient_toolbox.admin.model_admins.mixins.resolve', return_value=return_obj): + return_obj.kwargs = {"object_id": obj.id} + with mock.patch("ambient_toolbox.admin.model_admins.mixins.resolve", return_value=return_obj): obj_from_request = model_admin.get_object_from_request(request) self.assertEqual(obj_from_request, obj) diff --git a/tests/admin/model_admin_mixins/test_fetch_parent_object_inline_mixin.py b/tests/admin/model_admin_mixins/test_fetch_parent_object_inline_mixin.py index d13cef6..610881a 100644 --- a/tests/admin/model_admin_mixins/test_fetch_parent_object_inline_mixin.py +++ b/tests/admin/model_admin_mixins/test_fetch_parent_object_inline_mixin.py @@ -26,7 +26,7 @@ class FetchParentObjectInlineMixinTest(RequestProviderMixin, TestCase): def setUpTestData(cls): super().setUpTestData() - cls.super_user = User.objects.create(username='super_user', is_superuser=True) + cls.super_user = User.objects.create(username="super_user", is_superuser=True) admin.site.register(MySingleSignalModel, TestFetchParentObjectInlineMixinAdmin) @@ -48,8 +48,8 @@ def test_parent_model_is_set(self): inline = inline_list[0](parent_model=MySingleSignalModel, admin_site=admin.site) return_obj = MockResolverResponse() - return_obj.kwargs = {'object_id': obj.id} - with mock.patch.object(model_admin.inlines[0], '_resolve_url', return_value=return_obj): + return_obj.kwargs = {"object_id": obj.id} + with mock.patch.object(model_admin.inlines[0], "_resolve_url", return_value=return_obj): inline.get_formset(request=request, obj=obj) self.assertEqual(inline.parent_object, obj) diff --git a/tests/ambient_toolbox/test_test_structure_validator.py b/tests/ambient_toolbox/test_test_structure_validator.py index ff94ab1..1cfcf79 100644 --- a/tests/ambient_toolbox/test_test_structure_validator.py +++ b/tests/ambient_toolbox/test_test_structure_validator.py @@ -1,4 +1,5 @@ from pathlib import Path +from unittest import mock from django.conf import settings from django.test import TestCase, override_settings @@ -10,21 +11,21 @@ class TestStructureValidatorTest(TestCase): def test_init_regular(self): service = TestStructureValidator() - self.assertEqual(service.file_whitelist, ['__init__']) + self.assertEqual(service.file_whitelist, ["__init__"]) self.assertEqual(service.issue_list, []) - @override_settings(TEST_STRUCTURE_VALIDATOR_FILE_WHITELIST=['my_file']) + @override_settings(TEST_STRUCTURE_VALIDATOR_FILE_WHITELIST=["my_file"]) def test_get_file_whitelist_from_settings(self): service = TestStructureValidator() file_whitelist = service._get_file_whitelist() - self.assertEqual(file_whitelist, ['__init__', 'my_file']) + self.assertEqual(file_whitelist, ["__init__", "my_file"]) def test_get_file_whitelist_fallback(self): service = TestStructureValidator() file_whitelist = service._get_file_whitelist() - self.assertEqual(file_whitelist, ['__init__']) + self.assertEqual(file_whitelist, ["__init__"]) @override_settings(TEST_STRUCTURE_VALIDATOR_BASE_DIR=settings.BASE_PATH) def test_get_base_dir_from_settings(self): @@ -37,27 +38,27 @@ def test_get_base_dir_fallback(self): service = TestStructureValidator() base_dir = service._get_base_dir() - self.assertEqual(base_dir, '') + self.assertEqual(base_dir, "") - @override_settings(TEST_STRUCTURE_VALIDATOR_BASE_APP_NAME='my_project') + @override_settings(TEST_STRUCTURE_VALIDATOR_BASE_APP_NAME="my_project") def test_get_base_app_name_from_settings(self): service = TestStructureValidator() base_app_name = service._get_base_app_name() - self.assertEqual(base_app_name, 'my_project') + self.assertEqual(base_app_name, "my_project") def test_get_base_app_name_fallback(self): service = TestStructureValidator() base_app_name = service._get_base_app_name() - self.assertEqual(base_app_name, 'apps') + self.assertEqual(base_app_name, "apps") - @override_settings(TEST_STRUCTURE_VALIDATOR_APP_LIST=['apps.my_app', 'apps.other_app']) + @override_settings(TEST_STRUCTURE_VALIDATOR_APP_LIST=["apps.my_app", "apps.other_app"]) def test_get_app_list_from_settings(self): service = TestStructureValidator() base_app_name = service._get_app_list() - self.assertEqual(base_app_name, ['apps.my_app', 'apps.other_app']) + self.assertEqual(base_app_name, ["apps.my_app", "apps.other_app"]) def test_get_app_list_fallback(self): service = TestStructureValidator() @@ -65,39 +66,39 @@ def test_get_app_list_fallback(self): self.assertEqual(base_app_name, settings.INSTALLED_APPS) - @override_settings(TEST_STRUCTURE_VALIDATOR_IGNORED_DIRECTORY_LIST=['my_dir', 'other_dir']) + @override_settings(TEST_STRUCTURE_VALIDATOR_IGNORED_DIRECTORY_LIST=["my_dir", "other_dir"]) def test_get_ignored_directory_list_from_settings(self): service = TestStructureValidator() dir_list = service._get_ignored_directory_list() - self.assertEqual(dir_list, ['__pycache__', 'my_dir', 'other_dir']) + self.assertEqual(dir_list, ["__pycache__", "my_dir", "other_dir"]) def test_get_ignored_directory_list_fallback(self): service = TestStructureValidator() dir_list = service._get_ignored_directory_list() - self.assertEqual(dir_list, ['__pycache__']) + self.assertEqual(dir_list, ["__pycache__"]) def test_check_missing_test_prefix_correct_prefix(self): service = TestStructureValidator() result = service._check_missing_test_prefix( - root='root/path', - file='missing_prefix', - filename='test_my_file', - extension='.py', + root="root/path", + file="missing_prefix", + filename="test_my_file", + extension=".py", ) self.assertTrue(result) self.assertEqual(len(service.issue_list), 0) - @override_settings(TEST_STRUCTURE_VALIDATOR_FILE_WHITELIST=['my_file']) + @override_settings(TEST_STRUCTURE_VALIDATOR_FILE_WHITELIST=["my_file"]) def test_check_missing_test_prefix_wrong_prefix_but_whitelisted(self): service = TestStructureValidator() result = service._check_missing_test_prefix( - root='root/path', - file='missing_prefix', - filename='my_file', - extension='.py', + root="root/path", + file="missing_prefix", + filename="my_file", + extension=".py", ) self.assertTrue(result) @@ -106,10 +107,10 @@ def test_check_missing_test_prefix_wrong_prefix_but_whitelisted(self): def test_check_missing_test_prefix_wrong_prefix_but_not_py_file(self): service = TestStructureValidator() result = service._check_missing_test_prefix( - root='root/path', - file='missing_prefix', - filename='missing_prefix', - extension='.txt', + root="root/path", + file="missing_prefix", + filename="missing_prefix", + extension=".txt", ) self.assertTrue(result) @@ -118,10 +119,10 @@ def test_check_missing_test_prefix_wrong_prefix_but_not_py_file(self): def test_check_missing_test_prefix_wrong_prefix(self): service = TestStructureValidator() result = service._check_missing_test_prefix( - root='root/path', - file='missing_prefix', - filename='missing_prefix', - extension='.py', + root="root/path", + file="missing_prefix", + filename="missing_prefix", + extension=".py", ) self.assertFalse(result) @@ -129,54 +130,54 @@ def test_check_missing_test_prefix_wrong_prefix(self): def test_check_missing_init_init_found_files_in_dir(self): service = TestStructureValidator() - result = service._check_missing_init(root='root/path', init_found=True, number_of_py_files=1) + result = service._check_missing_init(root="root/path", init_found=True, number_of_py_files=1) self.assertTrue(result) self.assertEqual(len(service.issue_list), 0) def test_check_missing_init_no_init_no_files(self): service = TestStructureValidator() - result = service._check_missing_init(root='root/path', init_found=False, number_of_py_files=0) + result = service._check_missing_init(root="root/path", init_found=False, number_of_py_files=0) self.assertTrue(result) self.assertEqual(len(service.issue_list), 0) def test_check_missing_init_no_init_but_files(self): service = TestStructureValidator() - result = service._check_missing_init(root='root/path', init_found=False, number_of_py_files=1) + result = service._check_missing_init(root="root/path", init_found=False, number_of_py_files=1) self.assertFalse(result) self.assertEqual(len(service.issue_list), 1) @override_settings( - TEST_STRUCTURE_VALIDATOR_BASE_DIR=Path('/src/ambient_toolbox/'), - TEST_STRUCTURE_VALIDATOR_BASE_APP_NAME='my_project', + TEST_STRUCTURE_VALIDATOR_BASE_DIR=Path("/src/ambient_toolbox/"), + TEST_STRUCTURE_VALIDATOR_BASE_APP_NAME="my_project", ) def test_build_path_to_test_package_with_settings_path(self): service = TestStructureValidator() - path = service._build_path_to_test_package(app='my_project.my_app') + path = service._build_path_to_test_package(app="my_project.my_app") - self.assertEqual(path, Path('/src/ambient_toolbox/my_project/my_app/tests')) + self.assertEqual(path, Path("/src/ambient_toolbox/my_project/my_app/tests")) @override_settings( - TEST_STRUCTURE_VALIDATOR_BASE_DIR='/src/ambient_toolbox/', TEST_STRUCTURE_VALIDATOR_BASE_APP_NAME='my_project' + TEST_STRUCTURE_VALIDATOR_BASE_DIR="/src/ambient_toolbox/", TEST_STRUCTURE_VALIDATOR_BASE_APP_NAME="my_project" ) def test_build_path_to_test_package_with_settings_str(self): service = TestStructureValidator() - path = service._build_path_to_test_package(app='my_project.my_app') + path = service._build_path_to_test_package(app="my_project.my_app") - self.assertEqual(path, Path('/src/ambient_toolbox/my_project/my_app/tests')) + self.assertEqual(path, Path("/src/ambient_toolbox/my_project/my_app/tests")) def test_build_path_to_test_package_with_defaults(self): service = TestStructureValidator() - path = service._build_path_to_test_package(app='my_project.my_app') + path = service._build_path_to_test_package(app="my_project.my_app") - self.assertEqual(path, Path('my_project/my_app/tests')) + self.assertEqual(path, Path("my_project/my_app/tests")) @override_settings( TEST_STRUCTURE_VALIDATOR_BASE_DIR=settings.BASE_PATH, - TEST_STRUCTURE_VALIDATOR_APP_LIST=['testapp'], - TEST_STRUCTURE_VALIDATOR_BASE_APP_NAME='', + TEST_STRUCTURE_VALIDATOR_APP_LIST=["testapp"], + TEST_STRUCTURE_VALIDATOR_BASE_APP_NAME="", ) def test_process_functional(self): service = TestStructureValidator() @@ -188,7 +189,15 @@ def test_process_functional(self): complaint_list = sorted(service.issue_list) self.assertIn('Python file without "test_" prefix found:', complaint_list[0]) - self.assertIn('testapp/tests/subdirectory/missing_test_prefix.py', complaint_list[0]) + self.assertIn("testapp/tests/subdirectory/missing_test_prefix.py", complaint_list[0]) - self.assertIn('__init__.py missing in', complaint_list[1]) - self.assertIn('testapp/tests/missing_init', complaint_list[1]) + self.assertIn("__init__.py missing in", complaint_list[1]) + self.assertIn("testapp/tests/missing_init", complaint_list[1]) + + @mock.patch.object(TestStructureValidator, "_get_app_list", return_value=["invalidly_located_app"]) + def test_process_invalidly_located_app(self, mocked_get_app_list): + service = TestStructureValidator() + + service.process() + + mocked_get_app_list.assert_called_once() diff --git a/tests/drf/test_fields.py b/tests/drf/test_fields.py index 50c1999..83e4742 100644 --- a/tests/drf/test_fields.py +++ b/tests/drf/test_fields.py @@ -1,6 +1,9 @@ +from unittest import mock + +import pytest from django.test import TestCase from rest_framework import serializers -from rest_framework.serializers import ListSerializer +from rest_framework.serializers import ListSerializer, Serializer from ambient_toolbox.drf.fields import RecursiveField from testapp.models import ModelWithFkToSelf, ModelWithOneToOneToSelf @@ -12,29 +15,44 @@ class TestManyTrueSerializer(serializers.ModelSerializer): class Meta: model = ModelWithFkToSelf fields = [ - 'id', - 'children', + "id", + "children", ] +@pytest.mark.skip class TestManyFalseSerializer(serializers.ModelSerializer): peer = RecursiveField() class Meta: model = ModelWithOneToOneToSelf fields = [ - 'id', - 'peer', + "id", + "peer", ] class RecursiveFieldTest(TestCase): + @mock.patch.object(Serializer, "update") + def test_update_super_called(self, mocked_update): + field = RecursiveField() + field.update(instance=None, validated_data={}) + + mocked_update.assert_called_once_with(None, {}) + + @mock.patch.object(Serializer, "create") + def test_create_super_called(self, mocked_create): + field = RecursiveField() + field.create(validated_data={}) + + mocked_create.assert_called_once_with({}) + def test_many_true_regular(self): serializer = TestManyTrueSerializer() - self.assertIn('children', serializer.fields) - self.assertIsInstance(serializer.fields['children'], ListSerializer) - self.assertIsInstance(serializer.fields['children'].child, RecursiveField) + self.assertIn("children", serializer.fields) + self.assertIsInstance(serializer.fields["children"], ListSerializer) + self.assertIsInstance(serializer.fields["children"].child, RecursiveField) def test_many_true_representation(self): mwfts_1 = ModelWithFkToSelf.objects.create(parent=None) @@ -44,16 +62,16 @@ def test_many_true_representation(self): representation = serializer.to_representation(instance=mwfts_1) self.assertIsInstance(representation, dict) - self.assertIn('children', representation) - self.assertEqual(len(representation['children']), 1) - self.assertEqual(representation['children'][0]['id'], mwfts_2.id) - self.assertEqual(representation['children'][0]['children'], []) + self.assertIn("children", representation) + self.assertEqual(len(representation["children"]), 1) + self.assertEqual(representation["children"][0]["id"], mwfts_2.id) + self.assertEqual(representation["children"][0]["children"], []) def test_many_false_regular(self): serializer = TestManyFalseSerializer() - self.assertIn('peer', serializer.fields) - self.assertIsInstance(serializer.fields['peer'], RecursiveField) + self.assertIn("peer", serializer.fields) + self.assertIsInstance(serializer.fields["peer"], RecursiveField) def test_many_false_representation(self): mwotos_no_peer = ModelWithOneToOneToSelf.objects.create(peer=None) @@ -63,7 +81,7 @@ def test_many_false_representation(self): representation = serializer.to_representation(instance=mwotos_has_peer) self.assertIsInstance(representation, dict) - self.assertIn('peer', representation) - self.assertEqual(len(representation['peer']), 2) - self.assertEqual(representation['peer']['id'], mwotos_no_peer.id) - self.assertEqual(representation['peer']['peer'], None) + self.assertIn("peer", representation) + self.assertEqual(len(representation["peer"]), 2) + self.assertEqual(representation["peer"]["id"], mwotos_no_peer.id) + self.assertEqual(representation["peer"]["peer"], None) diff --git a/tests/mixins/validation.py b/tests/mixins/test_validation.py similarity index 62% rename from tests/mixins/validation.py rename to tests/mixins/test_validation.py index fb5dcc8..212296c 100644 --- a/tests/mixins/validation.py +++ b/tests/mixins/test_validation.py @@ -6,9 +6,13 @@ class CleanOnSaveMixinTest(TestCase): + def test_clean_regular(self): + obj = ModelWithCleanMixin() + self.assertIsNone(obj.save()) + def test_clean_is_called(self): obj = ModelWithCleanMixin() - with mock.patch.object(obj, 'clean') as mocked_method: + with mock.patch.object(obj, "clean") as mocked_method: obj.save() mocked_method.assert_called_once() diff --git a/tests/permissions/fixtures/test_declarations.py b/tests/permissions/fixtures/test_declarations.py index 840d937..f76eda8 100644 --- a/tests/permissions/fixtures/test_declarations.py +++ b/tests/permissions/fixtures/test_declarations.py @@ -6,25 +6,25 @@ class PermissionFixtureDeclarationTest(TestCase): def test_permission_model_declaration_regular(self): permission = PermissionModelDeclaration( - app_label='my_app', - codename_list=['view_mymodel'], - model='mymodel', + app_label="my_app", + codename_list=["view_mymodel"], + model="mymodel", ) - self.assertEqual(permission.app_label, 'my_app') - self.assertEqual(permission.codename_list, ['view_mymodel']) - self.assertEqual(permission.model, 'mymodel') + self.assertEqual(permission.app_label, "my_app") + self.assertEqual(permission.codename_list, ["view_mymodel"]) + self.assertEqual(permission.model, "mymodel") def test_group_permission_declaration_regular(self): permission = PermissionModelDeclaration( - app_label='my_app', - codename_list=['view_mymodel'], - model='mymodel', + app_label="my_app", + codename_list=["view_mymodel"], + model="mymodel", ) group = GroupPermissionDeclaration( - name='my_group', + name="my_group", permission_list=[permission], ) - self.assertEqual(group.name, 'my_group') + self.assertEqual(group.name, "my_group") self.assertEqual(group.permission_list, [permission]) diff --git a/tests/permissions/fixtures/test_helpers.py b/tests/permissions/fixtures/test_helpers.py index 1f3f441..e738ca8 100644 --- a/tests/permissions/fixtures/test_helpers.py +++ b/tests/permissions/fixtures/test_helpers.py @@ -5,10 +5,10 @@ class PermissionFixtureHelperTest(TestCase): def test_generate_default_permissions_regular(self): - permission_list = generate_default_permissions('mymodel') + permission_list = generate_default_permissions("mymodel") self.assertEqual(len(permission_list), 4) - self.assertIn('add_mymodel', permission_list) - self.assertIn('change_mymodel', permission_list) - self.assertIn('delete_mymodel', permission_list) - self.assertIn('view_mymodel', permission_list) + self.assertIn("add_mymodel", permission_list) + self.assertIn("change_mymodel", permission_list) + self.assertIn("delete_mymodel", permission_list) + self.assertIn("view_mymodel", permission_list) diff --git a/tests/permissions/fixtures/test_services_setup_service.py b/tests/permissions/fixtures/test_services_setup_service.py index a78d80d..47cc81a 100644 --- a/tests/permissions/fixtures/test_services_setup_service.py +++ b/tests/permissions/fixtures/test_services_setup_service.py @@ -10,17 +10,17 @@ class PermissionSetupServiceTest(TestCase): def setUpTestData(cls): super().setUpTestData() - cls.group, created = Group.objects.get_or_create(name='my_group') + cls.group, created = Group.objects.get_or_create(name="my_group") cls.permission_view = Permission.objects.get_by_natural_key( - app_label='testapp', codename='view_mysinglesignalmodel', model='mysinglesignalmodel' + app_label="testapp", codename="view_mysinglesignalmodel", model="mysinglesignalmodel" ) cls.permission_change = Permission.objects.get_by_natural_key( - app_label='testapp', codename='change_mysinglesignalmodel', model='mysinglesignalmodel' + app_label="testapp", codename="change_mysinglesignalmodel", model="mysinglesignalmodel" ) def test_init_regular(self): - group_declaration = GroupPermissionDeclaration(name='my_group', permission_list=[]) + group_declaration = GroupPermissionDeclaration(name="my_group", permission_list=[]) service = PermissionSetupService(group_declaration=group_declaration) self.assertEqual(service.group_declaration, group_declaration) @@ -28,10 +28,10 @@ def test_init_regular(self): def test_process_add_permission(self): group_declaration = GroupPermissionDeclaration( - name='my_group', + name="my_group", permission_list=[ PermissionModelDeclaration( - app_label='testapp', codename_list=['change_mysinglesignalmodel'], model='mysinglesignalmodel' + app_label="testapp", codename_list=["change_mysinglesignalmodel"], model="mysinglesignalmodel" ) ], ) @@ -49,7 +49,7 @@ def test_process_add_permission(self): def test_process_remove_permission(self): self.group.permissions.add(self.permission_view) - group_declaration = GroupPermissionDeclaration(name='my_group', permission_list=[]) + group_declaration = GroupPermissionDeclaration(name="my_group", permission_list=[]) service = PermissionSetupService(group_declaration=group_declaration) new_permissions, removed_permissions = service.process() @@ -64,10 +64,10 @@ def test_process_no_changes_permission(self): self.group.permissions.add(self.permission_view) group_declaration = GroupPermissionDeclaration( - name='my_group', + name="my_group", permission_list=[ PermissionModelDeclaration( - app_label='testapp', codename_list=['view_mysinglesignalmodel'], model='mysinglesignalmodel' + app_label="testapp", codename_list=["view_mysinglesignalmodel"], model="mysinglesignalmodel" ) ], ) @@ -86,10 +86,10 @@ def test_process_invalid_permission(self): self.group.permissions.add(self.permission_view) group_declaration = GroupPermissionDeclaration( - name='my_group', + name="my_group", permission_list=[ PermissionModelDeclaration( - app_label='testapp', codename_list=['invalid_permission'], model='mysinglesignalmodel' + app_label="testapp", codename_list=["invalid_permission"], model="mysinglesignalmodel" ) ], ) @@ -104,10 +104,10 @@ def test_process_invalid_app(self): self.group.permissions.add(self.permission_view) group_declaration = GroupPermissionDeclaration( - name='my_group', + name="my_group", permission_list=[ PermissionModelDeclaration( - app_label='invalid_app', codename_list=['view_mysinglesignalmodel'], model='mysinglesignalmodel' + app_label="invalid_app", codename_list=["view_mysinglesignalmodel"], model="mysinglesignalmodel" ) ], ) @@ -120,10 +120,10 @@ def test_process_invalid_model(self): self.group.permissions.add(self.permission_view) group_declaration = GroupPermissionDeclaration( - name='my_group', + name="my_group", permission_list=[ PermissionModelDeclaration( - app_label='testapp', codename_list=['view_mysinglesignalmodel'], model='invalid_model' + app_label="testapp", codename_list=["view_mysinglesignalmodel"], model="invalid_model" ) ], ) @@ -136,29 +136,29 @@ def test_process_duplicated_permission_declaration(self): self.group.permissions.add(self.permission_view) group_declaration = GroupPermissionDeclaration( - name='my_group', + name="my_group", permission_list=[ PermissionModelDeclaration( - app_label='testapp', codename_list=['view_mysinglesignalmodel'], model='mysinglesignalmodel' + app_label="testapp", codename_list=["view_mysinglesignalmodel"], model="mysinglesignalmodel" ), PermissionModelDeclaration( - app_label='testapp', codename_list=['view_mysinglesignalmodel'], model='mysinglesignalmodel' + app_label="testapp", codename_list=["view_mysinglesignalmodel"], model="mysinglesignalmodel" ), ], ) service = PermissionSetupService(group_declaration=group_declaration) - with self.assertRaisesMessage(ValueError, f'Permission {self.permission_view} declared twice.'): + with self.assertRaisesMessage(ValueError, f"Permission {self.permission_view} declared twice."): service.process() def test_process_dry_run_not_persisting(self): self.group.permissions.add(self.permission_view) group_declaration = GroupPermissionDeclaration( - name='my_group', + name="my_group", permission_list=[ PermissionModelDeclaration( - app_label='testapp', codename_list=['change_mysinglesignalmodel'], model='mysinglesignalmodel' + app_label="testapp", codename_list=["change_mysinglesignalmodel"], model="mysinglesignalmodel" ) ], ) diff --git a/tests/permissions/test_install_permission_fixtures_command.py b/tests/permissions/test_install_permission_fixtures_command.py index 23ec33e..36d8832 100644 --- a/tests/permissions/test_install_permission_fixtures_command.py +++ b/tests/permissions/test_install_permission_fixtures_command.py @@ -1,3 +1,4 @@ +from argparse import ArgumentParser from unittest import mock from django.test import TestCase @@ -12,17 +13,33 @@ class InstallPermissionFixturesCommandTest(TestCase): def setUpTestData(cls): super().setUpTestData() - @override_settings(GROUP_PERMISSION_FIXTURES=['testapp.permissions.TestGroupDeclaration']) - @mock.patch.object(PermissionSetupService, 'process', return_value=([], [])) + def test_add_arguments_regular(self): + command = Command() + + parser = ArgumentParser() + + command.add_arguments(parser) + + self.assertTrue("--dry-run" in [action.option_strings[0] for action in parser._actions]) + + @override_settings(GROUP_PERMISSION_FIXTURES=["testapp.permissions.TestGroupDeclaration"]) + @mock.patch.object(PermissionSetupService, "process", return_value=([], [])) def test_run_command_regular(self, mocked_process): command = Command() command.handle() mocked_process.assert_called_once() - @mock.patch.object(PermissionSetupService, 'process') + @mock.patch.object(PermissionSetupService, "process") def test_run_command_no_settings_variable(self, mocked_process): command = Command() command.handle() mocked_process.assert_not_called() + + @mock.patch.object(PermissionSetupService, "process") + def test_run_command_dry_run(self, mocked_process): + command = Command() + command.handle(dry_run=True) + + mocked_process.assert_not_called() diff --git a/tests/sentry/mock_data.py b/tests/sentry/mock_data.py index 2e1a845..05121b9 100644 --- a/tests/sentry/mock_data.py +++ b/tests/sentry/mock_data.py @@ -1,136 +1,136 @@ SENTRY_EVENT = { - 'level': 'error', - 'exception': { - 'values': [ + "level": "error", + "exception": { + "values": [ { - 'module': None, - 'type': 'ZeroDivisionError', - 'value': 'division by zero', - 'mechanism': {'type': 'django', 'handled': False}, - 'stacktrace': { - 'frames': [ + "module": None, + "type": "ZeroDivisionError", + "value": "division by zero", + "mechanism": {"type": "django", "handled": False}, + "stacktrace": { + "frames": [ { - 'filename': 'test/account/api/views.py', - 'abs_path': '/opt/project/backend/test/account/api/views.py', - 'function': 'perform_update', - 'module': 'test.account.api.views', - 'lineno': 123, - 'pre_context': ['test = 1/0'], - 'vars': { - 'self': '', - 'serializer': 'TestDetailSerializer()', - '__class__': "", - 'admin': '', - 'test_data': '123', + "filename": "test/account/api/views.py", + "abs_path": "/opt/project/backend/test/account/api/views.py", + "function": "perform_update", + "module": "test.account.api.views", + "lineno": 123, + "pre_context": ["test = 1/0"], + "vars": { + "self": "", + "serializer": "TestDetailSerializer()", + "__class__": "", + "admin": "", + "test_data": "123", }, - 'in_app': True, + "in_app": True, } ] }, } ] }, - 'request': { - 'url': 'http://localhost:8000/api/v2/test/17/', - 'query_string': '', - 'method': 'PUT', - 'env': {'SERVER_NAME': 'cdc3cb5b00af', 'SERVER_PORT': '8000', 'REMOTE_ADDR': '172.22.0.1'}, - 'headers': { - 'Content-Length': '1202', - 'Content-Type': 'application/json', - 'Host': 'localhost:8000', - 'User-Agent': 'Mozilla/5.0 (Macintosh;) Gecko/20100101 Firefox/112.0', - 'Accept': 'application/json, text/plain, */*', - 'Accept-Language': 'en', - 'Accept-Encoding': 'gzip, deflate, br', - 'Authorization': 'Token 123123123123123123123123123123123123abcd', - 'Origin': 'http://127.0.0.1:3000', - 'Dnt': '1', - 'Connection': 'keep-alive', - 'Referer': 'http://127.0.0.1:3000/', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'cross-site', + "request": { + "url": "http://localhost:8000/api/v2/test/17/", + "query_string": "", + "method": "PUT", + "env": {"SERVER_NAME": "cdc3cb5b00af", "SERVER_PORT": "8000", "REMOTE_ADDR": "172.22.0.1"}, + "headers": { + "Content-Length": "1202", + "Content-Type": "application/json", + "Host": "localhost:8000", + "User-Agent": "Mozilla/5.0 (Macintosh;) Gecko/20100101 Firefox/112.0", + "Accept": "application/json, text/plain, */*", + "Accept-Language": "en", + "Accept-Encoding": "gzip, deflate, br", + "Authorization": "Token 123123123123123123123123123123123123abcd", + "Origin": "http://127.0.0.1:3000", + "Dnt": "1", + "Connection": "keep-alive", + "Referer": "http://127.0.0.1:3000/", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "cross-site", }, - 'cookies': {}, - 'data': '', + "cookies": {}, + "data": "", }, - 'user': {'email': 'test@test.local', 'id': '1337', 'ip_address': '172.22.0.1', 'username': 'test@test.local'}, + "user": {"email": "test@test.local", "id": "1337", "ip_address": "172.22.0.1", "username": "test@test.local"}, } SCRUBBED_SENTRY_EVENT = { - 'level': 'error', - 'exception': { - 'values': [ + "level": "error", + "exception": { + "values": [ { - 'module': None, - 'type': 'ZeroDivisionError', - 'value': 'division by zero', - 'mechanism': {'type': 'django', 'handled': False}, - 'stacktrace': { - 'frames': [ + "module": None, + "type": "ZeroDivisionError", + "value": "division by zero", + "mechanism": {"type": "django", "handled": False}, + "stacktrace": { + "frames": [ { - 'filename': 'test/account/api/views.py', - 'abs_path': '/opt/project/backend/test/account/api/views.py', - 'function': 'perform_update', - 'module': 'test.account.api.views', - 'lineno': 123, - 'pre_context': ['test = 1/0'], - 'vars': { - 'self': "''", - 'serializer': '[Filtered]', # filtered due to standard filter - '__class__': '""', - 'admin': '[Filtered]', # filtered due to standard filter - 'test_data': "'123'", + "filename": "test/account/api/views.py", + "abs_path": "/opt/project/backend/test/account/api/views.py", + "function": "perform_update", + "module": "test.account.api.views", + "lineno": 123, + "pre_context": ["test = 1/0"], + "vars": { + "self": "''", + "serializer": "[Filtered]", # filtered due to standard filter + "__class__": "\"\"", + "admin": "[Filtered]", # filtered due to standard filter + "test_data": "'123'", }, - 'in_app': True, + "in_app": True, } ] }, } ] }, - 'request': { - 'url': 'http://localhost:8000/api/v2/test/17/', - 'query_string': '', - 'method': 'PUT', - 'env': {'SERVER_NAME': 'cdc3cb5b00af', 'SERVER_PORT': '8000', 'REMOTE_ADDR': '172.22.0.1'}, - 'headers': { - 'Content-Length': '1202', - 'Content-Type': 'application/json', - 'Host': 'localhost:8000', - 'User-Agent': 'Mozilla/5.0 (Macintosh;) Gecko/20100101 Firefox/112.0', - 'Accept': 'application/json, text/plain, */*', - 'Accept-Language': 'en', - 'Accept-Encoding': 'gzip, deflate, br', - 'Authorization': '[Filtered]', # filtered due to default sentry filter - 'Origin': 'http://127.0.0.1:3000', - 'Dnt': '1', - 'Connection': 'keep-alive', - 'Referer': 'http://127.0.0.1:3000/', - 'Sec-Fetch-Dest': 'empty', - 'Sec-Fetch-Mode': 'cors', - 'Sec-Fetch-Site': 'cross-site', + "request": { + "url": "http://localhost:8000/api/v2/test/17/", + "query_string": "", + "method": "PUT", + "env": {"SERVER_NAME": "cdc3cb5b00af", "SERVER_PORT": "8000", "REMOTE_ADDR": "172.22.0.1"}, + "headers": { + "Content-Length": "1202", + "Content-Type": "application/json", + "Host": "localhost:8000", + "User-Agent": "Mozilla/5.0 (Macintosh;) Gecko/20100101 Firefox/112.0", + "Accept": "application/json, text/plain, */*", + "Accept-Language": "en", + "Accept-Encoding": "gzip, deflate, br", + "Authorization": "[Filtered]", # filtered due to default sentry filter + "Origin": "http://127.0.0.1:3000", + "Dnt": "1", + "Connection": "keep-alive", + "Referer": "http://127.0.0.1:3000/", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "cross-site", }, - 'cookies': {}, - 'data': '', + "cookies": {}, + "data": "", }, - 'user': { - 'email': '[Filtered]', # filtered due to default sentry filter - 'id': '1337', - 'ip_address': '[Filtered]', # filtered due to default sentry filter - 'username': '[Filtered]', # filtered due to default sentry filter + "user": { + "email": "[Filtered]", # filtered due to default sentry filter + "id": "1337", + "ip_address": "[Filtered]", # filtered due to default sentry filter + "username": "[Filtered]", # filtered due to default sentry filter }, - '_meta': { - 'exception': { - 'values': { - '0': { - 'stacktrace': { - 'frames': { - '0': { - 'vars': { - 'serializer': {'': {'rem': [['!config', 's']]}}, - 'admin': {'': {'rem': [['!config', 's']]}}, + "_meta": { + "exception": { + "values": { + "0": { + "stacktrace": { + "frames": { + "0": { + "vars": { + "serializer": {"": {"rem": [["!config", "s"]]}}, + "admin": {"": {"rem": [["!config", "s"]]}}, } } } @@ -138,11 +138,11 @@ } } }, - 'request': {'headers': {'Authorization': {'': {'rem': [['!config', 's']]}}}}, - 'user': { - 'email': {'': {'rem': [['!config', 's']]}}, - 'ip_address': {'': {'rem': [['!config', 's']]}}, - 'username': {'': {'rem': [['!config', 's']]}}, + "request": {"headers": {"Authorization": {"": {"rem": [["!config", "s"]]}}}}, + "user": { + "email": {"": {"rem": [["!config", "s"]]}}, + "ip_address": {"": {"rem": [["!config", "s"]]}}, + "username": {"": {"rem": [["!config", "s"]]}}, }, }, } diff --git a/tests/sentry/test_sentry_helper.py b/tests/sentry/test_sentry_helper.py index e0a165f..344df32 100644 --- a/tests/sentry/test_sentry_helper.py +++ b/tests/sentry/test_sentry_helper.py @@ -8,22 +8,22 @@ class SentryHelperTest(TestCase): def test_strip_sensitive_data_from_sentry_event_regular(self): - event = {'user': {'email': 'mymail@example.com', 'ip_address': '127.0.0.1', 'username': 'my-user'}} + event = {"user": {"email": "mymail@example.com", "ip_address": "127.0.0.1", "username": "my-user"}} self.assertIsInstance(strip_sensitive_data_from_sentry_event(event, None), dict) def test_strip_sensitive_data_from_sentry_event_missing_key_email(self): - event = {'user': {'ip_address': '127.0.0.1', 'username': 'my-user'}} + event = {"user": {"ip_address": "127.0.0.1", "username": "my-user"}} self.assertIsInstance(strip_sensitive_data_from_sentry_event(event, None), dict) def test_strip_sensitive_data_from_sentry_event_missing_key_ip_address(self): - event = {'user': {'email': 'mymail@example.com', 'username': 'my-user'}} + event = {"user": {"email": "mymail@example.com", "username": "my-user"}} self.assertIsInstance(strip_sensitive_data_from_sentry_event(event, None), dict) def test_strip_sensitive_data_from_sentry_event_missing_key_username(self): - event = {'user': {'email': 'mymail@example.com', 'ip_address': '127.0.0.1'}} + event = {"user": {"email": "mymail@example.com", "ip_address": "127.0.0.1"}} self.assertIsInstance(strip_sensitive_data_from_sentry_event(event, None), dict) diff --git a/tests/templatetags/test_ai_number_tags.py b/tests/templatetags/test_ai_number_tags.py index 04c77fe..26c5519 100644 --- a/tests/templatetags/test_ai_number_tags.py +++ b/tests/templatetags/test_ai_number_tags.py @@ -1,6 +1,6 @@ from django.test import TestCase -from ambient_toolbox.templatetags.ai_number_tags import multiply +from ambient_toolbox.templatetags.ai_number_tags import currency, divide, multiply, subtract, to_int class AiNumberTagTest(TestCase): @@ -15,3 +15,24 @@ def test_multiply_german_float(self): def test_multiply_no_value(self): self.assertIsNone(multiply(None, 3)) + + def test_subtract_regular(self): + self.assertEqual(subtract(4, 1), 3) + + def test_divide_regular(self): + self.assertEqual(divide(8, 2), 4) + + def test_divide_no_value(self): + self.assertEqual(divide(None, 2), None) + + def test_to_int_case_number(self): + self.assertEqual(to_int("42"), 42) + + def test_to_int_case_text(self): + self.assertEqual(to_int("Aubrey"), 0) + + def test_currency_regular(self): + self.assertEqual(currency(12.4), "12,40€") + + def test_currency_no_value(self): + self.assertEqual(currency(None), "-") diff --git a/tests/test_admin_forms.py b/tests/test_admin_forms.py index 2190395..e339a52 100644 --- a/tests/test_admin_forms.py +++ b/tests/test_admin_forms.py @@ -1,15 +1,20 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout +from django import forms from django.test import TestCase from ambient_toolbox.admin.views.forms import AdminCrispyForm +class TestForm(AdminCrispyForm): + my_field = forms.Field() + + class AdminFormTest(TestCase): def test_admin_crispy_form_regular(self): # Form provides mostly styling, so we just validate that it renders - form = AdminCrispyForm() + form = TestForm() self.assertIsInstance(form.helper, FormHelper) self.assertIsInstance(form.helper.layout, Layout) - self.assertEqual(form.helper.form_method, 'post') + self.assertEqual(form.helper.form_method, "post") diff --git a/tests/test_admin_inlines.py b/tests/test_admin_inlines.py index f9cc5ab..db16d86 100644 --- a/tests/test_admin_inlines.py +++ b/tests/test_admin_inlines.py @@ -16,7 +16,7 @@ class AdminInlineTest(RequestProviderMixin, TestCase): def setUpTestData(cls): super().setUpTestData() - cls.super_user = User.objects.create(username='super_user', is_superuser=True) + cls.super_user = User.objects.create(username="super_user", is_superuser=True) def test_read_only_tabular_inline_admin_all_fields_readonly(self): obj = MySingleSignalModel(value=1) @@ -26,7 +26,7 @@ def test_read_only_tabular_inline_admin_all_fields_readonly(self): readonly_fields = admin_class.get_readonly_fields(request=self.get_request(), obj=fk_related_obj) self.assertEqual(len(readonly_fields), 1) - self.assertIn('single_signal', readonly_fields) + self.assertIn("single_signal", readonly_fields) def test_read_only_admin_tabular_inline_no_change_permissions(self): admin_class = TestReadOnlyTabularInline(parent_model=MySingleSignalModel, admin_site=admin.site) diff --git a/tests/test_admin_model_admins_classes.py b/tests/test_admin_model_admins_classes.py deleted file mode 100644 index 9f75d3c..0000000 --- a/tests/test_admin_model_admins_classes.py +++ /dev/null @@ -1,71 +0,0 @@ -from django.contrib import admin -from django.contrib.auth.models import User -from django.test import TestCase - -from ambient_toolbox.admin.model_admins.classes import EditableOnlyAdmin, ReadOnlyAdmin -from ambient_toolbox.tests.mixins import RequestProviderMixin -from testapp.models import MyMultipleSignalModel, MySingleSignalModel - - -class TestReadOnlyAdmin(ReadOnlyAdmin): - pass - - -class TestEditableOnlyAdmin(EditableOnlyAdmin): - pass - - -class AdminClassesTest(RequestProviderMixin, TestCase): - @classmethod - def setUpTestData(cls): - super().setUpTestData() - - cls.super_user = User.objects.create(username='super_user', is_superuser=True) - - admin.site.register(MySingleSignalModel, TestReadOnlyAdmin) - admin.site.register(MyMultipleSignalModel, TestEditableOnlyAdmin) - - @classmethod - def tearDownClass(cls): - super().tearDownClass() - - admin.site.unregister(MySingleSignalModel) - admin.site.unregister(MyMultipleSignalModel) - - def test_read_only_admin_all_fields_readonly(self): - obj = MySingleSignalModel(value=1) - - admin_class = TestReadOnlyAdmin(model=obj, admin_site=admin.site) - readonly_fields = admin_class.get_readonly_fields(request=self.get_request(), obj=obj) - - self.assertEqual(len(readonly_fields), 2) - self.assertIn('id', readonly_fields) - self.assertIn('value', readonly_fields) - - def test_read_only_admin_no_change_permissions(self): - admin_class = TestReadOnlyAdmin(model=MySingleSignalModel, admin_site=admin.site) - - request = self.get_request(self.super_user) - - self.assertFalse(admin_class.has_add_permission(request)) - self.assertFalse(admin_class.has_change_permission(request)) - self.assertFalse(admin_class.has_delete_permission(request)) - - def test_editable_only_admin_delete_action_removed(self): - obj = MyMultipleSignalModel(value=1) - admin_class = TestEditableOnlyAdmin(model=obj, admin_site=admin.site) - - request = self.get_request(self.super_user) - actions = admin_class.get_actions(request=request) - - self.assertNotIn('delete_selected', actions) - - def test_editable_only_admin_no_change_permissions(self): - admin_class = TestEditableOnlyAdmin(model=MyMultipleSignalModel, admin_site=admin.site) - - request = self.get_request(self.super_user) - - self.assertTrue(admin_class.has_change_permission(request)) - - self.assertFalse(admin_class.has_add_permission(request)) - self.assertFalse(admin_class.has_delete_permission(request)) diff --git a/tests/test_admin_view_mixins.py b/tests/test_admin_view_mixins.py index 9c15593..947674b 100644 --- a/tests/test_admin_view_mixins.py +++ b/tests/test_admin_view_mixins.py @@ -11,8 +11,8 @@ class TestView(AdminViewMixin, generic.TemplateView): model = MySingleSignalModel - admin_page_title = 'My fancy title' - template_name = 'testapp/test_template.html' + admin_page_title = "My fancy title" + template_name = "testapp/test_template.html" class AdminViewMixinTest(RequestProviderMixin, TestCase): @@ -22,8 +22,8 @@ def setUpTestData(cls): cls.view = TestView() - cls.super_user = User.objects.create(username='super_user', is_superuser=True) - cls.regular_user = User.objects.create(username='test_user', is_superuser=False) + cls.super_user = User.objects.create(username="super_user", is_superuser=True) + cls.regular_user = User.objects.create(username="test_user", is_superuser=False) # View needs a request since django 3.2 request = cls.get_request(cls.super_user) @@ -54,17 +54,17 @@ def test_admin_view_mixin_get_context_data_regular(self): context_data = self.view.get_context_data() # Simply assert custom fields are available - self.assertIn('site_header', context_data) - self.assertIn('site_title', context_data) - self.assertIn('name', context_data) - self.assertIn('original', context_data) - self.assertIn('is_nav_sidebar_enabled', context_data) - self.assertIn('available_apps', context_data) - self.assertIn('opts', context_data) - self.assertIn('app_label', context_data['opts']) - self.assertIn('verbose_name', context_data['opts']) - self.assertIn('verbose_name_plural', context_data['opts']) - self.assertIn('model_name', context_data['opts']) - self.assertIn('app_config', context_data['opts']) - self.assertIn('verbose_name', context_data['opts']['app_config']) - self.assertIn('has_permission', context_data) + self.assertIn("site_header", context_data) + self.assertIn("site_title", context_data) + self.assertIn("name", context_data) + self.assertIn("original", context_data) + self.assertIn("is_nav_sidebar_enabled", context_data) + self.assertIn("available_apps", context_data) + self.assertIn("opts", context_data) + self.assertIn("app_label", context_data["opts"]) + self.assertIn("verbose_name", context_data["opts"]) + self.assertIn("verbose_name_plural", context_data["opts"]) + self.assertIn("model_name", context_data["opts"]) + self.assertIn("app_config", context_data["opts"]) + self.assertIn("verbose_name", context_data["opts"]["app_config"]) + self.assertIn("has_permission", context_data) diff --git a/tests/test_context_manager.py b/tests/test_context_manager.py index ce3bcf3..ade087f 100644 --- a/tests/test_context_manager.py +++ b/tests/test_context_manager.py @@ -19,9 +19,9 @@ def test_single_signal_executed_regular(self): def test_single_signal_not_executed(self): kwargs = { - 'signal': signals.pre_save, - 'receiver': increase_value_no_dispatch_uid, - 'sender': MySingleSignalModel, + "signal": signals.pre_save, + "receiver": increase_value_no_dispatch_uid, + "sender": MySingleSignalModel, } with TempDisconnectSignal(**kwargs): @@ -31,15 +31,15 @@ def test_single_signal_not_executed(self): def test_multiple_signals_not_executed(self): kwargs_pre = { - 'signal': signals.pre_save, - 'receiver': increase_value_with_dispatch_uid, - 'sender': MyMultipleSignalModel, - 'dispatch_uid': 'test.mysinglesignalmodel.increase_value_with_uuid', + "signal": signals.pre_save, + "receiver": increase_value_with_dispatch_uid, + "sender": MyMultipleSignalModel, + "dispatch_uid": "test.mysinglesignalmodel.increase_value_with_uuid", } kwargs_post = { - 'signal': signals.post_save, - 'receiver': send_email, - 'sender': MyMultipleSignalModel, + "signal": signals.post_save, + "receiver": send_email, + "sender": MyMultipleSignalModel, } with TempDisconnectSignal(**kwargs_pre): @@ -53,10 +53,10 @@ def test_multiple_signals_not_executed(self): def test_multiple_signals_one_still_active(self): kwargs_pre = { - 'signal': signals.pre_save, - 'receiver': increase_value_with_dispatch_uid, - 'sender': MyMultipleSignalModel, - 'dispatch_uid': 'test.mysinglesignalmodel.increase_value_with_uuid', + "signal": signals.pre_save, + "receiver": increase_value_with_dispatch_uid, + "sender": MyMultipleSignalModel, + "dispatch_uid": "test.mysinglesignalmodel.increase_value_with_uuid", } with TempDisconnectSignal(**kwargs_pre): diff --git a/tests/test_managers.py b/tests/test_managers.py index c703712..edf10f0 100644 --- a/tests/test_managers.py +++ b/tests/test_managers.py @@ -1,16 +1,72 @@ from django.contrib.auth.models import User from django.test import TestCase +from ambient_toolbox.managers import ( + AbstractUserSpecificManager, + AbstractUserSpecificQuerySet, +) from testapp.models import MySingleSignalModel +class AbstractUserSpecificQuerySetTest(TestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + cls.cqs = AbstractUserSpecificQuerySet() + cls.user = User.objects.create(username="my-username") + + def test_default(self): + self.assertEqual(self.cqs.default(self.user), self.cqs) + + def test_visible_for_regular(self): + with self.assertRaisesMessage(NotImplementedError, "Please implement this method"): + self.cqs.visible_for(self.user) + + def test_editable_for_regular(self): + with self.assertRaisesMessage(NotImplementedError, "Please implement this method"): + self.cqs.editable_for(self.user) + + def test_deletable_for_regular(self): + with self.assertRaisesMessage(NotImplementedError, "Please implement this method"): + self.cqs.deletable_for(self.user) + + +class TestUserSpecificManager(AbstractUserSpecificManager): + pass + + +TestUserSpecificManager = TestUserSpecificManager.from_queryset(AbstractUserSpecificQuerySet) + + +class AbstractUserSpecificManagerTest(TestCase): + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + cls.manager = TestUserSpecificManager() + cls.user = User.objects.create(username="my-username") + + def test_visible_for_regular(self): + with self.assertRaisesMessage(NotImplementedError, "Please implement this method"): + self.manager.visible_for(self.user) + + def test_editable_for_regular(self): + with self.assertRaisesMessage(NotImplementedError, "Please implement this method"): + self.manager.editable_for(self.user) + + def test_deletable_for_regular(self): + with self.assertRaisesMessage(NotImplementedError, "Please implement this method"): + self.manager.deletable_for(self.user) + + class GloballyVisibleQuerySetTest(TestCase): @classmethod def setUpTestData(cls): super().setUpTestData() # Create test user - cls.user = User.objects.create(username='my-username') + cls.user = User.objects.create(username="my-username") # Create list of objects cls.object_list = [ @@ -20,12 +76,21 @@ def setUpTestData(cls): def test_visible_for_regular(self): self.assertGreater(len(self.object_list), 0) - self.assertEqual(MySingleSignalModel.objects.visible_for(self.user).count(), len(self.object_list)) + self.assertEqual( + MySingleSignalModel.objects.visible_for(self.user).count(), + len(self.object_list), + ) def test_editable_for_regular(self): self.assertGreater(len(self.object_list), 0) - self.assertEqual(MySingleSignalModel.objects.editable_for(self.user).count(), len(self.object_list)) + self.assertEqual( + MySingleSignalModel.objects.editable_for(self.user).count(), + len(self.object_list), + ) def test_deletable_for_regular(self): self.assertGreater(len(self.object_list), 0) - self.assertEqual(MySingleSignalModel.objects.deletable_for(self.user).count(), len(self.object_list)) + self.assertEqual( + MySingleSignalModel.objects.deletable_for(self.user).count(), + len(self.object_list), + ) diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 3e0a3f9..d5a06ff 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -18,13 +18,13 @@ def test_current_user_is_none_if_request_user_is_none(self): self.assertEqual(response.status_code, HTTPStatus.NO_CONTENT) def test_current_user_is_same_as_request_user(self): - new_user = Mock(user_name='test_user') + new_user = Mock(user_name="test_user") response = set_current_user(user=new_user) self.assertEqual(response.status_code, HTTPStatus.OK) def test_current_user_is_thread_safe(self): - user1 = Mock(user_name='user1') - user2 = Mock(user_name='user2') + user1 = Mock(user_name="user1") + user2 = Mock(user_name="user2") current_users = [] ready_event = threading.Event() proceed_event = threading.Event() @@ -43,12 +43,12 @@ def test_current_user_is_thread_safe(self): proceed_event.set() first_thread.join() self.assertEqual(current_users[0], user2) - self.assertEqual(current_users[0].user_name, 'user2') + self.assertEqual(current_users[0].user_name, "user2") self.assertEqual(current_users[1], user1) - self.assertEqual(current_users[1].user_name, 'user1') + self.assertEqual(current_users[1].user_name, "user1") def test_user_is_cleared_after_request(self): - user = Mock(user_name='test_user') + user = Mock(user_name="test_user") request = Mock(user=user) middleware = CurrentUserMiddleware(get_response=lambda request: HttpResponse(status=HTTPStatus.OK)) response = middleware(request) @@ -63,7 +63,7 @@ def test_replaced_user_is_reflected_in_middleware(self): # This should ideally be taken into account in our middleware since it # is also used to provide the user for `CommonInfo.lastmodified_by`. def get_response(request): - replaced_user = Mock(user_name='replaced_user') + replaced_user = Mock(user_name="replaced_user") request.user = replaced_user user_from_mw = CurrentUserMiddleware.get_current_user() if user_from_mw is not replaced_user: diff --git a/tests/test_models.py b/tests/test_models.py index c1811fe..b87110d 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -2,15 +2,24 @@ from unittest.mock import PropertyMock, patch from django.test import TestCase +from django.utils import timezone from freezegun import freeze_time from testapp.models import CommonInfoBasedModel class CommonInfoTest(TestCase): - @freeze_time('2022-06-26 10:00') + @freeze_time("2022-06-26 10:00") + def test_save_created_at_set(self): + obj = CommonInfoBasedModel.objects.create(value=1, value_b=1) + obj.created_at = None + obj.save() + + self.assertEqual(obj.created_at, timezone.now()) + + @freeze_time("2022-06-26 10:00") def test_save_update_fields_common_fields_set(self): - with freeze_time('2020-09-19'): + with freeze_time("2020-09-19"): obj = CommonInfoBasedModel.objects.create(value=1, value_b=1) obj.value = 2 obj.value_b = 999 @@ -20,7 +29,7 @@ def test_save_update_fields_common_fields_set(self): False, # default for force_insert False, # default for force_update None, # default for using - (x for x in ['value']), # update_fields is supposed to accept any Iterable[str] + (x for x in ["value"]), # update_fields is supposed to accept any Iterable[str] ) obj.save(*args) @@ -29,22 +38,22 @@ def test_save_update_fields_common_fields_set(self): self.assertEqual(obj.value_b, 1, "value_b should not have changed") self.assertEqual(obj.lastmodified_at, datetime.datetime(2022, 6, 26, 10)) - @patch('testapp.models.CommonInfoBasedModel.ALWAYS_UPDATE_FIELDS', new_callable=PropertyMock) - @freeze_time('2022-06-26 10:00') + @patch("testapp.models.CommonInfoBasedModel.ALWAYS_UPDATE_FIELDS", new_callable=PropertyMock) + @freeze_time("2022-06-26 10:00") def test_save_update_fields_common_fields_set_without_always_update(self, always_update_mock): always_update_mock.return_value = False - with freeze_time('2020-09-19'): + with freeze_time("2020-09-19"): obj = CommonInfoBasedModel.objects.create(value=1) obj.value = 2 - obj.save(update_fields=('value',)) + obj.save(update_fields=("value",)) obj.refresh_from_db() self.assertEqual(obj.value, 2) self.assertEqual(obj.lastmodified_at, datetime.datetime(2020, 9, 19)) - @freeze_time('2022-06-26 10:00') + @freeze_time("2022-06-26 10:00") def test_save_common_fields_set_without_update_fields(self): - with freeze_time('2020-09-19'): + with freeze_time("2020-09-19"): obj = CommonInfoBasedModel.objects.create(value=1) obj.value = 2 obj.save() diff --git a/tests/test_rest_api_mixins.py b/tests/test_rest_api_mixins.py index 16d8793..2cd042d 100644 --- a/tests/test_rest_api_mixins.py +++ b/tests/test_rest_api_mixins.py @@ -10,7 +10,7 @@ class BaseApiTest(BaseViewSetTestMixin, TestCase): def get_default_api_user(self) -> AbstractUser: - return User.objects.create(username='my-username', is_active=True) + return User.objects.create(username="my-username", is_active=True) class MySingleSignalModelApiViewTest(BaseApiTest): @@ -29,20 +29,20 @@ def setUpTestData(cls): def test_action_not_activated(self): with self.assertRaises(AttributeError): self.execute_request( - method='post', - url=reverse('my-single-signal-model-list'), - viewset_kwargs={'post': 'create'}, + method="post", + url=reverse("my-single-signal-model-list"), + viewset_kwargs={"post": "create"}, user=self.default_api_user, ) def test_list_authentication_required(self): - self.validate_authentication_required(url=reverse('my-single-signal-model-list'), method='get', view='list') + self.validate_authentication_required(url=reverse("my-single-signal-model-list"), method="get", view="list") def test_list_regular(self): response = self.execute_request( - method='get', - url=reverse('my-single-signal-model-list'), - viewset_kwargs={'get': 'list'}, + method="get", + url=reverse("my-single-signal-model-list"), + viewset_kwargs={"get": "list"}, user=self.default_api_user, ) diff --git a/tests/test_scrubbing_service.py b/tests/test_scrubbing_service.py index a1f1beb..acdcffd 100644 --- a/tests/test_scrubbing_service.py +++ b/tests/test_scrubbing_service.py @@ -1,3 +1,5 @@ +from unittest import mock + from django.test import TestCase, override_settings from ambient_toolbox.services.custom_scrubber import AbstractScrubbingService, ScrubbingError @@ -20,4 +22,9 @@ def test_scrubber_needs_to_be_installed(self): with self.assertRaisesMessage(ScrubbingError, "Scrubber settings validation failed"): self.service.process() - # todo write more tests + @mock.patch("ambient_toolbox.services.custom_scrubber.make_password") + def test_get_hashed_default_password_regular(self, mocked_make_password): + self.service._get_hashed_default_password() + mocked_make_password.assert_called_once_with(self.service.DEFAULT_USER_PASSWORD) + + # TODO: write more tests diff --git a/tests/test_utils_date.py b/tests/test_utils_date.py index 0465a2e..2ce6439 100644 --- a/tests/test_utils_date.py +++ b/tests/test_utils_date.py @@ -118,7 +118,7 @@ def test_tz_today_as_object_tz_not_active(self): def test_tz_today_as_str(self): frozen_date = datetime.datetime(year=2019, month=9, day=19, hour=10) with freeze_time(frozen_date): - self.assertEqual(tz_today('%d.%m.%Y'), '19.09.2019') + self.assertEqual(tz_today("%d.%m.%Y"), "19.09.2019") def test_add_months_one_month(self): source_date = datetime.date(year=2020, month=6, day=26) @@ -162,7 +162,7 @@ def test_add_minutes_negative_minutes(self): add_minutes(source_datetime, -2), datetime.datetime(year=2020, month=6, day=26, hour=7, minute=58) ) - @freeze_time('2020-06-26') + @freeze_time("2020-06-26") def test_get_next_month_regular(self): self.assertEqual(get_next_month(), datetime.date(year=2020, month=7, day=26)) @@ -172,40 +172,40 @@ def test_first_day_of_month_regular(self): def test_get_formatted_date_str_regular(self): source_date = datetime.date(year=2020, month=6, day=26) - self.assertEqual(get_formatted_date_str(source_date), '26.06.2020') + self.assertEqual(get_formatted_date_str(source_date), "26.06.2020") def test_get_time_from_seconds_one_hour(self): - self.assertEqual(get_time_from_seconds(3600), '01:00:00') + self.assertEqual(get_time_from_seconds(3600), "01:00:00") def test_get_time_from_seconds_one_minute(self): - self.assertEqual(get_time_from_seconds(60), '00:01:00') + self.assertEqual(get_time_from_seconds(60), "00:01:00") def test_get_time_from_seconds_big_hours(self): - self.assertEqual(get_time_from_seconds(3600 * 99), '99:00:00') + self.assertEqual(get_time_from_seconds(3600 * 99), "99:00:00") def test_get_time_from_seconds_huge_hours(self): - self.assertEqual(get_time_from_seconds(3600 * 1000), '1000:00:00') + self.assertEqual(get_time_from_seconds(3600 * 1000), "1000:00:00") def test_get_time_from_seconds_negative_seconds(self): with self.assertRaises(ValueError): get_time_from_seconds(-1) - @override_settings(TIME_ZONE='UTC') + @override_settings(TIME_ZONE="UTC") def test_datetime_format_regular(self): source_date = datetime.datetime(year=2020, month=6, day=26, hour=8, tzinfo=pytz.UTC) - self.assertEqual(datetime_format(source_date, '%d.%m.%Y %H:%M'), '26.06.2020 08:00') + self.assertEqual(datetime_format(source_date, "%d.%m.%Y %H:%M"), "26.06.2020 08:00") - @override_settings(TIME_ZONE='Europe/Cologne') + @override_settings(TIME_ZONE="Europe/Cologne") def test_datetime_format_wrong_timezone(self): source_date = datetime.datetime(year=2020, month=6, day=26, hour=8, tzinfo=datetime.timezone.utc) - self.assertEqual(datetime_format(source_date, '%d.%m.%Y %H:%M'), '26.06.2020 08:00') + self.assertEqual(datetime_format(source_date, "%d.%m.%Y %H:%M"), "26.06.2020 08:00") - @override_settings(TIME_ZONE='Europe/Berlin') + @override_settings(TIME_ZONE="Europe/Berlin") def test_datetime_format_different_timezone(self): source_date = datetime.datetime(year=2020, month=6, day=26, hour=8, tzinfo=pytz.UTC) - self.assertEqual(datetime_format(source_date, '%d.%m.%Y %H:%M'), '26.06.2020 10:00') + self.assertEqual(datetime_format(source_date, "%d.%m.%Y %H:%M"), "26.06.2020 10:00") - @freeze_time('2022-12-14') + @freeze_time("2022-12-14") def test_get_first_and_last_of_month_in_december(self): first_of_month, last_of_month = get_first_and_last_of_month() expected_first_of_month = datetime.date(day=1, month=12, year=2022) @@ -214,7 +214,7 @@ def test_get_first_and_last_of_month_in_december(self): self.assertEqual(expected_first_of_month, first_of_month) self.assertEqual(expected_last_of_month, last_of_month) - @freeze_time('2020-02-14') + @freeze_time("2020-02-14") def test_get_first_and_last_of_month_in_february_leap_year(self): first_of_month, last_of_month = get_first_and_last_of_month() expected_first_of_month = datetime.date(day=1, month=2, year=2020) @@ -223,7 +223,7 @@ def test_get_first_and_last_of_month_in_february_leap_year(self): self.assertEqual(expected_first_of_month, first_of_month) self.assertEqual(expected_last_of_month, last_of_month) - @freeze_time('2022-02-14') + @freeze_time("2022-02-14") def test_get_first_and_last_of_month_in_february_non_leap_year(self): first_of_month, last_of_month = get_first_and_last_of_month() expected_first_of_month = datetime.date(day=1, month=2, year=2022) @@ -232,7 +232,7 @@ def test_get_first_and_last_of_month_in_february_non_leap_year(self): self.assertEqual(expected_first_of_month, first_of_month) self.assertEqual(expected_last_of_month, last_of_month) - @freeze_time('2022-04-04') + @freeze_time("2022-04-04") def test_get_first_and_last_of_month_in_april(self): first_of_month, last_of_month = get_first_and_last_of_month() expected_first_of_month = datetime.date(day=1, month=4, year=2022) @@ -244,25 +244,25 @@ def test_get_first_and_last_of_month_in_april(self): def test_get_first_and_last_of_month_with_date_objects_passed(self): date_mapping = { datetime.date(day=14, month=12, year=2022): { - 'first': datetime.date(day=1, month=12, year=2022), - 'last': datetime.date(day=31, month=12, year=2022), + "first": datetime.date(day=1, month=12, year=2022), + "last": datetime.date(day=31, month=12, year=2022), }, datetime.date(day=14, month=2, year=2020): { - 'first': datetime.date(day=1, month=2, year=2020), - 'last': datetime.date(day=29, month=2, year=2020), + "first": datetime.date(day=1, month=2, year=2020), + "last": datetime.date(day=29, month=2, year=2020), }, datetime.date(day=14, month=2, year=2022): { - 'first': datetime.date(day=1, month=2, year=2022), - 'last': datetime.date(day=28, month=2, year=2022), + "first": datetime.date(day=1, month=2, year=2022), + "last": datetime.date(day=28, month=2, year=2022), }, datetime.date(day=4, month=4, year=2022): { - 'first': datetime.date(day=1, month=4, year=2022), - 'last': datetime.date(day=30, month=4, year=2022), + "first": datetime.date(day=1, month=4, year=2022), + "last": datetime.date(day=30, month=4, year=2022), }, } for date_object in date_mapping: first_of_month, last_of_month = get_first_and_last_of_month(date_object=date_object) - self.assertEqual(date_mapping[date_object]['first'], first_of_month) - self.assertEqual(date_mapping[date_object]['last'], last_of_month) + self.assertEqual(date_mapping[date_object]["first"], first_of_month) + self.assertEqual(date_mapping[date_object]["last"], last_of_month) diff --git a/tests/test_utils_file.py b/tests/test_utils_file.py index 784c79e..f3ff725 100644 --- a/tests/test_utils_file.py +++ b/tests/test_utils_file.py @@ -1,25 +1,33 @@ import pytest -from ambient_toolbox.utils.file import crc, md5_checksum +from ambient_toolbox.utils.file import crc, get_filename_without_ending, md5_checksum + + +def test_get_filename_without_ending_full_path(): + assert get_filename_without_ending("path/to/my/text-file.txt") == "text-file" + + +def test_get_filename_without_ending_only_filename(): + assert get_filename_without_ending("text-file.txt") == "text-file" @pytest.fixture def gen_test_file(tmp_path): def inner(content): - test_file = tmp_path / 'test_file.txt' + test_file = tmp_path / "test_file.txt" test_file.write_text(content) return test_file return inner -@pytest.mark.parametrize('test_func', [crc, md5_checksum]) +@pytest.mark.parametrize("test_func", [crc, md5_checksum]) def test_closes_file(mocker, test_func): """ Tests if the CRC and MD5 checksum functions use a context manager to open the file, to guarantee that the opened file descriptor is closed. """ - open_mock = mocker.patch('ambient_toolbox.utils.file.open') + open_mock = mocker.patch("ambient_toolbox.utils.file.open") open_mock.return_value.__enter__.return_value.read.return_value = None # to make f.read() return None. file_mock = mocker.Mock() test_func(file_mock) @@ -28,11 +36,11 @@ def test_closes_file(mocker, test_func): @pytest.mark.parametrize( - 'content, crc_result, md5_result', + "content, crc_result, md5_result", [ - ('The answer to life, the universe, and everything.', '31F49620', 'f81ab2f7fb6cacf50f973b0dc8faff44'), - ('The quick brown fox jumps over the lazy dog', '414FA339', '9e107d9d372bb6826bd81d3542a419d6'), - ('', '00000000', 'd41d8cd98f00b204e9800998ecf8427e'), + ("The answer to life, the universe, and everything.", "31F49620", "f81ab2f7fb6cacf50f973b0dc8faff44"), + ("The quick brown fox jumps over the lazy dog", "414FA339", "9e107d9d372bb6826bd81d3542a419d6"), + ("", "00000000", "d41d8cd98f00b204e9800998ecf8427e"), ], ) def test_crc_and_md5(gen_test_file, content, crc_result, md5_result): diff --git a/tests/test_utils_model.py b/tests/test_utils_model.py index 89ce921..16e3638 100644 --- a/tests/test_utils_model.py +++ b/tests/test_utils_model.py @@ -1,22 +1,31 @@ from django.test import TestCase from ambient_toolbox.utils import object_to_dict -from testapp.models import MySingleSignalModel +from testapp.models import ForeignKeyRelatedModel, MySingleSignalModel class UtilModelTest(TestCase): def test_object_to_dict_regular(self): obj = MySingleSignalModel.objects.create(value=17) - self.assertEqual(object_to_dict(obj), {'value': obj.value}) + self.assertEqual(object_to_dict(obj), {"value": obj.value}) def test_object_to_dict_blacklist(self): obj = MySingleSignalModel.objects.create(value=17) - self.assertEqual(object_to_dict(obj, ['value']), {}) + self.assertEqual(object_to_dict(obj, ["value"]), {}) def test_object_to_dict_with_id_with_blacklist(self): obj = MySingleSignalModel.objects.create(value=17) - self.assertEqual(object_to_dict(obj, ['value'], True), {'id': obj.id}) + self.assertEqual(object_to_dict(obj, ["value"], True), {"id": obj.id}) - def test_object_to_dict_with_id_no_blacklist(self): + def test_with_id_no_blacklist(self): obj = MySingleSignalModel.objects.create(value=17) - self.assertEqual(object_to_dict(obj, include_id=True), {'id': obj.id, 'value': obj.value}) + self.assertEqual(object_to_dict(obj, include_id=True), {"id": obj.id, "value": obj.value}) + + def test_object_to_dict_valid_fields_append(self): + obj = MySingleSignalModel.objects.create(value=17) + dummy_instance = ForeignKeyRelatedModel(single_signal=obj) + + valid_data = object_to_dict(dummy_instance) + + self.assertIn("single_signal_id", valid_data) + self.assertEqual(valid_data["single_signal_id"], obj.id) diff --git a/tests/test_utils_named_tuple.py b/tests/test_utils_named_tuple.py index c1f85b6..30977bf 100644 --- a/tests/test_utils_named_tuple.py +++ b/tests/test_utils_named_tuple.py @@ -21,10 +21,10 @@ def setUpClass(cls): ) cls.colors_choices = get_namedtuple_choices( - 'COLORS', + "COLORS", ( - (1, 'black', 'Black'), - (2, 'white', 'White'), + (1, "black", "Black"), + (2, "white", "White"), ), ) @@ -33,40 +33,39 @@ def test_get_namedtuple_choices_regular(self): self.assertEqual(self.colors_choices.white, 2) def test_get_namedtuple_choices_get_choices_regular(self): - self.assertEqual(self.colors_choices.get_choices(), [(1, 'Black'), (2, 'White')]) + self.assertEqual(self.colors_choices.get_choices(), [(1, "Black"), (2, "White")]) def test_get_namedtuple_choices_get_choices_dict_regular(self): - self.assertEqual(self.colors_choices.get_choices_dict(), OrderedDict([(1, 'Black'), (2, 'White')])) + self.assertEqual(self.colors_choices.get_choices_dict(), OrderedDict([(1, "Black"), (2, "White")])) def test_get_namedtuple_choices_get_all_regular(self): for index, color in enumerate(self.colors_choices.get_all()): + expected_tuple = "invalid_data" if index == 0: - expected_tuple = (1, 'black', 'Black') + expected_tuple = (1, "black", "Black") elif index == 1: - expected_tuple = (2, 'white', 'White') - else: - expected_tuple = 'invalid data' + expected_tuple = (2, "white", "White") self.assertEqual(color, expected_tuple) def test_get_namedtuple_choices_get_choices_tuple_regular(self): - self.assertEqual(self.colors_choices.get_choices_tuple(), ((1, 'black', 'Black'), (2, 'white', 'White'))) + self.assertEqual(self.colors_choices.get_choices_tuple(), ((1, "black", "Black"), (2, "white", "White"))) def test_get_namedtuple_choices_get_values_regular(self): self.assertEqual(self.colors_choices.get_values(), [1, 2]) def test_get_namedtuple_choices_get_value_by_name_regular(self): - self.assertEqual(self.colors_choices.get_value_by_name('black'), 1) - self.assertEqual(self.colors_choices.get_value_by_name('white'), 2) - self.assertFalse(self.colors_choices.get_value_by_name('no-existing')) + self.assertEqual(self.colors_choices.get_value_by_name("black"), 1) + self.assertEqual(self.colors_choices.get_value_by_name("white"), 2) + self.assertFalse(self.colors_choices.get_value_by_name("no-existing")) def test_get_namedtuple_choices_get_desc_by_value_regular(self): - self.assertEqual(self.colors_choices.get_desc_by_value(1), 'Black') - self.assertEqual(self.colors_choices.get_desc_by_value(2), 'White') + self.assertEqual(self.colors_choices.get_desc_by_value(1), "Black") + self.assertEqual(self.colors_choices.get_desc_by_value(2), "White") self.assertFalse(self.colors_choices.get_desc_by_value(-1)) def test_get_namedtuple_choices_get_name_by_value_regular(self): - self.assertEqual(self.colors_choices.get_name_by_value(1), 'black') - self.assertEqual(self.colors_choices.get_name_by_value(2), 'white') + self.assertEqual(self.colors_choices.get_name_by_value(1), "black") + self.assertEqual(self.colors_choices.get_name_by_value(2), "white") self.assertFalse(self.colors_choices.get_name_by_value(-1)) def test_get_namedtuple_choices_is_valid_regular(self): @@ -75,13 +74,26 @@ def test_get_namedtuple_choices_is_valid_regular(self): self.assertFalse(self.colors_choices.is_valid(-1)) def test_get_value_from_tuple_by_key_found(self): - self.assertEqual(get_value_from_tuple_by_key(self.MY_CHOICE_LIST, self.MY_CHOICE_TWO), 'Choice 2') + self.assertEqual(get_value_from_tuple_by_key(self.MY_CHOICE_LIST, self.MY_CHOICE_TWO), "Choice 2") def test_get_value_from_tuple_by_key_not_found(self): - self.assertEqual(get_value_from_tuple_by_key(self.MY_CHOICE_LIST, 99), '-') + self.assertEqual(get_value_from_tuple_by_key(self.MY_CHOICE_LIST, 99), "-") def test_get_key_from_tuple_by_value_found(self): - self.assertEqual(get_key_from_tuple_by_value(self.MY_CHOICE_LIST, 'Choice 2'), self.MY_CHOICE_TWO) + self.assertEqual(get_key_from_tuple_by_value(self.MY_CHOICE_LIST, "Choice 2"), self.MY_CHOICE_TWO) def test_get_key_from_tuple_by_value_not_found(self): - self.assertEqual(get_key_from_tuple_by_value(self.MY_CHOICE_LIST, 'Something odd'), '-') + self.assertEqual(get_key_from_tuple_by_value(self.MY_CHOICE_LIST, "Something odd"), "-") + + def test_get_values_case_is_instance(self): + choices = get_namedtuple_choices( + "TestChoices", + ( + (0, "zero", "Zero"), + (1, "one", "One"), + ([2, 3], "two_three", "Two Three"), + ), + ) + + # Überprüfen, ob die Methode get_values korrekt funktioniert + self.assertEqual(choices.get_values(), [0, 1, 2, 3]) diff --git a/tests/test_utils_string.py b/tests/test_utils_string.py index ca91e1d..122bc6e 100644 --- a/tests/test_utils_string.py +++ b/tests/test_utils_string.py @@ -18,115 +18,119 @@ class UtilsStringTest(TestCase): def test_distinct_regular(self): - not_distinct_list = ['Beer', 'Wine', 'Whiskey', 'Beer'] + not_distinct_list = ["Beer", "Wine", "Whiskey", "Beer"] distinct_list = distinct(not_distinct_list) self.assertEqual(len(distinct_list), 3) - self.assertIn('Beer', distinct_list) - self.assertIn('Whiskey', distinct_list) - self.assertIn('Wine', distinct_list) + self.assertIn("Beer", distinct_list) + self.assertIn("Whiskey", distinct_list) + self.assertIn("Wine", distinct_list) def test_slugify_file_name_regular(self): - filename = 'hola and hello.txt' + filename = "hola and hello.txt" slug = slugify_file_name(filename) - self.assertEqual(slug, 'hola_and_hello.txt') + self.assertEqual(slug, "hola_and_hello.txt") def test_slugify_file_name_nothing_to_slugify(self): - filename = 'hola.txt' + filename = "hola.txt" slug = slugify_file_name(filename) self.assertEqual(slug, filename) def test_slugify_file_name_max_length(self): - filename = 'a very long filename.txt' + filename = "a very long filename.txt" slug = slugify_file_name(filename, 6) - self.assertEqual(slug, 'a_very.txt') + self.assertEqual(slug, "a_very.txt") def test_smart_truncate_in_word(self): - my_sentence = 'I am a very interesting sentence.' + my_sentence = "I am a very interesting sentence." truncated_str = smart_truncate(my_sentence, 10) - self.assertEqual(truncated_str, 'I am a...') + self.assertEqual(truncated_str, "I am a...") def test_smart_truncate_after_word(self): - my_sentence = 'I am a very interesting sentence.' + my_sentence = "I am a very interesting sentence." truncated_str = smart_truncate(my_sentence, 14) - self.assertEqual(truncated_str, 'I am a very...') + self.assertEqual(truncated_str, "I am a very...") def test_smart_truncate_changed_postfix(self): - my_sentence = 'I am a very interesting sentence.' - truncated_str = smart_truncate(my_sentence, 10, '[...]') - self.assertEqual(truncated_str, 'I am a[...]') + my_sentence = "I am a very interesting sentence." + truncated_str = smart_truncate(my_sentence, 10, "[...]") + self.assertEqual(truncated_str, "I am a[...]") def test_smart_truncate_not_cutting_on_too_short_strings(self): - my_sentence = 'I am a very interesting sentence.' - truncated_str = smart_truncate(my_sentence, 100, '---') + my_sentence = "I am a very interesting sentence." + truncated_str = smart_truncate(my_sentence, 100, "---") self.assertEqual(truncated_str, my_sentence) + def test_smart_truncate_no_text(self): + truncated_str = smart_truncate(None, 100, "---") + self.assertEqual(truncated_str, "") + def test_float_to_string_regular(self): - self.assertEqual(float_to_string(5.61), '5,61') + self.assertEqual(float_to_string(5.61), "5,61") def test_float_to_string_value_replacement_not_used(self): - self.assertEqual(float_to_string(4.41, '-'), '4,41') + self.assertEqual(float_to_string(4.41, "-"), "4,41") def test_float_to_string_no_value_replacement_used(self): - self.assertEqual(float_to_string(None, 'Heureka'), 'Heureka') + self.assertEqual(float_to_string(None, "Heureka"), "Heureka") def test_float_to_string_value_greater_thousand(self): - self.assertEqual(float_to_string(1234.56), '1234,56') + self.assertEqual(float_to_string(1234.56), "1234,56") def test_date_to_string_regular(self): - self.assertEqual(date_to_string(datetime.date(2020, 9, 19)), '19.09.2020') + self.assertEqual(date_to_string(datetime.date(2020, 9, 19)), "19.09.2020") def test_date_to_string_other_format(self): - self.assertEqual(date_to_string(datetime.date(2020, 9, 19), str_format='%Y-%m-%d'), '2020-09-19') + self.assertEqual(date_to_string(datetime.date(2020, 9, 19), str_format="%Y-%m-%d"), "2020-09-19") def test_date_to_string_replacement_undefined(self): - self.assertEqual(date_to_string(None), '-') + self.assertEqual(date_to_string(None), "-") def test_date_to_string_replacement_defined(self): - self.assertEqual(date_to_string(None, 'no date'), 'no date') + self.assertEqual(date_to_string(None, "no date"), "no date") def test_datetime_to_string_regular(self): - self.assertEqual(datetime_to_string(datetime.datetime(2020, 9, 19, 8, tzinfo=pytz.UTC)), '19.09.2020 08:00') + self.assertEqual(datetime_to_string(datetime.datetime(2020, 9, 19, 8, tzinfo=pytz.UTC)), "19.09.2020 08:00") def test_datetime_to_string_other_format(self): self.assertEqual( - datetime_to_string(datetime.datetime(2020, 9, 19, 8, tzinfo=pytz.UTC), str_format='%Y-%m-%d'), '2020-09-19' + datetime_to_string(datetime.datetime(2020, 9, 19, 8, tzinfo=pytz.UTC), str_format="%Y-%m-%d"), "2020-09-19" ) def test_datetime_to_string_replacement_undefined(self): - self.assertEqual(datetime_to_string(None), '-') + self.assertEqual(datetime_to_string(None), "-") def test_datetime_to_string_replacement_defined(self): - self.assertEqual(datetime_to_string(None, 'no date'), 'no date') + self.assertEqual(datetime_to_string(None, "no date"), "no date") def test_number_to_string_regular(self): - self.assertEqual(number_to_string(5.61, decimal_digits=2), '5.61') + self.assertEqual(number_to_string(5.61, decimal_digits=2), "5.61") def test_number_to_string_value_replacement_not_used(self): - self.assertEqual(number_to_string(4.41, decimal_digits=2, replacement='-'), '4.41') + self.assertEqual(number_to_string(4.41, decimal_digits=2, replacement="-"), "4.41") def test_number_to_string_no_value_replacement_used(self): - self.assertEqual(number_to_string(None, replacement='Heureka'), 'Heureka') + self.assertEqual(number_to_string(None, replacement="Heureka"), "Heureka") def test_number_to_string_value_greater_thousand(self): - self.assertEqual(number_to_string(1234.56, decimal_digits=2), '1,234.56') + self.assertEqual(number_to_string(1234.56, decimal_digits=2), "1,234.56") def test_number_to_string_int_value_no_digits(self): - self.assertEqual(number_to_string(117), '117') + self.assertEqual(number_to_string(117), "117") def test_number_to_string_int_value_with_digits(self): - self.assertEqual(number_to_string(117, decimal_digits=2), '117.00') + self.assertEqual(number_to_string(117, decimal_digits=2), "117.00") def test_string_or_none_to_string_regular(self): - my_str = 'I am a string.' + my_str = "I am a string." self.assertEqual(string_or_none_to_string(my_str), my_str) def test_string_or_none_to_string_replacement_undefined(self): - self.assertEqual(string_or_none_to_string(None), '-') + self.assertEqual(string_or_none_to_string(None), "-") def test_string_or_none_to_string_replacement_defined(self): - self.assertEqual(string_or_none_to_string(None, 'no value'), 'no value') + self.assertEqual(string_or_none_to_string(None, "no value"), "no value") def test_encode_to_xml_regular(self): - xml_str = 'Something with an ampersand (&)' - self.assertEqual(encode_to_xml(xml_str), '<tag>Something with an ampersand (&)</tag>') + xml_str = "Something with an ampersand (&)" + self.assertEqual(encode_to_xml(xml_str), "<tag>Something with an ampersand (&)</tag>") diff --git a/tests/tests/mixins/test_django_message_framework.py b/tests/tests/mixins/test_django_message_framework.py index ec15338..410ce29 100644 --- a/tests/tests/mixins/test_django_message_framework.py +++ b/tests/tests/mixins/test_django_message_framework.py @@ -12,9 +12,9 @@ def setUpTestData(cls): cls.request = cls.get_request() def test_full_message_found(self): - messages.add_message(self.request, messages.INFO, 'My message') - self.assert_full_message_in_request(request=self.request, message='My message') + messages.add_message(self.request, messages.INFO, "My message") + self.assert_full_message_in_request(request=self.request, message="My message") def test_partial_message_found(self): - messages.add_message(self.request, messages.INFO, 'My message') - self.assert_partial_message_in_request(request=self.request, message='My') + messages.add_message(self.request, messages.INFO, "My message") + self.assert_partial_message_in_request(request=self.request, message="My") diff --git a/tests/tests/mixins/test_mixins.py b/tests/tests/mixins/test_mixins.py new file mode 100644 index 0000000..d716b9b --- /dev/null +++ b/tests/tests/mixins/test_mixins.py @@ -0,0 +1,57 @@ +from django.contrib.auth.models import AnonymousUser, User +from django.http import HttpResponse +from django.test import RequestFactory, TestCase +from django.views import generic + +from ambient_toolbox.tests.mixins import ClassBasedViewTestMixin + + +class TestView(generic.TemplateView): + def get(self, request, *args, **kwargs): + return HttpResponse(status=202) + + def post(self, request, *args, **kwargs): + return HttpResponse(status=203) + + def delete(self, request, *args, **kwargs): + return HttpResponse(status=204) + + +class ClassBasedViewTestMixinTest(ClassBasedViewTestMixin, TestCase): + view_class = TestView + + @classmethod + def setUpTestData(cls): + super().setUpTestData() + + factory = RequestFactory() + cls.request = factory.get("/admin") + cls.user = User.objects.create(username="my-username") + + def test_authentication_user_given(self): + self._authentication(self.request, self.user), self.user + self.assertEqual(self.request.user, self.user) + + def test_authentication_no_user_given(self): + self._authentication(self.request, None), self.user + self.assertEqual(self.request.user, AnonymousUser()) + + def test_get_response_regular(self): + response = self._get_response(method="get", user=self.user, data={"my_data": 42}) + self.assertIsInstance(response, HttpResponse) + self.assertEqual(response.status_code, 202) + + def test_get_regular(self): + response = self.get() + self.assertIsInstance(response, HttpResponse) + self.assertEqual(response.status_code, 202) + + def test_post_regular(self): + response = self.post() + self.assertIsInstance(response, HttpResponse) + self.assertEqual(response.status_code, 203) + + def test_delete_regular(self): + response = self.delete() + self.assertIsInstance(response, HttpResponse) + self.assertEqual(response.status_code, 204) diff --git a/tests/tests/mixins/models.py b/tests/tests/mixins/test_models.py similarity index 63% rename from tests/tests/mixins/models.py rename to tests/tests/mixins/test_models.py index bc32dfd..748d781 100644 --- a/tests/tests/mixins/models.py +++ b/tests/tests/mixins/test_models.py @@ -6,6 +6,3 @@ class PermissionModelMixinTest(TestCase): def test_meta_managed_false(self): self.assertFalse(MyPermissionModelMixin.Meta.managed) - - def test_meta_no_default_permissions(self): - self.assertEqual(len(MyPermissionModelMixin.Meta.default_permissions), 0) diff --git a/tests/tests/mixins/test_request_provider_mixin.py b/tests/tests/mixins/test_request_provider_mixin.py index 6daa6cc..0d6a772 100644 --- a/tests/tests/mixins/test_request_provider_mixin.py +++ b/tests/tests/mixins/test_request_provider_mixin.py @@ -14,7 +14,7 @@ def test_request_is_request(self): self.assertIsInstance(request, HttpRequest) def test_request_user_set(self): - user = User.objects.create(username='albertus_magnus') + user = User.objects.create(username="albertus_magnus") request = self.get_request(user) self.assertEqual(request.user, user) @@ -26,23 +26,23 @@ def test_django_messages_set_up_correctly(self): request = self.get_request(None) # This would fail if the django messages were not set up correctly - messages.add_message(request, messages.SUCCESS, 'I am a great message!') + messages.add_message(request, messages.SUCCESS, "I am a great message!") self.assertIsInstance(request.session, SessionBase) def test_django_session_set_up_correctly(self): request = self.get_request(None) - request.session['my_val'] = 27 + request.session["my_val"] = 27 request.session.modified = True - self.assertEqual(request.session['my_val'], 27) + self.assertEqual(request.session["my_val"], 27) def test_passed_user_is_none(self): request = self.get_request(None) self.assertIsNone(request.user) def test_passed_user_is_regular_user(self): - user = User.objects.create(username='albertus_magnus') + user = User.objects.create(username="albertus_magnus") request = self.get_request(user) self.assertEqual(request.user, user) @@ -58,8 +58,8 @@ def test_passed_user_is_other_type(self): def test_default_url_used(self): request = self.get_request() - self.assertEqual(request.build_absolute_uri(), 'http://testserver/') + self.assertEqual(request.build_absolute_uri(), "http://testserver/") def test_passed_url_used(self): - request = self.get_request(url='my-url') - self.assertEqual(request.build_absolute_uri(), 'http://testserver/my-url') + request = self.get_request(url="my-url") + self.assertEqual(request.build_absolute_uri(), "http://testserver/my-url") diff --git a/tests/tests/test_mail_backends.py b/tests/tests/test_mail_backends.py index 63b49d4..10f60b3 100644 --- a/tests/tests/test_mail_backends.py +++ b/tests/tests/test_mail_backends.py @@ -1,44 +1,62 @@ +from unittest import mock + from django.core.mail import EmailMultiAlternatives +from django.core.mail.backends.smtp import EmailBackend from django.test import TestCase, override_settings from ambient_toolbox.mail.backends.whitelist_smtp import WhitelistEmailBackend @override_settings( - EMAIL_BACKEND='ambient_toolbox.mail.backends.whitelist_smtp.WhitelistEmailBackend', - EMAIL_BACKEND_DOMAIN_WHITELIST=['valid.domain'], - EMAIL_BACKEND_REDIRECT_ADDRESS='%s@testuser.valid.domain', + EMAIL_BACKEND="ambient_toolbox.mail.backends.whitelist_smtp.WhitelistEmailBackend", + EMAIL_BACKEND_DOMAIN_WHITELIST=["valid.domain"], + EMAIL_BACKEND_REDIRECT_ADDRESS="%s@testuser.valid.domain", ) class MailBackendWhitelistBackendTest(TestCase): def test_whitify_mail_addresses_replace(self): - email_1 = 'albertus.magnus@example.com' - email_2 = 'thomas_von_aquin@example.com' + email_1 = "albertus.magnus@example.com" + email_2 = "thomas_von_aquin@example.com" processed_list = WhitelistEmailBackend.whitify_mail_addresses(mail_address_list=[email_1, email_2]) self.assertEqual(len(processed_list), 2) - self.assertEqual(processed_list[0], 'albertus.magnus_example.com@testuser.valid.domain') - self.assertEqual(processed_list[1], 'thomas_von_aquin_example.com@testuser.valid.domain') + self.assertEqual(processed_list[0], "albertus.magnus_example.com@testuser.valid.domain") + self.assertEqual(processed_list[1], "thomas_von_aquin_example.com@testuser.valid.domain") def test_whitify_mail_addresses_whitelisted_domain(self): - email = 'platon@valid.domain' + email = "platon@valid.domain" processed_list = WhitelistEmailBackend.whitify_mail_addresses(mail_address_list=[email]) self.assertEqual(len(processed_list), 1) self.assertEqual(processed_list[0], email) - @override_settings(EMAIL_BACKEND_REDIRECT_ADDRESS='') + @override_settings(EMAIL_BACKEND_REDIRECT_ADDRESS="") def test_whitify_mail_addresses_no_redirect_configured(self): - email = 'sokrates@example.com' + email = "sokrates@example.com" processed_list = WhitelistEmailBackend.whitify_mail_addresses(mail_address_list=[email]) self.assertEqual(len(processed_list), 0) def test_process_recipients_regular(self): mail = EmailMultiAlternatives( - 'Test subject', 'Here is the message.', 'from@example.com', ['to@example.com'], connection=None + "Test subject", "Here is the message.", "from@example.com", ["to@example.com"], connection=None ) backend = WhitelistEmailBackend() message_list = backend._process_recipients([mail]) self.assertEqual(len(message_list), 1) - self.assertEqual(message_list[0].to, ['to_example.com@testuser.valid.domain']) + self.assertEqual(message_list[0].to, ["to_example.com@testuser.valid.domain"]) + + @mock.patch.object(EmailBackend, "send_messages") + @mock.patch.object(WhitelistEmailBackend, "_process_recipients") + def test_send_messages_process_recipients_called(self, mocked_process_recipients, *args): + backend = WhitelistEmailBackend() + backend.send_messages([]) + + mocked_process_recipients.assert_called_once_with([]) + + @mock.patch.object(EmailBackend, "send_messages") + def test_send_messages_super_called(self, mocked_send_messages): + backend = WhitelistEmailBackend() + backend.send_messages([]) + + mocked_send_messages.assert_called_once_with([]) diff --git a/tests/view_layer/test_formset_mixins.py b/tests/view_layer/test_formset_mixins.py index f6d41ff..9e2792b 100644 --- a/tests/view_layer/test_formset_mixins.py +++ b/tests/view_layer/test_formset_mixins.py @@ -9,7 +9,7 @@ class ForeignKeyRelatedModelForm(forms.ModelForm): class Meta: model = ForeignKeyRelatedModel - fields = ('single_signal',) + fields = ("single_signal",) class MySingleSignalModelFormset(CountChildrenFormsetMixin, BaseInlineFormSet): @@ -47,10 +47,10 @@ def test_regular_with_data(self): ) formset = formset_class( - {'fkrm-INITIAL_FORMS': '2', 'fkrm-MIN_NUM_FORMS': '2', 'fkrm-MAX_NUM_FORMS': '3', 'fkrm-TOTAL_FORMS': '2'}, + {"fkrm-INITIAL_FORMS": "2", "fkrm-MIN_NUM_FORMS": "2", "fkrm-MAX_NUM_FORMS": "3", "fkrm-TOTAL_FORMS": "2"}, None, instance=mssm, - prefix='fkrm', + prefix="fkrm", ) formset.is_valid() diff --git a/tests/view_layer/test_htmx_response_mixin.py b/tests/view_layer/test_htmx_response_mixin.py index b4389bf..49c26c4 100644 --- a/tests/view_layer/test_htmx_response_mixin.py +++ b/tests/view_layer/test_htmx_response_mixin.py @@ -8,37 +8,37 @@ class HtmxResponseMixinTest(RequestProviderMixin, TestCase): class TestView(HtmxResponseMixin, generic.View): - hx_redirect_url = 'https://my-url.com' - hx_trigger = 'myEvent' + hx_redirect_url = "https://my-url.com" + hx_trigger = "myEvent" class TestViewWithTriggerDict(HtmxResponseMixin, generic.View): - hx_trigger = {'myEvent': None} + hx_trigger = {"myEvent": None} def test_dispatch_functional(self): view = self.TestView() response = view.dispatch(request=self.get_request(user=AnonymousUser())) - self.assertIn('HX-Redirect', response) - self.assertEqual(response['HX-Redirect'], 'https://my-url.com') + self.assertIn("HX-Redirect", response) + self.assertEqual(response["HX-Redirect"], "https://my-url.com") - self.assertIn('HX-Trigger', response) - self.assertEqual(response['HX-Trigger'], 'myEvent') + self.assertIn("HX-Trigger", response) + self.assertEqual(response["HX-Trigger"], "myEvent") def test_dispatch_trigger_with_dict(self): view = self.TestViewWithTriggerDict() response = view.dispatch(request=self.get_request(user=AnonymousUser())) - self.assertIn('HX-Trigger', response) - self.assertEqual(response['HX-Trigger'], "{\"myEvent\": null}") + self.assertIn("HX-Trigger", response) + self.assertEqual(response["HX-Trigger"], '{"myEvent": null}') def test_get_hx_redirect_url_regular(self): view = self.TestView() - self.assertEqual(view.get_hx_redirect_url(), 'https://my-url.com') + self.assertEqual(view.get_hx_redirect_url(), "https://my-url.com") def test_get_hx_trigger_regular(self): view = self.TestView() - self.assertEqual(view.get_hx_trigger(), 'myEvent') + self.assertEqual(view.get_hx_trigger(), "myEvent") diff --git a/tests/view_layer/test_meta_mixins.py b/tests/view_layer/test_meta_mixins.py index 1b88e5a..50656c2 100644 --- a/tests/view_layer/test_meta_mixins.py +++ b/tests/view_layer/test_meta_mixins.py @@ -1,3 +1,5 @@ +from unittest import mock + from django.contrib.auth.models import AnonymousUser, Permission, User from django.http import HttpResponse from django.test import TestCase @@ -12,36 +14,37 @@ class TestViewNoPerms(DjangoPermissionRequiredMixin, generic.View): pass class TestViewSinglePerm(DjangoPermissionRequiredMixin, generic.View): - permission_list = ['auth.change_user'] + permission_list = ["auth.change_user"] + login_view_name = "other-login-view" def get(self, *args, **kwargs): return HttpResponse(status=200) class TestViewMultiplePerms(DjangoPermissionRequiredMixin, generic.View): - permission_list = ['auth.change_user', 'auth.add_user'] + permission_list = ["auth.change_user", "auth.add_user"] def get_login_url(self): - return 'login/' + return "login/" class TestDifferentLoginNameView(DjangoPermissionRequiredMixin, generic.View): - permission_list = ['auth.change_user'] - login_view_name = 'other-login-view' + permission_list = ["auth.change_user"] + login_view_name = "other-login-view" @classmethod def setUpTestData(cls): super().setUpTestData() - cls.permission = Permission.objects.get_by_natural_key(app_label='auth', codename='change_user', model='user') + cls.permission = Permission.objects.get_by_natural_key(app_label="auth", codename="change_user", model="user") def setUp(self) -> None: super().setUp() - self.user = User.objects.create(username='test_user', email='test.user@ambient-toolbox.com') + self.user = User.objects.create(username="test_user", email="test.user@ambient-toolbox.com") def test_get_login_url(self): - self.assertEqual(self.TestViewMultiplePerms().get_login_url(), 'login/') + self.assertEqual(self.TestViewMultiplePerms().get_login_url(), "login/") def test_get_custom_login_url(self): - self.assertEqual(self.TestDifferentLoginNameView().get_login_url(), '/other/login/') + self.assertEqual(self.TestDifferentLoginNameView().get_login_url(), "/other/login/") def test_permissions_are_set_validation(self): with self.assertRaises(RuntimeError): @@ -80,6 +83,13 @@ def test_has_permissions_django_superuser_is_always_allowed(self): self.user.is_superuser = True self.assertTrue(self.TestViewSinglePerm().has_permissions(self.user)) + @mock.patch.object(TestViewSinglePerm, "passes_login_barrier", return_value=True) + @mock.patch.object(TestViewSinglePerm, "has_permissions", return_value=True) + def test_dispatch_regular(self, *args): + response = self.TestViewSinglePerm().dispatch(request=self.get_request(self.user)) + + self.assertEqual(response.status_code, 200) + def test_dispatch_lockout_on_missing_permissions(self): response = self.TestViewSinglePerm().dispatch(request=self.get_request(self.user)) @@ -91,3 +101,10 @@ def test_dispatch_working_on_having_permissions(self): response = view.dispatch(request=self.get_request(self.user)) self.assertEqual(response.status_code, 200) + + @mock.patch.object(TestViewSinglePerm, "passes_login_barrier", return_value=False) + def test_dispatch_passes_login_barrier_false(self, *args): + view = self.TestViewSinglePerm() + response = view.dispatch(request=self.get_request(self.user)) + + self.assertEqual(response.status_code, 302) diff --git a/tests/view_layer/test_views.py b/tests/view_layer/test_views.py index 0f44b8e..faa4dba 100644 --- a/tests/view_layer/test_views.py +++ b/tests/view_layer/test_views.py @@ -10,19 +10,19 @@ class UserInFormKwargsMixinTest(RequestProviderMixin, TestCase): def test_get_form_kwargs_regular(self): - user = User(username='my-user') + user = User(username="my-user") view = UserInFormKwargsMixinView() view.request = self.get_request(user=user) form_kwargs = view.get_form_kwargs() - self.assertIn('user', form_kwargs) - self.assertEqual(form_kwargs['user'], user) + self.assertIn("user", form_kwargs) + self.assertEqual(form_kwargs["user"], user) class ToggleViewTest(RequestProviderMixin, TestCase): def test_http_method_set_correctly(self): - self.assertEqual(ToggleView.http_method_names, ('post',)) + self.assertEqual(ToggleView.http_method_names, ("post",)) def test_post_raises_not_implemented_error(self): with self.assertRaises(NotImplementedError):