Skip to content

Commit a326870

Browse files
committed
Style/UI improvement
1 parent c470dac commit a326870

30 files changed

Lines changed: 769 additions & 237 deletions

File tree

django_email_learning/models.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,9 @@ def encrypted_value(cls, value: str, salt: str) -> str:
146146

147147
def _encrypt_password(self, password: str) -> str:
148148
if not self.salt:
149-
self.salt = base64.urlsafe_b64encode(uuid.uuid4().bytes).decode().rstrip("=")
149+
self.salt = (
150+
base64.urlsafe_b64encode(uuid.uuid4().bytes).decode().rstrip("=")
151+
)
150152
f = self._fernet(self.salt)
151153
return f.encrypt(password.encode()).decode()
152154

@@ -446,6 +448,15 @@ def save(self, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
446448
self.full_clean()
447449
super().save(*args, **kwargs)
448450

451+
@property
452+
def enrollments_count(self) -> dict[str, int]:
453+
return {
454+
"total": self.enrollment_set.count(),
455+
"completed": self.enrollment_set.filter(
456+
status=EnrollmentStatus.COMPLETED
457+
).count(),
458+
}
459+
449460
class Meta:
450461
unique_together = [["organization", "email"]]
451462

@@ -617,6 +628,23 @@ def schedule_first_content_delivery(self) -> None:
617628
else:
618629
raise ValidationError("No published content available to schedule.")
619630

631+
@property
632+
def progress_percentage(self) -> int:
633+
total_content = self.course.coursecontent_set.filter(is_published=True).count()
634+
if total_content == 0:
635+
return 0
636+
delivered_content = (
637+
ContentDelivery.objects.filter(
638+
enrollment=self,
639+
delivery_schedules__status=DeliveryStatus.DELIVERED,
640+
course_content__is_published=True,
641+
)
642+
.distinct()
643+
.count()
644+
)
645+
progress = int((delivered_content / total_content) * 100)
646+
return progress
647+
620648

621649
class Certificate(models.Model):
622650
enrollment = models.OneToOneField(

django_email_learning/platform/api/serializers.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,12 +485,20 @@ class EnrollmentSummaryResponse(BaseModel):
485485
id: int
486486
course_title: str
487487
status: EnrollmentStatus
488+
progress: int
489+
certificate_url: str | None = None
490+
491+
492+
class EnrollmentsCount(BaseModel):
493+
total: int
494+
completed: int
488495

489496

490497
class LearnerResponse(BaseModel):
491498
model_config = ConfigDict(from_attributes=True)
492499
id: int
493500
email: str
501+
enrollments_count: EnrollmentsCount
494502

495503

496504
class EventType(enum.StrEnum):

django_email_learning/platform/api/views.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from django.conf import settings
1111
from django.core.files.storage import default_storage
1212
from django.utils import timezone
13+
from django.urls import reverse
1314
from django.contrib.auth.models import User
1415
from django.contrib.auth.forms import PasswordResetForm
1516
from datetime import timedelta, datetime
@@ -19,6 +20,7 @@
1920
from django_email_learning.platform.api.pagniated_api_mixin import PaginatedApiMixin
2021
from django_email_learning.models import (
2122
ApiKey,
23+
Certificate,
2224
Course,
2325
CourseContent,
2426
Enrollment,
@@ -686,11 +688,24 @@ def get(self, request, *args, **kwargs) -> JsonResponse: # type: ignore[no-unty
686688
enrollments = Enrollment.objects.filter(learner=learner)
687689
enroolments_list = []
688690
for enrollment in enrollments:
691+
certificate = Certificate.objects.filter(enrollment=enrollment).first()
692+
certificate_url = None
693+
if certificate:
694+
certificate_url = request.build_absolute_uri(
695+
reverse(
696+
"django_email_learning:personalised:certificate",
697+
kwargs={
698+
"certificate_number": certificate.certificate_number
699+
},
700+
)
701+
)
689702
enroolments_list.append(
690703
serializers.EnrollmentSummaryResponse(
691704
id=enrollment.id,
692705
course_title=enrollment.course.title,
693706
status=EnrollmentStatus(enrollment.status),
707+
progress=enrollment.progress_percentage,
708+
certificate_url=certificate_url,
694709
)
695710
)
696711
return JsonResponse(

django_email_learning/platform/views.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,13 +97,15 @@ def get_shared_context(self) -> Dict[str, Any]:
9797
"api_keys": _("API Keys"),
9898
"content_delivery_job": _("Content Delivery Job"),
9999
"last_run": _("Last Run:"),
100+
"never_run": _("This job has never been executed."),
100101
"content_delivery_tooltip": _(
101102
"This job should run on a regular schedule to ensure timely content delivery. Configure a cron job or cloud scheduler to execute it at appropriate intervals, such as every five minutes."
102103
),
103104
}
104105
| self.get_locale_messages(),
105106
},
106107
"activeOrganizationId": active_organization_id,
108+
"favicon": DJANGO_EMAIL_LEARNING_SETTINGS.get("FAVICON"),
107109
}
108110

109111
def get_locale_messages(self) -> Dict[str, str]:
@@ -397,7 +399,7 @@ def get_locale_messages(self) -> Dict[str, str]:
397399
"failed": _("Failed"),
398400
"enrollment_details": _("Enrollment Details"),
399401
"enrollment_id": _("Enrollment ID"),
400-
"unvierfied": _("Unverified"),
402+
"unverified": _("Unverified"),
401403
"active": _("Active"),
402404
"completed": _("Completed"),
403405
"deactivated": _("Deactivated"),

django_email_learning/templates/platform/base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
{% block extra_head %}{% endblock %}
1717
<meta charset="UTF-8" />
1818
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
19-
<link rel="icon" type="image/png" href="{% static 'logo.png' %}" />
19+
<link rel="icon" type="image/png" href="{% if favicon %}{{ favicon }}{% else %}{% static 'logo.png' %}{% endif %}" />
2020
<title>{% block title %}{{ page_title }}{% endblock %}</title>
2121
</head>
2222

django_service/settings.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,6 @@
103103
"SITE_BASE_URL": "http://localhost:8000",
104104
"ENCRYPTION_SECRET_KEY": "your-very-secure-and-random-key",
105105
"FROM_EMAIL": os.environ.get("FROM_EMAIL", "webmaster@localhost"),
106-
"SIDEBAR": {
107-
"CUSTOM_COMPONENT": {
108-
"SCRIPT_URL": "https://cdn.jsdelivr.net/npm/ldrs/dist/auto/helix.js",
109-
"STYLE_URL": None,
110-
"COMPONENT_TAG": "<l-helix />",
111-
}
112-
},
113106
"LOGO": {
114107
"HORIZONTAL_LOCKUP": {
115108
"LIGHT_BACKGROUND": None,
@@ -123,9 +116,9 @@
123116
}
124117

125118

126-
# EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
119+
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
127120

128-
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
121+
# EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
129122
EMAIL_HOST = os.environ.get("EMAIL_HOST")
130123
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", 587))
131124
EMAIL_USE_TLS = True

frontend/personalised/certificate/Certificate.jsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ const CertificateContent = () => {
4949
borderRadius: 3,
5050
position: 'relative',
5151
backgroundImage: (() => {
52-
const band = alpha(theme.palette.secondary.main, 0.22);
53-
const glow = alpha(theme.palette.primary.main, 0.35);
54-
const baseStart = alpha(theme.palette.primary.main, 0.88);
55-
const baseEnd = alpha(theme.palette.secondary.main, 0.88);
52+
const band = alpha(theme.palette.primary.main, 0.22);
53+
const glow = alpha(theme.palette.secondary.main, 0.35);
54+
const baseStart = alpha(theme.palette.secondary.main, 0.88);
55+
const baseEnd = alpha(theme.palette.primary.main, 0.88);
5656
const hatch = alpha(theme.palette.common.white, 0.05);
5757
return [
5858
`linear-gradient(15deg, transparent 0 35%, ${band} 35% 41%, transparent 41% 100%)`,
@@ -77,7 +77,6 @@ const CertificateContent = () => {
7777
padding: "1mm",
7878
borderRadius: 2,
7979
boxSizing: 'border-box',
80-
overflow: 'scroll',
8180
width: '100%',
8281
height: '100%',
8382
}}><Paper
@@ -87,10 +86,10 @@ const CertificateContent = () => {
8786
height: '100%',
8887
display: 'flex',
8988
borderRadius: 2,
90-
border: `2px solid ${alpha(theme.palette.primary.main, 0.5)}`,
89+
border: `2px solid ${alpha(theme.palette.secondary.main, 0.5)}`,
9190
position: 'relative',
9291
backgroundImage: (() => {
93-
const fade = alpha(theme.palette.primary.main, 0.02);
92+
const fade = alpha(theme.palette.secondary.main, 0.02);
9493
return [
9594
`repeating-linear-gradient(135deg, ${fade} 0 1px, transparent 2px 6px)`,
9695
].join(", ");
@@ -105,18 +104,22 @@ const CertificateContent = () => {
105104
right: 16,
106105
height: 6,
107106
borderRadius: 999,
108-
background: `linear-gradient(90deg, ${theme.palette.primary.main} 0%, ${theme.palette.secondary.main} 100%)`,
107+
background: `linear-gradient(90deg, ${theme.palette.secondary.main} 0%, ${theme.palette.primary.main} 100%)`,
109108
opacity: 0.25,
110109
},
111110
flexDirection: 'column',
112111
alignItems: 'center',
113112
justifyContent: 'center',
114113
padding: 8,
115114
boxSizing: 'border-box',
115+
color: alpha(theme.palette.common.black, 0.88),
116+
'& .MuiTypography-root': {
117+
color: alpha(theme.palette.common.black, 0.88),
118+
},
116119
})}
117120
>
118121
{/* Your content here */}
119-
<WorkspacePremiumIcon sx={(theme) => ({ fontSize: 86, color: theme.palette.secondary.main, mb: 2 })} />
122+
<WorkspacePremiumIcon sx={(theme) => ({ fontSize: 86, color: theme.palette.primary.main, mb: 2 })} />
120123
<Typography
121124
variant="h1"
122125
gutterBottom

frontend/personalised/certificate_form/CertificateForm.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const CertificateForm = () => {
5151

5252
return (<Container maxWidth="sm" sx={{ mt: 4 }}>
5353
<Box display="flex" alignItems="center" my={4} justifyContent="center">
54-
<WorkspacePremiumIcon color="primary" sx={{ fontSize: 50, mr: 1 }} />
54+
<WorkspacePremiumIcon color="secondary" sx={{ fontSize: 50, mr: 1 }} />
5555
<Typography variant="h4" component="h1">
5656
{localeMessages['form_title']}
5757
</Typography>
@@ -70,15 +70,15 @@ const CertificateForm = () => {
7070
onChange={(e) => setFullName(e.target.value)}
7171
/>
7272
{error && <Alert severity="error">{error}</Alert>}
73-
<Button type="submit" variant="contained" color="primary" sx={{ mt: 3 }}>
73+
<Button type="submit" variant="contained" color="secondary" sx={{ mt: 3 }}>
7474
{localeMessages['submit']}
7575
</Button>
7676
</Box> : <Alert severity="success" sx={{ mt: 2 }}>{localeMessages['form_submission_success']}</Alert>}
7777
{certificateUrl && (
7878
<Box sx={{ mt: 2 }}>
7979
<Button
8080
variant="contained"
81-
color="primary"
81+
color="secondary"
8282
href={certificateUrl}
8383
target="_blank"
8484
rel="noopener noreferrer"

frontend/platform/course/Course.jsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ function Course() {
4848
.then(data => {
4949
setEnrollmentsCount([
5050
{ label: localeMessages["unverified"], value: data.enrollments_count.unverified, color: theme.palette.indigo[200] },
51-
{ label: localeMessages["active"], value: data.enrollments_count.active, color: theme.palette.secondary.main },
51+
{ label: localeMessages["active"], value: data.enrollments_count.active, color: theme.palette.primary.main },
5252
{ label: localeMessages["deactivated"], value: data.enrollments_count.deactivated, color: theme.palette.grey[300] },
53-
{ label: localeMessages["completed"], value: data.enrollments_count.completed, color: theme.palette.primary.main },
53+
{ label: localeMessages["completed"], value: data.enrollments_count.completed, color: theme.palette.secondary.main },
5454
]);
5555
})
5656
.catch(error => console.error('Error fetching course data:', error));
@@ -201,7 +201,7 @@ function Course() {
201201
showOrganizationSwitcher={false}
202202
>
203203
<Grid size={{xs: 12}} py={2} pl={2}>
204-
<Box p={2} sx={{ border: '1px solid', borderColor: 'grey.300', borderRadius: 1, minHeight: 300 }}>
204+
<Box p={2} sx={{ border: '1px solid', borderColor: 'border.main', backgroundColor: 'background.box', borderRadius: 2, minHeight: 300 }}>
205205
{userRole !== 'viewer' && <><Button variant="contained" startIcon={<DescriptionIcon sx={{ marginLeft: direction == 'rtl' ? 1 : 0 }} />} sx={{ marginBottom: 2 }} onClick={() => {
206206
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><LessonForm
207207
header={localeMessages["new_lesson"]}
@@ -219,7 +219,7 @@ function Course() {
219219
</Box>
220220
<Grid container spacing={2}>
221221
{ enrollmentsCount && <Grid size={{xs: 12, lg: 6}} mt={2} mb={2} >
222-
<Box py={2} sx={{ border: '1px solid', borderColor: 'grey.300', borderRadius: 1, backgroundColor: 'background.main', height: '100%' }}>
222+
<Box py={2} sx={{ border: '1px solid', borderColor: 'border.main', borderRadius: 2, backgroundColor: 'background.box', height: '100%' }}>
223223
<Typography variant="h6" align='center'>{localeMessages["enrollments_distribution"]}</Typography>
224224
<PieChart
225225
height={300}
@@ -249,14 +249,14 @@ function Course() {
249249
</Box>
250250
</Grid> }
251251
{ weeklyStats && <Grid size={{xs: 12, lg: 6}} mt={2} mb={2} >
252-
<Box py={2} sx={{ border: '1px solid', borderColor: 'grey.300', borderRadius: 1, backgroundColor: 'background.main', height: '100%' }}>
252+
<Box py={2} sx={{ border: '1px solid', borderColor: 'border.main', borderRadius: 2, backgroundColor: 'background.box', height: '100%' }}>
253253
<Typography variant="h6" align='center'>{localeMessages["weekly_enrollments"]}</Typography>
254254
<BarChart
255255
margin={{
256256
top: 60,
257257
}}
258258
xAxis={[{data: weeklyStats.map((stat) => stat.date)}]}
259-
series={[{ data: weeklyStats.map((stat) => stat.count), color: theme.palette.primary.main }]}
259+
series={[{ data: weeklyStats.map((stat) => stat.count), color: theme.palette.secondary.main }]}
260260
height={300}
261261
/>
262262
</Box>

frontend/platform/course/components/ContentTable.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { IconButton, Switch, TableContainer, Table, TableHead, TableRow, TableBo
22
import { useState, useEffect } from 'react';
33
import { getCookie } from '../../../src/utils.js';
44
import DeleteIcon from '@mui/icons-material/Delete';
5-
import DragHandleIcon from '@mui/icons-material/DragHandle';
5+
import DragIndicatorIcon from '@mui/icons-material/DragIndicator';
66
import { useAppContext } from '../../../src/render.jsx';
77

88

@@ -129,12 +129,12 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
129129
eventHandler(event);
130130
}
131131
}}>
132-
{ userRole !== 'viewer' && <TableCell align={direction == 'rtl' ? 'right' : 'left'} sx={{ cursor: 'grab', width: '40px', padding: '8px 0', textAlign: 'center' }}><DragHandleIcon
132+
{ userRole !== 'viewer' && <TableCell align={direction == 'rtl' ? 'right' : 'left'} sx={{ cursor: 'grab', width: '40px', padding: '8px 0', textAlign: 'center' }}><DragIndicatorIcon fontSize="small"
133133
onMouseDown={() => startDrag(content.id)}
134134
/></TableCell>}
135135
<TableCell align={direction == 'rtl' ? 'right' : 'left'}><Typography
136136
onClick={() => {let event = {type: 'content_clicked', content_id: content.id}; eventHandler(event);}}
137-
color='primary.dark' sx={{ cursor: 'pointer'}}>{content.title}</Typography></TableCell>
137+
color='secondary.dark' sx={{ cursor: 'pointer'}}>{content.title}</Typography></TableCell>
138138
<TableCell align={direction == 'rtl' ? 'right' : 'left'}>{formatPeriod(content.waiting_period)}</TableCell>
139139
<TableCell align={direction == 'rtl' ? 'right' : 'left'}>{localeMessages[content.type]}</TableCell>
140140
<TableCell align={direction == 'rtl' ? 'right' : 'left'}><Switch checked={content.is_published} onChange={() => TogglePublishContent(content.id, !content.is_published)} disabled={userRole == 'viewer'} /></TableCell>

0 commit comments

Comments
 (0)