Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions django_email_learning/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Answer,
CourseContent,
Organization,
OrganizationUser,
BlockedEmail,
)

Expand Down Expand Up @@ -91,3 +92,4 @@ def get_content_title(self, obj: CourseContent) -> str | None:
admin.site.register(Answer)
admin.site.register(Organization)
admin.site.register(BlockedEmail)
admin.site.register(OrganizationUser)
Empty file.
7 changes: 7 additions & 0 deletions django_email_learning/ports/email_sender_protocol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from typing import Protocol
from django.core.mail import EmailMultiAlternatives


class EmailSenderProtocol(Protocol):
def send_email(self, email: EmailMultiAlternatives) -> None:
...
Empty file.
Empty file.
33 changes: 33 additions & 0 deletions django_email_learning/services/deafults/email_sender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import logging
from django_email_learning.ports.email_sender_protocol import EmailSenderProtocol
from django.core.mail import EmailMultiAlternatives

logger = logging.getLogger(__name__)


class DjangoEmailSender(EmailSenderProtocol):
def _mask_email(self, email_address: str) -> str:
"""Mask email address for logging privacy."""
try:
username, domain = email_address.split("@")
masked_username = username[0] + "***"
return f"{masked_username}@{domain}"
except ValueError:
return "***@***"

def _mask_recipients(self, recipients: list[str]) -> str:
"""Mask all recipient email addresses for logging."""
if not recipients:
return "no recipients"
masked = [self._mask_email(recipient) for recipient in recipients]
return ", ".join(masked)

def send_email(self, email: EmailMultiAlternatives) -> None:
masked_recipients = self._mask_recipients(email.to)
try:
logger.info(f"Sending email to {masked_recipients}")
email.send()
logger.info(f"Email sent successfully to {masked_recipients}")
except Exception as e:
logger.error(f"Failed to send email to {masked_recipients}: {str(e)}")
raise
5 changes: 3 additions & 2 deletions frontend/course/Course.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function Course() {
const [lessonCache, setLessonCache] = useState("")
const [contentLoaded, setContentLoaded] = useState(false)

const userRole = localStorage.getItem('userRole');
const apiBaseUrl = localStorage.getItem('apiBaseUrl');
const organizationId = localStorage.getItem('activeOrganizationId');

Expand Down Expand Up @@ -120,7 +121,7 @@ function Course() {
>
<Grid size={{xs: 12, md: 9}} py={2} pl={2}>
<Box p={2} sx={{ border: '1px solid', borderColor: 'grey.300', borderRadius: 1, minHeight: 300 }}>
<Button variant="contained" startIcon={<DescriptionIcon />} sx={{ marginBottom: 2 }} onClick={() => {
{userRole !== 'viewer' && <><Button variant="contained" startIcon={<DescriptionIcon />} sx={{ marginBottom: 2 }} onClick={() => {
setDialogContent(<LessonForm
header="New Lesson"
initialContent={lessonCache}
Expand All @@ -134,7 +135,7 @@ function Course() {
cancelCallback={() => setDialogOpen(false)}
successCallback={resetDialog}
courseId={course_id} />);
setDialogOpen(true);}}>Add a Quiz</Button>
setDialogOpen(true);}}>Add a Quiz</Button></> }
<ContentTable courseId={course_id} loaded={contentLoaded} eventHandler={(event) => tableEventHandler(event)} />
</Box>
</Grid>
Expand Down
10 changes: 6 additions & 4 deletions frontend/course/components/ContentTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
const apiBaseUrl = localStorage.getItem('apiBaseUrl');
const organizationId = localStorage.getItem('activeOrganizationId');

const userRole = localStorage.getItem('userRole');

const formatPeriod = (period) => {
if (!period) {
return "";
Expand Down Expand Up @@ -97,7 +99,7 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
<TableCell>Waiting time</TableCell>
<TableCell>type</TableCell>
<TableCell>Published</TableCell>
<TableCell align='right'>Actions</TableCell>
{userRole !== 'viewer' && <TableCell align='right'>Actions</TableCell>}
</TableRow>
</TableHead>
<TableBody>
Expand All @@ -108,12 +110,12 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
color='primary.dark' sx={{ cursor: 'pointer'}}>{content.title}</Typography></TableCell>
<TableCell>{formatPeriod(content.waiting_period)}</TableCell>
<TableCell>{content.type}</TableCell>
<TableCell><Switch defaultChecked={content.is_published} onChange={() => TogglePublishContent(content.id, !content.is_published)} /></TableCell>
<TableCell align='right'>
<TableCell><Switch defaultChecked={content.is_published} onChange={() => TogglePublishContent(content.id, !content.is_published)} disabled={userRole == 'viewer'} /></TableCell>
{userRole !== 'viewer' && <TableCell align='right'>
<IconButton aria-label="delete" onClick={() => deleteContent(content.id)}>
<DeleteIcon />
</IconButton>
</TableCell>
</TableCell>}
</TableRow>
))}
</TableBody>
Expand Down
14 changes: 8 additions & 6 deletions frontend/course/components/LessonForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can
const [contentHelperText, setContentHelperText] = useState("");
const [errorMessage, setErrorMessage] = useState("");

const userRole = localStorage.getItem('userRole');
const apiBaseUrl = localStorage.getItem('apiBaseUrl');
const orgId = localStorage.getItem('activeOrganizationId');

Expand Down Expand Up @@ -126,9 +127,9 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can
{errorMessage}
</Alert>
)}
<RequiredTextField value={title} label="Lesson Title" name="lesson_title" sx={{ width: '100%' }} onChange={(e) => setTitle(e.target.value)} helperText={titleHelperText}/>
<RequiredTextField value={title} label="Lesson Title" name="lesson_title" sx={{ width: '100%' }} onChange={(e) => setTitle(e.target.value)} helperText={titleHelperText} disabled={userRole === 'viewer'} />
<Box sx={{ my: 2 }}>
<ContentEditor initialContent={content} contentUpdateCallback={handleContentChange}/>
<ContentEditor initialContent={content} contentUpdateCallback={handleContentChange} disabled={userRole === 'viewer'} />
<Typography color="errorText.main" sx={{ marginTop: 1, fontSize: '0.75rem' }}>
{contentHelperText}
</Typography>
Expand All @@ -144,19 +145,20 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can
onChange={(e) => setWaitingPeriod(e.target.value)}
sx={{ width: '200px', mr: 2 }}
inputProps={{ min: 1 }}
disabled={userRole === 'viewer'}
/>
<Select size="small" value={waitingPeriodUnit} onChange={(e) => setWaitingPeriodUnit(e.target.value)} name="waiting_period_unit" sx={{ width: '150px' }}>
<Select size="small" value={waitingPeriodUnit} onChange={(e) => setWaitingPeriodUnit(e.target.value)} name="waiting_period_unit" sx={{ width: '150px' }} disabled={userRole === 'viewer'}>
<MenuItem value="days">Days</MenuItem>
<MenuItem value="hours">Hours</MenuItem>
</Select>
</Tooltip>
<Box mt={2} textAlign="right">
<Button variant="outlined" sx={{ mr: 1 }} onClick={cancel}>
Cancel
Back
</Button>
<Button type="submit" variant="contained" onClick={() => {if(!lessonId) { addLesson(); } else { updateLesson(); }}}>
{userRole !== 'viewer' && <Button type="submit" variant="contained" onClick={() => {if(!lessonId) { addLesson(); } else { updateLesson(); }}}>
Save Lesson
</Button>
</Button>}
</Box>
</Box>
);
Expand Down
18 changes: 10 additions & 8 deletions frontend/course/components/QuestionForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ const QuestionForm = ({question, index, eventHandler}) => {
const [addingOption, setAddingOption] = useState(false);
const optionInputRef = useRef(null);

const userRole = localStorage.getItem('userRole');

const editQuestion = () => {
if (editMode && questionText.trim() === '') {
if (editMode && questionText.trim() === '' || userRole === 'viewer') {
return;
}
triggerUpdateEvent();
Expand Down Expand Up @@ -61,7 +63,7 @@ const QuestionForm = ({question, index, eventHandler}) => {
<Box key={index} sx={{ mb: 1, p: 2, border: '1px solid', borderColor: 'grey.300', borderRadius: 1 }}>
<Grid container spacing={2} alignItems="center">
<Grid size={{ xs: 12, md: 9 }}>
<EditIcon sx={{borderRadius: "50%", display: "inline-block", float: "left", mr: 1, fontSize: "0.9rem", border: 1, borderColor: "grey.200", color: "grey.400", padding: "4px", cursor: "pointer", ':hover': { backgroundColor: "primary.main", color: "white", borderColor: "primary.main" } }} onClick={editQuestion}/>
{userRole !== 'viewer' && <EditIcon sx={{borderRadius: "50%", display: "inline-block", float: "left", mr: 1, fontSize: "0.9rem", border: 1, borderColor: "grey.200", color: "grey.400", padding: "4px", cursor: "pointer", ':hover': { backgroundColor: "primary.main", color: "white", borderColor: "primary.main" } }} onClick={editQuestion}/>}
{!editMode ? (
<Typography onClick={editQuestion}>{index + 1}. {questionText}</Typography>
) : (
Expand All @@ -77,12 +79,12 @@ const QuestionForm = ({question, index, eventHandler}) => {
)}
</Grid>
<Grid size={{ xs: 12, md: 3 }} sx={{ textAlign: 'right' }}>
<Button variant="outlined" color="primary" sx={{ fontSize: '0.75rem', mt: 1 }} onClick={() => setAddingOption(true)} >
{userRole !== 'viewer' && <><Button variant="outlined" color="primary" sx={{ fontSize: '0.75rem', mt: 1 }} onClick={() => setAddingOption(true)} >
<RuleIcon /><Typography variant="button" sx={{ ml: 1, fontSize: '0.75rem' }}>Add Option</Typography>
</Button>
<Button variant="outlined" onClick={deleteCallback} sx={{ ml: 1, mt: 1, fontSize: '0.75rem' }}>
Delete
</Button>
</Button></>}
</Grid>
{addingOption && (<>
<Grid size={{ xs: 9 }} sx={{ display: 'flex', alignItems: 'center' }}>
Expand Down Expand Up @@ -125,7 +127,7 @@ const QuestionForm = ({question, index, eventHandler}) => {
<TableRow>
<TableCell>Options</TableCell>
<TableCell>Correct Answer</TableCell>
<TableCell align='right'>Actions</TableCell>
{userRole !== 'viewer' && <TableCell align='right'>Actions</TableCell>}
</TableRow>
</TableHead>
<TableBody>
Expand All @@ -146,16 +148,16 @@ const QuestionForm = ({question, index, eventHandler}) => {
}}
/>
)}</TableCell>
<TableCell><Switch onChange={(e)=>updateOption(idx, e.target.checked)} checked={option.isCorrect} /></TableCell>
<TableCell align='right'>
<TableCell><Switch onChange={(e)=>updateOption(idx, e.target.checked)} checked={option.isCorrect} disabled={userRole === 'viewer'} /></TableCell>
{userRole !== 'viewer' && <TableCell align='right'>
<EditIcon sx={{ cursor: 'pointer', mr: 1 }} onClick={() => {
setOptions(options.map((opt, i) => i === idx ? { ...opt, editMode: !opt.editMode } : opt));
}} />
<ClearIcon sx={{ cursor: 'pointer' }} onClick={() => {
const updatedOptions = options.filter((_, i) => i !== idx);
setOptions(updatedOptions);
}} />
</TableCell>
</TableCell>}
</TableRow>
))}
</TableBody>
Expand Down
18 changes: 10 additions & 8 deletions frontend/course/components/QuizForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId, contentId,
const dialogRef = useRef(null);
const apiBaseUrl = localStorage.getItem('apiBaseUrl');
const organizationId = localStorage.getItem('activeOrganizationId');

const userRole = localStorage.getItem('userRole');

const addQuiz = () => {
if (!validateQuiz()) {
Expand Down Expand Up @@ -201,9 +201,9 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId, contentId,
}} tabIndex={0} focusable="true">
<Typography variant="h2" sx={{ my: 2, fontSize: '1.5rem' }}>{ quizId ? "Update Quiz" : "New Quiz" }</Typography>
{errorMessage && <Alert severity="error" sx={{ mb: 2 }}>{errorMessage}</Alert>}
<RequiredTextField label="Quiz Title" value={title} onChange={(e) => setTitle(e.target.value)} sx={{ mb: 2, width: '100%' }} />
<Button variant="outlined" sx={{ mb: 2 }} onClick={() => setShowQuestionField(true)}>
<QuizIcon sx={{ mr: 1 }} /> Add Question</Button>
<RequiredTextField label="Quiz Title" value={title} onChange={(e) => setTitle(e.target.value)} sx={{ mb: 2, width: '100%' }} disabled={userRole === 'viewer'} />
{userRole !== 'viewer' && <Button variant="outlined" sx={{ mb: 2 }} onClick={() => setShowQuestionField(true)}>
<QuizIcon sx={{ mr: 1 }} /> Add Question</Button>}
{ showQuestionField && (
<Box sx={{ mb: 2, border: '1px solid', borderColor: 'grey.300', borderRadius: 1, p: 2 }}>
<Grid container spacing={2} alignItems="center">
Expand Down Expand Up @@ -240,6 +240,7 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId, contentId,
onChange={(e) => setRequiredScore(e.target.value)}
sx={{ width: '200px', mr: 2 }}
inputProps={{ min: 0, max: 100 }}
disabled={userRole === 'viewer'}
>
</RequiredTextField>
</Box>
Expand All @@ -254,19 +255,20 @@ const QuizForm = ({cancelCallback, successCallback, courseId, quizId, contentId,
onChange={(e) => setWaitingPeriod(e.target.value)}
sx={{ width: '200px', mr: 2 }}
inputProps={{ min: 1 }}
disabled={userRole === 'viewer'}
/>
<Select size="small" value={waitingPeriodUnit} onChange={(e) => setWaitingPeriodUnit(e.target.value)} name="waiting_period_unit" sx={{ width: '150px' }}>
<Select size="small" value={waitingPeriodUnit} onChange={(e) => setWaitingPeriodUnit(e.target.value)} name="waiting_period_unit" sx={{ width: '150px' }} disabled={userRole === 'viewer'}>
<MenuItem value="days">Days</MenuItem>
<MenuItem value="hours">Hours</MenuItem>
</Select>
</Tooltip>
<Box mt={2} textAlign="right">
<Button variant="outlined" sx={{ mr: 1, boxShadow: 'none' }} onClick={cancel}>
Cancel
Back
</Button>
<Button type="submit" variant="contained" color="primary" sx={{ boxShadow: 'none' }} onClick={() => {if(!quizId) { addQuiz(); } else { updateQuiz(); }}}>
{userRole !== 'viewer' && <Button type="submit" variant="contained" color="primary" sx={{ boxShadow: 'none' }} onClick={() => {if(!quizId) { addQuiz(); } else { updateQuiz(); }}}>
Save Quiz
</Button>
</Button>}
</Box>
</Box>
);
Expand Down
12 changes: 7 additions & 5 deletions frontend/courses/Courses.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function Courses() {
const [courses, setCourses] = useState([])
const [organizationId, setOrganizationId] = useState(null);
const [queryParameters, setQueryParameters] = useState("");
const userRole = localStorage.getItem('userRole');
const apiBaseUrl = localStorage.getItem('apiBaseUrl');
const platformBaseUrl = localStorage.getItem('platformBaseUrl');

Expand Down Expand Up @@ -106,23 +107,23 @@ function Courses() {
>
<Grid size={{xs: 12, md: 9}} py={2} pl={2}>
<Box p={2} sx={{ border: '1px solid', borderColor: 'border.main', backgroundColor: 'background.main', borderRadius: 1, minHeight: 300 }}>
<Button variant="contained" startIcon={<SchoolIcon />} sx={{ marginBottom: 2 }} onClick={() => {
{userRole !== 'viewer' && <Button variant="contained" startIcon={<SchoolIcon />} sx={{ marginBottom: 2 }} onClick={() => {
setDialogContent(<CourseForm
successCallback={handleCourseCreated}
failureCallback={handleCourseCreationFailed}
cancelCallback={() => setDialogOpen(false)}
activeOrganizationId={organizationId}
createMode={true}
/>);
setDialogOpen(true);}}>Add a Course</Button>
setDialogOpen(true);}}>Add a Course</Button>}
<TableContainer component={Paper}>
<Table sx={{ width: "100%" }} aria-label="Courses">
<TableHead>
<TableRow>
<TableCell>Title</TableCell>
<TableCell>Slug</TableCell>
<TableCell>Enabled</TableCell>
<TableCell align='right'>Actions</TableCell>
{userRole !== 'viewer' && <TableCell align='right'>Actions</TableCell>}
</TableRow>
</TableHead>
<TableBody>
Expand All @@ -140,9 +141,10 @@ function Courses() {
checked={course.enabled}
onChange={showEnableCoursePopup(course.id, course.enabled ? 'disable' : 'enable', course.title)}
slotProps={{ input: { 'aria-label': course.enabled ? 'Disable Course' : 'Enable Course' } }}
disabled={userRole === 'viewer'}
/>
</TableCell>
<TableCell align="right">
{userRole !== 'viewer' && <TableCell align="right">
<IconButton onClick={() => {
showEditCourseDialog(course);}}><EditIcon fontSize="small" /></IconButton>
<IconButton aria-label={`Delete ${course.title}`} onClick={() => {
Expand All @@ -152,7 +154,7 @@ function Courses() {
}} />);
setDialogOpen(true);
}}><DeleteIcon fontSize="small" /></IconButton>
</TableCell>
</TableCell>}
</TableRow>
))}
</TableBody>
Expand Down
Loading