CLASSROOMIO-T-155:Send email notification to students when exercise status changes

Merge branch 'CLASSROOMIO-T-155' of https://gitlab.com/thenextgreat/classroomio into CLASSROOMIO-T-155

remove consoles

CLASSROOMIO-T-155:Cleanup

Improve email and fix fetching exercise in lms

CLASSROOMIO-T-155: Improve from name

Merge branch 'main' of https://gitlab.com/thenextgreat/classroomio into CLASSROOMIO-T-155

CLASSROOMIO-T-155:Cleanup

CLASSROOMIO-T-155:Implemented the email notification

Merge branch 'main' into CLASSROOMIO-T-155

CLASSROOMIO-T-155:Working on the email notification


Co-authored-by: Ifeanyi Nwosu <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>


Merge-request: CLASSROOMIO-MR-107
Merged-by: Best Admin <[email protected]>
This commit is contained in:
emmanuel
2023-10-18 08:09:52 +00:00
committed by Space Cloud
co-authored by Rotimi Best
parent 21b0583077
commit bf73c8cf5a
8 changed files with 228 additions and 85 deletions
+8 -8
View File
@@ -64,7 +64,7 @@ export async function fetchLMSExercises(
total, total,
groupmember:submitted_by!inner ( groupmember:submitted_by!inner (
id, id,
profile!inner ( profile:profile_id!inner (
id id
) )
) )
@@ -79,12 +79,12 @@ export async function fetchLMSExercises(
group:group_id!inner ( group:group_id!inner (
organisation:organization_id!inner ( organisation:organization_id!inner (
id id
) ),
), groupmember!inner(
groupmember:group_id!inner( id,
id, profile:profile_id!inner (
profile!inner ( id
id )
) )
) )
) )
@@ -92,7 +92,7 @@ export async function fetchLMSExercises(
` `
) )
.eq('lesson.course.group.organisation.id', orgId) .eq('lesson.course.group.organisation.id', orgId)
.eq('lesson.course.groupmember.profile.id', profileId) .eq('lesson.course.group.groupmember.profile.id', profileId)
.eq('submission.groupmember.profile.id', profileId); .eq('submission.groupmember.profile.id', profileId);
return { return {
@@ -7,7 +7,8 @@ export const NOTIFICATION_NAME = {
SEND_TEACHER_STUDENT_BUY_REQUEST: 'SEND TEACHER STUDENT BUY REQUEST', SEND_TEACHER_STUDENT_BUY_REQUEST: 'SEND TEACHER STUDENT BUY REQUEST',
STUDENT_PROOVE_COURSE_PAYMENT: 'STUDENT PROOVE COURSE PAYMENT', STUDENT_PROOVE_COURSE_PAYMENT: 'STUDENT PROOVE COURSE PAYMENT',
STUDENT_COURSE_WELCOME: 'STUDENT COURSE WELCOME', STUDENT_COURSE_WELCOME: 'STUDENT COURSE WELCOME',
TEACHER_STUDENT_JOINED: 'TEACHER STUDENT JOINED' TEACHER_STUDENT_JOINED: 'TEACHER STUDENT JOINED',
SUBMISSION_UPDATE: 'SUBMISSION UPDATE'
}; };
const NAME_TO_PATH = { const NAME_TO_PATH = {
@@ -18,12 +19,12 @@ const NAME_TO_PATH = {
'/api/email/course/teacher_student_buycourse', '/api/email/course/teacher_student_buycourse',
[NOTIFICATION_NAME.STUDENT_PROOVE_COURSE_PAYMENT]: '/api/email/course/student_prove_payment', [NOTIFICATION_NAME.STUDENT_PROOVE_COURSE_PAYMENT]: '/api/email/course/student_prove_payment',
[NOTIFICATION_NAME.STUDENT_COURSE_WELCOME]: '/api/email/course/student_welcome', [NOTIFICATION_NAME.STUDENT_COURSE_WELCOME]: '/api/email/course/student_welcome',
[NOTIFICATION_NAME.TEACHER_STUDENT_JOINED]: '/api/email/course/teacher_student_joined' [NOTIFICATION_NAME.TEACHER_STUDENT_JOINED]: '/api/email/course/teacher_student_joined',
[NOTIFICATION_NAME.SUBMISSION_UPDATE]: '/api/email/course/submission_update'
}; };
const getAccessToken = async () => { const getAccessToken = async () => {
const { data } = await supabase.auth.getSession(); const { data } = await supabase.auth.getSession();
return data.session?.access_token || ''; return data.session?.access_token || '';
}; };
@@ -36,7 +37,6 @@ export const triggerSendEmail = async (name: string, body: { [k: string]: any })
console.error('Could not trigger send'); console.error('Could not trigger send');
return; return;
} }
const response = await fetch(path, { const response = await fetch(path, {
method: 'POST', method: 'POST',
headers: { headers: {
@@ -46,11 +46,10 @@ export const triggerSendEmail = async (name: string, body: { [k: string]: any })
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
if (!response.ok) { if (!response.ok) {
console.log(response);
throw new Error(await response.text()); throw new Error(await response.text());
} }
const result = await response.json(); const result = await response.json();
console.log(name, result);
} catch (error) { } catch (error) {
console.error(name, error); console.error(name, error);
} }
@@ -29,7 +29,7 @@ export function fetchSubmissions(course_id) {
` `
) )
.match({ .match({
course_id, course_id
}); });
} }
@@ -46,13 +46,16 @@ export async function fetchSubmission({ courseId, exerciseId, submittedBy }) {
.match({ .match({
submitted_by: submittedBy, submitted_by: submittedBy,
exercise_id: exerciseId, exercise_id: exerciseId,
course_id: courseId, course_id: courseId
}); });
} }
export async function updateSubmission({ id, status_id, total }, otherArgs) { export async function updateSubmission(
{ id, status_id, total }: { id?: number; status_id?: number; total?: number },
otherArgs?: any
) {
const toUpdate = { const toUpdate = {
status_id, status_id
}; };
if (typeof total === 'number') { if (typeof total === 'number') {
@@ -27,9 +27,9 @@ export async function POST({ request }) {
} }
await sendEmail({ await sendEmail({
from: `"${orgName} - ClassroomIO" <[email protected]>`, from: `"${orgName} (via ClassroomIO.com)" <[email protected]>`,
to, to,
subject: `[${orgName}] Welcome to Class 🎉`, subject: `${orgName} - Welcome to Class 🎉`,
content: ` content: `
<p>Hi there,</p> <p>Hi there,</p>
<p>Congratulations 🎉, you’ve successfully joined: <strong>${courseName}</strong></p> <p>Congratulations 🎉, you’ve successfully joined: <strong>${courseName}</strong></p>
@@ -0,0 +1,38 @@
import { json } from '@sveltejs/kit';
import { getSupabase } from '$lib/utils/functions/supabase';
import { sendEmail } from '$lib/utils/services/notification/send';
const supabase = getSupabase();
export async function POST({ request }) {
const { to, content, orgName } = await request.json();
const accessToken = request.headers.get('Authorization') || '';
if (!to || !content || !orgName) {
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
}
let user;
try {
const { data } = await supabase.auth.getUser(accessToken);
user = data.user;
} catch (error) {
console.error(error);
}
if (!user) {
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
}
await sendEmail({
from: `"${orgName} (via ClassroomIO.com)" <[email protected]>`,
to,
subject: `Submission Update`,
content
});
return json({
success: true,
message: 'Email sent'
});
}
@@ -29,7 +29,7 @@ export async function POST({ request }) {
const inviteLink = `${origin}/org/${orgSiteName}/courses`; const inviteLink = `${origin}/org/${orgSiteName}/courses`;
await sendEmail({ await sendEmail({
from: `"[${orgName}] - ClassroomIO" <[email protected]>`, from: `"${orgName} (via ClassroomIO.com)" <[email protected]>`,
to, to,
subject: `You have been invited to a course in ${orgName}!`, subject: `You have been invited to a course in ${orgName}!`,
content: ` content: `
+167 -63
View File
@@ -1,4 +1,4 @@
<script> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action'; import { dndzone } from 'svelte-dnd-action';
@@ -16,18 +16,59 @@
fetchSubmissions, fetchSubmissions,
updateSubmission, updateSubmission,
updateQuestionAnswer updateQuestionAnswer
} from '$lib/utils/services/submissions'; } from '$lib/utils/services/submissions/index';
import { formatAnswers } from '$lib/components/Course/function'; import { formatAnswers } from '$lib/components/Course/function';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import isSubmissionEarly from '$lib/utils/functions/isSubmissionEarly'; import isSubmissionEarly from '$lib/utils/functions/isSubmissionEarly';
import formatDate from '$lib/utils/functions/formatDate'; import formatDate from '$lib/utils/functions/formatDate';
import {
triggerSendEmail,
NOTIFICATION_NAME
} from '$lib/utils/services/notification/notification';
import { currentOrg } from '$lib/utils/store/org';
const flipDurationMs = 300; type items = {
id: number;
statusId: number;
isEarly: boolean;
submittedAt: string;
exercise: {
id: string;
title: string;
};
answers: string;
student: any;
lesson: {
id: string | any;
title: string | any;
};
};
type sectionType = {
id: number;
title: string;
value: number;
items: items[] | [];
};
export let data; export let data;
const { courseId } = data; const { courseId } = data;
let sections = [ const flipDurationMs = 300;
let exerciseDetails: { id: string; title: string };
let lessonDetails: { id: string; title: string };
let totalMark = 0;
let maxMark = 0;
let submissionIdData: { [key: number]: any } = {};
let submissionId: string | number | null;
let openExercise = false;
const submissionStatus: { [key: number]: string } = {
1: 'Submitted',
2: 'In Progress',
3: 'Graded'
};
let sections: sectionType[] = [
{ {
id: 1, id: 1,
title: 'Submitted', title: 'Submitted',
@@ -47,13 +88,56 @@
items: [] items: []
} }
]; ];
let submissionIdData = {};
let submissionId;
let openExercise = false; function getMaxPoints(questions) {
return (questions || []).reduce((acc, question) => acc + question.points, 0);
}
function handleItemFinalize(columnIdx, newItems) { function calculateTotal(grades: string[]): number {
let itemToWithNewStatus; if (!grades) return 0;
return Object.values(grades).reduce((acc, grade) => acc + parseInt(grade), 0);
}
const sendEmail = (submissionData) => {
maxMark = getMaxPoints(submissionData?.questions);
totalMark = calculateTotal(submissionData?.questionAnswerByPoint);
const { fullname, email } = submissionData?.student;
const { title, status_id }: { title: string; status_id: number } = submissionData;
const exerciseLink = `${$currentOrg.siteName}.classroomio.com/courses/${courseId}/lessons/${lessonDetails.id}/exercises/${exerciseDetails.id}`;
const content = `
<p>Hello ${fullname},</p>
<p>The status of your submitted exercise on <strong>${title}</strong> has been updated to ${
submissionStatus[status_id]
}</p>
${
status_id == 3
? `<p>Your score was ${totalMark}/${maxMark}</p>
<div>
<a class="button" href="${exerciseLink}">View your Result</a>
<div>`
: `<div>
<a class="button" href="${exerciseLink}">Open Exercise</a>
<div>`
}
<p>This exercise is for <strong>${
lessonDetails.title
}</strong> in a course you are taking titled <strong>${$course.title}</strong></p>
`;
triggerSendEmail(NOTIFICATION_NAME.SUBMISSION_UPDATE, {
to: email,
content,
orgName: $currentOrg?.name
});
};
function handleItemFinalize(
columnIdx: number,
newItems: { map: (arg0: (item: items) => items) => items[] }
) {
let itemToWithNewStatus: items | undefined;
const { id } = sections[columnIdx]; const { id } = sections[columnIdx];
@@ -74,11 +158,8 @@
...submissionIdData[itemToWithNewStatus.id], ...submissionIdData[itemToWithNewStatus.id],
status_id: itemToWithNewStatus.statusId status_id: itemToWithNewStatus.statusId
}; };
console.log(
`submissionIdData[itemToWithNewStatus.id]`,
submissionIdData[itemToWithNewStatus.id]
);
sendEmail(submissionIdData[itemToWithNewStatus.id]);
updateSubmission({ updateSubmission({
id: itemToWithNewStatus.id, id: itemToWithNewStatus.id,
status_id: itemToWithNewStatus.statusId status_id: itemToWithNewStatus.statusId
@@ -86,13 +167,13 @@
} }
} }
function handleDndConsiderCards(columnIdx) { function handleDndConsiderCards(columnIdx: number) {
return function (e) { return function (e) {
sections[columnIdx].items = e.detail.items; sections[columnIdx].items = e.detail.items;
}; };
} }
function handleDndFinalizeCards(columnIdx) { function handleDndFinalizeCards(columnIdx: number) {
return (e) => handleItemFinalize(columnIdx, e.detail.items); return (e) => handleItemFinalize(columnIdx, e.detail.items);
} }
@@ -101,15 +182,27 @@
} }
// Via dialog // Via dialog
function updateStatus({ submissionId, prevStatusId, nextStatusId, total }) { function updateStatus({
let itemToWithNewStatus; submissionId,
prevStatusId,
nextStatusId,
total
}: {
submissionId: number;
prevStatusId: number;
nextStatusId: number;
total: number;
}) {
let itemToWithNewStatus: items | undefined;
// Remove from current column // Remove from current column
const { items } = sections[prevStatusId - 1]; const { items } = sections[prevStatusId - 1];
sections[prevStatusId - 1].items = items.filter((item) => { sections[prevStatusId - 1].items = items?.filter((item) => {
if (item.id === submissionId) { if (item.id === submissionId) {
itemToWithNewStatus = Object.assign(item); itemToWithNewStatus = Object.assign(item);
itemToWithNewStatus.statusId = nextStatusId; if (itemToWithNewStatus) {
itemToWithNewStatus.statusId = nextStatusId;
}
return false; return false;
} }
@@ -117,7 +210,9 @@
}); });
console.log(`itemToWithNewStatus`, itemToWithNewStatus); console.log(`itemToWithNewStatus`, itemToWithNewStatus);
// Move to right column // Move to right column
sections[nextStatusId - 1].items = [...sections[nextStatusId - 1].items, itemToWithNewStatus]; if (itemToWithNewStatus) {
sections[nextStatusId - 1].items = [...sections[nextStatusId - 1].items, itemToWithNewStatus];
}
// If something changed // If something changed
if (itemToWithNewStatus) { if (itemToWithNewStatus) {
@@ -127,6 +222,7 @@
status_id: itemToWithNewStatus.statusId status_id: itemToWithNewStatus.statusId
}; };
sendEmail(submissionIdData[itemToWithNewStatus.id]);
// Update backend // Update backend
updateSubmission({ updateSubmission({
id: itemToWithNewStatus.id, id: itemToWithNewStatus.id,
@@ -136,19 +232,21 @@
} }
} }
async function handleSave(submission) { async function handleSave(submission: { questionAnswerByPoint: any; questionAnswers: any }) {
const { questionAnswerByPoint, questionAnswers } = submission; const { questionAnswerByPoint, questionAnswers } = submission;
let totalPoints = 0; let totalPoints = 0;
const updates = Object.keys(questionAnswerByPoint).map((questionId) => { const updates = Object.keys(questionAnswerByPoint).map((questionId) => {
const questionAnswer = questionAnswers.find((answer) => answer.question_id == questionId); const questionAnswer = questionAnswers.find(
(answer: { question_id: string }) => answer.question_id == questionId
);
const point = questionAnswerByPoint[questionId]; const point = questionAnswerByPoint[questionId];
totalPoints += parseInt(point, 10); totalPoints += parseInt(point, 10);
return updateQuestionAnswer({ point }, { id: questionAnswer.id }); return updateQuestionAnswer({ point }, { id: questionAnswer?.id });
}); });
updateSubmission({ updateSubmission({
@@ -168,49 +266,55 @@
} }
const { data: submissions } = await fetchSubmissions(courseId || $course.id); const { data: submissions } = await fetchSubmissions(courseId || $course.id);
const sectionById = {}; const sectionById: { [key: number]: sectionType[] } = {};
if (submissions) {
for (const submission of submissions) {
const { id, created_at, exercise, course, answers, groupmember, status_id } = submission;
for (const submission of submissions) { const isEarly = isSubmissionEarly(created_at, exercise.due_by);
const { id, created_at, exercise, course, answers, groupmember, status_id } = submission;
const isEarly = isSubmissionEarly(created_at, exercise.due_by);
const submissionItem = { const submissionItem = {
id, id,
statusId: status_id, statusId: status_id,
isEarly, isEarly,
submittedAt: formatDate(created_at), submittedAt: formatDate(created_at),
exercise: { exercise: {
id: exercise.id, id: exercise.id,
title: exercise.title title: exercise.title
}, },
answers, answers,
student: groupmember && groupmember.profile ? groupmember.profile : {}, student: groupmember && groupmember.profile ? groupmember.profile : {},
lesson: { lesson: {
id: exercise.lesson.id, id: exercise.lesson.id,
title: exercise.lesson.title title: exercise.lesson.title
}
};
exerciseDetails = { id: submissionItem.exercise.id, title: submissionItem.exercise.title };
lessonDetails = {
id: submissionItem.lesson.id,
title: submissionItem.lesson.title
};
submissionIdData[id] = {
id,
status_id,
isEarly,
title: exercise.title,
student: submissionItem.student,
questions: exercise.questions,
answers: formatAnswers({ questions: exercise.questions, answers }),
questionAnswers: answers,
questionAnswerByPoint: answers.reduce((acc, answer) => {
acc[answer.question_id] = answer.point;
return acc;
}, {})
};
if (Array.isArray(sectionById[status_id])) {
sectionById[status_id].push(submissionItem);
} else {
sectionById[status_id] = [submissionItem];
} }
};
submissionIdData[id] = {
id,
status_id,
isEarly,
title: exercise.title,
student: submissionItem.student,
questions: exercise.questions,
answers: formatAnswers({ questions: exercise.questions, answers }),
questionAnswers: answers,
questionAnswerByPoint: answers.reduce((acc, answer) => {
acc[answer.question_id] = answer.point;
return acc;
}, {})
};
if (Array.isArray(sectionById[status_id])) {
sectionById[status_id].push(submissionItem);
} else {
sectionById[status_id] = [submissionItem];
} }
} }
-1
View File
@@ -37,7 +37,6 @@
} }
]; ];
let sections: Section[] = cloneDeep(defaultSections); let sections: Section[] = cloneDeep(defaultSections);
interface Section { interface Section {
id: number; id: number;
title: string; title: string;