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.
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/PKG-INFO +1 -1
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/send_newsletters_job.py +1 -0
- django_email_learning-2.3.0/django_email_learning/management/commands/generate_embed_token.py +28 -0
- django_email_learning-2.3.0/django_email_learning/migrations/0010_organization_embed_token.py +17 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/organizations.py +24 -0
- django_email_learning-2.3.0/django_email_learning/platform/api/embed_snippet.py +30 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/urls.py +6 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/__init__.py +2 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/courses.py +36 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/courses.py +15 -0
- django_email_learning-2.3.0/django_email_learning/public/api/rate_limiting.py +29 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/api/serializers.py +18 -13
- django_email_learning-2.3.0/django_email_learning/public/api/urls.py +25 -0
- django_email_learning-2.3.0/django_email_learning/public/api/views.py +288 -0
- django_email_learning-2.3.0/django_email_learning/public/embed_script.py +135 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/urls.py +6 -0
- django_email_learning-2.3.0/django_email_learning/static/assets/Code-N0rr-xtz.js +1 -0
- django_email_learning-2.3.0/django_email_learning/static/assets/ContentEditor-CyZeeM_2.js +1 -0
- 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
- 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
- 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
- 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
- django_email_learning-2.3.0/django_email_learning/static/assets/analytics-CpRn6Yel.js +1 -0
- django_email_learning-2.3.0/django_email_learning/static/assets/course-B14SZXBW.js +33 -0
- 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
- 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
- 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
- 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
- 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
- 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
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/base.html +29 -0
- django_email_learning-2.3.0/django_email_learning/templates/emails/newsletter_sendout.html +95 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/analytics.html +1 -1
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/course.html +2 -1
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/courses.html +1 -1
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/learners.html +1 -1
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/newsletter.html +2 -2
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/organization.html +1 -1
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/organizations.html +1 -1
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/settings_api_keys.html +1 -1
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/pyproject.toml +1 -1
- django_email_learning-2.2.0/django_email_learning/public/api/urls.py +0 -14
- django_email_learning-2.2.0/django_email_learning/public/api/views.py +0 -116
- django_email_learning-2.2.0/django_email_learning/static/assets/ContentEditor-aar9LvuR.js +0 -1
- django_email_learning-2.2.0/django_email_learning/static/assets/analytics-vFt3TwJ1.js +0 -1
- django_email_learning-2.2.0/django_email_learning/static/assets/course-C1fZg5V8.js +0 -33
- django_email_learning-2.2.0/django_email_learning/templates/emails/newsletter_sendout.html +0 -13
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/LICENSE +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/README.md +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/admin.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/ai_service_protocol.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/apps.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/language_models.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/open_ai_adapter.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/serializers.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/urls.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ai/views.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/analytics/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/analytics/downloads.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/analytics/serializers.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/analytics/urls.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/analytics/views.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/apps.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/decorators.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/api/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/api/urls.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/api/views.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/check_imap_job.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/deactivate_inactive_enrollments_job.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/deliver_contents_job.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/executor_utils.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/job_metrics.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/queue_utils.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/registry.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/jobs/send_reminders_job.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/check_imap_connections.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/cleanup_job_executions.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/deactivate_inactive_enrollments.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/deliver_contents.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/rotate_encryption_key.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/send_newsletters.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/management/commands/send_reminders.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0001_initial.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0002_newsletter_feature.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0003_learner_photo_private_storage.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0004_sendout_blocked_reason.py +0 -0
- {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
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0006_sociallink.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/0007_backfill_social_links.py +0 -0
- {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
- {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
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/migrations/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/api_keys.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/course_contents.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/courses.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/deliveries.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enrollments.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enums/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enums/course_content_type.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enums/deactivation_reason.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enums/delivery_status.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/enums/enrollment_status.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/imap_connections.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/jobs.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/mixin_models.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/newsletters.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/models/submissions.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/apps.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/base_handler.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/group_enrollment/__init__.py +0 -0
- {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
- {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
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/migrations/0001_initial.py +0 -0
- {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
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/migrations/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/mixins.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/models.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/serializers.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/urls.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/oauth_integrations/views.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/api/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/api/serializers.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/api/urls.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/api/views.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/serializers.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/urls.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/personalised/views.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/newsletter_access_mixin.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/pagniated_api_mixin.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/assignments.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/common.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/courses.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/learners.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/lessons.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/misc.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/newsletters.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/oauth.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/organisations.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/serializers/quizzes.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/assignments.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/learners.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/misc.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/newsletters.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/oauth.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/api/views/organisations.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/features.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/serializers.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/urls.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/base.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/learners.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/misc.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/newsletters.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/platform/views/organisations.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/email_sender_protocol.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/imap_interface_protocol.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/job_executor_protocol.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/metric_recorder_protocol.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/ports/task_queue_protocol.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/api/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/newsletter_views.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/serializers.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/public/views.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/scripts/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/scripts/setup_dev.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_handler_service.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/abstract_command.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/command_request.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/enroll_command.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/exceptions/__init__.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/send_assignment_command.py +0 -0
- {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
- {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
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/send_lesson_command.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/send_quiz_command.py +0 -0
- {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
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/unsubscribe_command.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/command_models/verify_enrollment_command.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/__init__.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/database_delivery_queue.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/database_reminder_queue.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/database_sendout_queue.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/email_sender.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/imap_interface.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/log_based_metric_recorder.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/defaults/thread_pool_job_executor.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/email_sender_service.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/jwt_service.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/metrics_service.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/sanitize.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/storage_tools.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/services/utils.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/signals.py +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/README +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Add-DNm1SyP7.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Alert-C9BZ_0yq.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Assignment-9NgEvbck.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/AssignmentForm-BSU6HzBh.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Avatar-BgcwkYp5.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/BarChart-iFhHVNa-.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Base-Ca7y26tx.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Button-BeAdSA4d.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Cancel-DPlxL2nh.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/CheckCircle-BfciItNb.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Checkbox-BRX35f1Y.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Chip-CH8y6_z8.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/CloudUpload-Cy4xbdRS.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Container-D3PEmuWO.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/ContentCopy-D-rQgHyG.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/ContentEditor-CKKjA5Xn.css +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Delete-Cqi_Q3OO.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DeleteContentForm-WjUJzuUH.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DeleteCoursePopup-Bm2AHccd.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DeleteNewsletterDialog-F2mHZ45A.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DeleteUserDialog--aSnkNdL.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Dialog-Dnjk4ZKo.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DialogActions-Dyb2R8kn.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DialogContentText-CQdksgVi.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/DialogTitle-BwJi5rjs.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Edit-Cw_wtcbs.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/EmptyTableState-zZuBcjpl.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/EnableCourseSwitchPopup-CKnhMhCg.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/EnrollmentForm-qftqSHOy.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/EnrollmentList-DcMBT_0Z.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/FormControlLabel-DkPpSsM8.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/FormGroup-DYYcJ0Nr.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Grid-CaQNlbNT.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/ImageUpload-ux8OZta3.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/InfoOutlined-B9OPv4zc.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Layout-BBuJT7Vg.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/LinearProgress-DvO9MIvu.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/ListItemText-DG4NRDGR.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Lock-CFTd9FlH.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/MenuItem-De4h4Y61.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Modal-BGens4S0.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/NewsletterForm-BkbCz64Q.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Pagination-IGhmyRq6.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Public-CnlfcIAL.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/QuizForm-DwxE1IkB.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/RequiredTextField-RdX7wAKK.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/School-CRQRfzHf.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Select-BvElZvjt.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Stack-BT3rdKGE.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Switch-CEUO9dp9.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/SwitchBase-jqwq1b8w.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/TableCell-mnvybjBi.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Tabs-BLPrkQE3.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/TextField-DqJmnpXC.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/Tooltip-BN0hUIVb.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/WorkspacePremium-B0slSXpO.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/apiClient-DLC4iHBU.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/assignment_public-CrzrzkyL.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/certificate-DIuc9lng.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/certtificate_form-Cq5Sea-V.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/command_result-DgztzBA1.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/isMuiElement-DFZDks--.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/isMuiElement-bWJER0DN.js +0 -0
- {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
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/logo-h-light-CDWnatM_.png +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/logo-v-dark-Bs2lkb0t.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/logo-v-dark-IaqEVA7I.png +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/logo-v-light-CQaPHwc9.png +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/organization-BN0w5-_P.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/platform_newsletter_subscribers-Dq5h5FbR.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/preload-helper-CUg6q9T1.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/public_course-DEM3REtf.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/quiz_public-w8fT6Juo.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/render-CSlijdTr.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/useThemeProps-ByT4Z9AK.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/assets/with-selector-kU74hpO7.js +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/logo-h.png +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/static/logo.png +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment_reminder.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment_reminder.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment_review.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/assignment_review.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/base_amp.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/certificate_form.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/certificate_form.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/deactivation_deadline_passed.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/deactivation_deadline_passed.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/enrollment_verification.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/enrollment_verification.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/enrollment_verified.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/enrollment_verified.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/lesson.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/lesson.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/password_reset.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/password_reset.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/quiz.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/quiz.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/quiz_amp.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/quiz_reminder.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/emails/quiz_reminder.txt +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/newsletters/unsubscribe_invalid.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/newsletters/unsubscribed.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/assignment_public.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/base.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/certificate.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/certificate_form.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/command_result.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/personalised/quiz_public.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/base.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/platform/newsletter_subscribers.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/public/base.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/public/course.html +0 -0
- {django_email_learning-2.2.0 → django_email_learning-2.3.0}/django_email_learning/templates/public/organization.html +0 -0
- {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.
|
|
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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
)
|