django-pfx 1.4.dev100__tar.gz → 1.4.dev104__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.dev100 → django_pfx-1.4.dev104}/PKG-INFO +1 -1
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/django_pfx.egg-info/PKG-INFO +1 -1
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/django_pfx.egg-info/SOURCES.txt +3 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/default_settings.py +5 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/fields.py +13 -4
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +11 -11
- django_pfx-1.4.dev104/pfx/pfxcore/storage/__init__.py +3 -0
- django_pfx-1.4.dev104/pfx/pfxcore/storage/exceptions.py +3 -0
- django_pfx-1.4.dev104/pfx/pfxcore/storage/local_storage.py +56 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/storage/s3_storage.py +7 -6
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/__init__.py +2 -2
- django_pfx-1.4.dev104/pfx/pfxcore/views/media_rest_view_mixin.py +171 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/rest_views.py +1 -135
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/settings/common.py +2 -0
- django_pfx-1.4.dev100/pfx/pfxcore/storage/__init__.py +0 -3
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/.gitignore +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/.gitlab-ci.yml +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/.pre-commit-config.yaml +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/LICENSE +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/MANIFEST.in +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/README.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/django_pfx.egg-info/dependency_links.txt +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/django_pfx.egg-info/requires.txt +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/django_pfx.egg-info/top_level.txt +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/Makefile +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/conf.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/index.rst +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/api.views.rst +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/authentication.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/decorator.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/generate_openapi.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/getting_started.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/internationalisation.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/model.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/pfx_views.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/profiling.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/settings.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/doc/source/testing.md +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/img/pfx.png +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/img/pfx.svg +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/make_messages +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/manage.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/apidoc/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/apidoc/parameters.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/apidoc/schema.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/apidoc/tags.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/apps.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/decorator/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/decorator/rest.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/exceptions.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/http/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/http/json_response.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/management/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/management/commands/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/management/commands/profile.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/middleware/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/middleware/authentication.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/middleware/locale.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/middleware/profiling.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/migrations/0001_initial.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/migrations/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/models/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/models/cache_mixins.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/models/login_ban.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/models/not_null_fields.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/models/otp_user_mixin.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/models/pfx_models.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/models/pfx_user.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/serializers/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/serializers/json.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/settings.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/shortcuts.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/test.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/urls.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/authentication_views.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/fields.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/filters_views.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/locale_views.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/date_format.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/groups.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/list_count.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/list_items.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/list_order.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/list_search.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/subset.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/settings/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pfx/settings/dev.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/pyproject.toml +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/requirements.txt +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/serve-doc +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/setup.cfg +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/setup.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/apps.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/migrations/0001_initial.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/migrations/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/models.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/settings/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/settings/ci.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/settings/dev.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/settings/dev_custom_example.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/settings/dev_default.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/basic_api_errors.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/basic_api_test.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_api_doc.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_api_doc_search.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_auth_api.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_body_mixin.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_cache.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_client.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_fields_choices.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_fields_minutes_duration.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_filters.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_locale_api.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_perm_tests.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_permissions.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_perms_api.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_post_migrate_groups_update.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_profiling_middleware.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_settings.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_shortcuts.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_timezone_middleware.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_tools.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_user_queryset.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_view_decorators.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/tests/test_view_fields.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/urls.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests/views.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/migrations/0001_initial.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/migrations/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/settings/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/settings/ci.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/settings/common.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/settings/dev.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/settings/dev_custom_example.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/settings/dev_default.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/tests/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/tests/test_api.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/tests/test_auth_api.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/urls.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_base_user/views.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/migrations/0001_initial.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/migrations/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/models.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/settings/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/settings/ci.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/settings/common.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/settings/dev.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/settings/dev_custom_example.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/settings/dev_default.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/tests/__init__.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/tests/test_api.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/tests/test_auth_api.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/tests_custom_user/urls.py +0 -0
- {django_pfx-1.4.dev100 → django_pfx-1.4.dev104}/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.dev104
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: django-pfx
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.dev104
|
|
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
|
|
@@ -79,6 +79,8 @@ pfx/pfxcore/models/user_filtered_queryset_mixin.py
|
|
|
79
79
|
pfx/pfxcore/serializers/__init__.py
|
|
80
80
|
pfx/pfxcore/serializers/json.py
|
|
81
81
|
pfx/pfxcore/storage/__init__.py
|
|
82
|
+
pfx/pfxcore/storage/exceptions.py
|
|
83
|
+
pfx/pfxcore/storage/local_storage.py
|
|
82
84
|
pfx/pfxcore/storage/s3_storage.py
|
|
83
85
|
pfx/pfxcore/templates/registration/otp_code_email.txt
|
|
84
86
|
pfx/pfxcore/templates/registration/otp_code_subject.txt
|
|
@@ -91,6 +93,7 @@ pfx/pfxcore/views/authentication_views.py
|
|
|
91
93
|
pfx/pfxcore/views/fields.py
|
|
92
94
|
pfx/pfxcore/views/filters_views.py
|
|
93
95
|
pfx/pfxcore/views/locale_views.py
|
|
96
|
+
pfx/pfxcore/views/media_rest_view_mixin.py
|
|
94
97
|
pfx/pfxcore/views/ordered_rest_view_mixin.py
|
|
95
98
|
pfx/pfxcore/views/rest_views.py
|
|
96
99
|
pfx/pfxcore/views/parameters/__init__.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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import re
|
|
3
3
|
from datetime import timedelta
|
|
4
|
+
from importlib import import_module
|
|
4
5
|
|
|
5
6
|
from django.core.exceptions import ValidationError
|
|
6
7
|
from django.db import models
|
|
@@ -8,17 +9,26 @@ from django.db.models.signals import post_delete
|
|
|
8
9
|
from django.dispatch import receiver
|
|
9
10
|
from django.utils.translation import gettext_lazy as _
|
|
10
11
|
|
|
11
|
-
from pfx.pfxcore.
|
|
12
|
+
from pfx.pfxcore.shortcuts import settings
|
|
12
13
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
15
16
|
|
|
17
|
+
def get_storage_class(class_path):
|
|
18
|
+
ps = class_path.split('.')
|
|
19
|
+
return getattr(import_module('.'.join(ps[:-1])), ps[-1])()
|
|
20
|
+
|
|
21
|
+
|
|
16
22
|
class MediaField(models.JSONField):
|
|
17
23
|
def __init__(
|
|
18
24
|
self, *args, max_length=255, get_key=None, storage=None,
|
|
19
25
|
auto_delete=False, **kwargs):
|
|
20
26
|
self.get_key = get_key or self.get_default_key
|
|
21
|
-
|
|
27
|
+
if not storage and not settings.STORAGE_DEFAULT:
|
|
28
|
+
raise Exception(
|
|
29
|
+
"Missing storage. You have to set a storage "
|
|
30
|
+
"class on the field or define STORAGE_DEFAULT settings.")
|
|
31
|
+
self.storage = storage or get_storage_class(settings.STORAGE_DEFAULT)
|
|
22
32
|
self.auto_delete = auto_delete
|
|
23
33
|
super().__init__(
|
|
24
34
|
*args, max_length=max_length,
|
|
@@ -44,8 +54,7 @@ class MediaField(models.JSONField):
|
|
|
44
54
|
|
|
45
55
|
def upload(self, obj, file, filename, **kwargs):
|
|
46
56
|
key = self.get_key(obj, filename)
|
|
47
|
-
self.storage.upload(key, file, **kwargs)
|
|
48
|
-
return self.to_python(dict(key=key))
|
|
57
|
+
return self.to_python(self.storage.upload(key, file, **kwargs))
|
|
49
58
|
|
|
50
59
|
|
|
51
60
|
@receiver(post_delete)
|
|
@@ -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-05 17:31+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.py:86
|
|
51
51
|
msgid "Invalid value."
|
|
52
52
|
msgstr "Valeur invalide."
|
|
53
53
|
|
|
54
|
-
#: fields.py:
|
|
54
|
+
#: fields.py:101
|
|
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,
|
|
@@ -30,6 +29,7 @@ from .rest_views import (
|
|
|
30
29
|
SecuredRestViewMixin,
|
|
31
30
|
SlugDetailRestViewMixin,
|
|
32
31
|
SlugPermsDetailRestViewMixin,
|
|
32
|
+
UpdatePermsRestViewMixin,
|
|
33
33
|
UpdateRestViewMixin,
|
|
34
34
|
resource_not_found,
|
|
35
35
|
)
|
|
@@ -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'))
|
|
@@ -5,15 +5,10 @@ from json import JSONDecodeError
|
|
|
5
5
|
|
|
6
6
|
from django.conf import settings
|
|
7
7
|
from django.core.cache import cache
|
|
8
|
-
from django.core.exceptions import
|
|
9
|
-
FieldDoesNotExist,
|
|
10
|
-
FieldError,
|
|
11
|
-
ValidationError,
|
|
12
|
-
)
|
|
8
|
+
from django.core.exceptions import FieldError, ValidationError
|
|
13
9
|
from django.db import IntegrityError, transaction
|
|
14
10
|
from django.db.models import ForeignKey, Model, Q
|
|
15
11
|
from django.db.models.fields import AutoFieldMixin
|
|
16
|
-
from django.shortcuts import redirect
|
|
17
12
|
from django.urls import path
|
|
18
13
|
from django.utils.translation import gettext_lazy as _
|
|
19
14
|
from django.views import View
|
|
@@ -31,7 +26,6 @@ from pfx.pfxcore.exceptions import (
|
|
|
31
26
|
NotFoundError,
|
|
32
27
|
UnauthorizedError,
|
|
33
28
|
)
|
|
34
|
-
from pfx.pfxcore.fields import MediaField
|
|
35
29
|
from pfx.pfxcore.http import JsonResponse
|
|
36
30
|
from pfx.pfxcore.models import JSONReprMixin, UserFilteredQuerySetMixin
|
|
37
31
|
from pfx.pfxcore.shortcuts import (
|
|
@@ -42,7 +36,6 @@ from pfx.pfxcore.shortcuts import (
|
|
|
42
36
|
get_object,
|
|
43
37
|
model_permissions,
|
|
44
38
|
)
|
|
45
|
-
from pfx.pfxcore.storage.s3_storage import StorageException
|
|
46
39
|
|
|
47
40
|
from . import parameters
|
|
48
41
|
from .fields import VF
|
|
@@ -1202,133 +1195,6 @@ class DeletePermsRestViewMixin(DeleteRestViewMixin):
|
|
|
1202
1195
|
return self.request.user.has_perm(*self.get_model_perms('delete'))
|
|
1203
1196
|
|
|
1204
1197
|
|
|
1205
|
-
class MediaRestViewMixin(ModelMixin):
|
|
1206
|
-
"""Extension mixin to manage media fields."""
|
|
1207
|
-
|
|
1208
|
-
def _get_model_field(self, field):
|
|
1209
|
-
try:
|
|
1210
|
-
model_field = self.model._meta.get_field(field)
|
|
1211
|
-
if not isinstance(model_field, MediaField):
|
|
1212
|
-
raise NotFoundError # pragma: no cover
|
|
1213
|
-
except FieldDoesNotExist: # pragma: no cover
|
|
1214
|
-
raise NotFoundError
|
|
1215
|
-
return model_field
|
|
1216
|
-
|
|
1217
|
-
@rest_api(
|
|
1218
|
-
"/<int:pk>/<str:field>/upload-url/<str:filename>", method="get",
|
|
1219
|
-
priority_doc=-8)
|
|
1220
|
-
def field_media_upload_url(self, pk, field, filename, *args, **kwargs):
|
|
1221
|
-
"""Entrypoint for
|
|
1222
|
-
:code:`GET /<int:pk>/<str:field>/upload-url/<str:filename>` route.
|
|
1223
|
-
|
|
1224
|
-
Get the upload URL for a media file.
|
|
1225
|
-
|
|
1226
|
-
:param pk: The object pk
|
|
1227
|
-
:param field: The field name
|
|
1228
|
-
:param filename: The file name
|
|
1229
|
-
:returns: The JSON response
|
|
1230
|
-
:rtype: :class:`JsonResponse`
|
|
1231
|
-
---
|
|
1232
|
-
get:
|
|
1233
|
-
summary: Get upload URL
|
|
1234
|
-
description: |
|
|
1235
|
-
Get upload URL for a `MediaField` field.
|
|
1236
|
-
|
|
1237
|
-
You can upload a file ont the received URL. When the upload
|
|
1238
|
-
query is done, you have to confirm the process with an
|
|
1239
|
-
update request (`PUT`) on {model}. The body of this request
|
|
1240
|
-
must contain the name of the `MediaField` with the contents
|
|
1241
|
-
of the `file` value in the response of this request.
|
|
1242
|
-
|
|
1243
|
-
1. `GET /<int:pk>/<str:field>/upload-url/<str:filename>`
|
|
1244
|
-
→ `data`
|
|
1245
|
-
2. `PUT data.url aFile Content-Type: aFile.type`
|
|
1246
|
-
3. `PUT /<int:pk> {{field: data.file}}`
|
|
1247
|
-
parameters extras:
|
|
1248
|
-
pk: The {model} pk.
|
|
1249
|
-
field: The {model} field name. Must be the name of
|
|
1250
|
-
a `MediaField` field.
|
|
1251
|
-
filename: The desired filename.
|
|
1252
|
-
responses:
|
|
1253
|
-
200:
|
|
1254
|
-
description: The upload URL
|
|
1255
|
-
content:
|
|
1256
|
-
application/json:
|
|
1257
|
-
schema:
|
|
1258
|
-
properties:
|
|
1259
|
-
url:
|
|
1260
|
-
type: string
|
|
1261
|
-
format: uri
|
|
1262
|
-
file:
|
|
1263
|
-
type: object
|
|
1264
|
-
properties:
|
|
1265
|
-
name:
|
|
1266
|
-
type: string
|
|
1267
|
-
key:
|
|
1268
|
-
type: string
|
|
1269
|
-
"""
|
|
1270
|
-
obj = self.get_object(pk=pk)
|
|
1271
|
-
try:
|
|
1272
|
-
res = self._get_model_field(field).get_upload_url(
|
|
1273
|
-
self.request, obj, filename)
|
|
1274
|
-
except StorageException as e: # pragma: no cover
|
|
1275
|
-
logger.exception(e)
|
|
1276
|
-
raise APIError(_("Unexpected storage error", status=500))
|
|
1277
|
-
return JsonResponse(res)
|
|
1278
|
-
|
|
1279
|
-
@rest_api(
|
|
1280
|
-
"/<int:pk>/<str:field>", method="get",
|
|
1281
|
-
parameters=[parameters.MediaRedirect], priority_doc=-9)
|
|
1282
|
-
def field_media_get(self, pk, field, *args, **kwargs):
|
|
1283
|
-
"""Entrypoint for :code:`GET /<int:pk>/<str:field>` route.
|
|
1284
|
-
|
|
1285
|
-
Get the download URL for a media file.
|
|
1286
|
-
|
|
1287
|
-
:param pk: The object pk
|
|
1288
|
-
:param field: The field name
|
|
1289
|
-
:returns: The JSON response
|
|
1290
|
-
:rtype: :class:`JsonResponse`
|
|
1291
|
-
---
|
|
1292
|
-
get:
|
|
1293
|
-
summary: Get {model} file
|
|
1294
|
-
description: Get the URL for a media field file.
|
|
1295
|
-
parameters extras:
|
|
1296
|
-
pk: the {model} pk
|
|
1297
|
-
field: the {model} field name
|
|
1298
|
-
responses:
|
|
1299
|
-
200:
|
|
1300
|
-
description: The file URL, if `redirect` is `false`.
|
|
1301
|
-
content:
|
|
1302
|
-
application/json:
|
|
1303
|
-
schema:
|
|
1304
|
-
properties:
|
|
1305
|
-
url:
|
|
1306
|
-
type: string
|
|
1307
|
-
format: uri
|
|
1308
|
-
302:
|
|
1309
|
-
description: The redirect, if `redirect` is `true`.
|
|
1310
|
-
"""
|
|
1311
|
-
obj = self.get_object(pk=pk)
|
|
1312
|
-
try:
|
|
1313
|
-
url = self._get_model_field(field).get_url(self.request, obj)
|
|
1314
|
-
except StorageException as e: # pragma: no cover
|
|
1315
|
-
logger.exception(e)
|
|
1316
|
-
raise APIError(_("Unexpected storage error", status=500))
|
|
1317
|
-
if get_bool(self.request.GET, 'redirect'):
|
|
1318
|
-
return redirect(url)
|
|
1319
|
-
return JsonResponse(dict(url=url))
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
class MediaPermsRestViewMixin(MediaRestViewMixin):
|
|
1323
|
-
"""Extension mixin to check permissions."""
|
|
1324
|
-
|
|
1325
|
-
def field_media_upload_url_perm(self, *args, **kwargs):
|
|
1326
|
-
return self.request.user.has_perm(*self.get_model_perms('change'))
|
|
1327
|
-
|
|
1328
|
-
def field_media_get_perm(self, *args, **kwargs):
|
|
1329
|
-
return self.request.user.has_perm(*self.get_model_perms('view'))
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
1198
|
class SecuredRestViewMixin(View):
|
|
1333
1199
|
"""A view mixin to manage service permissions.
|
|
1334
1200
|
|
|
@@ -45,6 +45,8 @@ PFX_RESET_PASSWORD_URL = (
|
|
|
45
45
|
'http://localhost:8000/test?token={token}&uidb64={uidb64}')
|
|
46
46
|
PFX_SITE_NAME = 'Books Demo'
|
|
47
47
|
|
|
48
|
+
STORAGE_DEFAULT = 'pfx.pfxcore.storage.S3Storage'
|
|
49
|
+
|
|
48
50
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
|
49
51
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
50
52
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|