From cab9e7fc3f76d8b4601ab8f82f86cb1c417726d6 Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Tue, 31 Dec 2024 14:48:08 +0200 Subject: [PATCH 1/6] deps: add ruff and migrate to pyproject.toml refs: ATV-201 --- pyproject.toml | 11 +++++++++++ requirements-dev.in | 6 +----- requirements-dev.txt | 36 +++--------------------------------- setup.cfg | 36 ------------------------------------ 4 files changed, 15 insertions(+), 74 deletions(-) create mode 100644 pyproject.toml delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3bf2e09 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,11 @@ +[tool.ruff] +target-version = "py311" + +[tool.pytest.ini_options] +DJANGO_SETTINGS_MODULE = "atv.settings" +norecursedirs = [".git", "venv*", "var"] +doctest_optionflags = ["NORMALIZE_WHITESPACE", "IGNORE_EXCEPTION_DETAIL", "ALLOW_UNICODE"] + +[tool.coverage.run] +branch = true +omit = ["*migrations*", "*site-packages*", "*venv*", "*tests*"] diff --git a/requirements-dev.in b/requirements-dev.in index be0b931..347b193 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -1,15 +1,11 @@ -c requirements.txt -black factory_boy -flake8 -flake8-bugbear freezegun ipython -isort -pep8-naming pre-commit pytest pytest-cov pytest-django pytest-factoryboy +ruff snapshottest diff --git a/requirements-dev.txt b/requirements-dev.txt index 58c0799..7eb6fdf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,16 +6,8 @@ # asttokens==2.4.1 # via stack-data -attrs==24.2.0 - # via - # -c requirements.txt - # flake8-bugbear -black==24.10.0 - # via -r requirements-dev.in cfgv==3.4.0 # via pre-commit -click==8.1.7 - # via black coverage[toml]==7.6.1 # via pytest-cov decorator==5.1.1 @@ -34,13 +26,6 @@ fastdiff==0.3.0 # via snapshottest filelock==3.16.1 # via virtualenv -flake8==7.1.1 - # via - # -r requirements-dev.in - # flake8-bugbear - # pep8-naming -flake8-bugbear==24.8.19 - # via -r requirements-dev.in freezegun==1.5.1 # via -r requirements-dev.in identify==2.6.1 @@ -53,36 +38,23 @@ iniconfig==2.0.0 # via pytest ipython==8.27.0 # via -r requirements-dev.in -isort==5.13.2 - # via -r requirements-dev.in jedi==0.19.1 # via ipython matplotlib-inline==0.1.7 # via ipython -mccabe==0.7.0 - # via flake8 -mypy-extensions==1.0.0 - # via black nodeenv==1.9.1 # via pre-commit packaging==24.1 # via # -c requirements.txt - # black # pytest # pytest-factoryboy parso==0.8.4 # via jedi -pathspec==0.12.1 - # via black -pep8-naming==0.14.1 - # via -r requirements-dev.in pexpect==4.9.0 # via ipython platformdirs==4.3.6 - # via - # black - # virtualenv + # via virtualenv pluggy==1.5.0 # via pytest pre-commit==3.8.0 @@ -93,10 +65,6 @@ ptyprocess==0.7.0 # via pexpect pure-eval==0.2.3 # via stack-data -pycodestyle==2.12.1 - # via flake8 -pyflakes==3.2.0 - # via flake8 pygments==2.18.0 # via ipython pytest==8.3.3 @@ -119,6 +87,8 @@ pyyaml==6.0.2 # via # -c requirements.txt # pre-commit +ruff==0.8.4 + # via -r requirements-dev.in six==1.16.0 # via # -c requirements.txt diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 37216ea..0000000 --- a/setup.cfg +++ /dev/null @@ -1,36 +0,0 @@ -[pep8] -max-line-length = 120 -exclude = *migrations* -ignore = E309 - -[flake8] -max-line-length = 120 -exclude = *migrations* -max-complexity = 10 - -[tool:pytest] -DJANGO_SETTINGS_MODULE = atv.settings -norecursedirs = .git venv* var -doctest_optionflags = NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL ALLOW_UNICODE - -[coverage:run] -branch = True -omit = *migrations*,*site-packages*,*venv*,*tests* - -[isort] -atomic = true -combine_as_imports=true -indent = 4 -length_sort = false -multi_line_output = 3 -order_by_type = false -skip = migrations,venv -include_trailing_comma = True -force_grid_wrap = 0 -use_parentheses = True -line_length = 88 -default_section = THIRDPARTY -extra_standard_library = token,tokenize,enum,importlib -known_first_party = - atv,documents,services,utils -known_third_party = django,six From 06779198cac62400666d75dc3a57316a9e72f56f Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Tue, 31 Dec 2024 15:12:11 +0200 Subject: [PATCH 2/6] chore: replace black, isort, flake8 with ruff in pre commit refs: ATV-201 --- .pre-commit-config.yaml | 18 +++++++----------- pyproject.toml | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77dff34..be5c009 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,18 +12,14 @@ repos: - id: check-yaml - id: check-toml - id: check-added-large-files - - repo: https://github.com/psf/black - rev: 24.10.0 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.8.4 hooks: - - id: black - - repo: https://github.com/pycqa/flake8 - rev: 7.0.0 - hooks: - - id: flake8 - - repo: https://github.com/pycqa/isort - rev: 5.13.2 - hooks: - - id: isort + - id: ruff + name: ruff lint + - id: ruff-format + name: ruff format + args: [ --check ] - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook rev: v9.13.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index 3bf2e09..5dcf072 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,26 @@ [tool.ruff] target-version = "py311" +[tool.ruff.lint] +select = [ + # Pyflakes + "F", + # pycodestyle + "E", + "W", + # isort + "I", + # pep8-naming + "N", + # flake8-bugbear without opinionated rules + "B0", + # flake8-pie + "PIE", + # flake8-print + "T20", +] +extend-per-file-ignores = { "*/migrations/*" = ["E501"], "*/tests/*" = ["E501"] } + [tool.pytest.ini_options] DJANGO_SETTINGS_MODULE = "atv.settings" norecursedirs = [".git", "venv*", "var"] From 08bf2f682ff82545e9f06851d50de0905ad745e0 Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Tue, 31 Dec 2024 15:13:56 +0200 Subject: [PATCH 3/6] chore: update README on ruff refs: ATV-201 --- README.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 7ac56c5..5efde79 100644 --- a/README.md +++ b/README.md @@ -102,17 +102,15 @@ The project is now running at [localhost:8000](http://localhost:8000) ## Code format -This project uses -[`black`](https://github.com/ambv/black), -[`flake8`](https://gitlab.com/pycqa/flake8) and -[`isort`](https://github.com/timothycrosley/isort) -for code formatting and quality checking. Project follows the basic -black config, without any modifications. -Basic `black` commands: +This project uses [Ruff](https://docs.astral.sh/ruff/) for code formatting and quality checking. -* To let `black` do its magic: `black .` -* To see which files `black` would change: `black --check .` +Basic `ruff` commands: + +* lint: `ruff check` +* apply safe lint fixes: `ruff check --fix` +* check formatting: `ruff format --check` +* format: `ruff format` [`pre-commit`](https://pre-commit.com/) can be used to install and run all the formatting tools as git hooks automatically before a From 66c33ed3ab357d19769943b49638e13a04139d86 Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Thu, 2 Jan 2025 09:56:38 +0200 Subject: [PATCH 4/6] style: apply ruff * check --fix * format refs: ATV-201 --- documents/api/docs.py | 7 ++++--- documents/api/filtersets.py | 3 ++- .../migrations/0003_add_encrypted_document_content.py | 2 +- documents/migrations/0005_add_statushistory_model.py | 2 +- documents/migrations/0006_document_status_timestamp.py | 2 +- documents/migrations/0007_alter_document_content.py | 1 + documents/migrations/0008_alter_attachment_file.py | 1 + documents/migrations/0011_add_activity_model.py | 2 +- .../0015_alter_document_service_alter_document_user.py | 2 +- documents/tests/test_api_list_documents.py | 2 +- manage.py | 1 + 11 files changed, 15 insertions(+), 10 deletions(-) diff --git a/documents/api/docs.py b/documents/api/docs.py index 707921a..9e10607 100644 --- a/documents/api/docs.py +++ b/documents/api/docs.py @@ -1,10 +1,10 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import ( - extend_schema, - inline_serializer, OpenApiExample, OpenApiParameter, OpenApiResponse, + extend_schema, + inline_serializer, ) from rest_framework import serializers, status @@ -737,7 +737,8 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: # TODO: Uncomment when organization features are implemented # "or the document is owned by an organization and the user has permission to act " # "on behalf of that organization.\n\n" - "The following rules apply:\n" "* Drafts may be removed by the owning user.", + "The following rules apply:\n" + "* Drafts may be removed by the owning user.", # TODO: Uncomment when organization features are implemented # "or an organization's representative, " # "if the document is owned by an organization. This is possible even if the `lockedAfter` date has passed, " diff --git a/documents/api/filtersets.py b/documents/api/filtersets.py index 01bbce1..2d63b2f 100644 --- a/documents/api/filtersets.py +++ b/documents/api/filtersets.py @@ -1,4 +1,5 @@ -from django_filters import BaseInFilter, CharFilter, Filter, rest_framework as filters +from django_filters import BaseInFilter, CharFilter, Filter +from django_filters import rest_framework as filters from django_filters.constants import EMPTY_VALUES from rest_framework.exceptions import ValidationError diff --git a/documents/migrations/0003_add_encrypted_document_content.py b/documents/migrations/0003_add_encrypted_document_content.py index e0a3198..580767b 100644 --- a/documents/migrations/0003_add_encrypted_document_content.py +++ b/documents/migrations/0003_add_encrypted_document_content.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.3 on 2021-06-29 17:44 -from django.db import migrations import encrypted_fields.fields +from django.db import migrations class Migration(migrations.Migration): diff --git a/documents/migrations/0005_add_statushistory_model.py b/documents/migrations/0005_add_statushistory_model.py index 483e8b3..5bd344c 100644 --- a/documents/migrations/0005_add_statushistory_model.py +++ b/documents/migrations/0005_add_statushistory_model.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.9 on 2022-03-04 15:20 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/documents/migrations/0006_document_status_timestamp.py b/documents/migrations/0006_document_status_timestamp.py index 2daeae1..d3456c9 100644 --- a/documents/migrations/0006_document_status_timestamp.py +++ b/documents/migrations/0006_document_status_timestamp.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.9 on 2022-04-13 14:00 -from django.db import migrations, models import django.utils.timezone +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/documents/migrations/0007_alter_document_content.py b/documents/migrations/0007_alter_document_content.py index fc84546..d27d0d0 100644 --- a/documents/migrations/0007_alter_document_content.py +++ b/documents/migrations/0007_alter_document_content.py @@ -1,6 +1,7 @@ # Generated by Django 3.2.9 on 2022-05-03 08:33 from django.db import migrations + import documents.models diff --git a/documents/migrations/0008_alter_attachment_file.py b/documents/migrations/0008_alter_attachment_file.py index 52c1fc9..545b6bf 100644 --- a/documents/migrations/0008_alter_attachment_file.py +++ b/documents/migrations/0008_alter_attachment_file.py @@ -1,6 +1,7 @@ # Generated by Django 3.2.9 on 2022-06-08 14:15 from django.db import migrations + import documents.fields import documents.utils diff --git a/documents/migrations/0011_add_activity_model.py b/documents/migrations/0011_add_activity_model.py index 3445998..786eed0 100644 --- a/documents/migrations/0011_add_activity_model.py +++ b/documents/migrations/0011_add_activity_model.py @@ -1,7 +1,7 @@ # Generated by Django 3.2.16 on 2023-05-15 12:02 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/documents/migrations/0015_alter_document_service_alter_document_user.py b/documents/migrations/0015_alter_document_service_alter_document_user.py index 6c829c8..1f7bc41 100644 --- a/documents/migrations/0015_alter_document_service_alter_document_user.py +++ b/documents/migrations/0015_alter_document_service_alter_document_user.py @@ -1,8 +1,8 @@ # Generated by Django 4.2.9 on 2024-01-23 14:16 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): diff --git a/documents/tests/test_api_list_documents.py b/documents/tests/test_api_list_documents.py index 10828b3..de7cfd0 100644 --- a/documents/tests/test_api_list_documents.py +++ b/documents/tests/test_api_list_documents.py @@ -182,7 +182,7 @@ def test_document_batch_list_service_api_key(service_api_client, user): data = {**VALID_DOCUMENT_DATA, "user_id": user.uuid} other_service = ServiceFactory() document_ids = [] - for _i in range(0, 2): + for _i in range(2): response = service_api_client.post( reverse("documents-list"), data, format="multipart" ) diff --git a/manage.py b/manage.py index 4ba91c2..d92f731 100755 --- a/manage.py +++ b/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys From fbbbf23903a1491dcced1283cf061334b471ce81 Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Thu, 2 Jan 2025 13:35:11 +0200 Subject: [PATCH 5/6] style: add noqa N806 refs: ATV-201 --- services/migrations/0005_add_new_service_permissions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/migrations/0005_add_new_service_permissions.py b/services/migrations/0005_add_new_service_permissions.py index 1432c7f..d2bdaaa 100644 --- a/services/migrations/0005_add_new_service_permissions.py +++ b/services/migrations/0005_add_new_service_permissions.py @@ -5,8 +5,8 @@ def remove_manage_permissions(apps, schema_editor): """Remove old permissions.""" - ContentType = apps.get_model("contenttypes.ContentType") - Permission = apps.get_model("auth.Permission") + ContentType = apps.get_model("contenttypes.ContentType") # noqa: N806 + Permission = apps.get_model("auth.Permission") # noqa: N806 content_type = ContentType.objects.filter( model="service", app_label="services", @@ -22,8 +22,8 @@ def remove_manage_permissions(apps, schema_editor): def remove_fine_grained_permissions(apps, schema_editor): """Reverse new permissions.""" - ContentType = apps.get_model("contenttypes.ContentType") - Permission = apps.get_model("auth.Permission") + ContentType = apps.get_model("contenttypes.ContentType") # noqa: N806 + Permission = apps.get_model("auth.Permission") # noqa: N806 content_type = ContentType.objects.get( model="service", app_label="services", From e487b522170e100c30ac0c3d880368b8e140ebc8 Mon Sep 17 00:00:00 2001 From: Matti Eiden Date: Thu, 2 Jan 2025 13:35:26 +0200 Subject: [PATCH 6/6] style: manual reformatting to comply with ruff refs: ATV-201 --- atv/authentication.py | 8 +- atv/decorators.py | 5 +- audit_log/tasks.py | 6 +- audit_log/viewsets.py | 3 +- documents/api/docs.py | 512 +++++++++++------- documents/api/filtersets.py | 4 +- documents/api/querysets.py | 3 +- documents/api/viewsets.py | 6 +- .../commands/delete_expired_documents.py | 5 +- .../commands/remove_outdated_files.py | 3 +- documents/models.py | 19 +- documents/serializers/document.py | 21 +- documents/tasks.py | 3 +- documents/utils.py | 15 +- services/signals.py | 3 +- users/models.py | 3 +- utils/commands.py | 3 +- utils/exceptions.py | 12 +- 18 files changed, 401 insertions(+), 233 deletions(-) diff --git a/atv/authentication.py b/atv/authentication.py index 103fab0..4d0a5cc 100644 --- a/atv/authentication.py +++ b/atv/authentication.py @@ -30,9 +30,11 @@ def authenticate(self, request): if not service_key.user: raise exceptions.AuthenticationFailed("Permissions missing from API key.") - # Verified key is cached for 5 minutes. This improves subsequent requests' response times significantly. - # Default cache uses local memory caching. Keys aren't accessible from console or to other threads or pods - # TODO: if caching is refactored to use Redis or Memcached this needs to be reconsidered + # Verified key is cached for 5 minutes. This improves subsequent requests' + # response times significantly. Default cache uses local memory caching. Keys + # aren't accessible from console or to other threads or pods + # TODO: if caching is refactored to use Redis or Memcached this needs to be + # reconsidered cache.set(key, service_key, timeout=60 * 5) return service_key.user, service_key diff --git a/atv/decorators.py b/atv/decorators.py index 064d269..613b604 100644 --- a/atv/decorators.py +++ b/atv/decorators.py @@ -46,8 +46,9 @@ def permission_checker(request): def staff_required(required_permission=ServicePermissions.VIEW_DOCUMENTS): """ - Returns a decorator that checks if the user has staff permission on resources for the service - specified in the request. Required permission can be defined as an argument, defaults to 'view'. + Returns a decorator that checks if the user has staff permission on resources for + the service specified in the request. Required permission can be defined as an + argument, defaults to 'view'. """ if not isinstance(required_permission, ServicePermissions): diff --git a/audit_log/tasks.py b/audit_log/tasks.py index 8a75a03..7218809 100644 --- a/audit_log/tasks.py +++ b/audit_log/tasks.py @@ -21,7 +21,8 @@ def send_audit_log_entries(): if not (settings.ELASTIC_HOST and settings.ELASTIC_AUDIT_LOG_INDEX): logger.warning( - "Trying to send audit logs to Elasticsearch without proper configuration, process skipped" + "Trying to send audit logs to Elasticsearch without proper configuration," + " process skipped" ) return @@ -63,5 +64,6 @@ def clear_audit_log_entries(days_to_keep=30): if count := sent_entries.count(): sent_entries.delete() logger.info( - f"Cleared {count} sent audit logs, which were older than {days_to_keep} days." + f"Cleared {count} sent audit logs, which were older than {days_to_keep}" + " days." ) diff --git a/audit_log/viewsets.py b/audit_log/viewsets.py index d7fe133..a0f0427 100644 --- a/audit_log/viewsets.py +++ b/audit_log/viewsets.py @@ -135,7 +135,8 @@ def _get_ip_address(self) -> str: ] for ip in forwarded_for: try: - # This regexp matches IPv4 addresses without including the port number + # This regexp matches IPv4 addresses without including the port + # number regexp_for_ipv4 = re.match( r"(^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4})", ip ) diff --git a/documents/api/docs.py b/documents/api/docs.py index 9e10607..2e5da4a 100644 --- a/documents/api/docs.py +++ b/documents/api/docs.py @@ -261,7 +261,7 @@ "display_text": "samma på svenska", }, }, - "activity_timestamp": "2023-05-22T15:29:49.384845+03:00", + "activity_timestamp": "2023-05-22T15:29:49.384845+03:00", # noqa: E501 "show_to_user": True, } ], @@ -388,7 +388,10 @@ def _base_401_response(custom_message: str = None) -> OpenApiResponse: return OpenApiResponse( description=custom_message - or "Request’s credentials are missing or invalid. A valid JWT authentication is required.", + or ( + "Request’s credentials are missing or invalid. A valid JWT authentication" + " is required." + ), ) @@ -411,13 +414,18 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: attachment_viewset_docs = { "retrieve": extend_schema( summary="Download a document's attachment", - description="Permission to access the attachment is checked based on the containing document as follows:\n" - "* Admin users are allowed access to the attachment if the containing document was stored using the service " - "they are using and whose admins they are.\n" - "* Authenticated users are allowed access to the attachment if they are the owner of the containing document.", - # TODO: Uncomment when organization features are implemented - # " or the containing document is owned by an organization and the user has permission to act on behalf " - # "of that organization.", + description=( + "Permission to access the attachment is checked based on the containing" + " document as follows:\n" + "* Admin users are allowed access to the attachment if the containing" + " document was stored using the service they are using and whose admins" + " they are.\n" + "* Authenticated users are allowed access to the attachment if they are the" + " owner of the containing document." + # TODO: Uncomment when organization features are implemented + # " or the containing document is owned by an organization and the user has" + # " permission to act on behalf of that organization.", + ), responses={ (status.HTTP_200_OK, "application/octet-stream"): OpenApiResponse( description="Returns the attachment as a downloadable file.", @@ -425,17 +433,21 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), status.HTTP_401_UNAUTHORIZED: _base_401_response(), status.HTTP_403_FORBIDDEN: OpenApiResponse( - description="The authenticated user lacks the proper permissions to access the document. " - "Depending on the requested document, " - "either the user does not belong to the admin group of the service which owns the document, " - "the user does not own the document." - # TODO: Uncomment when organization features are implemented - # " or the user does not have permission to act on behalf " - # "of the organization which owns the document." + description=( + "The authenticated user lacks the proper permissions to access the" + " document. Depending on the requested document, either the user" + " does not belong to the admin group of the service which owns the" + " document, the user does not own the document." + # TODO: Uncomment when organization features are implemented + # " or the user does not have permission to act on behalf " + # "of the organization which owns the document." + ) ), status.HTTP_404_NOT_FOUND: OpenApiResponse( - description="No document was found with `documentId` or the document did not have " - "an attachment `attachmentId`." + description=( + "No document was found with `documentId` or the document did not" + " have an attachment `attachmentId`." + ) ), status.HTTP_500_INTERNAL_SERVER_ERROR: _base_500_response(), }, @@ -443,20 +455,24 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: ), "create": extend_schema( summary="Upload a new attachment to a document", - description="Permission to access the document is checked as follows:\n" - "* Authenticated users are allowed access to the document if they are the owner of the document.\n\n" - # TODO: Uncomment when organization features are implemented - # " or the document is owned by an organization and the user has permission to act on behalf " - # "of that organization.\n\n" - "The following rules apply:\n" - "* Drafts may be modified by the owning user or the owning service's admin\n" - # TODO: Uncomment when organization features are implemented - # Replace the previous line with the following - # "* Drafts may be modified by the owning user, " - # "the owning service's admin or an organization's representative, " - # "if the document is owned by an organization.\n" - "* Non-drafts may be modified by an admin.\n" - "* Documents may not be modified if their `lockedAfter` date has passed.", + description=( + "Permission to access the document is checked as follows:\n" + "* Authenticated users are allowed access to the document if they are the" + " owner of the document.\n\n" + # TODO: Uncomment when organization features are implemented + # " or the document is owned by an organization and the user has permission" + # " to act on behalf of that organization.\n\n" + "The following rules apply:\n" + "* Drafts may be modified by the owning user or the owning service's" + " admin\n" + # TODO: Uncomment when organization features are implemented + # Replace the previous line with the following + # "* Drafts may be modified by the owning user, " + # "the owning service's admin or an organization's representative, " + # "if the document is owned by an organization.\n" + "* Non-drafts may be modified by an admin.\n" + "* Documents may not be modified if their `lockedAfter` date has passed." + ), request={"application/octet-stream": OpenApiTypes.BINARY}, responses={ (status.HTTP_201_CREATED, "application/json"): OpenApiResponse( @@ -468,8 +484,11 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: status.HTTP_401_UNAUTHORIZED: _base_401_response(), # TODO: Uncomment when organization features are implemented # status.HTTP_403_FORBIDDEN: OpenApiResponse( - # description="The request contains an organization ID and the currently authenticated user " - # "does not have permission to act on behalf of that organization." + # description=( + # "The request contains an organization ID and the currently" + # " authenticated user does not have permission to act on behalf of" + # " that organization." + # ) # ), status.HTTP_500_INTERNAL_SERVER_ERROR: _base_500_response(), }, @@ -477,19 +496,24 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: ), "destroy": extend_schema( summary="Remove a document's attachment", - description="Permission to remove the attachment is checked based on the containing document as follows:\n" - "* Authenticated users are allowed to remove the attachment if they are the owner " - "of the containing document.\n\n" - # TODO: Uncomment when organization features are implemented - # "or the containing document is owned by an organization and the user has permission to act on behalf " - # "of that organization.\n\n" - "The following rules apply:\n" - "* Attachments of drafts may be removed by the owning user." - # TODO: Uncomment when organization features are implemented - # "or an organization's representative, " - # "if the containing document is owned by an organization." - "* Attachments may not be removed if the containing document's `lockedAfter` date has passed. " - "This should be done by removing the whole document.", + description=( + "Permission to remove the attachment is checked based on the containing" + " document as follows:\n" + "* Authenticated users are allowed to remove the attachment if they are the" + " owner of the containing document.\n\n" + # TODO: Uncomment when organization features are implemented + # "or the containing document is owned by an organization and the user has" + # " permission to act on behalf " + # "of that organization.\n\n" + "The following rules apply:\n" + "* Attachments of drafts may be removed by the owning user." + # TODO: Uncomment when organization features are implemented + # "or an organization's representative, " + # "if the containing document is owned by an organization." + "* Attachments may not be removed if the containing document's" + " `lockedAfter` date has passed. This should be done by removing the whole" + " document." + ), responses={ status.HTTP_204_NO_CONTENT: OpenApiResponse( description="The attachment was removed successfully.", @@ -497,19 +521,24 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), status.HTTP_401_UNAUTHORIZED: _base_401_response(), status.HTTP_403_FORBIDDEN: OpenApiResponse( - description="The authenticated user lacks the proper permissions to access the document. " - "Depending on the requested document, " - "either the user does not belong to the admin group of the service which owns the document " - "or the user does not own the document." - # TODO: Uncomment when organization features are implemented - # Replace the previous two lines with the following - # "either the user does not belong to the admin group of the service which owns the document, " - # "the user does not own the document or the user does not have permission to act on behalf " - # "of the organization which owns the document." + description=( + "The authenticated user lacks the proper permissions to access the" + " document. Depending on the requested document, either the user" + " does not belong to the admin group of the service which owns the" + " document or the user does not own the document." + # TODO: Uncomment when organization features are implemented + # Replace the previous two lines with the following + # "either the user does not belong to the admin group of the" + # " service which owns the document, the user does not own the" + # " document or the user does not have permission to act on behalf" + # " of the organization which owns the document." + ) ), status.HTTP_404_NOT_FOUND: OpenApiResponse( - description="No document was found with `documentId` or the document did not have " - "an attachment `attachmentId`." + description=( + "No document was found with `documentId` or the document did not" + " have an attachment `attachmentId`." + ) ), status.HTTP_500_INTERNAL_SERVER_ERROR: _base_500_response(), }, @@ -524,19 +553,25 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: document_viewset_docs = { "list": extend_schema( summary="Search for documents", - description="This endpoint can be used to fetch a list of documents. " - "The list can be filtered, sorted and paged using the appropriate query parameters.\n\n" - "The results will contain only those documents which are allowed for the current user.\n\n" - "* Admin users are allowed to fetch all documents which were stored using the service " - "they are using and whose admins they are.\n" - "* Authenticated users are allowed to fetch documents which are owned by them and " - "which were stored using the service they are using.", - # TODO: Uncomment when organization features are implemented - # "Documents owned by an organization are not returned even if such a document - # "was created by the current user.\n" - # "* Authenticated users may fetch documents owned by an organization by giving the organization's business ID " - # "in the search parameters. In this case the user's permission to act on behalf of the organization " - # "is verified and the results will contain only documents which are owned by the given organization.", + description=( + "This endpoint can be used to fetch a list of documents. " + "The list can be filtered, sorted and paged using the appropriate query" + " parameters.\n\n" + "The results will contain only those documents which are allowed for the" + " current user.\n\n" + "* Admin users are allowed to fetch all documents which were stored using" + " the service they are using and whose admins they are.\n" + "* Authenticated users are allowed to fetch documents which are owned by" + " them and which were stored using the service they are using." + # TODO: Uncomment when organization features are implemented + # "Documents owned by an organization are not returned even if such a" + # " document was created by the current user.\n" + # "* Authenticated users may fetch documents owned by an organization by" + # " giving the organization's business ID in the search parameters. In this" + # " case the user's permission to act on behalf of the organization is" + # " verified and the results will contain only documents which are owned by" + # " the given organization.", + ), parameters=[ OpenApiParameter( "status", @@ -551,18 +586,22 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: OpenApiParameter( "business_id", OpenApiTypes.STR, - description="Search for documents which are owned by the given business ID. " - "If this is given, the calling user must either be an admin", - # TODO: Uncomment when organization features are implemented - # Replace the previous line with the following - # "If this is given, the calling user must either be an admin or have permission to act " - # "on behalf of the organization", + description=( + "Search for documents which are owned by the given business ID. " + "If this is given, the calling user must either be an admin" + # TODO: Uncomment when organization features are implemented + # Replace the previous line with the following + # "If this is given, the calling user must either be an admin or " + # "have permission to act on behalf of the organization", + ), ), OpenApiParameter( "user_id", OpenApiTypes.UUID, - description="Search for documents which are owned by the given user ID. " - "If this is given, the calling user must be an admin of the service", + description=( + "Search for documents which are owned by the given user ID. " + "If this is given, the calling user must be an admin of the service" + ), ), OpenApiParameter( "transaction_id", @@ -572,9 +611,11 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: OpenApiParameter( "lookfor", OpenApiTypes.STR, - description="Search for documents with metadata matching key:value pairs separated by comma." - " ( key:value, key:value )." - " Lookup method for value is 'iexact', key must be exact and is case sensitive.", + description=( + "Search for documents with metadata matching key:value pairs" + " separated by comma. ( key:value, key:value ). Lookup method for" + " value is 'iexact', key must be exact and is case sensitive." + ), ), ], responses={ @@ -597,31 +638,38 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: ), "retrieve": extend_schema( summary="Fetch a document by ID", - description="This endpoint allows a user to fetch a document's details.\n\n" - "Permission to access the document is checked as follows:\n\n" - "* Admin users are allowed access to the document if it was stored using the service " - "they are using and whose admins they are.\n" - "* Authenticated users are allowed access to the document if they are the owner of the document.", - # TODO: Uncomment when organization features are implemented - # Replace the previous line with the following - # "* Authenticated users are allowed access to the document if they are the owner of the document " - # "or the document is owned by an organization and the user has permission to act on behalf " - # "of that organization.", + description=( + "This endpoint allows a user to fetch a document's details.\n\n" + "Permission to access the document is checked as follows:\n\n" + "* Admin users are allowed access to the document if it was stored using" + " the service they are using and whose admins they are.\n" + "* Authenticated users are allowed access to the document if they are the" + " owner of the document." + # TODO: Uncomment when organization features are implemented + # Replace the previous line with the following + # "* Authenticated users are allowed access to the document if they are the" + # " owner of the document or the document is owned by an organization and" + # " the user has permission to act on behalf of that organization." + ), responses={ (status.HTTP_200_OK, "application/json"): OpenApiResponse( response=DocumentSerializer, - description="The document was found and its contents are returned as JSON.", + description=( + "The document was found and its contents are returned as JSON." + ), ), (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), status.HTTP_401_UNAUTHORIZED: _base_401_response(), status.HTTP_403_FORBIDDEN: OpenApiResponse( - description="The authenticated user lacks the proper permissions to access the document. " - "Depending on the requested document, " - "either the user does not belong to the admin group of the service which owns the document, " - "the user does not own the document." - # TODO: Uncomment when organization features are implemented - # " or the user does not have permission to act on behalf " - # "of the organization which owns the document." + description=( + "The authenticated user lacks the proper permissions to access the" + " document. Depending on the requested document, either the user" + " does not belong to the admin group of the service which owns the" + " document, the user does not own the document." + # TODO: Uncomment when organization features are implemented + # " or the user does not have permission to act on behalf " + # "of the organization which owns the document." + ) ), status.HTTP_404_NOT_FOUND: OpenApiResponse( description="No document was found with `documentId`.", @@ -632,28 +680,34 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: ), "batch_list": extend_schema( summary="Fetch multiple documents by IDs", - description="This endpoint allows a service to fetch multiple documents by their IDs", - # TODO: Uncomment when organization features are implemented - # Replace the previous line with the following - # "* Authenticated users are allowed access to the document if they are the owner of the document " - # "or the document is owned by an organization and the user has permission to act on behalf " - # "of that organization.", + description=( + "This endpoint allows a service to fetch multiple documents by their IDs" + # TODO: Uncomment when organization features are implemented + # Replace the previous line with the following + # "* Authenticated users are allowed access to the document if they are the" + # " owner of the document or the document is owned by an organization and" + # " the user has permission to act on behalf of that organization." + ), request=serializers.JSONField(), responses={ (status.HTTP_200_OK, "application/json"): OpenApiResponse( response=DocumentSerializer, - description="The document/s was found and contents are returned as JSON.", + description=( + "The document/s was found and contents are returned as JSON." + ), ), (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), status.HTTP_401_UNAUTHORIZED: _base_401_response(), status.HTTP_403_FORBIDDEN: OpenApiResponse( - description="The authenticated user lacks the proper permissions to access the document. " - "Depending on the requested document, " - "either the user does not belong to the admin group of the service which owns the document, " - "the user does not own the document." - # TODO: Uncomment when organization features are implemented - # " or the user does not have permission to act on behalf " - # "of the organization which owns the document." + description=( + "The authenticated user lacks the proper permissions to access the" + " document. Depending on the requested document, either the user" + " does not belong to the admin group of the service which owns the" + " document, the user does not own the document." + # TODO: Uncomment when organization features are implemented + # " or the user does not have permission to act on behalf " + # "of the organization which owns the document." + ) ), status.HTTP_404_NOT_FOUND: OpenApiResponse( description="No document was found with `documentId`.", @@ -663,12 +717,16 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: ), "create": extend_schema( summary="Store a new document and its attachments", - description="Store a new document and its attachments.\n\n" - "This endpoint supports API key authentication to enable unauthenticated users to store documents. " - "Note that if this endpoint is used in such a way, accessing the stored documents later " - "will only be possible using the appropriate admin credentials.\n\n" - "This endpoint also supports authentication using a Bearer token, similarly to the other endpoints. " - "In this case the authenticated user is able to access the stored document normally afterwards.", + description=( + "Store a new document and its attachments.\n\n" + "This endpoint supports API key authentication to enable unauthenticated" + " users to store documents. Note that if this endpoint is used in such a" + " way, accessing the stored documents later will only be possible using " + "the appropriate admin credentials.\n\n" + "This endpoint also supports authentication using a Bearer token, similarly" + " to the other endpoints. In this case the authenticated user is able to" + " access the stored document normally afterwards." + ), responses={ (status.HTTP_201_CREATED, "application/json"): OpenApiResponse( response=DocumentSerializer, @@ -679,8 +737,11 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: status.HTTP_401_UNAUTHORIZED: _base_401_response(), # TODO: Uncomment when organization features are implemented # status.HTTP_403_FORBIDDEN: OpenApiResponse( - # description="The request contains an organization ID and the currently authenticated user " - # "does not have permission to act on behalf of that organization." + # description=( + # "The request contains an organization ID and the currently" + # " authenticated user does not have permission to act on behalf" + # " of that organization." + # ) # ), status.HTTP_500_INTERNAL_SERVER_ERROR: _base_500_response(), }, @@ -688,24 +749,29 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: ), "partial_update": extend_schema( summary="Update an existing document", - description="Permission to access the document is checked as follows:\n\n" - "* Admin users are allowed access to the document if it was stored using the service they are using " - "and whose admins they are.\n" - "* Authenticated users are allowed access to the document if they are the owner of the document.\n\n" - # TODO: Uncomment when organization features are implemented - # Replace the previous line with the following - # "* Authenticated users are allowed access to the document if they are the owner of the document " - # "or the document is owned by an organization and the user has permission to act " - # "on behalf of that organization.\n\n" - "The following rules apply:\n" - "* Drafts may be modified by the owning user or the owning service's admin.\n" - # TODO: Uncomment when organization features are implemented - # Replace the previous line with the following - # "* Drafts may be modified by the owning user, the owning service's admin " - # "or an organization's representative, " - # "if the document is owned by an organization.\n" - "* Non-drafts may be modified by an admin.\n" - "* Documents may not be modified if their `lockedAfter` date has passed.", + description=( + "Permission to access the document is checked as follows:\n\n" + "* Admin users are allowed access to the document if it was stored using" + "the service they are using and whose admins they are.\n" + "* Authenticated users are allowed access to the document if they are the" + " owner of the document.\n\n" + # TODO: Uncomment when organization features are implemented + # Replace the previous line with the following + # "* Authenticated users are allowed access to the document if they are the" + # " owner of the document " + # "or the document is owned by an organization and the user has permission" + # " to act on behalf of that organization.\n\n" + "The following rules apply:\n" + "* Drafts may be modified by the owning user or the owning service's admin." + "\n" + # TODO: Uncomment when organization features are implemented + # Replace the previous line with the following + # "* Drafts may be modified by the owning user, the owning service's admin " + # "or an organization's representative, " + # "if the document is owned by an organization.\n" + "* Non-drafts may be modified by an admin.\n" + "* Documents may not be modified if their `lockedAfter` date has passed." + ), responses={ (status.HTTP_200_OK, "application/json"): OpenApiResponse( response=DocumentSerializer, @@ -715,13 +781,15 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), status.HTTP_401_UNAUTHORIZED: _base_401_response(), status.HTTP_403_FORBIDDEN: OpenApiResponse( - description="The authenticated user lacks the proper permissions to access the document. " - "Depending on the requested document, " - "either the user does not belong to the admin group of the service which owns the document, " - "the user does not own the document." - # TODO: Uncomment when organization features are implemented - # " or the user does not have permission to act on behalf " - # "of the organization which owns the document." + description=( + "The authenticated user lacks the proper permissions to access the" + " document. Depending on the requested document, either the user" + " does not belong to the admin group of the service which owns the" + " document, the user does not own the document." + # TODO: Uncomment when organization features are implemented + # " or the user does not have permission to act on behalf " + # "of the organization which owns the document." + ) ), status.HTTP_404_NOT_FOUND: OpenApiResponse( description="No document was found with `documentId`.", @@ -732,28 +800,37 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: ), "destroy": extend_schema( summary="Remove an existing document and its attachments", - description="Permission to access the document is checked as follows:\n" - "* Authenticated users are allowed access to the document if they are the owner of the document.\n\n" - # TODO: Uncomment when organization features are implemented - # "or the document is owned by an organization and the user has permission to act " - # "on behalf of that organization.\n\n" - "The following rules apply:\n" - "* Drafts may be removed by the owning user.", - # TODO: Uncomment when organization features are implemented - # "or an organization's representative, " - # "if the document is owned by an organization. This is possible even if the `lockedAfter` date has passed, " - # "to enable a user to remove expired applications.", + description=( + "Permission to access the document is checked as follows:\n" + "* Authenticated users are allowed access to the document if they are the" + " owner of the document.\n\n" + # TODO: Uncomment when organization features are implemented + # "or the document is owned by an organization and the user has permission" + # " to act on behalf of that organization.\n\n" + "The following rules apply:\n" + "* Drafts may be removed by the owning user." + # TODO: Uncomment when organization features are implemented + # "or an organization's representative, " + # "if the document is owned by an organization. This is possible even if" + # " the `lockedAfter` date has passed, to enable a user to remove expired" + # " applications." + ), responses={ status.HTTP_204_NO_CONTENT: OpenApiResponse( - description="The specified Document and its attachments were removed successfully", + description=( + "The specified Document and its attachments were removed" + " successfully" + ), ), (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), status.HTTP_401_UNAUTHORIZED: _base_401_response(), status.HTTP_403_FORBIDDEN: OpenApiResponse( - description="The authenticated user lacks the proper permissions to access the document. " - "Depending on the requested document, " - "either the user does not belong to the admin group of the service which owns the document, " - "the user does not own the document." + description=( + "The authenticated user lacks the proper permissions to access the" + " document. Depending on the requested document, either the user" + " does not belong to the admin group of the service which owns the" + " document, the user does not own the document." + ) # TODO: Uncomment when organization features are implemented # " or the user does not have permission to act on behalf " # "of the organization which owns the document." @@ -772,8 +849,11 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: document_metadata_viewset_docs = { "retrieve": extend_schema( summary="List and filter non sensitive parts of users documents.", - description="""List users documents parts that doesn't contain sensitive information to easily see current - applications and documents of a single user across services.""", + description=( + "List users documents parts that doesn't contain sensitive information to" + " easily see current applications and documents of a single user across" + " services." + ), # "of that organization.", responses={ (status.HTTP_200_OK, "application/json"): OpenApiResponse( @@ -783,7 +863,10 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), status.HTTP_401_UNAUTHORIZED: _base_401_response(), status.HTTP_403_FORBIDDEN: OpenApiResponse( - description="Current authentication doesn't allow viewing of this users documents" + description=( + "Current authentication doesn't allow viewing of this users" + " documents" + ) ), status.HTTP_404_NOT_FOUND: OpenApiResponse( description="No user matches the given query.", @@ -821,7 +904,10 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: "retrieve": extend_schema( examples=[example_gdpr_api_repsonse], description="Used to fetch GDPR data of an user from single service.", - summary="List user's document details and number of deletable and undeletable documents.", + summary=( + "List user's document details and number of deletable and undeletable" + " documents." + ), responses={ (status.HTTP_200_OK, "application/json"): OpenApiResponse( response=GDPRSerializer, @@ -830,31 +916,49 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: ), (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), status.HTTP_401_UNAUTHORIZED: OpenApiResponse( - description="Authorization not provided. API Key authentication required." + description=( + "Authorization not provided. API Key authentication required." + ) ), status.HTTP_403_FORBIDDEN: OpenApiResponse( - description="Current authentication doesn't allow viewing of this users documents" + description=( + "Current authentication doesn't allow viewing of this users" + " documents" + ) ), status.HTTP_500_INTERNAL_SERVER_ERROR: _base_500_response(), }, ), "destroy": extend_schema( examples=[example_gdpr_api_repsonse], - description="Delete user's documents from a single service that aren't under contractual obligation.", - summary="List user's documents that weren't deleted. Deletable field should be zero.", + description=( + "Delete user's documents from a single service that aren't under" + " contractual obligation." + ), + summary=( + "List user's documents that weren't deleted. Deletable field should be" + " zero." + ), responses={ (status.HTTP_200_OK, "application/json"): OpenApiResponse( response=GDPRSerializer, - description="User was found and their deletable documents and attachments have been removed. " - "Documents with contractual oblications are returned in response body." - " Field 'total_deletable' should now be zero.", + description=( + "User was found and their deletable documents and attachments have" + " been removed. Documents with contractual obligations are returned" + " in response body. Field 'total_deletable' should now be zero." + ), ), (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), status.HTTP_401_UNAUTHORIZED: OpenApiResponse( - description="Authorization not provided. API Key authentication required." + description=( + "Authorization not provided. API Key authentication required." + ) ), status.HTTP_403_FORBIDDEN: OpenApiResponse( - description="Current authentication doesn't allow viewing of this users documents" + description=( + "Current authentication doesn't allow viewing of this users" + " documents" + ) ), status.HTTP_500_INTERNAL_SERVER_ERROR: _base_500_response(), }, @@ -869,8 +973,11 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: document_statistics_viewset_docs = { "list": extend_schema( summary="List and filter non sensitive parts of service's documents.", - description="""Lists non sensitive data of all documents in ATV. Service staff can fetch data from the - respective service. Currently the use case is to verify documents match between ATV and services.""", + description=( + "Lists non sensitive data of all documents in ATV. Service staff can fetch" + " data from the respective service. Currently the use case is to verify" + " documents match between ATV and services." + ), responses={ (status.HTTP_200_OK, "application/json"): OpenApiResponse( response=DocumentMetadataSerializer, @@ -878,11 +985,13 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: ), (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), status.HTTP_401_UNAUTHORIZED: OpenApiResponse( - "Request's credentials are missing or invalid. An API-key is required, or an " - "user token associated with statistics service.", + "Request's credentials are missing or invalid. An API-key is required," + " or an user token associated with statistics service.", ), status.HTTP_403_FORBIDDEN: OpenApiResponse( - description="Current authentication doesn't allow viewing document statistics" + description=( + "Current authentication doesn't allow viewing document statistics" + ) ), status.HTTP_500_INTERNAL_SERVER_ERROR: _base_500_response(), }, @@ -891,17 +1000,23 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: OpenApiParameter( "statuses", OpenApiTypes.STR, - description="Search for documents with the given statuses separated by comma.", + description=( + "Search for documents with the given statuses separated by comma." + ), ), OpenApiParameter( "types", OpenApiTypes.STR, - description="Search for documents with the given types separated by comma", + description=( + "Search for documents with the given types separated by comma" + ), ), OpenApiParameter( "services", OpenApiTypes.STR, - description="Search for documents with the given services separated by comma", + description=( + "Search for documents with the given services separated by comma" + ), ), OpenApiParameter( "transaction_id", @@ -921,34 +1036,49 @@ def _base_500_response(custom_message: str = None) -> OpenApiResponse: document_status_history_viewset_docs = { "list": extend_schema( summary="Lists document's status and activity history", - description="""Lists all document's statuses and activities related to the statuses""", + description=( + "Lists all document's statuses and activities related to the statuses" + ), responses={ (status.HTTP_200_OK, "application/json"): OpenApiResponse( response=StatusHistorySerializer, description="Request was allowed and document statuses were listed", ), (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), - status.HTTP_401_UNAUTHORIZED: "Request's credentials are missing or invalid.", + status.HTTP_401_UNAUTHORIZED: ( + "Request's credentials are missing or invalid." + ), status.HTTP_500_INTERNAL_SERVER_ERROR: _base_500_response(), }, examples=[example_get_status_activity_data, example_error], ), "create": extend_schema( summary="Create new status and/or activity for document", - description="Allows creating either individual new status, activity or both status and activity. See example", + description=( + "Allows creating either individual new status, activity or both status and" + " activity. See example" + ), request=CreateStatusHistorySerializer, responses={ (status.HTTP_201_CREATED, "application/json"): OpenApiResponse( response=StatusHistorySerializer, - description="New Status and/or Activity was created. All Activities related to the status is returned.", + description=( + "New Status and/or Activity was created. All Activities related to" + " the status is returned." + ), ), (status.HTTP_200_OK, "application/json"): OpenApiResponse( response=StatusHistorySerializer, - description="""Return HTTP 200 OK, if nothing changed but request was correctly formated. - For example if same status is posted twice. Returns latest StatusHistory object.""", + description=( + "Return HTTP 200 OK, if nothing changed but request was correctly" + " formated. For example if same status is posted twice. Returns" + " latest StatusHistory object." + ), ), (status.HTTP_400_BAD_REQUEST, "application/json"): _base_400_response(), - status.HTTP_401_UNAUTHORIZED: "Request's credentials are missing or invalid.", + status.HTTP_401_UNAUTHORIZED: ( + "Request's credentials are missing or invalid." + ), status.HTTP_500_INTERNAL_SERVER_ERROR: _base_500_response(), }, examples=[example_create_status_activity_data], diff --git a/documents/api/filtersets.py b/documents/api/filtersets.py index 2d63b2f..a02a81d 100644 --- a/documents/api/filtersets.py +++ b/documents/api/filtersets.py @@ -48,8 +48,8 @@ def filter(self, qs, value): raise ValidationError( detail={ "Invalid Query": [ - "Enter query in format 'key:value' without quotes." - " You can have multiple key and value pairs, separated by comma" + "Enter query in format 'key:value' without quotes. You can" + " have multiple key and value pairs, separated by comma" ] } ) diff --git a/documents/api/querysets.py b/documents/api/querysets.py index 034614d..c4c02a6 100644 --- a/documents/api/querysets.py +++ b/documents/api/querysets.py @@ -65,7 +65,8 @@ def get_document_metadata_queryset( user: User, api_key: ServiceAPIKey = None ) -> QuerySet: """ - Superusers and staff(API key) are allowed to see document metadata of all services for all users. + Superusers and staff(API key) are allowed to see document metadata of all services + for all users. Token users can only see their own documents metadata from across all services. """ queryset = ( diff --git a/documents/api/viewsets.py b/documents/api/viewsets.py index 546fda0..b6a11d3 100644 --- a/documents/api/viewsets.py +++ b/documents/api/viewsets.py @@ -273,7 +273,8 @@ def get_queryset(self): service_api_key = get_service_api_key_from_request(self.request) return get_document_metadata_queryset(user, service_api_key) - # Use retrieve to allow using user__uuid as a lookup_field to list documents of single user + # Use retrieve to allow using user__uuid as a lookup_field to list documents of + # single user def retrieve(self, request, *args, **kwargs): with self.record_action(): service_api_key = get_service_api_key_from_request(request) @@ -502,7 +503,8 @@ def create(self, request, *args, **kwargs): serializer = CreateAnonymousDocumentSerializer(data=data) - # If the data is not valid, it will raise a ValidationError and return Bad Request + # If the data is not valid, it will raise a ValidationError and return Bad + # Request serializer.is_valid(raise_exception=True) with self.record_action(): diff --git a/documents/management/commands/delete_expired_documents.py b/documents/management/commands/delete_expired_documents.py index 038661c..8c97ef1 100644 --- a/documents/management/commands/delete_expired_documents.py +++ b/documents/management/commands/delete_expired_documents.py @@ -4,7 +4,10 @@ class Command(BaseCommand): - help = "Delete documents and related objects and files that have reached their deletion date" + help = ( + "Delete documents and related objects and files that have reached their" + " deletion date" + ) def handle(self, *args, **kwargs): delete_expired_documents() diff --git a/documents/management/commands/remove_outdated_files.py b/documents/management/commands/remove_outdated_files.py index e3eced6..609c479 100644 --- a/documents/management/commands/remove_outdated_files.py +++ b/documents/management/commands/remove_outdated_files.py @@ -18,7 +18,8 @@ class Command(BaseCommand): files_deleted = 0 def remove_document_outdated_files(self, document: Document, path, dry_run): - """For a given document, remove the files that don't have an associated Attachment""" + """For a given document, remove the files that don't have an associated + Attachment""" document_path = get_document_attachment_directory_path(document) for filename in os.listdir(path): diff --git a/documents/models.py b/documents/models.py index c7a9a8c..23ee8cb 100644 --- a/documents/models.py +++ b/documents/models.py @@ -34,7 +34,8 @@ class Activity(models.Model): default=dict, blank=True, help_text=_( - "Structure with links related to activity with display text in multiple languages" + "Structure with links related to activity with display text in multiple" + " languages" ), ) show_to_user = models.BooleanField( @@ -155,8 +156,9 @@ class Document(UUIDModel, TimestampedModel): verbose_name=_("TOS function ID"), help_text=_( "UUID without dashes. Should correspond with a Function instance " - "(e.g. the id from https://api.hel.fi/helerm/v1/function/eb30af1d9d654ebc98287ca25f231bf6/) " - "which is applied to the stored document when considering storage time." + "(e.g. the id from" + " https://api.hel.fi/helerm/v1/function/eb30af1d9d654ebc98287ca25f231bf6/" + ") which is applied to the stored document when considering storage time." ), ) tos_record_id = models.CharField( @@ -164,8 +166,9 @@ class Document(UUIDModel, TimestampedModel): verbose_name=_("TOS record ID"), help_text=_( "UUID without dashes. Should correspond to a record ID " - "(e.g. records[].id from https://api.hel.fi/helerm/v1/function/eb30af1d9d654ebc98287ca25f231bf6/) " - "within a Function instance which is applied to the stored document when " + "(e.g. records[].id from" + " https://api.hel.fi/helerm/v1/function/eb30af1d9d654ebc98287ca25f231bf6/" + ") within a Function instance which is applied to the stored document when " "considering storage time." ), ) @@ -224,7 +227,8 @@ class Document(UUIDModel, TimestampedModel): default=timezone.now, verbose_name=_("status_timestamp"), help_text=_( - "Date and time when document status was last changed. Field is automatically set." + "Date and time when document status was last changed. Field is" + " automatically set." ), ) type = models.CharField( @@ -264,7 +268,8 @@ class Document(UUIDModel, TimestampedModel): delete_after = models.DateField( null=True, help_text=_( - "Date which after the document and related attachments are permanently deleted" + "Date which after the document and related attachments are permanently" + " deleted" ), ) diff --git a/documents/serializers/document.py b/documents/serializers/document.py index 4cc41ee..18e6112 100644 --- a/documents/serializers/document.py +++ b/documents/serializers/document.py @@ -47,7 +47,8 @@ class DocumentStatisticsSerializer(serializers.ModelSerializer): source="service.name", required=False, read_only=True ) attachments = AttachmentNameSerializer(many=True) - # Attachment count included here just for clarity. Field is added to response body in to_representation + # Attachment count included here just for clarity. Field is added to + # response body in to_representation attachment_count = serializers.HiddenField(default=0) user_id = serializers.UUIDField(source="user.uuid", read_only=True) @@ -69,7 +70,8 @@ class Meta: def to_representation(self, instance): representation = super().to_representation(instance) - # Calculate attachment count here instead of aggregating it to queryset for performance reasons + # Calculate attachment count here instead of aggregating it to queryset + # for performance reasons representation["attachment_count"] = len(representation["attachments"]) return representation @@ -142,7 +144,7 @@ def to_representation(self, instance): class DocumentSerializer(serializers.ModelSerializer): - """Basic "read" serializer for the Document model""" + """Basic "read" serializer for the Document model.""" user_id = serializers.UUIDField( source="user.uuid", required=False, default=None, read_only=True @@ -189,7 +191,8 @@ def update(self, document, validated_data): # If the document has been locked, no further updates are allowed if document.locked_after and document.locked_after <= now(): raise DocumentLockedException() - # Deletable field can be changed from True to False but not the other way + # Deletable field can be changed from True to False but not the other + # way if validated_data.get("deletable") is True and not document.deletable: raise PermissionDenied( detail="Field 'deletable' can't be changed if set to False" @@ -218,10 +221,11 @@ def to_representation(self, instance): class CreateAnonymousDocumentSerializer(serializers.ModelSerializer): - """Create a Document with Attachment for an anonymous user submitting the document - through a Service authorized with an API key. + """Create a Document with Attachment for an anonymous user submitting the + document through a Service authorized with an API key. - Also handles the creation of the associated Attachments through `CreateAttachmentSerializer`. + Also handles the creation of the associated Attachments through + `CreateAttachmentSerializer`. """ user_id = serializers.UUIDField(source="user.uuid", required=False, default=None) @@ -254,7 +258,8 @@ class Meta: ) def validate(self, attrs): - # Validate that no additional fields are being passed (to sanitize the input) + # Validate that no additional fields are being passed (to sanitize the + # input) if hasattr(self, "initial_data"): invalid_keys = set(self.initial_data.keys()) - set(self.fields.keys()) if invalid_keys: diff --git a/documents/tasks.py b/documents/tasks.py index 8bea043..1ae9b96 100644 --- a/documents/tasks.py +++ b/documents/tasks.py @@ -14,7 +14,8 @@ def delete_expired_documents(): total, by_type_dict = documents_to_delete_qs.delete() if total != 0: logger.info( - f"Deleted {total} objects: {', '.join([f'{i[1]} {i[0]}' for i in by_type_dict.items()])}." + f"Deleted {total} objects:" + f" {', '.join([f'{i[1]} {i[0]}' for i in by_type_dict.items()])}." ) else: logger.info("Nothing to delete.") diff --git a/documents/utils.py b/documents/utils.py index 72875a9..6b6f98c 100644 --- a/documents/utils.py +++ b/documents/utils.py @@ -10,7 +10,8 @@ def get_attachment_file_path(instance, filename): - """File will be uploaded to MEDIA_ROOT/ATTACHMENT_MEDIA_DIR//""" + """File will be uploaded to + MEDIA_ROOT/ATTACHMENT_MEDIA_DIR//""" return f"{settings.ATTACHMENT_MEDIA_DIR}/{instance.document.id}/{filename}" @@ -24,8 +25,8 @@ def get_document_attachment_directory_path(instance): def get_decrypted_file(file, file_name): nonce = file[:16] - # Perform same nonce checks here as Pycryptodome, so we can raise a more user - # friendly error message + # Perform same nonce checks here as Pycryptodome, so we can raise a more + # user-friendly error message if not isinstance(nonce, (bytes, bytearray, memoryview)) or len(nonce) != 16: raise ValueError("Data is corrupted.") tag = file[16:32] @@ -45,9 +46,11 @@ def get_decrypted_file(file, file_name): raise ValueError("AES Key incorrect or data is corrupted") -# TODO: Consider scanning files on download as well to improve chance of catching most recent threats to protect users -# if clamav virus databases didn't include the virus' profile at the time of upload -# Note this function is mocked in testing because there is no clamav connection during pipeline testing +# TODO: Consider scanning files on download as well to improve chance of catching most +# recent threats to protect users if clamav virus databases didn't include the virus' +# profile at the time of upload +# Note this function is mocked in testing because there is no clamav connection during +# pipeline testing def virus_scan_attachment_file(file_data): cd = pyclamd.ClamdNetworkSocket(host=settings.CLAMAV_HOST) if cd.scan_stream(file_data) is not None: diff --git a/services/signals.py b/services/signals.py index 4d9362f..ff4249b 100644 --- a/services/signals.py +++ b/services/signals.py @@ -39,7 +39,8 @@ def create_service_api_key_user( instance.user = user instance.save() logger.debug( - f"Created user {user} for ServiceAPIKey {instance} permission group for service: {instance.name}" + f"Created user {user} for ServiceAPIKey {instance} permission group for " + f"service: {instance.name}" ) for perm in Service._meta.permissions: assign_perm(f"{perm[0]}", user, instance.service) diff --git a/users/models.py b/users/models.py index 2b3b47f..1a5dbbb 100644 --- a/users/models.py +++ b/users/models.py @@ -9,7 +9,8 @@ def __str__(self): def clean(self): super().clean() - # Prevent personal details from being saved to ATV user model, ATV doesn't need to know anything else than uuid + # Prevent personal details from being saved to ATV user model, ATV doesn't need + # to know anything else than uuid self.first_name = self.last_name = self.email = "" class Meta: diff --git a/utils/commands.py b/utils/commands.py index 81eb4f0..9885878 100644 --- a/utils/commands.py +++ b/utils/commands.py @@ -16,7 +16,8 @@ def add_arguments(self, parser): def setup_logging(self, verbosity): """ The values passed by the command are: - Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output + Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, + 3=very verbose output So output of 2 or 3 (i.e. > 1) will be verbose Defaults to 1 diff --git a/utils/exceptions.py b/utils/exceptions.py index abba271..46c1e56 100644 --- a/utils/exceptions.py +++ b/utils/exceptions.py @@ -37,13 +37,21 @@ def custom_exception_handler(exc, _context=None) -> Response: # Validation errors require a special processing to use the same format # as with the spec if isinstance(exc, ValidationError): + + def exception_value_to_str(value): + if isinstance(value, list): + return value[0] + return f"Required fields: {[key for key in value.keys()]}" + response = Response( status=status.HTTP_400_BAD_REQUEST, data=_get_error_wrapper( - list( # TODO: Refactor this to produce cleaner error message for nested errors + list( + # TODO: Refactor this to produce cleaner error message for nested + # errors _get_error_detail( "INVALID_FIELD", - f"{k}: {v[0] if isinstance(v, list) else f'Required fields: {[key for key in v.keys()]}'}", + f"{k}: {exception_value_to_str(v)}", ) for k, v in exc.detail.items() )