django-pfx 1.7.6.dev7__tar.gz → 1.7.6.dev9__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.
Files changed (313) hide show
  1. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/.gitlab-ci.yml +2 -0
  2. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/PKG-INFO +1 -1
  3. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/django_pfx.egg-info/PKG-INFO +1 -1
  4. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/django_pfx.egg-info/SOURCES.txt +17 -0
  5. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/django_pfx.egg-info/scm_file_list.json +17 -0
  6. django_pfx-1.7.6.dev9/django_pfx.egg-info/scm_version.json +8 -0
  7. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/index.rst +1 -0
  8. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/settings.md +37 -0
  9. django_pfx-1.7.6.dev9/doc/source/user_jobs.md +401 -0
  10. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/settings/dev.py +4 -0
  11. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/locale/fr/LC_MESSAGES/django.po +33 -33
  12. django_pfx-1.7.6.dev9/pfx/userjobs/migrations/0005_swappable_user_job.py +71 -0
  13. django_pfx-1.7.6.dev9/pfx/userjobs/models/__init__.py +2 -0
  14. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/models/user_job.py +51 -11
  15. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/models/user_job_attachment.py +3 -1
  16. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/providers/user_job_websocket_channel_provider.py +20 -1
  17. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/tasks.py +8 -5
  18. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/views/user_job_attachment_view.py +5 -2
  19. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/views/user_job_view.py +6 -6
  20. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/views/user_websocket_consumer.py +4 -1
  21. django_pfx-1.7.6.dev9/tests_custom_user_job/migrations/0001_initial.py +102 -0
  22. django_pfx-1.7.6.dev9/tests_custom_user_job/models.py +67 -0
  23. django_pfx-1.7.6.dev9/tests_custom_user_job/providers.py +19 -0
  24. django_pfx-1.7.6.dev9/tests_custom_user_job/scope.py +21 -0
  25. django_pfx-1.7.6.dev9/tests_custom_user_job/settings/common.py +133 -0
  26. django_pfx-1.7.6.dev9/tests_custom_user_job/settings/dev_default.py +12 -0
  27. django_pfx-1.7.6.dev9/tests_custom_user_job/tests/__init__.py +1 -0
  28. django_pfx-1.7.6.dev9/tests_custom_user_job/tests/test_user_job.py +171 -0
  29. django_pfx-1.7.6.dev9/tests_custom_user_job/urls.py +8 -0
  30. django_pfx-1.7.6.dev9/tests_user_email_login/__init__.py +0 -0
  31. django_pfx-1.7.6.dev9/tests_user_email_login/migrations/__init__.py +0 -0
  32. django_pfx-1.7.6.dev9/tests_user_email_login/settings/__init__.py +0 -0
  33. django_pfx-1.7.6.dev9/tests_user_email_login/settings/ci.py +18 -0
  34. django_pfx-1.7.6.dev9/tests_user_email_login/settings/dev.py +13 -0
  35. django_pfx-1.7.6.dev9/tests_user_email_login/settings/dev_custom_example.py +28 -0
  36. django_pfx-1.7.6.dev7/django_pfx.egg-info/scm_version.json +0 -8
  37. django_pfx-1.7.6.dev7/pfx/userjobs/models/__init__.py +0 -2
  38. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/.gitignore +0 -0
  39. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/.pre-commit-config.yaml +0 -0
  40. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/LICENSE +0 -0
  41. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/MANIFEST.in +0 -0
  42. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/README.md +0 -0
  43. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/django_pfx.egg-info/dependency_links.txt +0 -0
  44. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/django_pfx.egg-info/requires.txt +0 -0
  45. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/django_pfx.egg-info/top_level.txt +0 -0
  46. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/Makefile +0 -0
  47. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/conf.py +0 -0
  48. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/api.views.rst +0 -0
  49. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/authentication.md +0 -0
  50. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/decorator.md +0 -0
  51. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/generate_openapi.md +0 -0
  52. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/getting_started.md +0 -0
  53. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/internationalisation.md +0 -0
  54. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/model.md +0 -0
  55. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/pfx_views.md +0 -0
  56. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/profiling.md +0 -0
  57. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/doc/source/testing.md +0 -0
  58. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/img/pfx.png +0 -0
  59. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/img/pfx.svg +0 -0
  60. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/make_messages +0 -0
  61. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/manage.py +0 -0
  62. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/__init__.py +0 -0
  63. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/__init__.py +0 -0
  64. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/apps.py +0 -0
  65. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/email_backend/__init__.py +0 -0
  66. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/email_backend/q_mailjet_email_backend.py +0 -0
  67. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/exceptions.py +0 -0
  68. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/mailjet/__init__.py +0 -0
  69. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/mailjet/mailjet.py +0 -0
  70. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/mailjet/message_result.py +0 -0
  71. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/migrations/0001_initial.py +0 -0
  72. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/migrations/0002_alter_email_headers.py +0 -0
  73. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/migrations/0003_alter_email_options.py +0 -0
  74. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/migrations/0004_alter_email_mail_from.py +0 -0
  75. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/migrations/__init__.py +0 -0
  76. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/models/__init__.py +0 -0
  77. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/models/email.py +0 -0
  78. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/providers/__init__.py +0 -0
  79. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/providers/email_django_q_executor.py +0 -0
  80. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/shortcuts.py +0 -0
  81. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/tasks.py +0 -0
  82. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/templates/emails/base_template.html +0 -0
  83. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/templates/emails/base_template.txt +0 -0
  84. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/templates/emails/base_template_subject.txt +0 -0
  85. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/urls.py +0 -0
  86. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/views/__init__.py +0 -0
  87. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/emails/views/email_rest_view.py +0 -0
  88. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/__init__.py +0 -0
  89. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/apidoc/__init__.py +0 -0
  90. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/apidoc/parameters.py +0 -0
  91. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/apidoc/schema.py +0 -0
  92. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/apidoc/tags.py +0 -0
  93. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/apps.py +0 -0
  94. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/decorator/__init__.py +0 -0
  95. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/decorator/rest.py +0 -0
  96. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/default_settings.py +0 -0
  97. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/exceptions.py +0 -0
  98. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/fields/__init__.py +0 -0
  99. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/fields/decimal_field.py +0 -0
  100. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/fields/media_field.py +0 -0
  101. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/fields/minutes_duration_field.py +0 -0
  102. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/fields/rich_text_field.py +0 -0
  103. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/http/__init__.py +0 -0
  104. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/http/json_response.py +0 -0
  105. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.mo +0 -0
  106. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/locale/fr/LC_MESSAGES/django.po +0 -0
  107. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/management/__init__.py +0 -0
  108. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/management/commands/__init__.py +0 -0
  109. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/management/commands/makeapidoc.py +0 -0
  110. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/management/commands/profile.py +0 -0
  111. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/mfa/__init__.py +0 -0
  112. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/mfa/authenticator.py +0 -0
  113. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/mfa/base.py +0 -0
  114. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/mfa/email.py +0 -0
  115. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/mfa/legacy.py +0 -0
  116. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/mfa/sms.py +0 -0
  117. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/middleware/__init__.py +0 -0
  118. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/middleware/authentication.py +0 -0
  119. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/middleware/locale.py +0 -0
  120. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/middleware/profiling.py +0 -0
  121. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/middleware/request_too_big_middleware.py +0 -0
  122. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/middleware/ws_cookie_authentication_middleware.py +0 -0
  123. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/migrations/0001_initial.py +0 -0
  124. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/migrations/0002_pfxpermissionsuser.py +0 -0
  125. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/migrations/0003_delete_pfxpermissionsuser.py +0 -0
  126. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/migrations/0004_alter_loginban_failed_counter_and_more.py +0 -0
  127. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/migrations/0005_mfadevice_mfadevice_mfa_device_unique_backend.py +0 -0
  128. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/migrations/0006_alter_pfxuser_managers.py +0 -0
  129. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/migrations/0007_alter_pfxuser_options.py +0 -0
  130. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/migrations/__init__.py +0 -0
  131. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/migrations/operations/__init__.py +0 -0
  132. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/migrations/operations/permissions.py +0 -0
  133. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/__init__.py +0 -0
  134. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/abstract_pfx_base_user.py +0 -0
  135. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/attachment_mixin.py +0 -0
  136. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/cache_mixins.py +0 -0
  137. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/login_ban.py +0 -0
  138. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/mfa_device.py +0 -0
  139. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/mfa_user_mixin.py +0 -0
  140. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/not_null_fields.py +0 -0
  141. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/ordered_model_mixin.py +0 -0
  142. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/pfx_models.py +0 -0
  143. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/pfx_user.py +0 -0
  144. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/models/user_filtered_queryset_mixin.py +0 -0
  145. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/serializers/__init__.py +0 -0
  146. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/serializers/json.py +0 -0
  147. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/settings.py +0 -0
  148. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/shortcuts.py +0 -0
  149. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/sms/__init__.py +0 -0
  150. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/sms/backends/__init__.py +0 -0
  151. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/sms/backends/base.py +0 -0
  152. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/sms/backends/console.py +0 -0
  153. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/storage/__init__.py +0 -0
  154. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/storage/exceptions.py +0 -0
  155. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/storage/local_storage.py +0 -0
  156. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/storage/s3_storage.py +0 -0
  157. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/templates/registration/otp_code_email.txt +0 -0
  158. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/templates/registration/otp_code_subject.txt +0 -0
  159. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/templates/registration/password_reset_email.txt +0 -0
  160. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/templates/registration/password_reset_subject.txt +0 -0
  161. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/templates/registration/welcome_email.txt +0 -0
  162. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/templates/registration/welcome_subject.txt +0 -0
  163. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/test.py +0 -0
  164. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/urls.py +0 -0
  165. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/version_check.py +0 -0
  166. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/__init__.py +0 -0
  167. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/authentication_views.py +0 -0
  168. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/fields.py +0 -0
  169. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/filters_views.py +0 -0
  170. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/locale_views.py +0 -0
  171. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/media_rest_view_mixin.py +0 -0
  172. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/mfa_authentication_views.py +0 -0
  173. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/ordered_rest_view_mixin.py +0 -0
  174. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/__init__.py +0 -0
  175. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/date_format.py +0 -0
  176. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/groups.py +0 -0
  177. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/list_count.py +0 -0
  178. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/list_items.py +0 -0
  179. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/list_mode.py +0 -0
  180. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/list_order.py +0 -0
  181. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/list_search.py +0 -0
  182. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/media_redirect.py +0 -0
  183. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/meta_fields.py +0 -0
  184. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/meta_filters.py +0 -0
  185. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/meta_orders.py +0 -0
  186. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/subset.py +0 -0
  187. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/subset_limit.py +0 -0
  188. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/subset_offset.py +0 -0
  189. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/subset_page.py +0 -0
  190. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/subset_page_size.py +0 -0
  191. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/parameters/subset_page_subset.py +0 -0
  192. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/pfxcore/views/rest_views.py +0 -0
  193. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/settings/__init__.py +0 -0
  194. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/__init__.py +0 -0
  195. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/apps.py +0 -0
  196. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/locale/fr/LC_MESSAGES/django.mo +0 -0
  197. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/migrations/0001_initial.py +0 -0
  198. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/migrations/0002_alter_userjob_options_alter_userjob_auto_queued_and_more.py +0 -0
  199. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/migrations/0003_clean_scheduled_tasks.py +0 -0
  200. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/migrations/0004_userjob_summary_url.py +0 -0
  201. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/migrations/__init__.py +0 -0
  202. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/providers/__init__.py +0 -0
  203. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/providers/user_job_django_q_executor.py +0 -0
  204. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/urls.py +0 -0
  205. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pfx/userjobs/views/__init__.py +0 -0
  206. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/pyproject.toml +0 -0
  207. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/requirements.txt +0 -0
  208. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/serve-doc +0 -0
  209. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/setup.cfg +0 -0
  210. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/setup.py +0 -0
  211. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/__init__.py +0 -0
  212. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/apps.py +0 -0
  213. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/channel_group_send_calls.py +0 -0
  214. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/locale/fr/LC_MESSAGES/django.po +0 -0
  215. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/mfa_urls.py +0 -0
  216. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/migrations/0001_initial.py +0 -0
  217. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/migrations/__init__.py +0 -0
  218. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/models.py +0 -0
  219. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/settings/__init__.py +0 -0
  220. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/settings/ci.py +0 -0
  221. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/settings/common.py +0 -0
  222. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/settings/dev.py +0 -0
  223. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/settings/dev_custom_example.py +0 -0
  224. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/settings/dev_default.py +0 -0
  225. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/__init__.py +0 -0
  226. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/basic_api_errors.py +0 -0
  227. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/basic_api_test.py +0 -0
  228. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/mfa_test_utils.py +0 -0
  229. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_api_doc.py +0 -0
  230. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_api_doc_search.py +0 -0
  231. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_auth_api.py +0 -0
  232. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_body_mixin.py +0 -0
  233. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_cache.py +0 -0
  234. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_client.py +0 -0
  235. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_emails_email_api.py +0 -0
  236. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_emails_email_model.py +0 -0
  237. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_emails_q_mailjet_email_backend.py +0 -0
  238. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_emails_send_email_message.py +0 -0
  239. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_fields_choices.py +0 -0
  240. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_fields_date.py +0 -0
  241. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_fields_decimal.py +0 -0
  242. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_fields_minutes_duration.py +0 -0
  243. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_fields_one2many.py +0 -0
  244. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_fields_rich_text.py +0 -0
  245. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_fields_time.py +0 -0
  246. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_filters.py +0 -0
  247. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_locale_api.py +0 -0
  248. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_mfa_legacy.py +0 -0
  249. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_mfa_login_api.py +0 -0
  250. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_mfa_setup_api.py +0 -0
  251. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_ordered_rest_view_mixin.py +0 -0
  252. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_perm_tests.py +0 -0
  253. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_permissions.py +0 -0
  254. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_perms_api.py +0 -0
  255. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_post_migrate_groups_update.py +0 -0
  256. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_profiling_middleware.py +0 -0
  257. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_settings.py +0 -0
  258. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_shortcuts.py +0 -0
  259. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_timezone_middleware.py +0 -0
  260. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_tools.py +0 -0
  261. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_user_job_api.py +0 -0
  262. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_user_job_messages.py +0 -0
  263. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_user_job_model.py +0 -0
  264. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_user_queryset.py +0 -0
  265. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_user_websocket.py +0 -0
  266. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_version_check.py +0 -0
  267. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_view_decorators.py +0 -0
  268. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/tests/test_view_fields.py +0 -0
  269. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/urls.py +0 -0
  270. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests/views.py +0 -0
  271. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/__init__.py +0 -0
  272. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/migrations/__init__.py +0 -0
  273. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/settings/__init__.py +0 -0
  274. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/settings/ci.py +0 -0
  275. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/settings/common.py +0 -0
  276. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/settings/dev.py +0 -0
  277. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/settings/dev_custom_example.py +0 -0
  278. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/settings/dev_default.py +0 -0
  279. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/tests/__init__.py +0 -0
  280. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/tests/test_api.py +0 -0
  281. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/tests/test_auth_api.py +0 -0
  282. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/urls.py +0 -0
  283. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_base_user/views.py +0 -0
  284. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/__init__.py +0 -0
  285. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/migrations/0001_initial.py +0 -0
  286. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/migrations/__init__.py +0 -0
  287. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/models.py +0 -0
  288. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/settings/__init__.py +0 -0
  289. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/settings/ci.py +0 -0
  290. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/settings/common.py +0 -0
  291. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/settings/dev.py +0 -0
  292. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/settings/dev_custom_example.py +0 -0
  293. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/settings/dev_default.py +0 -0
  294. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/tests/__init__.py +0 -0
  295. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/tests/test_api.py +0 -0
  296. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/tests/test_auth_api.py +0 -0
  297. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/urls.py +0 -0
  298. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_custom_user/views.py +0 -0
  299. {django_pfx-1.7.6.dev7/tests_user_email_login → django_pfx-1.7.6.dev9/tests_custom_user_job}/__init__.py +0 -0
  300. {django_pfx-1.7.6.dev7/tests_user_email_login → django_pfx-1.7.6.dev9/tests_custom_user_job}/migrations/__init__.py +0 -0
  301. {django_pfx-1.7.6.dev7/tests_user_email_login → django_pfx-1.7.6.dev9/tests_custom_user_job}/settings/__init__.py +0 -0
  302. {django_pfx-1.7.6.dev7/tests_user_email_login → django_pfx-1.7.6.dev9/tests_custom_user_job}/settings/ci.py +0 -0
  303. {django_pfx-1.7.6.dev7/tests_user_email_login → django_pfx-1.7.6.dev9/tests_custom_user_job}/settings/dev.py +0 -0
  304. {django_pfx-1.7.6.dev7/tests_user_email_login → django_pfx-1.7.6.dev9/tests_custom_user_job}/settings/dev_custom_example.py +0 -0
  305. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_user_email_login/migrations/0001_initial.py +0 -0
  306. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_user_email_login/models.py +0 -0
  307. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_user_email_login/settings/common.py +0 -0
  308. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_user_email_login/settings/dev_default.py +0 -0
  309. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_user_email_login/tests/__init__.py +0 -0
  310. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_user_email_login/tests/test_api.py +0 -0
  311. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_user_email_login/tests/test_auth_api.py +0 -0
  312. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_user_email_login/urls.py +0 -0
  313. {django_pfx-1.7.6.dev7 → django_pfx-1.7.6.dev9}/tests_user_email_login/views.py +0 -0
@@ -39,6 +39,7 @@ quality check:
39
39
  - python manage.py makemigrations tests_base_user --check --dry-run
40
40
  - python manage.py makemigrations tests_custom_user --check --dry-run
41
41
  - python manage.py makemigrations tests_user_email_login --check --dry-run
42
+ - python manage.py makemigrations tests_custom_user_job --check --dry-run
42
43
 
43
44
  test:
44
45
  image: $IMAGE
@@ -54,6 +55,7 @@ test:
54
55
  - coverage run -a --source='.' manage.py test tests_base_user
55
56
  - coverage run -a --source='.' manage.py test tests_custom_user
56
57
  - coverage run -a --source='.' manage.py test tests_user_email_login
58
+ - coverage run -a --source='.' manage.py test tests_custom_user_job
57
59
  - coverage report
58
60
  - coverage xml
59
61
  - coverage html
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-pfx
3
- Version: 1.7.6.dev7
3
+ Version: 1.7.6.dev9
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.4
2
2
  Name: django-pfx
3
- Version: 1.7.6.dev7
3
+ Version: 1.7.6.dev9
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
@@ -32,6 +32,7 @@ doc/source/pfx_views.md
32
32
  doc/source/profiling.md
33
33
  doc/source/settings.md
34
34
  doc/source/testing.md
35
+ doc/source/user_jobs.md
35
36
  img/pfx.png
36
37
  img/pfx.svg
37
38
  pfx/__init__.py
@@ -177,6 +178,7 @@ pfx/userjobs/migrations/0001_initial.py
177
178
  pfx/userjobs/migrations/0002_alter_userjob_options_alter_userjob_auto_queued_and_more.py
178
179
  pfx/userjobs/migrations/0003_clean_scheduled_tasks.py
179
180
  pfx/userjobs/migrations/0004_userjob_summary_url.py
181
+ pfx/userjobs/migrations/0005_swappable_user_job.py
180
182
  pfx/userjobs/migrations/__init__.py
181
183
  pfx/userjobs/models/__init__.py
182
184
  pfx/userjobs/models/user_job.py
@@ -276,6 +278,21 @@ tests_custom_user/settings/dev_default.py
276
278
  tests_custom_user/tests/__init__.py
277
279
  tests_custom_user/tests/test_api.py
278
280
  tests_custom_user/tests/test_auth_api.py
281
+ tests_custom_user_job/__init__.py
282
+ tests_custom_user_job/models.py
283
+ tests_custom_user_job/providers.py
284
+ tests_custom_user_job/scope.py
285
+ tests_custom_user_job/urls.py
286
+ tests_custom_user_job/migrations/0001_initial.py
287
+ tests_custom_user_job/migrations/__init__.py
288
+ tests_custom_user_job/settings/__init__.py
289
+ tests_custom_user_job/settings/ci.py
290
+ tests_custom_user_job/settings/common.py
291
+ tests_custom_user_job/settings/dev.py
292
+ tests_custom_user_job/settings/dev_custom_example.py
293
+ tests_custom_user_job/settings/dev_default.py
294
+ tests_custom_user_job/tests/__init__.py
295
+ tests_custom_user_job/tests/test_user_job.py
279
296
  tests_user_email_login/__init__.py
280
297
  tests_user_email_login/models.py
281
298
  tests_user_email_login/urls.py
@@ -20,6 +20,7 @@
20
20
  "doc/source/profiling.md",
21
21
  "doc/source/settings.md",
22
22
  "doc/source/testing.md",
23
+ "doc/source/user_jobs.md",
23
24
  "img/pfx.png",
24
25
  "img/pfx.svg",
25
26
  "make_messages",
@@ -163,6 +164,7 @@
163
164
  "pfx/userjobs/migrations/0002_alter_userjob_options_alter_userjob_auto_queued_and_more.py",
164
165
  "pfx/userjobs/migrations/0003_clean_scheduled_tasks.py",
165
166
  "pfx/userjobs/migrations/0004_userjob_summary_url.py",
167
+ "pfx/userjobs/migrations/0005_swappable_user_job.py",
166
168
  "pfx/userjobs/migrations/__init__.py",
167
169
  "pfx/userjobs/models/__init__.py",
168
170
  "pfx/userjobs/models/user_job.py",
@@ -269,6 +271,21 @@
269
271
  "tests_custom_user/tests/test_auth_api.py",
270
272
  "tests_custom_user/urls.py",
271
273
  "tests_custom_user/views.py",
274
+ "tests_custom_user_job/__init__.py",
275
+ "tests_custom_user_job/migrations/0001_initial.py",
276
+ "tests_custom_user_job/migrations/__init__.py",
277
+ "tests_custom_user_job/models.py",
278
+ "tests_custom_user_job/providers.py",
279
+ "tests_custom_user_job/scope.py",
280
+ "tests_custom_user_job/settings/__init__.py",
281
+ "tests_custom_user_job/settings/ci.py",
282
+ "tests_custom_user_job/settings/common.py",
283
+ "tests_custom_user_job/settings/dev.py",
284
+ "tests_custom_user_job/settings/dev_custom_example.py",
285
+ "tests_custom_user_job/settings/dev_default.py",
286
+ "tests_custom_user_job/tests/__init__.py",
287
+ "tests_custom_user_job/tests/test_user_job.py",
288
+ "tests_custom_user_job/urls.py",
272
289
  "tests_user_email_login/__init__.py",
273
290
  "tests_user_email_login/migrations/0001_initial.py",
274
291
  "tests_user_email_login/migrations/__init__.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.7.5",
3
+ "distance": 9,
4
+ "node": "g7626d2f586fb8dbaa121ce3d2676a0e0c185f7fe",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-01"
8
+ }
@@ -21,6 +21,7 @@ Topics
21
21
  source/model
22
22
  source/internationalisation
23
23
  source/authentication
24
+ source/user_jobs
24
25
  source/settings
25
26
  source/testing
26
27
  source/profiling
@@ -93,3 +93,40 @@ Refer to [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/H
93
93
 
94
94
  ### PFX_FORCE_USER_FILTERED_QUERYSET
95
95
  Force the use of a specific queryset that is segregated by user.
96
+
97
+ ## User jobs
98
+
99
+ See [User jobs](./user_jobs.md).
100
+
101
+ ### PFX_USER_JOB_MODEL
102
+ The model used for user jobs, as an `app_label.ModelName` reference
103
+ (optional, default `userjobs.UserJob`).
104
+
105
+ It must inherit `pfx.userjobs.models.AbstractUserJob`, see
106
+ [Extending the model](./user_jobs.md#extending-the-model).
107
+
108
+ ### PFX_USER_JOB_EXECUTOR
109
+ The class queuing the jobs and registering the periodic tasks
110
+ (optional, default `pfx.userjobs.providers.UserJobDjangoQExecutor`).
111
+
112
+ ### PFX_USER_JOB_WEBSOCKET_PROVIDER
113
+ The class sending the job status changes to the clients
114
+ (optional, default
115
+ `pfx.userjobs.providers.UserJobWebsocketChannelProvider`).
116
+
117
+ Override it to narrow the audience of a job, see
118
+ [Providers](./user_jobs.md#providers).
119
+
120
+ ### PFX_USER_JOB_TIMEOUT_SUMMARY
121
+ The summary given to a job failed for exceeding its timeout
122
+ (optional, default *Timeout exceeds. Our support team has been
123
+ notified.*).
124
+
125
+ ### PFX_USER_JOB_NEVER_STARTED_SUMMARY
126
+ The summary given to a job failed for never having started
127
+ (optional, default *This job never started. Our support team has been
128
+ notified.*).
129
+
130
+ ### USER_JOB_FAILED_SHELF_LIFE_HOURS
131
+ How long a failed job is kept, in hours, unless its own `shelf_life` is
132
+ longer (optional, default `24`).
@@ -0,0 +1,401 @@
1
+ # User jobs
2
+
3
+ A user job is a long task run out of the request/response cycle, on
4
+ behalf of the user who started it: an export, a mailing, a document
5
+ generation. The API answers immediately with the job, the task runs in a
6
+ worker, and its progress is pushed back to the browser over a websocket.
7
+
8
+ PFX keeps the job itself: its status, its result summary, the files it
9
+ produced, and how long all of that is kept.
10
+
11
+ ## Setup
12
+
13
+ The app needs a queue and a channel layer:
14
+
15
+ ```bash
16
+ pip install django-q2
17
+ pip install channels[daphne] channels-redis
18
+ ```
19
+
20
+ ```python
21
+ # settings.py
22
+ INSTALLED_APPS = [
23
+ ...
24
+ 'pfx.userjobs',
25
+ 'django_q',
26
+ ]
27
+ ```
28
+
29
+ Both are checked when the app is loaded, and are replaceable, see
30
+ [Providers](#providers).
31
+
32
+ ```python
33
+ # urls.py
34
+ urlpatterns = [
35
+ path('api/', include('pfx.userjobs.urls')),
36
+ ]
37
+ ```
38
+
39
+ ```python
40
+ # asgi.py
41
+ from pfx.userjobs.urls import websocket_urlpatterns as userjobs_ws
42
+ ```
43
+
44
+ ## Writing a job
45
+
46
+ A job is a module level function taking the job as first argument,
47
+ followed by the arguments it was created with:
48
+
49
+ ```python
50
+ def export_books(job, author_pk):
51
+ books = Book.objects.filter(author_id=author_pk)
52
+ return dict(summary=f"{books.count()} books exported.")
53
+ ```
54
+
55
+ It is found by its dotted path, so it must be importable and stay where
56
+ it is as long as jobs referencing it may still be queued.
57
+
58
+ ### The result
59
+
60
+ The returned dictionary describes what the user is shown:
61
+
62
+ | Key | |
63
+ | --- | --- |
64
+ | `status` | `done` (the default), `warning` or `failed` |
65
+ | `summary` | one line, shown as the result of the job |
66
+ | `summary_url` | where the summary links to |
67
+ | `message` | a longer text, shown when the job is opened |
68
+ | `stacktrace` | technical details, for a failure |
69
+
70
+ An exception that escapes the function fails the job: the summary
71
+ becomes `settings.PFX_SYSTEM_ERROR_USER_MESSAGE` and the traceback is
72
+ kept in `stacktrace`. Nothing is shown to the user that the function did
73
+ not choose to show.
74
+
75
+ `failed()` reports a failure the function handles itself:
76
+
77
+ ```python
78
+ def export_books(job, author_pk):
79
+ try:
80
+ ...
81
+ except SourceUnavailable:
82
+ job.failed(summary="The catalog is unavailable, try again later.")
83
+ ```
84
+
85
+ ### Attachments
86
+
87
+ A job produces files through `UserJobAttachment`. They are listed in the
88
+ job and downloaded from the job REST API:
89
+
90
+ ```python
91
+ from pfx.userjobs.models import UserJobAttachment
92
+
93
+
94
+ def export_books(job, author_pk):
95
+ attachment = UserJobAttachment.objects.create(job=job, is_result=True)
96
+ attachment.file = UserJobAttachment._meta.get_field('file').upload(
97
+ attachment, pdf_bytes, "books.pdf")
98
+ attachment.save()
99
+ return dict(summary="The export is ready.")
100
+ ```
101
+
102
+ They are deleted with the job, files included.
103
+
104
+ ### Jobs that outlive the worker
105
+
106
+ A function returning `status='started'` leaves the job running: PFX does
107
+ not terminate it. Use it for a task handed over to a third party, and
108
+ call `terminated()` or `failed()` from wherever the answer comes back:
109
+
110
+ ```python
111
+ def send_campaign(job, campaign_pk):
112
+ provider.send(campaign_pk, callback_job=job.pk)
113
+ return dict(status=job.STATUS_STARTED)
114
+ ```
115
+
116
+ Such a job is still failed after its `timeout`, see
117
+ [Life cycle](#life-cycle).
118
+
119
+ ## Starting a job
120
+
121
+ ```python
122
+ from pfx.userjobs.models import get_user_job_model
123
+
124
+
125
+ class BookRestView(RestView):
126
+ @rest_api("/<int:pk>/export", method="get")
127
+ def export(self, pk, *args, **kwargs):
128
+ job = get_user_job_model().objects.create(
129
+ name="Books export",
130
+ user=self.request.user,
131
+ func='myapp.jobs.export_books',
132
+ args=[pk],
133
+ timeout=300)
134
+ return JsonResponse(dict(job=job.pk))
135
+ ```
136
+
137
+ | Field | |
138
+ | --- | --- |
139
+ | `name` | what the user sees in their job list |
140
+ | `user` | who started it, and who may see it |
141
+ | `func` | dotted path of the function |
142
+ | `args` | its arguments, stored as JSON |
143
+ | `timeout` | in seconds, given to the queue |
144
+ | `cluster` | the queue cluster, `default` |
145
+ | `shelf_life` | how long the finished job is kept, 30 days |
146
+ | `lock` | see [One job at a time](#one-job-at-a-time) |
147
+ | `auto_queued` | queue the job on save, `True` |
148
+
149
+ Creating the job queues it. With `auto_queued=False` it is created idle
150
+ and queued later by `enqueue()`, which is what a job created outside of
151
+ a request usually wants.
152
+
153
+ Always go through `get_user_job_model()` rather than importing `UserJob`:
154
+ the model is swappable, see [Extending the model](#extending-the-model).
155
+
156
+ ### One job at a time
157
+
158
+ A non empty `lock` is unique among the jobs that are not finished: as
159
+ long as one is queued or running, creating another with the same lock
160
+ raises an `APIError`, answered as *This operation is already in
161
+ progress.*
162
+
163
+ ```python
164
+ get_user_job_model().objects.create(
165
+ ...,
166
+ lock=f'export-books-{author.pk}')
167
+ ```
168
+
169
+ ## Life cycle
170
+
171
+ ```{mermaid}
172
+
173
+ stateDiagram-v2
174
+ [*] --> not_queued: created
175
+ not_queued --> queued: enqueue()
176
+ queued --> started: the worker picks it up
177
+ started --> done: the function returns
178
+ started --> warning: status='warning'
179
+ started --> failed: status='failed', or an exception
180
+ done --> [*]: shelf life
181
+ warning --> [*]: shelf life
182
+ failed --> [*]: shelf life
183
+
184
+ ```
185
+
186
+ Two periodic tasks watch over the jobs, registered on the queue after
187
+ each `migrate`:
188
+
189
+ `pfx.userjobs.tasks.task_user_jobs_update`, every 30 minutes, closes the
190
+ jobs nothing else will:
191
+
192
+ - a job whose queue task failed,
193
+ - a job still `started` more than `timeout` + 5 minutes after it started,
194
+ failed with `PFX_USER_JOB_TIMEOUT_SUMMARY`,
195
+ - a job still `queued` after 3 days, failed with
196
+ `PFX_USER_JOB_NEVER_STARTED_SUMMARY`,
197
+ - a job still `not_queued` after 3 days, deleted.
198
+
199
+ `pfx.userjobs.tasks.task_user_jobs_cleanup`, every night, deletes the
200
+ finished jobs past their retention, see [Retention](#retention).
201
+
202
+ ## Retention
203
+
204
+ A job is kept for its `shelf_life` after it ended, 30 days by default. A
205
+ negative `shelf_life` keeps it forever.
206
+
207
+ Failed jobs are kept for `USER_JOB_FAILED_SHELF_LIFE_HOURS` hours, 24 by
208
+ default, unless their own `shelf_life` is longer: a failure is of no use
209
+ to the user once they have read it, but a job worth keeping stays.
210
+
211
+ Deleting a job deletes its attachments and their files.
212
+
213
+ ## The REST API
214
+
215
+ `/user-jobs/user-jobs` lists the jobs of the authenticated user, most
216
+ recent first. A user only ever sees their own jobs.
217
+
218
+ | | |
219
+ | --- | --- |
220
+ | `GET /user-jobs/user-jobs` | the list |
221
+ | `GET /user-jobs/user-jobs/<pk>` | one job and its attachments |
222
+ | `PUT /user-jobs/user-jobs/<pk>` | mark as read |
223
+ | `DELETE /user-jobs/user-jobs/<pk>` | delete a finished job |
224
+ | `GET /user-jobs/attachments/<pk>/file` | download an attachment |
225
+
226
+ Everything is read only but `read_status`: writing the current status of
227
+ the job into it marks the job as read. The `unread` filter returns the
228
+ jobs whose `read_status` does not match their `status` yet, which is what
229
+ a notification badge counts.
230
+
231
+ The list also filters on `status`, on `status_exclude` and on
232
+ `created_after`.
233
+
234
+ A job can only be deleted once it is `done`, `warning` or `failed`, a
235
+ superuser excepted.
236
+
237
+ ## The websocket
238
+
239
+ The client connects to `ws/user` and receives a message on every status
240
+ change of its jobs:
241
+
242
+ ```json
243
+ {
244
+ "model": "base.UserJob",
245
+ "event": "status_changed",
246
+ "item": {"pk": 12, "status": {"value": "done", "label": "Completed"}}
247
+ }
248
+ ```
249
+
250
+ `event` is `status_changed` or `deleted`, and `item` carries the job as
251
+ the REST API returns it, with its attachments.
252
+
253
+ `model` discriminates the message among the producers sharing the
254
+ channel. Despite what it looks like it is a wire protocol value, not a
255
+ model reference: it stays `base.UserJob` whatever
256
+ `PFX_USER_JOB_MODEL` is, because `react-pfx` matches it as is. Making it
257
+ follow the model would silently stop the clients from showing
258
+ anything.
259
+
260
+ The message is sent to the channel group of the user, and the client
261
+ subscribes to the same group when it connects. Both names come from the
262
+ websocket provider, which is where a project narrows the audience of a
263
+ job, see [Providers](#providers).
264
+
265
+ ## Extending the model
266
+
267
+ `UserJob` knows nothing about the domain of a project, and that is often
268
+ not enough: a multi-tenant project segregating its data by tenant needs
269
+ the jobs to be segregated too, otherwise a user working on several
270
+ tenants sees, on one of them, the results of the jobs they started on
271
+ another.
272
+
273
+ The model is swappable. `AbstractUserJob` carries every field and the
274
+ whole machinery, and the replacement inherits it: fields, constraints,
275
+ managers and behaviour can be added, nothing has to be rewritten.
276
+
277
+ ```python
278
+ # myapp/models.py
279
+ from pfx.userjobs.models import AbstractUserJob
280
+
281
+
282
+ class UserJobQuerySet(TenantQuerySetMixin, PFXUserJobQuerySet):
283
+ pass
284
+
285
+
286
+ class UserJob(AbstractUserJob):
287
+ tenant = models.ForeignKey(Tenant, on_delete=models.PROTECT)
288
+
289
+ objects = UserJobQuerySet.as_manager()
290
+
291
+ class Meta(AbstractUserJob.Meta):
292
+ db_table = 'myapp_user_job'
293
+
294
+ def save(self, *args, **kw):
295
+ if self.tenant_id is None:
296
+ self.tenant = current_tenant()
297
+ return super().save(*args, **kw)
298
+ ```
299
+
300
+ ```python
301
+ # settings.py
302
+ PFX_USER_JOB_MODEL = 'myapp.UserJob'
303
+ ```
304
+
305
+ The setting takes an `app_label.ModelName`, the Django convention for a
306
+ swappable model, as `AUTH_USER_MODEL`.
307
+
308
+ `objects` is the manager the project narrows, and the one the rest views
309
+ and the attachments go through. `all_objects`, inherited from
310
+ `AbstractUserJob`, must stay unnarrowed: the worker running a job and
311
+ the periodic tasks use it, and they run outside of any request.
312
+
313
+ ### The migration
314
+
315
+ `userjobs.0005_swappable_user_job` repoints the attachments to the
316
+ swapped model, so it has to run after the migration creating it. PFX
317
+ depends on the *first* migration of its app, which is not enough as soon
318
+ as the model is added later: the project migration declares the
319
+ ordering.
320
+
321
+ ```python
322
+ class Migration(migrations.Migration):
323
+ dependencies = [('userjobs', '0004_userjob_summary_url')]
324
+ run_before = [('userjobs', '0005_swappable_user_job')]
325
+ ```
326
+
327
+ Nothing else is needed: the foreign key of the attachments follows the
328
+ setting, PFX alters it itself.
329
+
330
+ ### Taking over the PFX table
331
+
332
+ Swapping the model leaves the `userjobs_user_job` table behind, with the
333
+ jobs it holds and the attachments pointing at them. PFX drops it if it
334
+ is empty; a project with jobs to keep takes it over in the migration
335
+ above, instead of creating a table of its own. Renaming is enough, the
336
+ columns are the ones of `AbstractUserJob`:
337
+
338
+ ```python
339
+ operations = [
340
+ migrations.SeparateDatabaseAndState(
341
+ state_operations=[
342
+ migrations.CreateModel(name='UserJob', ...)],
343
+ database_operations=[migrations.RunSQL(
344
+ "ALTER TABLE userjobs_user_job RENAME TO myapp_user_job;",
345
+ "ALTER TABLE myapp_user_job RENAME TO userjobs_user_job;")]),
346
+ migrations.AddField(model_name='userjob', name='tenant', ...),
347
+ ]
348
+ ```
349
+
350
+ A field the jobs cannot do without is added nullable, filled from what
351
+ is known of the existing jobs, then made mandatory.
352
+
353
+ `ALTER TABLE ... RENAME` leaves the indexes named after the old table.
354
+ Only the unique constraint on `lock` has to follow, Django knows it by
355
+ name:
356
+
357
+ ```sql
358
+ ALTER INDEX unique_active_lock
359
+ RENAME TO myapp_userjob_unique_active_lock;
360
+ ```
361
+
362
+ ### What the project still owns
363
+
364
+ The swap is invisible to PFX, which always goes through
365
+ `get_user_job_model()`, but not to the project: a
366
+ `from pfx.userjobs.models import UserJob` left anywhere in it now points
367
+ at the swapped out model, and raises *Manager isn't available* the
368
+ first time it is used.
369
+
370
+ ## Providers
371
+
372
+ Two classes are replaceable, both validated when the app is loaded.
373
+
374
+ `PFX_USER_JOB_EXECUTOR` queues the jobs and registers the periodic
375
+ tasks. The default one uses Django Q2.
376
+
377
+ `PFX_USER_JOB_WEBSOCKET_PROVIDER` sends the status changes. Its two
378
+ group names are what a project overrides to narrow the audience of a
379
+ job, and they must stay consistent with each other:
380
+
381
+ ```python
382
+ class TenantUserJobWebsocketChannelProvider(UserJobWebsocketChannelProvider):
383
+ @classmethod
384
+ def group_name(cls, job):
385
+ """The group notified of a status change."""
386
+ return f'group_user_{job.user.pk}_tenant_{job.tenant_id}'
387
+
388
+ @classmethod
389
+ def consumer_group_name(cls, consumer):
390
+ """The group a client subscribes to."""
391
+ return (
392
+ f'group_user_{consumer.user.pk}'
393
+ f'_tenant_{consumer.scope["tenant"].pk}')
394
+ ```
395
+
396
+ The consumer is passed whole so that an override can use its scope, set
397
+ by the websocket middlewares, and not only its user.
398
+
399
+ ## Settings
400
+
401
+ See [Settings](./settings.md#user-jobs).
@@ -8,6 +8,10 @@ INSTALLED_APPS = [
8
8
  'pfx.emails',
9
9
  ]
10
10
  DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
11
+
12
+ # The swappable user job model must be resolvable by the migration
13
+ # autodetector.
14
+ PFX_USER_JOB_MODEL = 'userjobs.UserJob'
11
15
  STORAGE_DEFAULT = 'pfx.pfxcore.storage.LocalStorage'
12
16
 
13
17
  CACHE_MEMORY = {