Skip to content

Commit 4fc88f8

Browse files
committed
feat: #101 viewer UI access
1 parent e998232 commit 4fc88f8

16 files changed

Lines changed: 130 additions & 36 deletions

File tree

django_email_learning/admin.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
Answer,
1111
CourseContent,
1212
Organization,
13+
OrganizationUser,
1314
BlockedEmail,
1415
)
1516

@@ -91,3 +92,4 @@ def get_content_title(self, obj: CourseContent) -> str | None:
9192
admin.site.register(Answer)
9293
admin.site.register(Organization)
9394
admin.site.register(BlockedEmail)
95+
admin.site.register(OrganizationUser)

django_email_learning/ports/__init__.py

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from typing import Protocol
2+
from django.core.mail import EmailMultiAlternatives
3+
4+
5+
class EmailSenderProtocol(Protocol):
6+
def send_email(self, email: EmailMultiAlternatives) -> None:
7+
...

django_email_learning/services/__init__.py

Whitespace-only changes.

django_email_learning/services/deafults/__init__.py

Whitespace-only changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import logging
2+
from django_email_learning.ports.email_sender_protocol import EmailSenderProtocol
3+
from django.core.mail import EmailMultiAlternatives
4+
5+
logger = logging.getLogger(__name__)
6+
7+
8+
class DjangoEmailSender(EmailSenderProtocol):
9+
def _mask_email(self, email_address: str) -> str:
10+
"""Mask email address for logging privacy."""
11+
try:
12+
username, domain = email_address.split("@")
13+
masked_username = username[0] + "***"
14+
return f"{masked_username}@{domain}"
15+
except ValueError:
16+
return "***@***"
17+
18+
def _mask_recipients(self, recipients: list[str]) -> str:
19+
"""Mask all recipient email addresses for logging."""
20+
if not recipients:
21+
return "no recipients"
22+
masked = [self._mask_email(recipient) for recipient in recipients]
23+
return ", ".join(masked)
24+
25+
def send_email(self, email: EmailMultiAlternatives) -> None:
26+
masked_recipients = self._mask_recipients(email.to)
27+
try:
28+
logger.info(f"Sending email to {masked_recipients}")
29+
email.send()
30+
logger.info(f"Email sent successfully to {masked_recipients}")
31+
except Exception as e:
32+
logger.error(f"Failed to send email to {masked_recipients}: {str(e)}")
33+
raise

frontend/course/Course.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ function Course() {
2121
const [lessonCache, setLessonCache] = useState("")
2222
const [contentLoaded, setContentLoaded] = useState(false)
2323

24+
const userRole = localStorage.getItem('userRole');
2425
const apiBaseUrl = localStorage.getItem('apiBaseUrl');
2526
const organizationId = localStorage.getItem('activeOrganizationId');
2627

@@ -120,7 +121,7 @@ function Course() {
120121
>
121122
<Grid size={{xs: 12, md: 9}} py={2} pl={2}>
122123
<Box p={2} sx={{ border: '1px solid', borderColor: 'grey.300', borderRadius: 1, minHeight: 300 }}>
123-
<Button variant="contained" startIcon={<DescriptionIcon />} sx={{ marginBottom: 2 }} onClick={() => {
124+
{userRole !== 'viewer' && <><Button variant="contained" startIcon={<DescriptionIcon />} sx={{ marginBottom: 2 }} onClick={() => {
124125
setDialogContent(<LessonForm
125126
header="New Lesson"
126127
initialContent={lessonCache}
@@ -134,7 +135,7 @@ function Course() {
134135
cancelCallback={() => setDialogOpen(false)}
135136
successCallback={resetDialog}
136137
courseId={course_id} />);
137-
setDialogOpen(true);}}>Add a Quiz</Button>
138+
setDialogOpen(true);}}>Add a Quiz</Button></> }
138139
<ContentTable courseId={course_id} loaded={contentLoaded} eventHandler={(event) => tableEventHandler(event)} />
139140
</Box>
140141
</Grid>

frontend/course/components/ContentTable.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
99
const apiBaseUrl = localStorage.getItem('apiBaseUrl');
1010
const organizationId = localStorage.getItem('activeOrganizationId');
1111

12+
const userRole = localStorage.getItem('userRole');
13+
1214
const formatPeriod = (period) => {
1315
if (!period) {
1416
return "";
@@ -97,7 +99,7 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
9799
<TableCell>Waiting time</TableCell>
98100
<TableCell>type</TableCell>
99101
<TableCell>Published</TableCell>
100-
<TableCell align='right'>Actions</TableCell>
102+
{userRole !== 'viewer' && <TableCell align='right'>Actions</TableCell>}
101103
</TableRow>
102104
</TableHead>
103105
<TableBody>
@@ -108,12 +110,12 @@ const ContentTable = ({ courseId, eventHandler, loaded = false }) => {
108110
color='primary.dark' sx={{ cursor: 'pointer'}}>{content.title}</Typography></TableCell>
109111
<TableCell>{formatPeriod(content.waiting_period)}</TableCell>
110112
<TableCell>{content.type}</TableCell>
111-
<TableCell><Switch defaultChecked={content.is_published} onChange={() => TogglePublishContent(content.id, !content.is_published)} /></TableCell>
112-
<TableCell align='right'>
113+
<TableCell><Switch defaultChecked={content.is_published} onChange={() => TogglePublishContent(content.id, !content.is_published)} disabled={userRole == 'viewer'} /></TableCell>
114+
{userRole !== 'viewer' && <TableCell align='right'>
113115
<IconButton aria-label="delete" onClick={() => deleteContent(content.id)}>
114116
<DeleteIcon />
115117
</IconButton>
116-
</TableCell>
118+
</TableCell>}
117119
</TableRow>
118120
))}
119121
</TableBody>

frontend/course/components/LessonForm.jsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can
1313
const [contentHelperText, setContentHelperText] = useState("");
1414
const [errorMessage, setErrorMessage] = useState("");
1515

16+
const userRole = localStorage.getItem('userRole');
1617
const apiBaseUrl = localStorage.getItem('apiBaseUrl');
1718
const orgId = localStorage.getItem('activeOrganizationId');
1819

@@ -126,9 +127,9 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can
126127
{errorMessage}
127128
</Alert>
128129
)}
129-
<RequiredTextField value={title} label="Lesson Title" name="lesson_title" sx={{ width: '100%' }} onChange={(e) => setTitle(e.target.value)} helperText={titleHelperText}/>
130+
<RequiredTextField value={title} label="Lesson Title" name="lesson_title" sx={{ width: '100%' }} onChange={(e) => setTitle(e.target.value)} helperText={titleHelperText} disabled={userRole === 'viewer'} />
130131
<Box sx={{ my: 2 }}>
131-
<ContentEditor initialContent={content} contentUpdateCallback={handleContentChange}/>
132+
<ContentEditor initialContent={content} contentUpdateCallback={handleContentChange} disabled={userRole === 'viewer'} />
132133
<Typography color="errorText.main" sx={{ marginTop: 1, fontSize: '0.75rem' }}>
133134
{contentHelperText}
134135
</Typography>
@@ -144,19 +145,20 @@ function LessonForm({ header, initialTitle, initialContent, onContentChange, can
144145
onChange={(e) => setWaitingPeriod(e.target.value)}
145146
sx={{ width: '200px', mr: 2 }}
146147
inputProps={{ min: 1 }}
148+
disabled={userRole === 'viewer'}
147149
/>
148-
<Select size="small" value={waitingPeriodUnit} onChange={(e) => setWaitingPeriodUnit(e.target.value)} name="waiting_period_unit" sx={{ width: '150px' }}>
150+
<Select size="small" value={waitingPeriodUnit} onChange={(e) => setWaitingPeriodUnit(e.target.value)} name="waiting_period_unit" sx={{ width: '150px' }} disabled={userRole === 'viewer'}>
149151
<MenuItem value="days">Days</MenuItem>
150152
<MenuItem value="hours">Hours</MenuItem>
151153
</Select>
152154
</Tooltip>
153155
<Box mt={2} textAlign="right">
154156
<Button variant="outlined" sx={{ mr: 1 }} onClick={cancel}>
155-
Cancel
157+
Back
156158
</Button>
157-
<Button type="submit" variant="contained" onClick={() => {if(!lessonId) { addLesson(); } else { updateLesson(); }}}>
159+
{userRole !== 'viewer' && <Button type="submit" variant="contained" onClick={() => {if(!lessonId) { addLesson(); } else { updateLesson(); }}}>
158160
Save Lesson
159-
</Button>
161+
</Button>}
160162
</Box>
161163
</Box>
162164
);

frontend/course/components/QuestionForm.jsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ const QuestionForm = ({question, index, eventHandler}) => {
1414
const [addingOption, setAddingOption] = useState(false);
1515
const optionInputRef = useRef(null);
1616

17+
const userRole = localStorage.getItem('userRole');
18+
1719
const editQuestion = () => {
18-
if (editMode && questionText.trim() === '') {
20+
if (editMode && questionText.trim() === '' || userRole === 'viewer') {
1921
return;
2022
}
2123
triggerUpdateEvent();
@@ -61,7 +63,7 @@ const QuestionForm = ({question, index, eventHandler}) => {
6163
<Box key={index} sx={{ mb: 1, p: 2, border: '1px solid', borderColor: 'grey.300', borderRadius: 1 }}>
6264
<Grid container spacing={2} alignItems="center">
6365
<Grid size={{ xs: 12, md: 9 }}>
64-
<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}/>
66+
{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}/>}
6567
{!editMode ? (
6668
<Typography onClick={editQuestion}>{index + 1}. {questionText}</Typography>
6769
) : (
@@ -77,12 +79,12 @@ const QuestionForm = ({question, index, eventHandler}) => {
7779
)}
7880
</Grid>
7981
<Grid size={{ xs: 12, md: 3 }} sx={{ textAlign: 'right' }}>
80-
<Button variant="outlined" color="primary" sx={{ fontSize: '0.75rem', mt: 1 }} onClick={() => setAddingOption(true)} >
82+
{userRole !== 'viewer' && <><Button variant="outlined" color="primary" sx={{ fontSize: '0.75rem', mt: 1 }} onClick={() => setAddingOption(true)} >
8183
<RuleIcon /><Typography variant="button" sx={{ ml: 1, fontSize: '0.75rem' }}>Add Option</Typography>
8284
</Button>
8385
<Button variant="outlined" onClick={deleteCallback} sx={{ ml: 1, mt: 1, fontSize: '0.75rem' }}>
8486
Delete
85-
</Button>
87+
</Button></>}
8688
</Grid>
8789
{addingOption && (<>
8890
<Grid size={{ xs: 9 }} sx={{ display: 'flex', alignItems: 'center' }}>
@@ -125,7 +127,7 @@ const QuestionForm = ({question, index, eventHandler}) => {
125127
<TableRow>
126128
<TableCell>Options</TableCell>
127129
<TableCell>Correct Answer</TableCell>
128-
<TableCell align='right'>Actions</TableCell>
130+
{userRole !== 'viewer' && <TableCell align='right'>Actions</TableCell>}
129131
</TableRow>
130132
</TableHead>
131133
<TableBody>
@@ -146,16 +148,16 @@ const QuestionForm = ({question, index, eventHandler}) => {
146148
}}
147149
/>
148150
)}</TableCell>
149-
<TableCell><Switch onChange={(e)=>updateOption(idx, e.target.checked)} checked={option.isCorrect} /></TableCell>
150-
<TableCell align='right'>
151+
<TableCell><Switch onChange={(e)=>updateOption(idx, e.target.checked)} checked={option.isCorrect} disabled={userRole === 'viewer'} /></TableCell>
152+
{userRole !== 'viewer' && <TableCell align='right'>
151153
<EditIcon sx={{ cursor: 'pointer', mr: 1 }} onClick={() => {
152154
setOptions(options.map((opt, i) => i === idx ? { ...opt, editMode: !opt.editMode } : opt));
153155
}} />
154156
<ClearIcon sx={{ cursor: 'pointer' }} onClick={() => {
155157
const updatedOptions = options.filter((_, i) => i !== idx);
156158
setOptions(updatedOptions);
157159
}} />
158-
</TableCell>
160+
</TableCell>}
159161
</TableRow>
160162
))}
161163
</TableBody>

0 commit comments

Comments
 (0)