diff --git a/django_email_learning/platform/urls.py b/django_email_learning/platform/urls.py index b9679dc..552eb7d 100644 --- a/django_email_learning/platform/urls.py +++ b/django_email_learning/platform/urls.py @@ -1,11 +1,12 @@ from django.urls import path from django.views.generic import RedirectView -from django_email_learning.platform.views import Courses, Organizations +from django_email_learning.platform.views import CourseView, Courses, Organizations app_name = "email_learning" urlpatterns = [ path("courses/", Courses.as_view(), name="courses_view"), + path("courses//", CourseView.as_view(), name="course_detail_view"), path("organizations/", Organizations.as_view(), name="organizations_view"), path( "", diff --git a/django_email_learning/platform/views.py b/django_email_learning/platform/views.py index a02d480..99b28d1 100644 --- a/django_email_learning/platform/views.py +++ b/django_email_learning/platform/views.py @@ -3,8 +3,11 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from django.urls import reverse -from django_email_learning.models import Organization -from django_email_learning.decorators import is_platform_admin +from django_email_learning.models import Organization, OrganizationUser, Course +from django_email_learning.decorators import ( + is_platform_admin, + is_an_organization_member, +) from typing import Dict, Any @@ -19,10 +22,19 @@ def get_context_data(self, **kwargs) -> Dict[str, Any]: # type: ignore[no-untyp def get_shared_context(self) -> Dict[str, Any]: """Get shared context for all platform views""" + active_organization_id = self.get_or_set_active_organization() + if self.request.user.is_superuser: + role = "admin" + else: + role = OrganizationUser.objects.get( # type: ignore[misc] + user=self.request.user, + organization_id=active_organization_id, + ).role return { "api_base_url": reverse("django_email_learning:api:root")[:-1], "platform_base_url": reverse("django_email_learning:platform:root")[:-1], - "active_organization_id": self.get_or_set_active_organization(), + "active_organization_id": active_organization_id, + "user_role": role, "is_platform_admin": ( self.request.user.is_superuser or ( @@ -62,6 +74,18 @@ def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] return context +@method_decorator(login_required, name="dispatch") +@method_decorator(is_an_organization_member(), name="dispatch") +class CourseView(BasePlatformView): + template_name = "platform/course.html" + + def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] + context = super().get_context_data(**kwargs) + course = Course.objects.get(pk=self.kwargs["course_id"]) + context["page_title"] = course.title + return context + + @method_decorator(login_required, name="dispatch") @method_decorator(is_platform_admin(), name="dispatch") class Organizations(BasePlatformView): diff --git a/django_email_learning/templates/platform/base.html b/django_email_learning/templates/platform/base.html index c89108d..3ca58c9 100644 --- a/django_email_learning/templates/platform/base.html +++ b/django_email_learning/templates/platform/base.html @@ -9,6 +9,7 @@ localStorage.setItem('activeOrganizationId', '{{ active_organization_id }}'); localStorage.setItem('apiBaseUrl', '{{ api_base_url }}'); localStorage.setItem('platformBaseUrl', '{{ platform_base_url }}'); + localStorage.setItem('userRole', '{{ user_role }}'); localStorage.setItem('isPlatformAdmin', {{ is_platform_admin|yesno:"true,false" }}); diff --git a/django_email_learning/templates/platform/course.html b/django_email_learning/templates/platform/course.html new file mode 100644 index 0000000..7de95f7 --- /dev/null +++ b/django_email_learning/templates/platform/course.html @@ -0,0 +1,5 @@ +{% extends "platform/base.html" %} +{% load django_vite %} +{% block extra_head %} + +{% endblock %} diff --git a/frontend/courses/Courses.jsx b/frontend/courses/Courses.jsx index a2ceb51..af97f18 100644 --- a/frontend/courses/Courses.jsx +++ b/frontend/courses/Courses.jsx @@ -20,6 +20,7 @@ function Courses() { const [organizationId, setOrganizationId] = useState(null); const [queryParameters, setQueryParameters] = useState(""); const apiBaseUrl = localStorage.getItem('apiBaseUrl'); + const platformBaseUrl = localStorage.getItem('platformBaseUrl'); const renderCourses = () => { if (!organizationId) { @@ -130,7 +131,7 @@ function Courses() { sx={{ '&:last-child td, &:last-child th': { border: 0 } }} > - {course.title} + {course.title} {course.slug} diff --git a/tests/api/conftest.py b/tests/conftest.py similarity index 55% rename from tests/api/conftest.py rename to tests/conftest.py index a25830d..b696a3f 100644 --- a/tests/api/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,16 @@ from django.contrib.auth.models import User from django_email_learning.models import OrganizationUser from django.test import Client +from django_email_learning.models import ( + ImapConnection, + Quiz, + Lesson, + Course, + BlockedEmail, + Learner, + Enrollment, + CourseContent, +) import pytest @@ -91,3 +101,78 @@ def _get_client(role_name): return role_map.get(role_name) return _get_client(request.param) + + +@pytest.fixture() +def imap_connection(db) -> ImapConnection: + connection = ImapConnection( + server="IMAP.example.com", + port=993, + email="user@example.com", + password="my_secret_password", + organization_id=1, + ) + connection.save() + return connection + + +@pytest.fixture() +def quiz(db) -> Quiz: + quiz = Quiz(title="Sample Quiz", required_score=70) + quiz.save() + return quiz + + +@pytest.fixture() +def lesson(db) -> Lesson: + lesson = Lesson(title="Sample Lesson", content="Lesson Content", is_published=True) + lesson.save() + return lesson + + +@pytest.fixture() +def course(db, imap_connection) -> Course: + course = Course( + title="Sample Course", + slug="sample-course", + imap_connection=imap_connection, + organization_id=1, + ) + course.save() + return course + + +@pytest.fixture() +def blocked_email(db) -> BlockedEmail: + blocked_email = BlockedEmail(email="blacklisted@email.com") + blocked_email.save() + return blocked_email + + +@pytest.fixture() +def learner(db) -> Learner: + learner = Learner(email="user@example.com") + learner.save() + return learner + + +@pytest.fixture() +def enrollment(db, learner, course) -> Enrollment: + enrollment = Enrollment.objects.create(learner=learner, course=course) + return enrollment + + +@pytest.fixture +def course_lesson_content(db, course, lesson) -> CourseContent: + content = CourseContent.objects.create( + course=course, priority=1, type="lesson", lesson=lesson, waiting_period=10 + ) + return content + + +@pytest.fixture +def course_quiz_content(db, course, quiz) -> CourseContent: + content = CourseContent.objects.create( + course=course, priority=2, type="quiz", quiz=quiz, waiting_period=5 + ) + return content diff --git a/tests/platform/__init__.py b/tests/platform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/platform/test_views/test_course_details_view.py b/tests/platform/test_views/test_course_details_view.py new file mode 100644 index 0000000..8e0c728 --- /dev/null +++ b/tests/platform/test_views/test_course_details_view.py @@ -0,0 +1,42 @@ +from django.urls import reverse +import pytest + + +def get_url(course_id: int = 1) -> str: + return reverse( + "django_email_learning:platform:course_detail_view", + kwargs={"course_id": course_id}, + ) + + +def test_anonymous_user_redirects_to_login(anonymous_client): + response = anonymous_client.get(get_url()) + assert response.status_code == 302 + assert "/login/" in response.url + + +@pytest.mark.parametrize( + "client,role", + [ + ("superadmin", "admin"), + ("platform_admin", "admin"), + ("editor", "editor"), + ("viewer", "viewer"), + ], + indirect=["client"], +) +def test_authenticated_user_access_course_detail_view(client, role, course): + response = client.get(get_url(course.id)) + assert response.status_code == 200 + assert response.context["user_role"] == role + + +def test_context_values(superadmin_client, course): + response = superadmin_client.get(get_url(course.id)) + assert response.status_code == 200 + assert "api_base_url" in response.context + assert "platform_base_url" in response.context + assert "active_organization_id" in response.context + assert "user_role" in response.context + assert response.context["page_title"] == course.title + assert response.context["is_platform_admin"] is True diff --git a/tests/platform/test_views/test_courses_view.py b/tests/platform/test_views/test_courses_view.py new file mode 100644 index 0000000..5792b43 --- /dev/null +++ b/tests/platform/test_views/test_courses_view.py @@ -0,0 +1,39 @@ +from django.urls import reverse +import pytest + + +def get_url() -> str: + return reverse("django_email_learning:platform:courses_view") + + +def test_anonymous_user_redirects_to_login(anonymous_client): + response = anonymous_client.get(get_url()) + assert response.status_code == 302 + assert "/login/" in response.url + + +@pytest.mark.parametrize( + "client,role", + [ + ("superadmin", "admin"), + ("platform_admin", "admin"), + ("editor", "editor"), + ("viewer", "viewer"), + ], + indirect=["client"], +) +def test_authenticated_user_access_courses_view(client, role): + response = client.get(get_url()) + assert response.status_code == 200 + assert response.context["user_role"] == role + + +def test_context_values(superadmin_client): + response = superadmin_client.get(get_url()) + assert response.status_code == 200 + assert "api_base_url" in response.context + assert "platform_base_url" in response.context + assert "active_organization_id" in response.context + assert "user_role" in response.context + assert response.context["page_title"] == "Courses" + assert response.context["is_platform_admin"] is True diff --git a/tests/test_models/conftest.py b/tests/test_models/conftest.py deleted file mode 100644 index 24d6325..0000000 --- a/tests/test_models/conftest.py +++ /dev/null @@ -1,86 +0,0 @@ -from django_email_learning.models import ( - ImapConnection, - Quiz, - Lesson, - Course, - BlockedEmail, - Learner, - Enrollment, - CourseContent, -) -import pytest - - -@pytest.fixture() -def imap_connection(db) -> ImapConnection: - connection = ImapConnection( - server="IMAP.example.com", - port=993, - email="user@example.com", - password="my_secret_password", - organization_id=1, - ) - connection.save() - return connection - - -@pytest.fixture() -def quiz(db) -> Quiz: - quiz = Quiz(title="Sample Quiz", required_score=70) - quiz.save() - return quiz - - -@pytest.fixture() -def lesson(db) -> Lesson: - lesson = Lesson(title="Sample Lesson", content="Lesson Content", is_published=True) - lesson.save() - return lesson - - -@pytest.fixture() -def course(db, imap_connection) -> Course: - course = Course( - title="Sample Course", - slug="sample-course", - imap_connection=imap_connection, - organization_id=1, - ) - course.save() - return course - - -@pytest.fixture() -def blocked_email(db) -> BlockedEmail: - blocked_email = BlockedEmail(email="blacklisted@email.com") - blocked_email.save() - return blocked_email - - -@pytest.fixture() -def learner(db) -> Learner: - learner = Learner(email="user@example.com") - learner.save() - return learner - - -@pytest.fixture() -def enrollment(db, learner, course) -> Enrollment: - enrollment = Enrollment.objects.create(learner=learner, course=course) - return enrollment - - -@pytest.fixture -def course_lesson_content(db, course, lesson) -> CourseContent: - content = CourseContent.objects.create( - course=course, priority=1, type="lesson", lesson=lesson, waiting_period=10 - ) - return content - - -@pytest.fixture -def course_quiz_content(db, course, quiz) -> CourseContent: - content = CourseContent.objects.create( - course=course, priority=2, type="quiz", quiz=quiz, waiting_period=5 - ) - return content