Skip to content

Commit ef93c81

Browse files
committed
feat: #26 Public Org View - PR1
1 parent 01c1465 commit ef93c81

21 files changed

Lines changed: 327 additions & 0 deletions

File tree

django_email_learning/public/__init__.py

Whitespace-only changes.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from pydantic import BaseModel, field_serializer
2+
3+
4+
class PublicCourseSerializer(BaseModel):
5+
id: int
6+
title: str
7+
slug: str
8+
description: str | None = None
9+
imap_email: str | None = None
10+
11+
@field_serializer("description")
12+
def serialize_description_with_br(self, description: str | None) -> str | None:
13+
if description is not None:
14+
return description.replace("\n", "<br />")
15+
return description
16+
17+
18+
class OrganizationSerializer(BaseModel):
19+
id: int
20+
name: str
21+
logo_url: str | None = None
22+
description: str | None = None
23+
courses: list[PublicCourseSerializer] = []
24+
25+
@field_serializer("description")
26+
def serialize_description_with_br(self, description: str | None) -> str | None:
27+
if description is not None:
28+
return description.replace("\n", "<br>")
29+
return description
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.urls import path
2+
from django_email_learning.public.views import OrganizationView
3+
4+
app_name = "email_learning"
5+
6+
urlpatterns = [
7+
path(
8+
"organizations/<int:organization_id>/",
9+
OrganizationView.as_view(),
10+
name="organization_view",
11+
),
12+
]
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from django.views.generic import TemplateView
2+
from django.db.models import Prefetch
3+
from django_email_learning.models import Organization, Course
4+
from django.http import Http404
5+
from django_email_learning.public.serializers import (
6+
OrganizationSerializer,
7+
PublicCourseSerializer,
8+
)
9+
10+
11+
class OrganizationView(TemplateView):
12+
template_name = "public/organization.html"
13+
14+
def get_context_data(self, **kwargs) -> dict: # type: ignore[no-untyped-def]
15+
organization_id: int = kwargs.get("organization_id") # type: ignore[assignment]
16+
context = super().get_context_data(**kwargs)
17+
# Add any additional context if needed
18+
organization_details = Organization.objects.filter(
19+
id=organization_id
20+
).prefetch_related(
21+
Prefetch(
22+
"course_set",
23+
queryset=Course.objects.filter(enabled=True),
24+
to_attr="courses",
25+
),
26+
)
27+
if organization_details.exists():
28+
organization = organization_details.first()
29+
if not organization:
30+
raise Http404("Organization does not exist")
31+
courses = []
32+
for course in organization.courses:
33+
course_data = PublicCourseSerializer(
34+
id=course.id,
35+
title=course.title,
36+
slug=course.slug,
37+
description=course.description,
38+
imap_email=course.imap_connection.email
39+
if course.imap_connection
40+
else None,
41+
)
42+
courses.append(course_data)
43+
organization_data = OrganizationSerializer(
44+
id=organization.id,
45+
name=organization.name,
46+
logo_url=organization.logo.url if organization.logo else None,
47+
description=organization.description,
48+
courses=courses,
49+
)
50+
context["organization_json"] = organization_data.model_dump_json()
51+
context["organization"] = organization_data.model_dump()
52+
context["page_title"] = organization.name
53+
return context
54+
55+
# If organization not found, raise 404
56+
raise Http404("Organization does not exist")
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{% load django_vite %}
2+
{% load static %}
3+
<!doctype html>
4+
<html lang="en">
5+
<head>
6+
<script>
7+
const error_message = "{{ error_message }}";
8+
const ref = "{{ ref }}";
9+
</script>
10+
{% vite_hmr_client %}
11+
{% vite_react_refresh %}
12+
{% block head_script %}{% endblock %}
13+
<meta charset="UTF-8" />
14+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
15+
<link rel="icon" type="image/png" href="{% static 'logo.png' %}" />
16+
<title>{% block title %}{{ page_title }}{% endblock %}</title>
17+
</head>
18+
<body>
19+
<div id="root">
20+
</div>
21+
</body>
22+
</html>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "public/base.html" %}
2+
{% load django_vite %}
3+
{% block head_script %}
4+
<script>
5+
const organization = {{ organization_json | safe }};
6+
</script>
7+
{% vite_asset 'public/organization/Organization.jsx' %}
8+
{% endblock %}

django_email_learning/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from django_email_learning.platform import urls as platform_urls
44
from django_email_learning.personalised.api import urls as personalised_api_urls
55
from django_email_learning.personalised import urls as personalised_urls
6+
from django_email_learning.public import urls as public_urls
67

78
app_name = "django_email_learning"
89

@@ -13,5 +14,6 @@
1314
include(personalised_api_urls, namespace="api_personalised"),
1415
),
1516
path("platform/", include(platform_urls, namespace="platform")),
17+
path("public/", include(public_urls, namespace="public")),
1618
path("my/", include(personalised_urls, namespace="personalised")),
1719
]

django_service/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,6 @@
185185
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
186186

187187
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
188+
189+
MEDIA_URL = "/media/"
190+
MEDIA_ROOT = os.path.join(BASE_DIR, "media")

django_service/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@
4141
# Serve static files during development
4242
if settings.DEBUG:
4343
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
44+
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
4445
# Uncomment the line below if you have media files (user uploads)
4546
# urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
import { Alert, Box, Button, Typography } from '@mui/material';
3+
import RequiredTextField from '../../src/components/RequiredTextField.jsx';
4+
5+
const EnrollmentForm = ({course_title, course_slug, onCancel}) => {
6+
7+
const emailRef = React.useRef('');
8+
const [errorMessage, setErrorMessage] = React.useState('');
9+
10+
const validateForm = () => {
11+
const email = emailRef.current.value;
12+
if (!email) {
13+
setErrorMessage('Email is required');
14+
return false;
15+
}
16+
// Simple email regex for validation
17+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
18+
if (!emailRegex.test(email)) {
19+
setErrorMessage('Please enter a valid email address');
20+
return false;
21+
}
22+
setErrorMessage('');
23+
return true;
24+
}
25+
26+
const enroll = () => {
27+
if (validateForm()) {
28+
// TODO: call the public API to enroll the user when the endpoint is ready
29+
console.log('Enrolling with email:', emailRef.current.value, 'for course:', course_slug);
30+
}
31+
}
32+
33+
return (<Box sx={{padding: 4}}>
34+
<Typography variant='h3'> Enroll for {course_title}</Typography>
35+
{errorMessage && <Alert severity="error" sx={{ mt: 2 }}>{errorMessage}</Alert>}
36+
<RequiredTextField label="email" name="email" type="email" fullWidth margin="normal" inputRef={emailRef} onKeyDown={(e) => {
37+
if (e.key === 'Enter') {
38+
enroll();
39+
}
40+
}} />
41+
<input type="hidden" name="course_slug" value={course_slug} />
42+
<Box sx={{ mt: 2, textAlign: 'right' }}>
43+
<Button variant="outlined" sx={{ mr: 1 }} onClick={onCancel}>
44+
Cancel
45+
</Button>
46+
<Button variant="contained" color="primary" type="submit" onClick={enroll}>
47+
Submit
48+
</Button>
49+
</Box>
50+
</Box>);
51+
52+
};
53+
54+
export default EnrollmentForm;

0 commit comments

Comments
 (0)