django-email-learning 2.2.0__tar.gz → 2.3.0__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 (330) hide show
  1. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/PKG-INFO +1 -1
  2. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/send_newsletters_job.py +1 -0
  3. django_email_learning-2.3.0/django_email_learning/management/commands/generate_embed_token.py +28 -0
  4. django_email_learning-2.3.0/django_email_learning/migrations/0010_organization_embed_token.py +17 -0
  5. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/organizations.py +24 -0
  6. django_email_learning-2.3.0/django_email_learning/platform/api/embed_snippet.py +30 -0
  7. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/urls.py +6 -0
  8. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/__init__.py +2 -0
  9. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/courses.py +36 -0
  10. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/courses.py +15 -0
  11. django_email_learning-2.3.0/django_email_learning/public/api/rate_limiting.py +29 -0
  12. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/api/serializers.py +18 -13
  13. django_email_learning-2.3.0/django_email_learning/public/api/urls.py +25 -0
  14. django_email_learning-2.3.0/django_email_learning/public/api/views.py +288 -0
  15. django_email_learning-2.3.0/django_email_learning/public/embed_script.py +135 -0
  16. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/urls.py +6 -0
  17. django_email_learning-2.3.0/django_email_learning/static/assets/Code-N0rr-xtz.js +1 -0
  18. django_email_learning-2.3.0/django_email_learning/static/assets/ContentEditor-CyZeeM_2.js +1 -0
  19. django_email_learning-2.2.0/django_email_learning/static/assets/CourseForm-DElF8k1y.js → django_email_learning-2.3.0/django_email_learning/static/assets/CourseForm-Cd4bIYqc.js +1 -1
  20. django_email_learning-2.2.0/django_email_learning/static/assets/LessonForm-CMtSL_02.js → django_email_learning-2.3.0/django_email_learning/static/assets/LessonForm-BXMj7irD.js +1 -1
  21. django_email_learning-2.2.0/django_email_learning/static/assets/OrganizationForm-bzLD8D3r.js → django_email_learning-2.3.0/django_email_learning/static/assets/OrganizationForm-C1oc_n7x.js +1 -1
  22. django_email_learning-2.2.0/django_email_learning/static/assets/UserForm-YCJs9pma.js → django_email_learning-2.3.0/django_email_learning/static/assets/UserForm-Dh1yJ_kX.js +1 -1
  23. django_email_learning-2.3.0/django_email_learning/static/assets/analytics-CpRn6Yel.js +1 -0
  24. django_email_learning-2.3.0/django_email_learning/static/assets/course-B14SZXBW.js +33 -0
  25. django_email_learning-2.2.0/django_email_learning/static/assets/courses-c_0sDl1c.js → django_email_learning-2.3.0/django_email_learning/static/assets/courses-OeS3xF7Z.js +2 -2
  26. django_email_learning-2.2.0/django_email_learning/static/assets/learners-CN2si8cF.js → django_email_learning-2.3.0/django_email_learning/static/assets/learners-CjvP6ZlV.js +1 -1
  27. django_email_learning-2.2.0/django_email_learning/static/assets/organizations-D9xrsOp4.js → django_email_learning-2.3.0/django_email_learning/static/assets/organizations-CzH3b6g-.js +2 -2
  28. django_email_learning-2.2.0/django_email_learning/static/assets/platform_newsletter-DUfCvY75.js → django_email_learning-2.3.0/django_email_learning/static/assets/platform_newsletter-CX63H4Qy.js +1 -1
  29. django_email_learning-2.2.0/django_email_learning/static/assets/platform_organization-B78-xS3O.js → django_email_learning-2.3.0/django_email_learning/static/assets/platform_organization-DCpNN-I3.js +2 -2
  30. django_email_learning-2.2.0/django_email_learning/static/assets/settings_api_keys-FpBodIWw.js → django_email_learning-2.3.0/django_email_learning/static/assets/settings_api_keys-CViAH7ey.js +1 -1
  31. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/base.html +29 -0
  32. django_email_learning-2.3.0/django_email_learning/templates/emails/newsletter_sendout.html +95 -0
  33. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/analytics.html +1 -1
  34. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/course.html +2 -1
  35. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/courses.html +1 -1
  36. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/learners.html +1 -1
  37. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/newsletter.html +2 -2
  38. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/organization.html +1 -1
  39. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/organizations.html +1 -1
  40. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/settings_api_keys.html +1 -1
  41. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/pyproject.toml +1 -1
  42. django_email_learning-2.2.0/django_email_learning/public/api/urls.py +0 -14
  43. django_email_learning-2.2.0/django_email_learning/public/api/views.py +0 -116
  44. django_email_learning-2.2.0/django_email_learning/static/assets/ContentEditor-aar9LvuR.js +0 -1
  45. django_email_learning-2.2.0/django_email_learning/static/assets/analytics-vFt3TwJ1.js +0 -1
  46. django_email_learning-2.2.0/django_email_learning/static/assets/course-C1fZg5V8.js +0 -33
  47. django_email_learning-2.2.0/django_email_learning/templates/emails/newsletter_sendout.html +0 -13
  48. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/LICENSE +0 -0
  49. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/README.md +0 -0
  50. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/__init__.py +0 -0
  51. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/admin.py +0 -0
  52. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/__init__.py +0 -0
  53. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/ai_service_protocol.py +0 -0
  54. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/apps.py +0 -0
  55. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/language_models.py +0 -0
  56. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/open_ai_adapter.py +0 -0
  57. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/serializers.py +0 -0
  58. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/urls.py +0 -0
  59. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/views.py +0 -0
  60. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/analytics/__init__.py +0 -0
  61. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/analytics/downloads.py +0 -0
  62. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/analytics/serializers.py +0 -0
  63. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/analytics/urls.py +0 -0
  64. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/analytics/views.py +0 -0
  65. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/apps.py +0 -0
  66. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/decorators.py +0 -0
  67. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/__init__.py +0 -0
  68. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/api/__init__.py +0 -0
  69. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/api/urls.py +0 -0
  70. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/api/views.py +0 -0
  71. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/check_imap_job.py +0 -0
  72. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/deactivate_inactive_enrollments_job.py +0 -0
  73. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/deliver_contents_job.py +0 -0
  74. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/executor_utils.py +0 -0
  75. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/job_metrics.py +0 -0
  76. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/queue_utils.py +0 -0
  77. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/registry.py +0 -0
  78. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/send_reminders_job.py +0 -0
  79. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/__init__.py +0 -0
  80. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/__init__.py +0 -0
  81. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/check_imap_connections.py +0 -0
  82. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/cleanup_job_executions.py +0 -0
  83. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/deactivate_inactive_enrollments.py +0 -0
  84. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/deliver_contents.py +0 -0
  85. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/rotate_encryption_key.py +0 -0
  86. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/send_newsletters.py +0 -0
  87. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/send_reminders.py +0 -0
  88. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0001_initial.py +0 -0
  89. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0002_newsletter_feature.py +0 -0
  90. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0003_learner_photo_private_storage.py +0 -0
  91. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0004_sendout_blocked_reason.py +0 -0
  92. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0005_jobexecution_failed_status_and_error.py +0 -0
  93. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0006_sociallink.py +0 -0
  94. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0007_backfill_social_links.py +0 -0
  95. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0008_remove_organization_legacy_social_fields.py +0 -0
  96. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0009_add_more_social_link_platforms.py +0 -0
  97. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/__init__.py +0 -0
  98. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/__init__.py +0 -0
  99. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/api_keys.py +0 -0
  100. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/course_contents.py +0 -0
  101. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/courses.py +0 -0
  102. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/deliveries.py +0 -0
  103. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enrollments.py +0 -0
  104. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enums/__init__.py +0 -0
  105. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enums/course_content_type.py +0 -0
  106. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enums/deactivation_reason.py +0 -0
  107. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enums/delivery_status.py +0 -0
  108. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enums/enrollment_status.py +0 -0
  109. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/imap_connections.py +0 -0
  110. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/jobs.py +0 -0
  111. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/mixin_models.py +0 -0
  112. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/newsletters.py +0 -0
  113. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/submissions.py +0 -0
  114. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/__init__.py +0 -0
  115. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/apps.py +0 -0
  116. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/base_handler.py +0 -0
  117. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/group_enrollment/__init__.py +0 -0
  118. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/group_enrollment/base_group_enrollment_handler.py +0 -0
  119. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/group_enrollment/google_group_enrollment_handler.py +0 -0
  120. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/migrations/0001_initial.py +0 -0
  121. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/migrations/0002_session_access_token.py +0 -0
  122. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/migrations/__init__.py +0 -0
  123. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/mixins.py +0 -0
  124. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/models.py +0 -0
  125. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/serializers.py +0 -0
  126. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/urls.py +0 -0
  127. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/views.py +0 -0
  128. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/api/__init__.py +0 -0
  129. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/api/serializers.py +0 -0
  130. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/api/urls.py +0 -0
  131. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/api/views.py +0 -0
  132. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/serializers.py +0 -0
  133. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/urls.py +0 -0
  134. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/views.py +0 -0
  135. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/__init__.py +0 -0
  136. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/__init__.py +0 -0
  137. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/newsletter_access_mixin.py +0 -0
  138. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/pagniated_api_mixin.py +0 -0
  139. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/__init__.py +0 -0
  140. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/assignments.py +0 -0
  141. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/common.py +0 -0
  142. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/courses.py +0 -0
  143. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/learners.py +0 -0
  144. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/lessons.py +0 -0
  145. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/misc.py +0 -0
  146. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/newsletters.py +0 -0
  147. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/oauth.py +0 -0
  148. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/organisations.py +0 -0
  149. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/quizzes.py +0 -0
  150. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/assignments.py +0 -0
  151. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/learners.py +0 -0
  152. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/misc.py +0 -0
  153. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/newsletters.py +0 -0
  154. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/oauth.py +0 -0
  155. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/organisations.py +0 -0
  156. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/features.py +0 -0
  157. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/serializers.py +0 -0
  158. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/urls.py +0 -0
  159. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/__init__.py +0 -0
  160. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/base.py +0 -0
  161. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/learners.py +0 -0
  162. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/misc.py +0 -0
  163. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/newsletters.py +0 -0
  164. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/organisations.py +0 -0
  165. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/__init__.py +0 -0
  166. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/email_sender_protocol.py +0 -0
  167. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/imap_interface_protocol.py +0 -0
  168. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/job_executor_protocol.py +0 -0
  169. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/metric_recorder_protocol.py +0 -0
  170. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/task_queue_protocol.py +0 -0
  171. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/__init__.py +0 -0
  172. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/api/__init__.py +0 -0
  173. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/newsletter_views.py +0 -0
  174. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/serializers.py +0 -0
  175. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/views.py +0 -0
  176. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/scripts/__init__.py +0 -0
  177. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/scripts/setup_dev.py +0 -0
  178. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/__init__.py +0 -0
  179. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_handler_service.py +0 -0
  180. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/__init__.py +0 -0
  181. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/abstract_command.py +0 -0
  182. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/command_request.py +0 -0
  183. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/enroll_command.py +0 -0
  184. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/exceptions/__init__.py +0 -0
  185. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/exceptions/blocked_email_error.py +0 -0
  186. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/exceptions/enrollment_already_exists_error.py +0 -0
  187. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/exceptions/invalid_course_slug_error.py +0 -0
  188. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/exceptions/invalid_enrollment_error.py +0 -0
  189. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/exceptions/invalid_verification_code_error.py +0 -0
  190. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/exceptions/learner_cap_exceeded_error.py +0 -0
  191. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/send_assignment_command.py +0 -0
  192. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/send_assignment_reminder_command.py +0 -0
  193. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/send_assignment_review_command.py +0 -0
  194. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/send_lesson_command.py +0 -0
  195. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/send_quiz_command.py +0 -0
  196. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/send_quiz_reminder_command.py +0 -0
  197. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/unsubscribe_command.py +0 -0
  198. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/verify_enrollment_command.py +0 -0
  199. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/__init__.py +0 -0
  200. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/database_delivery_queue.py +0 -0
  201. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/database_reminder_queue.py +0 -0
  202. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/database_sendout_queue.py +0 -0
  203. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/email_sender.py +0 -0
  204. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/imap_interface.py +0 -0
  205. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/log_based_metric_recorder.py +0 -0
  206. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/thread_pool_job_executor.py +0 -0
  207. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/email_sender_service.py +0 -0
  208. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/jwt_service.py +0 -0
  209. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/metrics_service.py +0 -0
  210. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/sanitize.py +0 -0
  211. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/storage_tools.py +0 -0
  212. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/utils.py +0 -0
  213. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/signals.py +0 -0
  214. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/README +0 -0
  215. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Add-DNm1SyP7.js +0 -0
  216. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Alert-C9BZ_0yq.js +0 -0
  217. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Assignment-9NgEvbck.js +0 -0
  218. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/AssignmentForm-BSU6HzBh.js +0 -0
  219. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Avatar-BgcwkYp5.js +0 -0
  220. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/BarChart-iFhHVNa-.js +0 -0
  221. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Base-Ca7y26tx.js +0 -0
  222. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Button-BeAdSA4d.js +0 -0
  223. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Cancel-DPlxL2nh.js +0 -0
  224. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/CheckCircle-BfciItNb.js +0 -0
  225. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Checkbox-BRX35f1Y.js +0 -0
  226. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Chip-CH8y6_z8.js +0 -0
  227. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/CloudUpload-Cy4xbdRS.js +0 -0
  228. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Container-D3PEmuWO.js +0 -0
  229. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/ContentCopy-D-rQgHyG.js +0 -0
  230. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/ContentEditor-CKKjA5Xn.css +0 -0
  231. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Delete-Cqi_Q3OO.js +0 -0
  232. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DeleteContentForm-WjUJzuUH.js +0 -0
  233. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DeleteCoursePopup-Bm2AHccd.js +0 -0
  234. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DeleteNewsletterDialog-F2mHZ45A.js +0 -0
  235. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DeleteUserDialog--aSnkNdL.js +0 -0
  236. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Dialog-Dnjk4ZKo.js +0 -0
  237. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DialogActions-Dyb2R8kn.js +0 -0
  238. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DialogContentText-CQdksgVi.js +0 -0
  239. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DialogTitle-BwJi5rjs.js +0 -0
  240. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Edit-Cw_wtcbs.js +0 -0
  241. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/EmptyTableState-zZuBcjpl.js +0 -0
  242. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/EnableCourseSwitchPopup-CKnhMhCg.js +0 -0
  243. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/EnrollmentForm-qftqSHOy.js +0 -0
  244. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/EnrollmentList-DcMBT_0Z.js +0 -0
  245. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/FormControlLabel-DkPpSsM8.js +0 -0
  246. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/FormGroup-DYYcJ0Nr.js +0 -0
  247. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Grid-CaQNlbNT.js +0 -0
  248. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/ImageUpload-ux8OZta3.js +0 -0
  249. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/InfoOutlined-B9OPv4zc.js +0 -0
  250. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Layout-BBuJT7Vg.js +0 -0
  251. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/LinearProgress-DvO9MIvu.js +0 -0
  252. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/ListItemText-DG4NRDGR.js +0 -0
  253. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Lock-CFTd9FlH.js +0 -0
  254. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/MenuItem-De4h4Y61.js +0 -0
  255. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Modal-BGens4S0.js +0 -0
  256. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/NewsletterForm-BkbCz64Q.js +0 -0
  257. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Pagination-IGhmyRq6.js +0 -0
  258. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Public-CnlfcIAL.js +0 -0
  259. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/QuizForm-DwxE1IkB.js +0 -0
  260. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/RequiredTextField-RdX7wAKK.js +0 -0
  261. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/School-CRQRfzHf.js +0 -0
  262. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Select-BvElZvjt.js +0 -0
  263. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Stack-BT3rdKGE.js +0 -0
  264. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Switch-CEUO9dp9.js +0 -0
  265. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/SwitchBase-jqwq1b8w.js +0 -0
  266. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/TableCell-mnvybjBi.js +0 -0
  267. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Tabs-BLPrkQE3.js +0 -0
  268. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/TextField-DqJmnpXC.js +0 -0
  269. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Tooltip-BN0hUIVb.js +0 -0
  270. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/WorkspacePremium-B0slSXpO.js +0 -0
  271. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/apiClient-DLC4iHBU.js +0 -0
  272. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/assignment_public-CrzrzkyL.js +0 -0
  273. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/certificate-DIuc9lng.js +0 -0
  274. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/certtificate_form-Cq5Sea-V.js +0 -0
  275. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/command_result-DgztzBA1.js +0 -0
  276. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/isMuiElement-DFZDks--.js +0 -0
  277. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/isMuiElement-bWJER0DN.js +0 -0
  278. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/logo-h-dark-BW-XhXGy.png +0 -0
  279. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/logo-h-light-CDWnatM_.png +0 -0
  280. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/logo-v-dark-Bs2lkb0t.js +0 -0
  281. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/logo-v-dark-IaqEVA7I.png +0 -0
  282. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/logo-v-light-CQaPHwc9.png +0 -0
  283. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/organization-BN0w5-_P.js +0 -0
  284. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/platform_newsletter_subscribers-Dq5h5FbR.js +0 -0
  285. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/preload-helper-CUg6q9T1.js +0 -0
  286. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/public_course-DEM3REtf.js +0 -0
  287. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/quiz_public-w8fT6Juo.js +0 -0
  288. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/render-CSlijdTr.js +0 -0
  289. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/useThemeProps-ByT4Z9AK.js +0 -0
  290. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/with-selector-kU74hpO7.js +0 -0
  291. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/logo-h.png +0 -0
  292. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/logo.png +0 -0
  293. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment.html +0 -0
  294. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment.txt +0 -0
  295. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment_reminder.html +0 -0
  296. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment_reminder.txt +0 -0
  297. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment_review.html +0 -0
  298. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment_review.txt +0 -0
  299. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/base_amp.html +0 -0
  300. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/certificate_form.html +0 -0
  301. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/certificate_form.txt +0 -0
  302. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/deactivation_deadline_passed.html +0 -0
  303. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/deactivation_deadline_passed.txt +0 -0
  304. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/enrollment_verification.html +0 -0
  305. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/enrollment_verification.txt +0 -0
  306. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/enrollment_verified.html +0 -0
  307. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/enrollment_verified.txt +0 -0
  308. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/lesson.html +0 -0
  309. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/lesson.txt +0 -0
  310. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/password_reset.html +0 -0
  311. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/password_reset.txt +0 -0
  312. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/quiz.html +0 -0
  313. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/quiz.txt +0 -0
  314. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/quiz_amp.html +0 -0
  315. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/quiz_reminder.html +0 -0
  316. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/quiz_reminder.txt +0 -0
  317. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/newsletters/unsubscribe_invalid.html +0 -0
  318. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/newsletters/unsubscribed.html +0 -0
  319. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/assignment_public.html +0 -0
  320. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/base.html +0 -0
  321. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/certificate.html +0 -0
  322. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/certificate_form.html +0 -0
  323. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/command_result.html +0 -0
  324. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/quiz_public.html +0 -0
  325. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/base.html +0 -0
  326. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/newsletter_subscribers.html +0 -0
  327. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/public/base.html +0 -0
  328. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/public/course.html +0 -0
  329. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/public/organization.html +0 -0
  330. {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/urls.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-email-learning
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: A platform for creating and delivering learning materials via email within a Django application. It provides tools for content management, user role-based administration, and scheduler integration for automated content delivery.
5
5
  License-Expression: BSD-3-Clause
6
6
  License-File: LICENSE
@@ -180,6 +180,7 @@ class SendNewslettersJob:
180
180
  "body": sendout.body,
181
181
  "newsletter_title": sendout.newsletter.title,
182
182
  "unsubscribe_url": full_unsubscribe_url,
183
+ "social_links": sendout.newsletter.organization.social_links.all(),
183
184
  }
184
185
 
185
186
  msg = EmailMultiAlternatives(
@@ -0,0 +1,28 @@
1
+ from django.core.management.base import BaseCommand, CommandParser
2
+
3
+ from django_email_learning.models import Organization
4
+
5
+
6
+ class Command(BaseCommand):
7
+ help = (
8
+ "Generate (or rotate) the embed_token for an organization, used to authorize the "
9
+ "cross-origin embeddable enroll/newsletter-subscribe API. Rotating overwrites the "
10
+ "existing token, immediately invalidating it for anyone still using the old value."
11
+ )
12
+
13
+ def add_arguments(self, parser: CommandParser) -> None:
14
+ parser.add_argument("organization_id", type=int, help="ID of the organization to generate a token for")
15
+
16
+ def handle(self, *args, **options) -> None: # type: ignore[no-untyped-def]
17
+ organization_id = options["organization_id"]
18
+ try:
19
+ organization = Organization.objects.get(id=organization_id)
20
+ except Organization.DoesNotExist:
21
+ self.stdout.write(self.style.ERROR(f"Organization {organization_id} does not exist."))
22
+ return
23
+
24
+ organization.embed_token = Organization.generate_embed_token()
25
+ organization.save(update_fields=["embed_token"])
26
+
27
+ self.stdout.write(self.style.SUCCESS(f"Embed token for organization {organization_id} ({organization.name}):"))
28
+ self.stdout.write(organization.embed_token)
@@ -0,0 +1,17 @@
1
+ # Generated by Django 6.0.6 on 2026-07-19 13:15
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+ dependencies = [
8
+ ("django_email_learning", "0009_add_more_social_link_platforms"),
9
+ ]
10
+
11
+ operations = [
12
+ migrations.AddField(
13
+ model_name="organization",
14
+ name="embed_token",
15
+ field=models.CharField(blank=True, editable=False, max_length=64, null=True, unique=True),
16
+ ),
17
+ ]
@@ -1,3 +1,5 @@
1
+ import base64
2
+ import uuid
1
3
  from typing import Any
2
4
 
3
5
  from django.conf import settings
@@ -19,10 +21,32 @@ class Organization(models.Model):
19
21
  is_public = models.BooleanField(default=True)
20
22
  created_at = models.DateTimeField(auto_now_add=True, null=True)
21
23
  updated_at = models.DateTimeField(auto_now=True, null=True)
24
+ embed_token = models.CharField(max_length=64, unique=True, null=True, blank=True, editable=False)
22
25
 
23
26
  def __str__(self) -> str:
24
27
  return self.name
25
28
 
29
+ @staticmethod
30
+ def generate_embed_token() -> str:
31
+ """Generates an opaque, publishable identifier for the embeddable enroll/
32
+ newsletter-subscribe API (see EMBEDDABLE_ENROLLMENT_ENABLED).
33
+
34
+ Unlike ApiKey.key, this is not a secret - it's designed to sit in a
35
+ third-party site's public page source - so it's stored unencrypted and
36
+ looked up by direct equality rather than decrypted per-row.
37
+ """
38
+ return base64.urlsafe_b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes).decode().rstrip("=")
39
+
40
+ def get_or_create_embed_token(self) -> str:
41
+ """Returns the organization's embed_token, generating and persisting one
42
+ on first use so features built on it (e.g. the course embed snippet)
43
+ work without requiring an admin to run generate_embed_token first.
44
+ """
45
+ if not self.embed_token:
46
+ self.embed_token = self.generate_embed_token()
47
+ self.save(update_fields=["embed_token"])
48
+ return self.embed_token
49
+
26
50
  @property
27
51
  def public_url(self) -> str | None:
28
52
  if not self.is_public:
@@ -0,0 +1,30 @@
1
+ from django.utils.html import escape
2
+
3
+
4
+ def build_embed_script_tag(script_url: str) -> str:
5
+ """The one-time <script> tag that loads the shared, deployment-generic
6
+ del-enroll-form custom element definition (django_email_learning.public.embed_script).
7
+ Goes once anywhere on the page (e.g. a shared footer/header template).
8
+ """
9
+ return f'<script src="{escape(script_url)}"></script>'
10
+
11
+
12
+ def build_embed_widget_tag(*, token: str, course_slug: str, newsletter_title: str | None) -> str:
13
+ """Builds the <del-enroll-form> placeholder tag that pairs with
14
+ build_embed_script_tag(). Meant to be placed wherever the organization
15
+ wants the enrollment form to appear, and repeated for multiple placements
16
+ (each with its own course_id).
17
+
18
+ All values land in HTML attributes/text, escaped via Django's escape() -
19
+ the only encoding needed since (unlike the old inline-script design) none
20
+ of this is embedded in JS anymore.
21
+ """
22
+ attrs = [
23
+ f'token="{escape(token)}"',
24
+ f'course_id="{escape(course_slug)}"',
25
+ ]
26
+ if newsletter_title:
27
+ attrs.append("news_letter_check")
28
+ attrs.append(f'newsletter_title="{escape(newsletter_title)}"')
29
+
30
+ return f"<del-enroll-form {' '.join(attrs)}></del-enroll-form>"
@@ -5,6 +5,7 @@ from django_email_learning.platform.api.views import (
5
5
  ApiKeyView,
6
6
  CourseContentView,
7
7
  CourseView,
8
+ EmbedSnippetView,
8
9
  EnrollmentsStatisticsView,
9
10
  EnrollmentsView,
10
11
  EnrollmentView,
@@ -102,6 +103,11 @@ urlpatterns = [
102
103
  SingleCourseView.as_view(),
103
104
  name="courses_detail",
104
105
  ),
106
+ path(
107
+ "organizations/<int:organization_id>/courses/<int:course_id>/embed_snippet/",
108
+ EmbedSnippetView.as_view(),
109
+ name="course_embed_snippet",
110
+ ),
105
111
  path(
106
112
  "organizations/<int:organization_id>/courses/<int:course_id>/contents/",
107
113
  CourseContentView.as_view(),
@@ -7,6 +7,7 @@ from django_email_learning.platform.api.views.courses import (
7
7
  CourseContentView,
8
8
  CourseCreationMixin,
9
9
  CourseView,
10
+ EmbedSnippetView,
10
11
  ReorderCourseContentView,
11
12
  SendLessonToPlatformUser,
12
13
  SingleCourseContentView,
@@ -56,6 +57,7 @@ __all__ = [
56
57
  "CourseCreationMixin",
57
58
  "CourseView",
58
59
  "CourseContentView",
60
+ "EmbedSnippetView",
59
61
  "ReorderCourseContentView",
60
62
  "SingleCourseContentView",
61
63
  "SingleCourseView",
@@ -1,10 +1,12 @@
1
1
  import json
2
2
  import logging
3
3
 
4
+ from django.conf import settings
4
5
  from django.core.exceptions import ValidationError as DjangoValidationError
5
6
  from django.db import models, transaction
6
7
  from django.db.utils import IntegrityError
7
8
  from django.http import HttpRequest, JsonResponse
9
+ from django.urls import reverse
8
10
  from django.utils.decorators import method_decorator
9
11
  from django.views import View
10
12
  from django.views.decorators.csrf import ensure_csrf_cookie
@@ -17,6 +19,11 @@ from django_email_learning.models import (
17
19
  CourseContentType,
18
20
  )
19
21
  from django_email_learning.platform.api import serializers
22
+ from django_email_learning.platform.api.embed_snippet import (
23
+ build_embed_script_tag,
24
+ build_embed_widget_tag,
25
+ )
26
+ from django_email_learning.public.api.views import embeddable_enrollment_enabled
20
27
  from django_email_learning.services.command_models.send_lesson_command import (
21
28
  SendLessonCommand,
22
29
  )
@@ -379,6 +386,35 @@ class SingleCourseView(CourseCreationMixin, View):
379
386
  return JsonResponse({"error": str(e)}, status=409)
380
387
 
381
388
 
389
+ @method_decorator(accessible_for(roles={"admin", "editor", "instructor", "viewer"}), name="get")
390
+ class EmbedSnippetView(View):
391
+ def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
392
+ if not embeddable_enrollment_enabled():
393
+ return JsonResponse({"error": "Embeddable enrollment is not enabled for this deployment."}, status=404)
394
+
395
+ try:
396
+ course = Course.objects.select_related("organization", "newsletter").get(
397
+ id=kwargs["course_id"], organization_id=kwargs["organization_id"]
398
+ )
399
+ except Course.DoesNotExist:
400
+ return JsonResponse({"error": "Course not found"}, status=404)
401
+
402
+ if not course.public_url:
403
+ return JsonResponse({"error": "Course must be public and enabled to be embeddable."}, status=409)
404
+
405
+ token = course.organization.get_or_create_embed_token()
406
+ script_path = reverse("django_email_learning:public:embed_script")
407
+ script_url = f"{settings.DJANGO_EMAIL_LEARNING['SITE_BASE_URL']}{script_path}"
408
+
409
+ script_html = build_embed_script_tag(script_url)
410
+ widget_html = build_embed_widget_tag(
411
+ token=token,
412
+ course_slug=course.slug,
413
+ newsletter_title=course.newsletter.title if course.newsletter else None,
414
+ )
415
+ return JsonResponse({"script_html": script_html, "widget_html": widget_html}, status=200)
416
+
417
+
382
418
  @method_decorator(accessible_for(roles={"admin", "editor", "instructor"}), name="post")
383
419
  class SendLessonToPlatformUser(View):
384
420
  def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
@@ -14,6 +14,7 @@ from django_email_learning.platform.views.base import (
14
14
  QUIZ_DEFAULTS,
15
15
  BasePlatformView,
16
16
  )
17
+ from django_email_learning.public.api.views import embeddable_enrollment_enabled
17
18
 
18
19
 
19
20
  @method_decorator(login_required, name="dispatch")
@@ -167,6 +168,7 @@ class CourseView(BasePlatformView):
167
168
  context["appContext"]["courseEnabled"] = course.enabled
168
169
  context["appContext"]["coursePublicUrl"] = course.public_url
169
170
  context["appContext"]["courseHasContent"] = CourseContent.objects.filter(course=course).exists()
171
+ context["appContext"]["embeddableEnrollmentEnabled"] = embeddable_enrollment_enabled()
170
172
  context["appContext"]["customComponent"] = None
171
173
  context["appContext"]["quizDefaults"] = {
172
174
  "limitedAttempts": QUIZ_DEFAULTS.get("LIMITED_ATTEMPTS", True),
@@ -191,6 +193,19 @@ class CourseView(BasePlatformView):
191
193
  "view_public_course_page": _("Public page"),
192
194
  "copy_public_course_link": _("Copy public course link"),
193
195
  "public_course_link_copied": _("Link copied!"),
196
+ "add_to_your_site": _("Add to your site"),
197
+ "embed_code_dialog_title": _("Embed on your site"),
198
+ "embed_code_dialog_description": _(
199
+ "Paste these snippets into your own website's HTML to let visitors enroll directly from your site."
200
+ ),
201
+ "embed_script_step_title": _("1. Add this once to your site (e.g. in your footer)"),
202
+ "embed_widget_step_title": _("2. Place this wherever you want the enrollment form to appear"),
203
+ "embed_code_loading": _("Loading embed code..."),
204
+ "embed_code_error": _("Couldn't load the embed code. Please try again."),
205
+ "copy_embed_script": _("Copy script"),
206
+ "copy_embed_widget": _("Copy widget tag"),
207
+ "embed_code_copied": _("Copied!"),
208
+ "close": _("Close"),
194
209
  "enable_course": _("Enable COURSE_NAME"),
195
210
  "course_enable_confirmation": _("Are you sure you want to enable the course COURSE_NAME?"),
196
211
  "continue": _("Continue"),
@@ -0,0 +1,29 @@
1
+ from django.core.cache import cache
2
+ from django.http import HttpRequest
3
+
4
+
5
+ def get_client_ip(request: HttpRequest) -> str:
6
+ return request.META.get("REMOTE_ADDR", "unknown")
7
+
8
+
9
+ def is_rate_limited(key: str, *, limit: int, window_seconds: int) -> bool:
10
+ """Fixed-window request counter backed by Django's cache framework.
11
+
12
+ Returns True once `limit` calls for `key` have been seen within the
13
+ current `window_seconds` window. Relies on whatever CACHES backend is
14
+ configured; a per-process LocMemCache under-counts across multiple
15
+ worker processes, so a shared backend (e.g. Redis) is recommended in
16
+ production for this to be effective.
17
+ """
18
+ count = cache.get(key)
19
+ if count is None:
20
+ cache.set(key, 1, timeout=window_seconds)
21
+ return False
22
+ if count >= limit:
23
+ return True
24
+ try:
25
+ cache.incr(key)
26
+ except ValueError:
27
+ # Key expired between the get() and incr() calls above; start over.
28
+ cache.set(key, 1, timeout=window_seconds)
29
+ return False
@@ -4,11 +4,8 @@ from typing import List
4
4
  from pydantic import BaseModel, Field, field_validator
5
5
 
6
6
 
7
- class EnrollmentRequest(BaseModel):
8
- organization_id: int
7
+ class EmailValidatedRequest(BaseModel):
9
8
  email: str
10
- course_slug: str = Field(min_length=1)
11
- subscribe_to_newsletter: bool = False
12
9
 
13
10
  @field_validator("email")
14
11
  def validate_email(cls, v: str) -> str:
@@ -18,13 +15,21 @@ class EnrollmentRequest(BaseModel):
18
15
  return v
19
16
 
20
17
 
21
- class NewsletterSubscribeRequest(BaseModel):
22
- email: str
23
- newsletter_ids: List[int] = Field(min_length=1)
18
+ class EnrollmentRequest(EmailValidatedRequest):
19
+ organization_id: int
20
+ course_slug: str = Field(min_length=1)
21
+ subscribe_to_newsletter: bool = False
24
22
 
25
- @field_validator("email")
26
- def validate_email(cls, v: str) -> str:
27
- email_regex = r"^[\w\.-]+@[\w\.-]+\.\w+$"
28
- if not re.match(email_regex, v):
29
- raise ValueError("Invalid email format")
30
- return v
23
+
24
+ class EmbeddableEnrollmentRequest(EmailValidatedRequest):
25
+ """Same as EnrollmentRequest but without organization_id: the embed API
26
+ resolves the organization from the embed_token in the URL instead of
27
+ trusting a caller-supplied id.
28
+ """
29
+
30
+ course_slug: str = Field(min_length=1)
31
+ subscribe_to_newsletter: bool = False
32
+
33
+
34
+ class NewsletterSubscribeRequest(EmailValidatedRequest):
35
+ newsletter_ids: List[int] = Field(min_length=1)
@@ -0,0 +1,25 @@
1
+ from django.urls import path
2
+
3
+ from django_email_learning.public.api.views import (
4
+ EmbeddableEnrollView,
5
+ EmbeddableNewsletterSubscribeView,
6
+ EnrollView,
7
+ NewsletterSubscribeView,
8
+ )
9
+
10
+ app_name = "django_email_learning"
11
+
12
+ urlpatterns = [
13
+ path("enrollments/", EnrollView.as_view(), name="enroll"),
14
+ path(
15
+ "organizations/<int:organization_id>/newsletters/subscribe/",
16
+ NewsletterSubscribeView.as_view(),
17
+ name="newsletter_subscribe",
18
+ ),
19
+ path("embed/<str:token>/enrollments/", EmbeddableEnrollView.as_view(), name="embed_enroll"),
20
+ path(
21
+ "embed/<str:token>/newsletters/subscribe/",
22
+ EmbeddableNewsletterSubscribeView.as_view(),
23
+ name="embed_newsletter_subscribe",
24
+ ),
25
+ ]
@@ -0,0 +1,288 @@
1
+ import json
2
+ import logging
3
+ from typing import List
4
+
5
+ from django.conf import settings
6
+ from django.http import HttpRequest, HttpResponse, JsonResponse
7
+ from django.utils.decorators import method_decorator
8
+ from django.views import View
9
+ from django.views.decorators.csrf import csrf_exempt
10
+ from pydantic import ValidationError
11
+
12
+ from django_email_learning.models import Course, Newsletter, NewsletterSubscriber, Organization
13
+ from django_email_learning.public.api.rate_limiting import get_client_ip, is_rate_limited
14
+ from django_email_learning.public.api.serializers import (
15
+ EmbeddableEnrollmentRequest,
16
+ EnrollmentRequest,
17
+ NewsletterSubscribeRequest,
18
+ )
19
+ from django_email_learning.services.command_models.enroll_command import EnrollCommand
20
+ from django_email_learning.services.command_models.exceptions.blocked_email_error import (
21
+ BlockedEmailError,
22
+ )
23
+ from django_email_learning.services.command_models.exceptions.enrollment_already_exists_error import (
24
+ EnrollmentAlreadyExistsError,
25
+ )
26
+ from django_email_learning.services.command_models.exceptions.learner_cap_exceeded_error import (
27
+ LearnerCapExceededError,
28
+ )
29
+ from django_email_learning.services.utils import mask_email
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+ DEFAULT_RATE_LIMITS = {
34
+ "PER_IP_LIMIT": 20,
35
+ "PER_IP_WINDOW_SECONDS": 300,
36
+ "PER_EMAIL_LIMIT": 5,
37
+ "PER_EMAIL_WINDOW_SECONDS": 3600,
38
+ "PER_TOKEN_LIMIT": 60,
39
+ "PER_TOKEN_WINDOW_SECONDS": 300,
40
+ }
41
+
42
+ TOO_MANY_REQUESTS_MESSAGE = "Too many requests. Please try again later."
43
+ NOT_FOUND_MESSAGE = "Not found"
44
+ INVALID_JSON_MESSAGE = "Invalid JSON payload"
45
+
46
+
47
+ def embeddable_enrollment_enabled() -> bool:
48
+ return bool(getattr(settings, "DJANGO_EMAIL_LEARNING", {}).get("EMBEDDABLE_ENROLLMENT_ENABLED", False))
49
+
50
+
51
+ def get_rate_limit_settings() -> dict:
52
+ configured = getattr(settings, "DJANGO_EMAIL_LEARNING", {}).get("EMBEDDABLE_ENROLLMENT_RATE_LIMITS", {})
53
+ return {**DEFAULT_RATE_LIMITS, **configured}
54
+
55
+
56
+ def _default_max_subscribers() -> int:
57
+ return (
58
+ getattr(settings, "DJANGO_EMAIL_LEARNING", {}).get("NEWSLETTERS", {}).get("MAX_SUBSCRIBER_PER_NEWSLETTER", 500)
59
+ )
60
+
61
+
62
+ def _perform_enrollment(
63
+ *, organization_id: int, email: str, course_slug: str, subscribe_to_newsletter: bool
64
+ ) -> JsonResponse:
65
+ try:
66
+ course = Course.objects.get(slug=course_slug, organization_id=organization_id, is_public=True)
67
+ except Course.DoesNotExist:
68
+ return JsonResponse({"error": "Course not found"}, status=404)
69
+
70
+ command = EnrollCommand(email=email, course_slug=course_slug, organization_id=organization_id)
71
+ try:
72
+ command.execute()
73
+ except EnrollmentAlreadyExistsError as e:
74
+ logger.info(f"Enrollment already exists: {e}")
75
+ return JsonResponse({"status": "already_enrolled"}, status=200)
76
+ except BlockedEmailError as e:
77
+ logger.error(f"Blocked email error: {e}")
78
+ return JsonResponse({"error": str(e)}, status=403)
79
+ except LearnerCapExceededError as e:
80
+ logger.info(f"Learner cap exceeded: {e}")
81
+ return JsonResponse({"error": str(e)}, status=403)
82
+
83
+ if subscribe_to_newsletter and course.newsletter_id:
84
+ NewsletterSubscriber.objects.get_or_create(newsletter_id=course.newsletter_id, email=email)
85
+
86
+ return JsonResponse({"status": "enrolled"}, status=200)
87
+
88
+
89
+ def _perform_newsletter_subscribe(
90
+ *, organization_id: int, email: str, newsletter_ids: List[int], max_subscribers: int
91
+ ) -> JsonResponse:
92
+ newsletters = {n.id: n for n in Newsletter.objects.filter(id__in=newsletter_ids, organization_id=organization_id)}
93
+
94
+ invalid = set(newsletter_ids) - set(newsletters)
95
+ if invalid:
96
+ return JsonResponse({"error": "One or more newsletter IDs are invalid."}, status=400)
97
+
98
+ for newsletter in newsletters.values():
99
+ already_subscribed = NewsletterSubscriber.objects.filter(newsletter=newsletter, email=email).exists()
100
+ if not already_subscribed and newsletter.subscribers.count() >= max_subscribers:
101
+ return JsonResponse(
102
+ {"error": f'Newsletter "{newsletter.title}" has reached its maximum number of subscribers.'},
103
+ status=400,
104
+ )
105
+
106
+ for newsletter_id in newsletters:
107
+ NewsletterSubscriber.objects.get_or_create(newsletter_id=newsletter_id, email=email)
108
+
109
+ return JsonResponse({"status": "subscribed"}, status=200)
110
+
111
+
112
+ class EnrollView(View):
113
+ def post(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
114
+ try:
115
+ payload = EnrollmentRequest.model_validate(json.loads(request.body))
116
+ except ValidationError as e:
117
+ return JsonResponse({"error": str(e)}, status=400)
118
+
119
+ return _perform_enrollment(
120
+ organization_id=payload.organization_id,
121
+ email=payload.email,
122
+ course_slug=payload.course_slug,
123
+ subscribe_to_newsletter=payload.subscribe_to_newsletter,
124
+ )
125
+
126
+
127
+ class NewsletterSubscribeView(View):
128
+ def get_max_subscribers(self) -> int:
129
+ return _default_max_subscribers()
130
+
131
+ def post(self, request, organization_id: int, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
132
+ payload = json.loads(request.body)
133
+ try:
134
+ data = NewsletterSubscribeRequest.model_validate(payload)
135
+ except ValidationError as e:
136
+ return JsonResponse({"error": str(e)}, status=400)
137
+
138
+ return _perform_newsletter_subscribe(
139
+ organization_id=organization_id,
140
+ email=data.email,
141
+ newsletter_ids=data.newsletter_ids,
142
+ max_subscribers=self.get_max_subscribers(),
143
+ )
144
+
145
+
146
+ class PublicCorsMixin:
147
+ """Adds a wide-open CORS policy on top of a view, for endpoints meant to be
148
+ called cross-origin from third-party sites embedding a widget. These take
149
+ no cookies/credentials, so an open origin policy carries no session-hijack
150
+ risk on its own; callers still go through EMBEDDABLE_ENROLLMENT_ENABLED, a
151
+ per-organization embed_token, and rate limiting below.
152
+ """
153
+
154
+ def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: # type: ignore[no-untyped-def]
155
+ if request.method == "OPTIONS":
156
+ response = HttpResponse(status=204)
157
+ else:
158
+ response = super().dispatch(request, *args, **kwargs) # type: ignore[misc]
159
+ response["Access-Control-Allow-Origin"] = "*"
160
+ response["Access-Control-Allow-Methods"] = "POST, OPTIONS"
161
+ response["Access-Control-Allow-Headers"] = "Content-Type"
162
+ response["Access-Control-Max-Age"] = "86400"
163
+ return response
164
+
165
+
166
+ class EmbedTokenResolverMixin:
167
+ """Resolves the Organization addressed by the embed_token URL segment
168
+ before dispatching to the view, and 404s if embedding is disabled
169
+ deployment-wide or the token doesn't match any organization. A bad token
170
+ is intentionally indistinguishable from a disabled feature, so callers
171
+ can't use this to enumerate valid tokens.
172
+ """
173
+
174
+ organization: Organization
175
+
176
+ def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: # type: ignore[no-untyped-def]
177
+ if not embeddable_enrollment_enabled():
178
+ return JsonResponse({"error": NOT_FOUND_MESSAGE}, status=404)
179
+ try:
180
+ self.organization = Organization.objects.get(embed_token=kwargs.get("token"))
181
+ except Organization.DoesNotExist:
182
+ return JsonResponse({"error": NOT_FOUND_MESSAGE}, status=404)
183
+ return super().dispatch(request, *args, **kwargs) # type: ignore[misc]
184
+
185
+
186
+ @method_decorator(csrf_exempt, name="dispatch")
187
+ class EmbeddableEnrollView(EmbedTokenResolverMixin, PublicCorsMixin, View):
188
+ """Cross-origin counterpart to EnrollView for embedding on third-party
189
+ sites, addressed by a per-organization embed_token in the URL rather than
190
+ a caller-supplied organization_id (which would let anyone target any
191
+ organization). Disabled unless
192
+ DJANGO_EMAIL_LEARNING["EMBEDDABLE_ENROLLMENT_ENABLED"] is set, since
193
+ opening this up is a per-deployment decision, not a default.
194
+ """
195
+
196
+ def post(self, request, token: str, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
197
+ rate_limits = get_rate_limit_settings()
198
+ client_ip = get_client_ip(request)
199
+
200
+ if is_rate_limited(
201
+ f"public_api:embed_enroll:ip:{client_ip}",
202
+ limit=rate_limits["PER_IP_LIMIT"],
203
+ window_seconds=rate_limits["PER_IP_WINDOW_SECONDS"],
204
+ ):
205
+ logger.warning(f"Embedded enrollment rate limit exceeded for IP {client_ip}")
206
+ return JsonResponse({"error": TOO_MANY_REQUESTS_MESSAGE}, status=429)
207
+
208
+ if is_rate_limited(
209
+ f"public_api:embed_enroll:token:{token}",
210
+ limit=rate_limits["PER_TOKEN_LIMIT"],
211
+ window_seconds=rate_limits["PER_TOKEN_WINDOW_SECONDS"],
212
+ ):
213
+ logger.warning(f"Embedded enrollment rate limit exceeded for organization {self.organization.id}")
214
+ return JsonResponse({"error": TOO_MANY_REQUESTS_MESSAGE}, status=429)
215
+
216
+ try:
217
+ payload = EmbeddableEnrollmentRequest.model_validate(json.loads(request.body))
218
+ except json.JSONDecodeError:
219
+ return JsonResponse({"error": INVALID_JSON_MESSAGE}, status=400)
220
+ except ValidationError as e:
221
+ return JsonResponse({"error": str(e)}, status=400)
222
+
223
+ if is_rate_limited(
224
+ f"public_api:embed_enroll:email:{payload.email.lower()}",
225
+ limit=rate_limits["PER_EMAIL_LIMIT"],
226
+ window_seconds=rate_limits["PER_EMAIL_WINDOW_SECONDS"],
227
+ ):
228
+ logger.warning(f"Embedded enrollment rate limit exceeded for email {mask_email(payload.email)}")
229
+ return JsonResponse({"error": TOO_MANY_REQUESTS_MESSAGE}, status=429)
230
+
231
+ return _perform_enrollment(
232
+ organization_id=self.organization.id,
233
+ email=payload.email,
234
+ course_slug=payload.course_slug,
235
+ subscribe_to_newsletter=payload.subscribe_to_newsletter,
236
+ )
237
+
238
+
239
+ @method_decorator(csrf_exempt, name="dispatch")
240
+ class EmbeddableNewsletterSubscribeView(EmbedTokenResolverMixin, PublicCorsMixin, View):
241
+ """Cross-origin counterpart to NewsletterSubscribeView, gated by the same
242
+ embed_token/EMBEDDABLE_ENROLLMENT_ENABLED checks as EmbeddableEnrollView.
243
+ """
244
+
245
+ def get_max_subscribers(self) -> int:
246
+ return _default_max_subscribers()
247
+
248
+ def post(self, request, token: str, *args, **kwargs) -> JsonResponse: # type: ignore[no-untyped-def]
249
+ rate_limits = get_rate_limit_settings()
250
+ client_ip = get_client_ip(request)
251
+
252
+ if is_rate_limited(
253
+ f"public_api:embed_newsletter_subscribe:ip:{client_ip}",
254
+ limit=rate_limits["PER_IP_LIMIT"],
255
+ window_seconds=rate_limits["PER_IP_WINDOW_SECONDS"],
256
+ ):
257
+ logger.warning(f"Embedded newsletter subscribe rate limit exceeded for IP {client_ip}")
258
+ return JsonResponse({"error": TOO_MANY_REQUESTS_MESSAGE}, status=429)
259
+
260
+ if is_rate_limited(
261
+ f"public_api:embed_newsletter_subscribe:token:{token}",
262
+ limit=rate_limits["PER_TOKEN_LIMIT"],
263
+ window_seconds=rate_limits["PER_TOKEN_WINDOW_SECONDS"],
264
+ ):
265
+ logger.warning(f"Embedded newsletter subscribe rate limit exceeded for organization {self.organization.id}")
266
+ return JsonResponse({"error": TOO_MANY_REQUESTS_MESSAGE}, status=429)
267
+
268
+ try:
269
+ data = NewsletterSubscribeRequest.model_validate(json.loads(request.body))
270
+ except json.JSONDecodeError:
271
+ return JsonResponse({"error": INVALID_JSON_MESSAGE}, status=400)
272
+ except ValidationError as e:
273
+ return JsonResponse({"error": str(e)}, status=400)
274
+
275
+ if is_rate_limited(
276
+ f"public_api:embed_newsletter_subscribe:email:{data.email.lower()}",
277
+ limit=rate_limits["PER_EMAIL_LIMIT"],
278
+ window_seconds=rate_limits["PER_EMAIL_WINDOW_SECONDS"],
279
+ ):
280
+ logger.warning(f"Embedded newsletter subscribe rate limit exceeded for email {mask_email(data.email)}")
281
+ return JsonResponse({"error": TOO_MANY_REQUESTS_MESSAGE}, status=429)
282
+
283
+ return _perform_newsletter_subscribe(
284
+ organization_id=self.organization.id,
285
+ email=data.email,
286
+ newsletter_ids=data.newsletter_ids,
287
+ max_subscribers=self.get_max_subscribers(),
288
+ )