diff --git a/django_email_learning/public/views.py b/django_email_learning/public/views.py index fe0f2470..1ad1943f 100644 --- a/django_email_learning/public/views.py +++ b/django_email_learning/public/views.py @@ -2,6 +2,8 @@ from django.db.models import Prefetch from django_email_learning.models import Organization, Course from django.http import Http404 +from django.urls import reverse +from django.conf import settings from django_email_learning.public.serializers import ( OrganizationSerializer, PublicCourseSerializer, @@ -47,6 +49,10 @@ def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def] description=organization.description, courses=courses, ) + enroll_api_path = reverse("django_email_learning:api_public:enroll") + context[ + "enroll_api_url" + ] = f"{settings.DJANGO_EMAIL_LEARNING['SITE_BASE_URL']}{enroll_api_path}" context["organization_json"] = organization_data.model_dump_json() context["organization"] = organization_data.model_dump() context["page_title"] = organization.name diff --git a/django_email_learning/templates/public/organization.html b/django_email_learning/templates/public/organization.html index 8a76b2ee..692bb9b1 100644 --- a/django_email_learning/templates/public/organization.html +++ b/django_email_learning/templates/public/organization.html @@ -3,6 +3,7 @@ {% block head_script %} {% vite_asset 'public/organization/Organization.jsx' %} {% endblock %} diff --git a/frontend/public/components/EnrollmentForm.jsx b/frontend/public/components/EnrollmentForm.jsx index a19ca9fd..84f2c8d2 100644 --- a/frontend/public/components/EnrollmentForm.jsx +++ b/frontend/public/components/EnrollmentForm.jsx @@ -1,11 +1,14 @@ import React from 'react'; -import { Alert, Box, Button, Typography } from '@mui/material'; +import { Alert, Box, Button, CircularProgress, Typography } from '@mui/material'; import RequiredTextField from '../../src/components/RequiredTextField.jsx'; +import { getCookie } from '../../src/utils.js'; -const EnrollmentForm = ({course_title, course_slug, onCancel}) => { + +const EnrollmentForm = ({course_title, course_slug, organization_id, endpoint, onCancle, onComplete}) => { const emailRef = React.useRef(''); const [errorMessage, setErrorMessage] = React.useState(''); + const [isProcessing, setIsProcessing] = React.useState(false); const validateForm = () => { const email = emailRef.current.value; @@ -25,13 +28,40 @@ const EnrollmentForm = ({course_title, course_slug, onCancel}) => { const enroll = () => { if (validateForm()) { - // TODO: call the public API to enroll the user when the endpoint is ready - console.log('Enrolling with email:', emailRef.current.value, 'for course:', course_slug); + setIsProcessing(true); + fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken'), + }, + body: JSON.stringify({ + email: emailRef.current.value, + course_slug: course_slug, + organization_id: organization_id, + }), + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + // Handle success + setIsProcessing(false); + onComplete(); + }) + .catch(error => { + setIsProcessing(false); + setErrorMessage('Enrollment failed. Please try again.'); + }); } } - return ( + return ( Enroll for {course_title} + { isProcessing ? : <> {errorMessage && {errorMessage}} { if (e.key === 'Enter') { @@ -40,13 +70,14 @@ const EnrollmentForm = ({course_title, course_slug, onCancel}) => { }} /> - + } ); }; diff --git a/frontend/public/organization/Organization.jsx b/frontend/public/organization/Organization.jsx index 3732bb30..8fb38336 100644 --- a/frontend/public/organization/Organization.jsx +++ b/frontend/public/organization/Organization.jsx @@ -1,21 +1,42 @@ -import { useState } from 'react' +import { useState, useEffect } from 'react' import render from '../../src/render.jsx'; import Layout from '../components/Layout.jsx'; import EnrollmentForm from '../components/EnrollmentForm.jsx'; -import { Box, Button, Dialog, Grid, Typography } from '@mui/material'; +import { Alert, Box, Button, Dialog, Grid, Typography } from '@mui/material'; function Organization() { const [displayModal, setDisplayModal] = useState(false); const [modalContent, setModalContent] = useState(null); + const [courses, setCourses] = useState([]); + + useEffect(() => { + for (let course of organization.courses) { + course.enrolled = false; + } + setCourses(organization.courses); + }, []); const showModalForCourse = (course) => { // Logic to show modal for specific course - setModalContent( {setDisplayModal(false); setModalContent(null);}} />); + setModalContent( {setDisplayModal(false); setModalContent(null);}} onComplete={() => completeEnrollment(course)} />); setDisplayModal(true); } + const completeEnrollment = (course) => { + setDisplayModal(false); + setModalContent(null); + // Disable the enrolled course button + let updatedCourses = courses.map(c => { + if (c.id === course.id) { + return { ...c, enrolled: true }; + } + return c; + }); + setCourses(updatedCourses); + } + return { organization.logo_url && @@ -27,14 +48,15 @@ function Organization() { Courses: { organization.courses.length > 0 ? ( - { organization.courses.map((course) => ( + { courses.map((course) => ( {course.title} - + { course.enrolled && You are enrolled in this course. } ))} diff --git a/tests/public/test_views/test_public_organization_view.py b/tests/public/test_views/test_public_organization_view.py index 8dfe55e4..66ea8753 100644 --- a/tests/public/test_views/test_public_organization_view.py +++ b/tests/public/test_views/test_public_organization_view.py @@ -9,6 +9,7 @@ def test_organization_view_anonymous_client(db, anonymous_client): assert response.status_code == 200 assert response.context["organization"]["id"] == 1 assert response.context["page_title"] == response.context["organization"]["name"] + assert response.context["enroll_api_url"].startswith("http") assert "organization_json" in response.context # No course added yet, so courses list should be empty