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 <ifeanyi@digdippa.com>
Co-authored-by: Rotimi Best <rotimiibitoyeemma@gmail.com>


Merge-request: CLASSROOMIO-MR-107
Merged-by: Best Admin <best@digdippa.com>
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,
groupmember:submitted_by!inner (
id,
profile!inner (
profile:profile_id!inner (
id
)
)
@@ -79,12 +79,12 @@ export async function fetchLMSExercises(
group:group_id!inner (
organisation:organization_id!inner (
id
)
),
groupmember:group_id!inner(
id,
profile!inner (
id
),
groupmember!inner(
id,
profile:profile_id!inner (
id
)
)
)
)
@@ -92,7 +92,7 @@ export async function fetchLMSExercises(
`
)
.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);
return {
@@ -7,7 +7,8 @@ export const NOTIFICATION_NAME = {
SEND_TEACHER_STUDENT_BUY_REQUEST: 'SEND TEACHER STUDENT BUY REQUEST',
STUDENT_PROOVE_COURSE_PAYMENT: 'STUDENT PROOVE COURSE PAYMENT',
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 = {
@@ -18,12 +19,12 @@ const NAME_TO_PATH = {
'/api/email/course/teacher_student_buycourse',
[NOTIFICATION_NAME.STUDENT_PROOVE_COURSE_PAYMENT]: '/api/email/course/student_prove_payment',
[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 { data } = await supabase.auth.getSession();
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');
return;
}
const response = await fetch(path, {
method: 'POST',
headers: {
@@ -46,11 +46,10 @@ export const triggerSendEmail = async (name: string, body: { [k: string]: any })
body: JSON.stringify(body)
});
if (!response.ok) {
console.log(response);
throw new Error(await response.text());
}
const result = await response.json();
console.log(name, result);
} catch (error) {
console.error(name, error);
}
@@ -29,7 +29,7 @@ export function fetchSubmissions(course_id) {
`
)
.match({
course_id,
course_id
});
}
@@ -46,13 +46,16 @@ export async function fetchSubmission({ courseId, exerciseId, submittedBy }) {
.match({
submitted_by: submittedBy,
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 = {
status_id,
status_id
};
if (typeof total === 'number') {
@@ -27,9 +27,9 @@ export async function POST({ request }) {
}
await sendEmail({
from: `"${orgName} - ClassroomIO" <help@classroomio.com>`,
from: `"${orgName} (via ClassroomIO.com)" <help@classroomio.com>`,
to,
subject: `[${orgName}] Welcome to Class 🎉`,
subject: `${orgName} - Welcome to Class 🎉`,
content: `
<p>Hi there,</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)" <help@classroomio.com>`,
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`;
await sendEmail({
from: `"[${orgName}] - ClassroomIO" <help@classroomio.com>`,
from: `"${orgName} (via ClassroomIO.com)" <help@classroomio.com>`,
to,
subject: `You have been invited to a course in ${orgName}!`,
content: `
+167 -63
View File
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { onMount } from 'svelte';
import { flip } from 'svelte/animate';
import { dndzone } from 'svelte-dnd-action';
@@ -16,18 +16,59 @@
fetchSubmissions,
updateSubmission,
updateQuestionAnswer
} from '$lib/utils/services/submissions';
} from '$lib/utils/services/submissions/index';
import { formatAnswers } from '$lib/components/Course/function';
import { snackbar } from '$lib/components/Snackbar/store';
import isSubmissionEarly from '$lib/utils/functions/isSubmissionEarly';
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;
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,
title: 'Submitted',
@@ -47,13 +88,56 @@
items: []
}
];
let submissionIdData = {};
let submissionId;
let openExercise = false;
function getMaxPoints(questions) {
return (questions || []).reduce((acc, question) => acc + question.points, 0);
}
function handleItemFinalize(columnIdx, newItems) {
let itemToWithNewStatus;
function calculateTotal(grades: string[]): number {
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];
@@ -74,11 +158,8 @@
...submissionIdData[itemToWithNewStatus.id],
status_id: itemToWithNewStatus.statusId
};
console.log(
`submissionIdData[itemToWithNewStatus.id]`,
submissionIdData[itemToWithNewStatus.id]
);
sendEmail(submissionIdData[itemToWithNewStatus.id]);
updateSubmission({
id: itemToWithNewStatus.id,
status_id: itemToWithNewStatus.statusId
@@ -86,13 +167,13 @@
}
}
function handleDndConsiderCards(columnIdx) {
function handleDndConsiderCards(columnIdx: number) {
return function (e) {
sections[columnIdx].items = e.detail.items;
};
}
function handleDndFinalizeCards(columnIdx) {
function handleDndFinalizeCards(columnIdx: number) {
return (e) => handleItemFinalize(columnIdx, e.detail.items);
}
@@ -101,15 +182,27 @@
}
// Via dialog
function updateStatus({ submissionId, prevStatusId, nextStatusId, total }) {
let itemToWithNewStatus;
function updateStatus({
submissionId,
prevStatusId,
nextStatusId,
total
}: {
submissionId: number;
prevStatusId: number;
nextStatusId: number;
total: number;
}) {
let itemToWithNewStatus: items | undefined;
// Remove from current column
const { items } = sections[prevStatusId - 1];
sections[prevStatusId - 1].items = items.filter((item) => {
sections[prevStatusId - 1].items = items?.filter((item) => {
if (item.id === submissionId) {
itemToWithNewStatus = Object.assign(item);
itemToWithNewStatus.statusId = nextStatusId;
if (itemToWithNewStatus) {
itemToWithNewStatus.statusId = nextStatusId;
}
return false;
}
@@ -117,7 +210,9 @@
});
console.log(`itemToWithNewStatus`, itemToWithNewStatus);
// 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 (itemToWithNewStatus) {
@@ -127,6 +222,7 @@
status_id: itemToWithNewStatus.statusId
};
sendEmail(submissionIdData[itemToWithNewStatus.id]);
// Update backend
updateSubmission({
id: itemToWithNewStatus.id,
@@ -136,19 +232,21 @@
}
}
async function handleSave(submission) {
async function handleSave(submission: { questionAnswerByPoint: any; questionAnswers: any }) {
const { questionAnswerByPoint, questionAnswers } = submission;
let totalPoints = 0;
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];
totalPoints += parseInt(point, 10);
return updateQuestionAnswer({ point }, { id: questionAnswer.id });
return updateQuestionAnswer({ point }, { id: questionAnswer?.id });
});
updateSubmission({
@@ -168,49 +266,55 @@
}
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 { id, created_at, exercise, course, answers, groupmember, status_id } = submission;
const isEarly = isSubmissionEarly(created_at, exercise.due_by);
const isEarly = isSubmissionEarly(created_at, exercise.due_by);
const submissionItem = {
id,
statusId: status_id,
isEarly,
submittedAt: formatDate(created_at),
exercise: {
id: exercise.id,
title: exercise.title
},
answers,
student: groupmember && groupmember.profile ? groupmember.profile : {},
lesson: {
id: exercise.lesson.id,
title: exercise.lesson.title
const submissionItem = {
id,
statusId: status_id,
isEarly,
submittedAt: formatDate(created_at),
exercise: {
id: exercise.id,
title: exercise.title
},
answers,
student: groupmember && groupmember.profile ? groupmember.profile : {},
lesson: {
id: exercise.lesson.id,
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);
interface Section {
id: number;
title: string;