django-pfx 1.4.dev102__tar.gz → 1.4.dev106__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/PKG-INFO +2 -1
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/django_pfx.egg-info/PKG-INFO +2 -1
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/django_pfx.egg-info/SOURCES.txt +8 -1
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/django_pfx.egg-info/requires.txt +1 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/default_settings.py +5 -0
- django_pfx-1.4.dev106/pfx/pfxcore/fields/__init__.py +3 -0
- django_pfx-1.4.dev106/pfx/pfxcore/fields/media_field.py +60 -0
- django_pfx-1.4.dev102/pfx/pfxcore/fields.py → django_pfx-1.4.dev106/pfx/pfxcore/fields/minutes_duration_field.py +0 -46
- django_pfx-1.4.dev106/pfx/pfxcore/fields/nh3_field.py +83 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +11 -11
- django_pfx-1.4.dev106/pfx/pfxcore/storage/__init__.py +3 -0
- django_pfx-1.4.dev106/pfx/pfxcore/storage/exceptions.py +3 -0
- django_pfx-1.4.dev106/pfx/pfxcore/storage/local_storage.py +56 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/storage/s3_storage.py +7 -6
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/__init__.py +1 -2
- django_pfx-1.4.dev106/pfx/pfxcore/views/media_rest_view_mixin.py +171 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/rest_views.py +1 -135
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/requirements.txt +1 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/setup.cfg +1 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/common.py +2 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/__init__.py +1 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_fields_minutes_duration.py +1 -1
- django_pfx-1.4.dev106/tests/tests/test_fields_nh3.py +38 -0
- django_pfx-1.4.dev102/pfx/pfxcore/storage/__init__.py +0 -3
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/.gitignore +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/.gitlab-ci.yml +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/.pre-commit-config.yaml +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/LICENSE +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/MANIFEST.in +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/README.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/django_pfx.egg-info/top_level.txt +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/Makefile +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/conf.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/index.rst +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/api.views.rst +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/authentication.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/decorator.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/generate_openapi.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/getting_started.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/internationalisation.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/model.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/pfx_views.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/profiling.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/settings.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/doc/source/testing.md +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/img/pfx.png +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/img/pfx.svg +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/make_messages +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/manage.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/apps.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/decorator/rest.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/exceptions.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/http/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/http/json_response.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/management/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/middleware/authentication.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/middleware/locale.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/middleware/profiling.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/0001_initial.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/login_ban.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/otp_user_mixin.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/pfx_user.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/serializers/json.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/settings.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/shortcuts.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/test.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/urls.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/authentication_views.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/fields.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/filters_views.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/locale_views.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/settings/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pfx/settings/dev.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/pyproject.toml +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/serve-doc +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/setup.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/apps.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/migrations/0001_initial.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/migrations/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/models.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/ci.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/dev.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/dev_custom_example.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/settings/dev_default.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/basic_api_errors.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/basic_api_test.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_api_doc.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_api_doc_search.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_auth_api.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_body_mixin.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_cache.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_client.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_fields_choices.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_filters.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_locale_api.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_perm_tests.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_permissions.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_perms_api.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_post_migrate_groups_update.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_profiling_middleware.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_settings.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_shortcuts.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_timezone_middleware.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_tools.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_user_queryset.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_view_decorators.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/tests/test_view_fields.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/urls.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests/views.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/migrations/0001_initial.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/migrations/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/ci.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/common.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/dev.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/dev_custom_example.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/settings/dev_default.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/tests/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/tests/test_api.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/tests/test_auth_api.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/urls.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_base_user/views.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/migrations/0001_initial.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/migrations/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/models.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/ci.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/common.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/dev.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/dev_custom_example.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/settings/dev_default.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/tests/__init__.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/tests/test_api.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/tests/test_auth_api.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/urls.py +0 -0
- {django_pfx-1.4.dev102 → django_pfx-1.4.dev106}/tests_custom_user/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: django-pfx
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.dev106
|
|
4
4
|
Summary: Django PFX is a toolkit designed to streamline the development of RESTful APIs using the Django framework.
|
|
5
5
|
Author: Hervé Martinet
|
|
6
6
|
Author-email: herve.martinet@gmail.com
|
|
@@ -34,6 +34,7 @@ Requires-Dist: apispec
|
|
|
34
34
|
Requires-Dist: pyyaml
|
|
35
35
|
Requires-Dist: pytz
|
|
36
36
|
Requires-Dist: dill
|
|
37
|
+
Requires-Dist: nh3
|
|
37
38
|
Provides-Extra: otp
|
|
38
39
|
Requires-Dist: pyotp; extra == "otp"
|
|
39
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: django-pfx
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.dev106
|
|
4
4
|
Summary: Django PFX is a toolkit designed to streamline the development of RESTful APIs using the Django framework.
|
|
5
5
|
Author: Hervé Martinet
|
|
6
6
|
Author-email: herve.martinet@gmail.com
|
|
@@ -34,6 +34,7 @@ Requires-Dist: apispec
|
|
|
34
34
|
Requires-Dist: pyyaml
|
|
35
35
|
Requires-Dist: pytz
|
|
36
36
|
Requires-Dist: dill
|
|
37
|
+
Requires-Dist: nh3
|
|
37
38
|
Provides-Extra: otp
|
|
38
39
|
Requires-Dist: pyotp; extra == "otp"
|
|
39
40
|
|
|
@@ -37,7 +37,6 @@ pfx/pfxcore/__init__.py
|
|
|
37
37
|
pfx/pfxcore/apps.py
|
|
38
38
|
pfx/pfxcore/default_settings.py
|
|
39
39
|
pfx/pfxcore/exceptions.py
|
|
40
|
-
pfx/pfxcore/fields.py
|
|
41
40
|
pfx/pfxcore/settings.py
|
|
42
41
|
pfx/pfxcore/shortcuts.py
|
|
43
42
|
pfx/pfxcore/test.py
|
|
@@ -48,6 +47,10 @@ pfx/pfxcore/apidoc/schema.py
|
|
|
48
47
|
pfx/pfxcore/apidoc/tags.py
|
|
49
48
|
pfx/pfxcore/decorator/__init__.py
|
|
50
49
|
pfx/pfxcore/decorator/rest.py
|
|
50
|
+
pfx/pfxcore/fields/__init__.py
|
|
51
|
+
pfx/pfxcore/fields/media_field.py
|
|
52
|
+
pfx/pfxcore/fields/minutes_duration_field.py
|
|
53
|
+
pfx/pfxcore/fields/nh3_field.py
|
|
51
54
|
pfx/pfxcore/http/__init__.py
|
|
52
55
|
pfx/pfxcore/http/json_response.py
|
|
53
56
|
pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo
|
|
@@ -79,6 +82,8 @@ pfx/pfxcore/models/user_filtered_queryset_mixin.py
|
|
|
79
82
|
pfx/pfxcore/serializers/__init__.py
|
|
80
83
|
pfx/pfxcore/serializers/json.py
|
|
81
84
|
pfx/pfxcore/storage/__init__.py
|
|
85
|
+
pfx/pfxcore/storage/exceptions.py
|
|
86
|
+
pfx/pfxcore/storage/local_storage.py
|
|
82
87
|
pfx/pfxcore/storage/s3_storage.py
|
|
83
88
|
pfx/pfxcore/templates/registration/otp_code_email.txt
|
|
84
89
|
pfx/pfxcore/templates/registration/otp_code_subject.txt
|
|
@@ -91,6 +96,7 @@ pfx/pfxcore/views/authentication_views.py
|
|
|
91
96
|
pfx/pfxcore/views/fields.py
|
|
92
97
|
pfx/pfxcore/views/filters_views.py
|
|
93
98
|
pfx/pfxcore/views/locale_views.py
|
|
99
|
+
pfx/pfxcore/views/media_rest_view_mixin.py
|
|
94
100
|
pfx/pfxcore/views/ordered_rest_view_mixin.py
|
|
95
101
|
pfx/pfxcore/views/rest_views.py
|
|
96
102
|
pfx/pfxcore/views/parameters/__init__.py
|
|
@@ -138,6 +144,7 @@ tests/tests/test_cache.py
|
|
|
138
144
|
tests/tests/test_client.py
|
|
139
145
|
tests/tests/test_fields_choices.py
|
|
140
146
|
tests/tests/test_fields_minutes_duration.py
|
|
147
|
+
tests/tests/test_fields_nh3.py
|
|
141
148
|
tests/tests/test_filters.py
|
|
142
149
|
tests/tests/test_locale_api.py
|
|
143
150
|
tests/tests/test_ordered_rest_view_mixin.py
|
|
@@ -21,6 +21,8 @@ PFX_OPENAPI_TEMPLATE = {}
|
|
|
21
21
|
PFX_PROFILING_SQL = ""
|
|
22
22
|
PFX_PROFILING_CPROFILE = ""
|
|
23
23
|
|
|
24
|
+
STORAGE_DEFAULT = None
|
|
25
|
+
|
|
24
26
|
STORAGE_S3_AWS_REGION = None
|
|
25
27
|
STORAGE_S3_AWS_ACCESS_KEY = None
|
|
26
28
|
STORAGE_S3_AWS_SECRET_KEY = None
|
|
@@ -29,6 +31,9 @@ STORAGE_S3_AWS_S3_BUCKET = None
|
|
|
29
31
|
STORAGE_S3_AWS_PUT_URL_EXPIRE = None
|
|
30
32
|
STORAGE_S3_AWS_GET_URL_EXPIRE = None
|
|
31
33
|
|
|
34
|
+
STORAGE_LOCAL_ROOT = None
|
|
35
|
+
STORAGE_LOCAL_X_ACCEL_REDIRECT = False
|
|
36
|
+
|
|
32
37
|
PFX_TEST_MODE = False
|
|
33
38
|
|
|
34
39
|
PFX_AUTH_GROUPS_CREATE_ONLY = False
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from importlib import import_module
|
|
3
|
+
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.db.models.signals import post_delete
|
|
6
|
+
from django.dispatch import receiver
|
|
7
|
+
|
|
8
|
+
from pfx.pfxcore.shortcuts import settings
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_storage_class(class_path):
|
|
14
|
+
ps = class_path.split('.')
|
|
15
|
+
return getattr(import_module('.'.join(ps[:-1])), ps[-1])()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MediaField(models.JSONField):
|
|
19
|
+
def __init__(
|
|
20
|
+
self, *args, max_length=255, get_key=None, storage=None,
|
|
21
|
+
auto_delete=False, **kwargs):
|
|
22
|
+
self.get_key = get_key or self.get_default_key
|
|
23
|
+
if not storage and not settings.STORAGE_DEFAULT:
|
|
24
|
+
raise Exception(
|
|
25
|
+
"Missing storage. You have to set a storage "
|
|
26
|
+
"class on the field or define STORAGE_DEFAULT settings.")
|
|
27
|
+
self.storage = storage or get_storage_class(settings.STORAGE_DEFAULT)
|
|
28
|
+
self.auto_delete = auto_delete
|
|
29
|
+
super().__init__(
|
|
30
|
+
*args, max_length=max_length,
|
|
31
|
+
default=kwargs.pop('default', dict),
|
|
32
|
+
blank=kwargs.pop('blank', True),
|
|
33
|
+
**kwargs)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def get_default_key(obj, filename):
|
|
37
|
+
return f"{type(obj).__name__}/{obj.pk}/{filename}"
|
|
38
|
+
|
|
39
|
+
def to_python(self, value):
|
|
40
|
+
return super().to_python(self.storage.to_python(value))
|
|
41
|
+
|
|
42
|
+
def get_upload_url(self, request, obj, filename):
|
|
43
|
+
key = self.get_key(obj, filename)
|
|
44
|
+
url = self.storage.get_upload_url(request, key)
|
|
45
|
+
return dict(url=url, file=dict(name=filename, key=key))
|
|
46
|
+
|
|
47
|
+
def get_url(self, request, obj):
|
|
48
|
+
return self.storage.get_url(
|
|
49
|
+
request, self.value_from_object(obj)['key'])
|
|
50
|
+
|
|
51
|
+
def upload(self, obj, file, filename, **kwargs):
|
|
52
|
+
key = self.get_key(obj, filename)
|
|
53
|
+
return self.to_python(self.storage.upload(key, file, **kwargs))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@receiver(post_delete)
|
|
57
|
+
def post_delete_media(sender, instance, **kwargs):
|
|
58
|
+
for field in sender._meta.fields:
|
|
59
|
+
if isinstance(field, MediaField) and field.auto_delete:
|
|
60
|
+
field.storage.delete(field.value_from_object(instance))
|
|
@@ -4,57 +4,11 @@ from datetime import timedelta
|
|
|
4
4
|
|
|
5
5
|
from django.core.exceptions import ValidationError
|
|
6
6
|
from django.db import models
|
|
7
|
-
from django.db.models.signals import post_delete
|
|
8
|
-
from django.dispatch import receiver
|
|
9
7
|
from django.utils.translation import gettext_lazy as _
|
|
10
8
|
|
|
11
|
-
from pfx.pfxcore.storage import DefaultStorage
|
|
12
|
-
|
|
13
9
|
logger = logging.getLogger(__name__)
|
|
14
10
|
|
|
15
11
|
|
|
16
|
-
class MediaField(models.JSONField):
|
|
17
|
-
def __init__(
|
|
18
|
-
self, *args, max_length=255, get_key=None, storage=None,
|
|
19
|
-
auto_delete=False, **kwargs):
|
|
20
|
-
self.get_key = get_key or self.get_default_key
|
|
21
|
-
self.storage = storage or DefaultStorage()
|
|
22
|
-
self.auto_delete = auto_delete
|
|
23
|
-
super().__init__(
|
|
24
|
-
*args, max_length=max_length,
|
|
25
|
-
default=kwargs.pop('default', dict),
|
|
26
|
-
blank=kwargs.pop('blank', True),
|
|
27
|
-
**kwargs)
|
|
28
|
-
|
|
29
|
-
@staticmethod
|
|
30
|
-
def get_default_key(obj, filename):
|
|
31
|
-
return f"{type(obj).__name__}/{obj.pk}/{filename}"
|
|
32
|
-
|
|
33
|
-
def to_python(self, value):
|
|
34
|
-
return super().to_python(self.storage.to_python(value))
|
|
35
|
-
|
|
36
|
-
def get_upload_url(self, request, obj, filename):
|
|
37
|
-
key = self.get_key(obj, filename)
|
|
38
|
-
url = self.storage.get_upload_url(request, key)
|
|
39
|
-
return dict(url=url, file=dict(name=filename, key=key))
|
|
40
|
-
|
|
41
|
-
def get_url(self, request, obj):
|
|
42
|
-
return self.storage.get_url(
|
|
43
|
-
request, self.value_from_object(obj)['key'])
|
|
44
|
-
|
|
45
|
-
def upload(self, obj, file, filename, **kwargs):
|
|
46
|
-
key = self.get_key(obj, filename)
|
|
47
|
-
self.storage.upload(key, file, **kwargs)
|
|
48
|
-
return self.to_python(dict(key=key))
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@receiver(post_delete)
|
|
52
|
-
def post_delete_media(sender, instance, **kwargs):
|
|
53
|
-
for field in sender._meta.fields:
|
|
54
|
-
if isinstance(field, MediaField) and field.auto_delete:
|
|
55
|
-
field.storage.delete(field.value_from_object(instance))
|
|
56
|
-
|
|
57
|
-
|
|
58
12
|
class MinutesDurationField(models.DurationField):
|
|
59
13
|
RE_FLOAT = re.compile(r'^[0-9]*(\.[0-9]*)?$')
|
|
60
14
|
RE_HH_MM = re.compile(r'^([0-9]*):([0-5][0-9])?$')
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from django.conf import settings
|
|
2
|
+
from django.db import models
|
|
3
|
+
from django.utils.safestring import mark_safe
|
|
4
|
+
|
|
5
|
+
import nh3
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_nh3_default_options():
|
|
9
|
+
nh3_args = {}
|
|
10
|
+
nh3_settings = {
|
|
11
|
+
"NH3_ALLOWED_TAGS": "tags",
|
|
12
|
+
"NH3_ALLOWED_ATTRIBUTES": "attributes",
|
|
13
|
+
"NH3_STRIP_COMMENTS": "strip_comments",
|
|
14
|
+
"NH3_URL_SCHEMES": "url_schemes",
|
|
15
|
+
"NH3_ATTRIBUTE_FILTER": "attribute_filter",
|
|
16
|
+
"NH3_LINK_REL": "link_rel",
|
|
17
|
+
"NH3_GENERIC_ATTRIBUTE_PREFIXES": "generic_attribute_prefixes",
|
|
18
|
+
"NH3_TAG_ATTRIBUTE_VALUES": "tag_attribute_values",
|
|
19
|
+
"NH3_SET_TAG_ATTRIBUTE_VALUES": "set_tag_attribute_values",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
for setting, kwarg in nh3_settings.items():
|
|
23
|
+
if hasattr(settings, setting):
|
|
24
|
+
attr = getattr(settings, setting)
|
|
25
|
+
nh3_args[kwarg] = attr
|
|
26
|
+
|
|
27
|
+
return nh3_args
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NH3Field(models.TextField):
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
allowed_tags=None,
|
|
34
|
+
allowed_attributes=None,
|
|
35
|
+
url_schemes=None,
|
|
36
|
+
strip_comments=None,
|
|
37
|
+
attribute_filter=None,
|
|
38
|
+
link_rel=None,
|
|
39
|
+
generic_attribute_prefixes=None,
|
|
40
|
+
tag_attribute_values=None,
|
|
41
|
+
set_tag_attribute_values=None,
|
|
42
|
+
*args,
|
|
43
|
+
**kwargs,
|
|
44
|
+
):
|
|
45
|
+
super().__init__(*args, **kwargs)
|
|
46
|
+
|
|
47
|
+
self.nh3_kwargs = get_nh3_default_options()
|
|
48
|
+
|
|
49
|
+
if allowed_tags:
|
|
50
|
+
self.nh3_kwargs["tags"] = allowed_tags
|
|
51
|
+
if allowed_attributes:
|
|
52
|
+
self.nh3_kwargs["attributes"] = allowed_attributes
|
|
53
|
+
if url_schemes:
|
|
54
|
+
self.nh3_kwargs["url_schemes"] = url_schemes
|
|
55
|
+
if strip_comments:
|
|
56
|
+
self.nh3_kwargs["strip_comments"] = strip_comments
|
|
57
|
+
if attribute_filter:
|
|
58
|
+
self.nh3_kwargs["attribute_filter"] = attribute_filter
|
|
59
|
+
if link_rel:
|
|
60
|
+
self.nh3_kwargs["link_rel"] = link_rel
|
|
61
|
+
if generic_attribute_prefixes:
|
|
62
|
+
self.nh3_kwargs["generic_attribute_prefixes"] = (
|
|
63
|
+
generic_attribute_prefixes)
|
|
64
|
+
if tag_attribute_values:
|
|
65
|
+
self.nh3_kwargs["tag_attribute_values"] = tag_attribute_values
|
|
66
|
+
if set_tag_attribute_values:
|
|
67
|
+
self.nh3_kwargs["set_tag_attribute_values"] = (
|
|
68
|
+
set_tag_attribute_values)
|
|
69
|
+
|
|
70
|
+
def pre_save(self, model_instance, add):
|
|
71
|
+
data = getattr(model_instance, self.attname)
|
|
72
|
+
if data is None:
|
|
73
|
+
return data
|
|
74
|
+
clean_value = nh3.clean(data, **self.nh3_kwargs) if data else ""
|
|
75
|
+
setattr(model_instance, self.attname, mark_safe(clean_value))
|
|
76
|
+
return clean_value
|
|
77
|
+
|
|
78
|
+
def from_db_value(self, value, expression, connection):
|
|
79
|
+
if value is None:
|
|
80
|
+
return value
|
|
81
|
+
# Values are sanitised before saving, so any value returned from the DB
|
|
82
|
+
# is safe to render unescaped.
|
|
83
|
+
return mark_safe(value)
|
|
@@ -7,7 +7,7 @@ msgid ""
|
|
|
7
7
|
msgstr ""
|
|
8
8
|
"Project-Id-Version: \n"
|
|
9
9
|
"Report-Msgid-Bugs-To: \n"
|
|
10
|
-
"POT-Creation-Date: 2025-
|
|
10
|
+
"POT-Creation-Date: 2025-03-18 12:26+0100\n"
|
|
11
11
|
"PO-Revision-Date: 2021-06-22 23:31+0200\n"
|
|
12
12
|
"Last-Translator: \n"
|
|
13
13
|
"Language-Team: \n"
|
|
@@ -47,11 +47,11 @@ msgstr "Interdit"
|
|
|
47
47
|
msgid "Resource not found"
|
|
48
48
|
msgstr "Ressource non trouvée"
|
|
49
49
|
|
|
50
|
-
#: fields.py:
|
|
50
|
+
#: fields/minutes_duration_field.py:31
|
|
51
51
|
msgid "Invalid value."
|
|
52
52
|
msgstr "Valeur invalide."
|
|
53
53
|
|
|
54
|
-
#: fields.py:
|
|
54
|
+
#: fields/minutes_duration_field.py:46
|
|
55
55
|
msgid ""
|
|
56
56
|
"Invalid format, it can be a number in hours, “1:05”, “:05”, “1h 5m”, “1.5h” "
|
|
57
57
|
"or “30m”."
|
|
@@ -267,6 +267,10 @@ msgstr "Un nouveau code d'authentification a été envoyé par e-mail."
|
|
|
267
267
|
msgid "Invalid value for {filter} filter"
|
|
268
268
|
msgstr "Valeur invalide pour le filtre {filter}"
|
|
269
269
|
|
|
270
|
+
#: views/media_rest_view_mixin.py:95 views/media_rest_view_mixin.py:143
|
|
271
|
+
msgid "Unexpected storage error"
|
|
272
|
+
msgstr "Erreur de stockage inattendue"
|
|
273
|
+
|
|
270
274
|
#: views/ordered_rest_view_mixin.py:32
|
|
271
275
|
#, python-brace-format
|
|
272
276
|
msgid "object parameter is mandatory for move={move}."
|
|
@@ -277,27 +281,23 @@ msgstr "Le paramètre object est requis pour move={move}."
|
|
|
277
281
|
msgid "object {pk} does not exists in this move context."
|
|
278
282
|
msgstr "object {pk} n'existe pas dans ce contexte de déplacement."
|
|
279
283
|
|
|
280
|
-
#: views/rest_views.py:
|
|
284
|
+
#: views/rest_views.py:239
|
|
281
285
|
#, python-brace-format
|
|
282
286
|
msgid "{obj} cannot be deleted because it is referenced by other objects."
|
|
283
287
|
msgstr ""
|
|
284
288
|
"{obj} ne peut pas être supprimé car il est référencé par d’autres objets."
|
|
285
289
|
|
|
286
|
-
#: views/rest_views.py:
|
|
290
|
+
#: views/rest_views.py:324
|
|
287
291
|
#, python-brace-format
|
|
288
292
|
msgid "{model} {obj} created."
|
|
289
293
|
msgstr "{model} {obj} créé."
|
|
290
294
|
|
|
291
|
-
#: views/rest_views.py:
|
|
295
|
+
#: views/rest_views.py:325
|
|
292
296
|
#, python-brace-format
|
|
293
297
|
msgid "{model} {obj} updated."
|
|
294
298
|
msgstr "{model} {obj} modifié."
|
|
295
299
|
|
|
296
|
-
#: views/rest_views.py:
|
|
300
|
+
#: views/rest_views.py:1170
|
|
297
301
|
#, python-brace-format
|
|
298
302
|
msgid "{model} {obj} deleted."
|
|
299
303
|
msgstr "{model} {obj} supprimé."
|
|
300
|
-
|
|
301
|
-
#: views/rest_views.py:1276 views/rest_views.py:1316
|
|
302
|
-
msgid "Unexpected storage error"
|
|
303
|
-
msgstr "Erreur de stockage inattendue"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from django.http import FileResponse
|
|
6
|
+
|
|
7
|
+
from pfx.pfxcore.shortcuts import settings
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LocalStorage:
|
|
13
|
+
direct = True
|
|
14
|
+
|
|
15
|
+
def to_python(self, value):
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
def get_url(self, request, key):
|
|
19
|
+
return Path(settings.STORAGE_LOCAL_ROOT, key)
|
|
20
|
+
|
|
21
|
+
def upload(self, key, file, **kwargs):
|
|
22
|
+
key_list = key.split('/')
|
|
23
|
+
filename = key_list[-1]
|
|
24
|
+
relative_path = Path(*key_list[:-1])
|
|
25
|
+
dirname = Path(settings.STORAGE_LOCAL_ROOT, relative_path)
|
|
26
|
+
dirname.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
ext = ''.join(filename.partition('.')[-2:])
|
|
28
|
+
hashname = f'{hashlib.sha1(file).hexdigest()}{ext}'
|
|
29
|
+
final_key = str(Path(relative_path, hashname))
|
|
30
|
+
with open(Path(dirname, hashname), 'wb') as f:
|
|
31
|
+
f.write(file)
|
|
32
|
+
with open(Path(dirname, hashname), 'rb') as f:
|
|
33
|
+
response = FileResponse(
|
|
34
|
+
f, as_attachment=True, filename=filename)
|
|
35
|
+
return {
|
|
36
|
+
'key': final_key,
|
|
37
|
+
'name': filename,
|
|
38
|
+
'content-length': response.get('Content-Length'),
|
|
39
|
+
'content-type': response.get('Content-Type'),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
def delete(self, value):
|
|
43
|
+
if 'key' not in value:
|
|
44
|
+
return value # pragma: no cover
|
|
45
|
+
path = Path(settings.STORAGE_LOCAL_ROOT, value['key'])
|
|
46
|
+
path.unlink(missing_ok=True)
|
|
47
|
+
while True:
|
|
48
|
+
path = path.parent
|
|
49
|
+
if path == Path(settings.STORAGE_LOCAL_ROOT):
|
|
50
|
+
break
|
|
51
|
+
if any(path.iterdir()):
|
|
52
|
+
break
|
|
53
|
+
try:
|
|
54
|
+
path.rmdir()
|
|
55
|
+
except FileNotFoundError:
|
|
56
|
+
pass
|
|
@@ -6,14 +6,14 @@ from botocore.exceptions import ClientError
|
|
|
6
6
|
|
|
7
7
|
from pfx.pfxcore.shortcuts import settings
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
from .exceptions import StorageException
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
pass
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
14
12
|
|
|
15
13
|
|
|
16
14
|
class S3Storage:
|
|
15
|
+
direct = False
|
|
16
|
+
|
|
17
17
|
@staticmethod
|
|
18
18
|
def s3_client():
|
|
19
19
|
return boto3.client(
|
|
@@ -60,15 +60,16 @@ class S3Storage:
|
|
|
60
60
|
def upload(self, key, file, **kwargs):
|
|
61
61
|
try:
|
|
62
62
|
if isinstance(file, IOBase):
|
|
63
|
-
|
|
63
|
+
self.s3_client().upload_fileobj(
|
|
64
64
|
file, settings.STORAGE_S3_AWS_S3_BUCKET, key,
|
|
65
65
|
ExtraArgs=kwargs)
|
|
66
66
|
else:
|
|
67
|
-
|
|
67
|
+
self.s3_client().upload_file(
|
|
68
68
|
file, settings.STORAGE_S3_AWS_S3_BUCKET, key,
|
|
69
69
|
ExtraArgs=kwargs)
|
|
70
70
|
except ClientError:
|
|
71
71
|
raise StorageException
|
|
72
|
+
return dict(key=key)
|
|
72
73
|
|
|
73
74
|
def delete(self, value):
|
|
74
75
|
if 'key' not in value:
|
|
@@ -8,6 +8,7 @@ from .authentication_views import (
|
|
|
8
8
|
from .fields import VF, FieldType, ViewField, ViewModelField
|
|
9
9
|
from .filters_views import Filter, FilterGroup, ModelFilter
|
|
10
10
|
from .locale_views import LocaleRestView
|
|
11
|
+
from .media_rest_view_mixin import MediaPermsRestViewMixin, MediaRestViewMixin
|
|
11
12
|
from .ordered_rest_view_mixin import OrderedRestViewMixin
|
|
12
13
|
from .rest_views import (
|
|
13
14
|
BaseRestView,
|
|
@@ -20,8 +21,6 @@ from .rest_views import (
|
|
|
20
21
|
DetailRestViewMixin,
|
|
21
22
|
ListPermsRestViewMixin,
|
|
22
23
|
ListRestViewMixin,
|
|
23
|
-
MediaPermsRestViewMixin,
|
|
24
|
-
MediaRestViewMixin,
|
|
25
24
|
ModelBodyMixin,
|
|
26
25
|
ModelMixin,
|
|
27
26
|
ModelResponseMixin,
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from django.core.exceptions import FieldDoesNotExist
|
|
4
|
+
from django.http import FileResponse, HttpResponse
|
|
5
|
+
from django.shortcuts import redirect
|
|
6
|
+
from django.utils.translation import gettext_lazy as _
|
|
7
|
+
|
|
8
|
+
from pfx.pfxcore.decorator import rest_api
|
|
9
|
+
from pfx.pfxcore.exceptions import APIError, NotFoundError
|
|
10
|
+
from pfx.pfxcore.fields import MediaField
|
|
11
|
+
from pfx.pfxcore.http import JsonResponse
|
|
12
|
+
from pfx.pfxcore.shortcuts import get_bool, settings
|
|
13
|
+
from pfx.pfxcore.storage.s3_storage import StorageException
|
|
14
|
+
|
|
15
|
+
from . import parameters
|
|
16
|
+
from .rest_views import ModelMixin
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class MediaRestViewMixin(ModelMixin):
|
|
22
|
+
"""Extension mixin to manage media fields."""
|
|
23
|
+
|
|
24
|
+
def _get_model_field(self, field):
|
|
25
|
+
try:
|
|
26
|
+
model_field = self.model._meta.get_field(field)
|
|
27
|
+
if not isinstance(model_field, MediaField):
|
|
28
|
+
raise NotFoundError # pragma: no cover
|
|
29
|
+
except FieldDoesNotExist: # pragma: no cover
|
|
30
|
+
raise NotFoundError
|
|
31
|
+
return model_field
|
|
32
|
+
|
|
33
|
+
@rest_api(
|
|
34
|
+
"/<int:pk>/<str:field>/upload-url/<str:filename>", method="get",
|
|
35
|
+
priority_doc=-8)
|
|
36
|
+
def field_media_upload_url(self, pk, field, filename, *args, **kwargs):
|
|
37
|
+
"""Entrypoint for
|
|
38
|
+
:code:`GET /<int:pk>/<str:field>/upload-url/<str:filename>` route.
|
|
39
|
+
|
|
40
|
+
Get the upload URL for a media file.
|
|
41
|
+
|
|
42
|
+
:param pk: The object pk
|
|
43
|
+
:param field: The field name
|
|
44
|
+
:param filename: The file name
|
|
45
|
+
:returns: The JSON response
|
|
46
|
+
:rtype: :class:`JsonResponse`
|
|
47
|
+
---
|
|
48
|
+
get:
|
|
49
|
+
summary: Get upload URL
|
|
50
|
+
description: |
|
|
51
|
+
Get upload URL for a `MediaField` field.
|
|
52
|
+
|
|
53
|
+
You can upload a file ont the received URL. When the upload
|
|
54
|
+
query is done, you have to confirm the process with an
|
|
55
|
+
update request (`PUT`) on {model}. The body of this request
|
|
56
|
+
must contain the name of the `MediaField` with the contents
|
|
57
|
+
of the `file` value in the response of this request.
|
|
58
|
+
|
|
59
|
+
1. `GET /<int:pk>/<str:field>/upload-url/<str:filename>`
|
|
60
|
+
→ `data`
|
|
61
|
+
2. `PUT data.url aFile Content-Type: aFile.type`
|
|
62
|
+
3. `PUT /<int:pk> {{field: data.file}}`
|
|
63
|
+
parameters extras:
|
|
64
|
+
pk: The {model} pk.
|
|
65
|
+
field: The {model} field name. Must be the name of
|
|
66
|
+
a `MediaField` field.
|
|
67
|
+
filename: The desired filename.
|
|
68
|
+
responses:
|
|
69
|
+
200:
|
|
70
|
+
description: The upload URL
|
|
71
|
+
content:
|
|
72
|
+
application/json:
|
|
73
|
+
schema:
|
|
74
|
+
properties:
|
|
75
|
+
url:
|
|
76
|
+
type: string
|
|
77
|
+
format: uri
|
|
78
|
+
file:
|
|
79
|
+
type: object
|
|
80
|
+
properties:
|
|
81
|
+
name:
|
|
82
|
+
type: string
|
|
83
|
+
key:
|
|
84
|
+
type: string
|
|
85
|
+
"""
|
|
86
|
+
obj = self.get_object(pk=pk)
|
|
87
|
+
mediaField = self._get_model_field(field)
|
|
88
|
+
if mediaField.storage.direct:
|
|
89
|
+
raise APIError("Unavailable for direct storage")
|
|
90
|
+
try:
|
|
91
|
+
res = self._get_model_field(field).get_upload_url(
|
|
92
|
+
self.request, obj, filename)
|
|
93
|
+
except StorageException as e: # pragma: no cover
|
|
94
|
+
logger.exception(e)
|
|
95
|
+
raise APIError(_("Unexpected storage error", status=500))
|
|
96
|
+
return JsonResponse(res)
|
|
97
|
+
|
|
98
|
+
@rest_api(
|
|
99
|
+
"/<int:pk>/<str:field>", method="get",
|
|
100
|
+
parameters=[parameters.MediaRedirect], priority_doc=-9)
|
|
101
|
+
def field_media_get(self, pk, field, *args, **kwargs):
|
|
102
|
+
"""Entrypoint for :code:`GET /<int:pk>/<str:field>` route.
|
|
103
|
+
|
|
104
|
+
Get the download URL for a media file.
|
|
105
|
+
|
|
106
|
+
:param pk: The object pk
|
|
107
|
+
:param field: The field name
|
|
108
|
+
:returns: The JSON response
|
|
109
|
+
:rtype: :class:`JsonResponse`
|
|
110
|
+
---
|
|
111
|
+
get:
|
|
112
|
+
summary: Get {model} file
|
|
113
|
+
description: Get the URL for a media field file.
|
|
114
|
+
parameters extras:
|
|
115
|
+
pk: the {model} pk
|
|
116
|
+
field: the {model} field name
|
|
117
|
+
responses:
|
|
118
|
+
200:
|
|
119
|
+
description: |
|
|
120
|
+
The file stream if storage is direct,
|
|
121
|
+
otherwise the file URL, only if `redirect` is `false`.
|
|
122
|
+
content:
|
|
123
|
+
application/json:
|
|
124
|
+
schema:
|
|
125
|
+
properties:
|
|
126
|
+
url:
|
|
127
|
+
type: string
|
|
128
|
+
format: uri
|
|
129
|
+
application/octet-stream:
|
|
130
|
+
schema:
|
|
131
|
+
type: file
|
|
132
|
+
302:
|
|
133
|
+
description: |
|
|
134
|
+
The redirect, for undirect storage
|
|
135
|
+
if `redirect` is `true`.
|
|
136
|
+
"""
|
|
137
|
+
obj = self.get_object(pk=pk)
|
|
138
|
+
mediaField = self._get_model_field(field)
|
|
139
|
+
try:
|
|
140
|
+
url = mediaField.get_url(self.request, obj)
|
|
141
|
+
except StorageException as e: # pragma: no cover
|
|
142
|
+
logger.exception(e)
|
|
143
|
+
raise APIError(_("Unexpected storage error", status=500))
|
|
144
|
+
|
|
145
|
+
if mediaField.storage.direct:
|
|
146
|
+
filename = getattr(obj, field).get('name')
|
|
147
|
+
if settings.STORAGE_LOCAL_X_ACCEL_REDIRECT:
|
|
148
|
+
response = HttpResponse()
|
|
149
|
+
response["Content-Disposition"] = (
|
|
150
|
+
f"attachment; filename={filename}")
|
|
151
|
+
response['X-Accel-Redirect'] = (
|
|
152
|
+
f"/filestore/{getattr(obj, field).get('key')}")
|
|
153
|
+
return response
|
|
154
|
+
response = FileResponse(
|
|
155
|
+
open(mediaField.get_url(self.request, obj), 'rb'),
|
|
156
|
+
as_attachment=True, filename=getattr(obj, field).get('name'))
|
|
157
|
+
return response
|
|
158
|
+
|
|
159
|
+
if get_bool(self.request.GET, 'redirect'):
|
|
160
|
+
return redirect(url)
|
|
161
|
+
return JsonResponse(dict(url=url))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class MediaPermsRestViewMixin(MediaRestViewMixin):
|
|
165
|
+
"""Extension mixin to check permissions."""
|
|
166
|
+
|
|
167
|
+
def field_media_upload_url_perm(self, *args, **kwargs):
|
|
168
|
+
return self.request.user.has_perm(*self.get_model_perms('change'))
|
|
169
|
+
|
|
170
|
+
def field_media_get_perm(self, *args, **kwargs):
|
|
171
|
+
return self.request.user.has_perm(*self.get_model_perms('view'))
|