From 97ab5236fb32c638921726fbcc04f7d864574963 Mon Sep 17 00:00:00 2001 From: Payam Date: Mon, 6 Apr 2026 15:36:08 +0400 Subject: [PATCH] feat: #334 add tos config --- django_email_learning/public/views.py | 12 +++++++ django_service/settings.py | 1 + docs/source/installation.rst | 12 +++++++ frontend/public/components/EnrollmentForm.jsx | 23 ++++++++++-- .../test_views/test_public_course_view.py | 35 +++++++++++++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/django_email_learning/public/views.py b/django_email_learning/public/views.py index 9f3b030..641e473 100644 --- a/django_email_learning/public/views.py +++ b/django_email_learning/public/views.py @@ -17,6 +17,10 @@ ) +def get_terms_of_service_url() -> str | None: + return settings.DJANGO_EMAIL_LEARNING.get("TERMS_OF_SERVICE_URL") # type: ignore[return-value] + + def get_organization_json_ld_links(organization: Organization) -> dict[str, object]: json_ld_links: dict[str, object] = {"url": organization.public_url} same_as: list[str] = [] @@ -165,6 +169,7 @@ def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] "organization": organization_data.model_dump(), "enrollApiUrl": f"{settings.DJANGO_EMAIL_LEARNING['SITE_BASE_URL']}{enroll_api_path}", "direction": "rtl" if lang_info["bidi"] else "ltr", + "termsOfServiceUrl": get_terms_of_service_url(), "localeMessages": { "courses": _("Courses"), "enroll_now": _("Enroll Now"), @@ -188,6 +193,9 @@ def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] "linkedin_page": _("LinkedIn Page"), "youtube_channel": _("YouTube Channel"), "website": _("Website"), + "terms_of_service_confirmation": _( + "By enrolling, you agree to our Terms of Service." + ), }, } context["organization_name"] = organization.name @@ -270,6 +278,7 @@ def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] "organization": organization_data.model_dump(), "enrollApiUrl": f"{settings.DJANGO_EMAIL_LEARNING['SITE_BASE_URL']}{enroll_api_path}", "direction": "rtl" if lang_info["bidi"] else "ltr", + "termsOfServiceUrl": get_terms_of_service_url(), "localeMessages": { "enroll_now": _("Enroll Now"), "enrol_for_course": _("Enroll for COURSE_NAME"), @@ -294,6 +303,9 @@ def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] "continue": _("Continue"), "target_audience_title": _("Who is this course for?"), "external_references_title": _("External References"), + "terms_of_service_confirmation": _( + "By enrolling, you agree to our Terms of Service." + ), }, } context["course_title"] = course.title diff --git a/django_service/settings.py b/django_service/settings.py index cbc3773..a064bea 100644 --- a/django_service/settings.py +++ b/django_service/settings.py @@ -107,6 +107,7 @@ "SITE_BASE_URL": "http://localhost:8000", "ENCRYPTION_SECRET_KEY": "your-very-secure-and-random-key", "FROM_EMAIL": os.environ.get("FROM_EMAIL", "webmaster@localhost"), + "TERMS_OF_SERVICE_URL": "https://www.example.com/terms", "AI": { "OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY"), "TEXT_EDITING_MODEL": LanguageModel.GPT_4O_MINI.model_name, diff --git a/docs/source/installation.rst b/docs/source/installation.rst index e53964e..c2c04a0 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -138,6 +138,18 @@ The default email address for outgoing course emails. If not specified, falls ba 'FROM_EMAIL': 'courses@yourdomain.com', } +**TERMS_OF_SERVICE_URL** + +Optional link to your terms of service. When provided, this link is displayed in the public enrollment dialog so learners can review your terms before submitting their email address. + +.. code-block:: python + + DJANGO_EMAIL_LEARNING = { + 'SITE_BASE_URL': 'https://yourdomain.com', + 'ENCRYPTION_SECRET_KEY': 'your-very-long-random-string', + 'TERMS_OF_SERVICE_URL': 'https://yourdomain.com/terms', + } + **SIDEBAR.CUSTOM_COMPONENT** Optional configuration for injecting a custom component in the platform sidebar. diff --git a/frontend/public/components/EnrollmentForm.jsx b/frontend/public/components/EnrollmentForm.jsx index b3885cc..f6d9734 100644 --- a/frontend/public/components/EnrollmentForm.jsx +++ b/frontend/public/components/EnrollmentForm.jsx @@ -12,7 +12,7 @@ const EnrollmentForm = ({course_title, course_slug, organization_id, endpoint, o const [isProcessing, setIsProcessing] = React.useState(false); const [csrfToken, setCsrfToken] = React.useState(getCookie('csrftoken')); const [showReloadDialog, setShowReloadDialog] = React.useState(false); - const { localeMessages } = useAppContext(); + const { localeMessages, termsOfServiceUrl } = useAppContext(); useEffect(() => { if (!csrfToken) { @@ -95,8 +95,27 @@ const EnrollmentForm = ({course_title, course_slug, organization_id, endpoint, o enroll(); } }} /> + {termsOfServiceUrl && ( + + + + )} - + diff --git a/tests/public/test_views/test_public_course_view.py b/tests/public/test_views/test_public_course_view.py index fcb341a..500bd41 100644 --- a/tests/public/test_views/test_public_course_view.py +++ b/tests/public/test_views/test_public_course_view.py @@ -1,8 +1,22 @@ import json +from django.conf import settings +from django.test import override_settings from django.urls import reverse from django_email_learning.models import ExternalReference +import pytest + + +@pytest.fixture +def tos_settings(request): + with override_settings( + DJANGO_EMAIL_LEARNING={ + **settings.DJANGO_EMAIL_LEARNING, + "TERMS_OF_SERVICE_URL": request.param, + } + ): + yield settings.DJANGO_EMAIL_LEARNING["TERMS_OF_SERVICE_URL"] def test_course_view_anonymous_client( @@ -65,3 +79,24 @@ def test_course_view_non_existent_course(db, anonymous_client): ) response = anonymous_client.get(url) assert response.status_code == 404 + + +@pytest.mark.parametrize( + "tos_settings", + ["https://example.com/terms", None], + indirect=True, +) +def test_course_view_includes_terms_of_service_url_when_configured( + db, anonymous_client, course, tos_settings +): + course.enabled = True + course.save() + + url = reverse( + "django_email_learning:public:course_view", + kwargs={"organization_id": 1, "course_slug": course.slug}, + ) + response = anonymous_client.get(url) + + assert response.status_code == 200 + assert response.context["appContext"]["termsOfServiceUrl"] == tos_settings