CLASSROOMIO-T-154: Notify tutors when a student submits an exercise

CLASSROOMIO-T-154: Fix textarea grade issue

CLASSROOMIO-T-154: Get it to work properly

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

CLASSROOMIO-T-154: PR fixes
CLASSROOMIO-T-154:Updated the code

Merge branch 'main' into CLASSROOMIO-T-154

CLASSROOMIO-T-154:Notify tutors when a student submits an exercise


Co-authored-by: Ifeanyi Nwosu <ifeanyi@digdippa.com>
Co-authored-by: Rotimi Best <rotimiibitoyeemma@gmail.com>


Merge-request: CLASSROOMIO-MR-109
Merged-by: Best Admin <best@digdippa.com>
This commit is contained in:
emmanuel
2023-10-20 04:16:07 +00:00
committed by Space Cloud
co-authored by Rotimi Best
parent 91f28a345f
commit 0f2d60534d
15 changed files with 159 additions and 91 deletions
@@ -88,23 +88,6 @@
/> />
</div> </div>
<div class="ml-4 w-2/5 sticky top-0"> <div class="ml-4 w-2/5 sticky top-0">
<div class="flex items-center justify-between">
<Select
bind:value={status}
options={SELECTABLE_STATUS}
selectClassName={getStatusColor(status)}
onChange={handleStatusChange}
/>
<PrimaryButton
onClick={() => {
handleSave(data);
}}
label="Save"
variant={VARIANTS.CONTAINED_SUCCESS}
className="py-3 px-8"
/>
</div>
<div class="border border-gray-300 rounded-md mt-2"> <div class="border border-gray-300 rounded-md mt-2">
<div <div
class="hover:bg-gray-100 dark:bg-neutral-800 border-b border-t-0 border-l-0 border-r-0 border-gray-300 p-3" class="hover:bg-gray-100 dark:bg-neutral-800 border-b border-t-0 border-l-0 border-r-0 border-gray-300 p-3"
@@ -141,6 +124,26 @@
<p class="dark:text-white ml-2 text-sm">{data.student.fullname}</p> <p class="dark:text-white ml-2 text-sm">{data.student.fullname}</p>
{/if} {/if}
</div> </div>
<div class="flex items-center text-sm p-3">
<p class="dark:text-white font-bold w-2/5">Status</p>
<Select
bind:value={status}
options={SELECTABLE_STATUS}
selectClassName={getStatusColor(status)}
onChange={handleStatusChange}
className="w-full"
/>
</div>
<div class="flex items-center justify-center w-full p-3">
<PrimaryButton
onClick={() => {
handleSave(data);
}}
label="Submit Grades"
variant={VARIANTS.CONTAINED_SUCCESS}
className="py-3 px-8 w-full"
/>
</div>
<!-- <div class="flex items-center text-sm p-3"> <!-- <div class="flex items-center text-sm p-3">
<p class="dark:text-white font-bold w-1/2">Teacher</p> <p class="dark:text-white font-bold w-1/2">Teacher</p>
<p class="dark:text-white">rotimi-best</p> <p class="dark:text-white">rotimi-best</p>
@@ -19,9 +19,14 @@
import { submitExercise } from '$lib/utils/services/courses'; import { submitExercise } from '$lib/utils/services/courses';
import { fetchSubmission } from '$lib/utils/services/submissions'; import { fetchSubmission } from '$lib/utils/services/submissions';
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
import {
NOTIFICATION_NAME,
triggerSendEmail
} from '$lib/utils/services/notification/notification';
import { lesson } from '../store/lessons';
export let preview = false; export let preview;
export let exerciseId = ''; export let exerciseId = '';
let currentQuestion = {}; let currentQuestion = {};
@@ -33,10 +38,40 @@
$questionnaireMetaData.currentQuestionIndex += 1; $questionnaireMetaData.currentQuestionIndex += 1;
} }
const getStudent = (people, profileId) => {
return people.find((person) => person.profile_id === profileId);
};
const sendEmail = () => {
const student = getStudent($group.students, $profile.id);
const teacherFullname = $group.tutors[0]?.fullname;
const teacherEmail = $group.tutors[0]?.email;
if (!student || !teacherFullname || !teacherEmail) return;
const baseUrl = `https://app.classroomio.com/courses/${$course.id}`;
const exerciseLink = `${baseUrl}/lessons/${$lesson.id}/exercises/${exerciseId}`;
const submissionLink = `${baseUrl}/submissions`;
const content = `
<p>Hello ${teacherFullname},</p>
<p>A student ${student.profile.fullname} just submitted an exercise <a href=${exerciseLink}>${$questionnaire.title}</a>
<p>You can get started grading by clicking "Open Submissions"</p>
<div>
<a class="button" href=${submissionLink}>Open Submissions</a>
</div>
`;
triggerSendEmail(NOTIFICATION_NAME.EXERCISE_SUBMISSION_UPDATE, {
to: teacherEmail,
content,
orgName: $currentOrg?.name,
exerciseTitle: $questionnaire.title
});
};
function onSubmit(id, value, moveToNextQuestion = false) { function onSubmit(id, value, moveToNextQuestion = false) {
const { answers } = $questionnaireMetaData; const { answers } = $questionnaireMetaData;
const { questions } = $questionnaire; const { questions } = $questionnaire;
const prevAnswer = answers[id] || []; const prevAnswer = answers[id] || [];
const formattedAnswer = const formattedAnswer =
typeof value === 'string' ? value : removeDuplicate([...prevAnswer, ...(value || [])]); typeof value === 'string' ? value : removeDuplicate([...prevAnswer, ...(value || [])]);
@@ -69,6 +104,7 @@
$course.id, $course.id,
getGroupMemberId($group.people, $profile.id) getGroupMemberId($group.people, $profile.id)
); );
sendEmail();
} }
} }
@@ -153,7 +189,6 @@
$: $questionnaireMetaData.progressValue = getProgressValue( $: $questionnaireMetaData.progressValue = getProgressValue(
$questionnaireMetaData.currentQuestionIndex $questionnaireMetaData.currentQuestionIndex
); );
$: checkForSubmission($group.people, $profile.id, $course.id); $: checkForSubmission($group.people, $profile.id, $course.id);
</script> </script>
@@ -69,10 +69,12 @@
</script> </script>
<PageBody bind:isPageNavHidden={isStudent} padding="px-4 overflow-x-hidden"> <PageBody bind:isPageNavHidden={isStudent} padding="px-4 overflow-x-hidden">
<ContentSwitcher bind:selectedIndex class="mb-2"> <RoleBasedSecurity allowedRoles={[1, 2]}>
<Switch text={`Questions ${0}`} /> <ContentSwitcher bind:selectedIndex class="mb-2">
<Switch text="Analytics" /> <Switch text="Questions" />
</ContentSwitcher> <Switch text="Analytics" />
</ContentSwitcher>
</RoleBasedSecurity>
{#if selectedIndex === 0} {#if selectedIndex === 0}
<div <div
@@ -56,7 +56,7 @@
} }
function handleCursor(event) { function handleCursor(event) {
if (!resize) { if (!resize && sidebarRef) {
const isNearLeftBorder = event.clientX - sidebarRef.getBoundingClientRect().left < 8; const isNearLeftBorder = event.clientX - sidebarRef.getBoundingClientRect().left < 8;
const isNearRightBorder = sidebarRef.getBoundingClientRect().right - event.clientX < 8; const isNearRightBorder = sidebarRef.getBoundingClientRect().right - event.clientX < 8;
@@ -69,7 +69,7 @@
} }
function startDragging(event) { function startDragging(event) {
if (event.button === 0) { if (event.button === 0 && sidebarRef) {
event.preventDefault(); event.preventDefault();
const isNearRightBorder = sidebarRef.getBoundingClientRect().right - event.clientX < 8; const isNearRightBorder = sidebarRef.getBoundingClientRect().right - event.clientX < 8;
@@ -1,10 +1,10 @@
<script> <script lang="ts">
import CodeSnippet from '$lib/components/CodeSnippet/index.svelte'; import CodeSnippet from '$lib/components/CodeSnippet/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import Checkbox from '$lib/components/Form/Checkbox.svelte'; import Checkbox from '$lib/components/Form/Checkbox.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte'; import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import Grade from '$lib/components/Question/Grade.svelte';
export let title = ''; export let title = '';
export let code; export let code;
@@ -17,7 +17,7 @@
export let disabled = false; export let disabled = false;
export let isPreview = false; export let isPreview = false;
export let nextButtonProps = {}; export let nextButtonProps = {};
export let grade; export let grade: number | undefined;
export let gradeMax = 0; export let gradeMax = 0;
export let disableGrading = false; export let disableGrading = false;
@@ -63,21 +63,10 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<HtmlRender className="mt-4"> <HtmlRender className="mt-4">
<svelte:fragment slot="content"> <svelte:fragment slot="content">
<h3 class={!isNaN(grade) ? 'w-3/4' : ''}>{title}</h3> <h3 class={typeof grade === 'number' ? 'w-3/4' : ''}>{title}</h3>
</svelte:fragment> </svelte:fragment>
</HtmlRender> </HtmlRender>
{#if !isNaN(grade)} <Grade {gradeMax} {grade} {disableGrading} />
<div class="flex items-center">
<TextField
placeholder="Points"
bind:value={grade}
type="number"
className="w-20"
isDisabled={disableGrading}
/>
<p class="dark:text-white ml-2 text-lg">/ {gradeMax}</p>
</div>
{/if}
</div> </div>
{#if code} {#if code}
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import TextField from '$lib/components/Form/TextField.svelte';
export let gradeMax = 0;
export let disableGrading = false;
export let grade: number | undefined = 0;
</script>
{#if typeof grade === 'number'}
<div class="flex items-center">
<TextField
placeholder="Points"
bind:value={grade}
type="number"
className="w-12"
isDisabled={disableGrading}
/>
<p class="dark:text-white ml-2 text-lg flex items-center">
<span class="mr-1">/</span> <span>{gradeMax}</span>
</p>
</div>
{/if}
@@ -1,10 +1,10 @@
<script> <script lang="ts">
import CodeSnippet from '$lib/components/CodeSnippet/index.svelte'; import CodeSnippet from '$lib/components/CodeSnippet/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import RadioItem from '$lib/components/Form/RadioItem.svelte'; import RadioItem from '$lib/components/Form/RadioItem.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte'; import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import Grade from '$lib/components/Question/Grade.svelte';
export let title = ''; export let title = '';
export let code = ''; export let code = '';
@@ -17,7 +17,7 @@
export let disabled = false; export let disabled = false;
export let isPreview = false; export let isPreview = false;
export let nextButtonProps = {}; export let nextButtonProps = {};
export let grade = undefined; export let grade: number;
export let gradeMax = 0; export let gradeMax = 0;
export let disableGrading = false; export let disableGrading = false;
export let labelClassName = ''; export let labelClassName = '';
@@ -65,23 +65,12 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<HtmlRender className="mt-4"> <HtmlRender className="mt-4">
<svelte:fragment slot="content"> <svelte:fragment slot="content">
<h3 class="dark:text-white {labelClassName} {!isNaN(grade) && 'w-3/4'}"> <h3 class="dark:text-white {labelClassName} {typeof grade === 'number' && 'w-3/4'}">
{@html title} {title}
</h3> </h3>
</svelte:fragment> </svelte:fragment>
</HtmlRender> </HtmlRender>
{#if !isNaN(grade)} <Grade {gradeMax} {grade} {disableGrading} />
<div class="flex items-center">
<TextField
placeholder="Points"
bind:value={grade}
type="number"
className="w-20"
isDisabled={disableGrading}
/>
<p class="dark:text-white ml-2 text-lg">/ {gradeMax}</p>
</div>
{/if}
</div> </div>
{#if code} {#if code}
@@ -1,13 +1,13 @@
<script> <script lang="ts">
import CodeSnippet from '$lib/components/CodeSnippet/index.svelte'; import CodeSnippet from '$lib/components/CodeSnippet/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import TextArea from '$lib/components/Form/TextArea.svelte'; import TextArea from '$lib/components/Form/TextArea.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte'; import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import Grade from '$lib/components/Question/Grade.svelte';
export let title = ''; export let title = '';
export let code; export let code = '';
export let name = ''; export let name = '';
export let onSubmit = () => {}; export let onSubmit = () => {};
export let onPrevious = () => {}; export let onPrevious = () => {};
@@ -16,7 +16,7 @@
export let isLast = false; export let isLast = false;
export let isPreview = false; export let isPreview = false;
export let disabled = false; export let disabled = false;
export let grade; export let grade: number | undefined;
export let gradeMax = 0; export let gradeMax = 0;
export let disableGrading = false; export let disableGrading = false;
@@ -34,24 +34,13 @@
</script> </script>
<form on:submit|preventDefault={handleFormSubmit}> <form on:submit|preventDefault={handleFormSubmit}>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between mb-2">
<HtmlRender className="mt-4"> <HtmlRender className="mt-4">
<svelte:fragment slot="content"> <svelte:fragment slot="content">
<h3 class={!isNaN(grade) ? 'w-3/4' : ''}>{title}</h3> <h3 class={typeof grade === 'number' ? 'w-3/4' : ''}>{title}</h3>
</svelte:fragment> </svelte:fragment>
</HtmlRender> </HtmlRender>
{#if !isNaN(grade)} <Grade {gradeMax} {grade} {disableGrading} />
<div class="flex items-center">
<TextField
placeholder="Points"
bind:value={grade}
type="number"
className="w-20"
isDisabled={disableGrading}
/>
<p class="dark:text-white ml-2 text-lg">/ {gradeMax}</p>
</div>
{/if}
</div> </div>
{#if code} {#if code}
+3
View File
@@ -1,3 +1,4 @@
import { browser } from '$app/environment';
import type { Course } from '../types'; import type { Course } from '../types';
export const isCourseFree = (cost: number) => !(Number(cost) > 0); export const isCourseFree = (cost: number) => !(Number(cost) > 0);
@@ -18,6 +19,8 @@ export const getStudentInviteLink = (_course: Course, orgSiteName: string) => {
}; };
export const getTextFromHTML = (html: string): string => { export const getTextFromHTML = (html: string): string => {
if (!browser) return html;
const dummyDiv = document.createElement('div'); const dummyDiv = document.createElement('div');
dummyDiv.innerHTML = html; dummyDiv.innerHTML = html;
@@ -8,7 +8,8 @@ export const NOTIFICATION_NAME = {
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' SUBMISSION_UPDATE: 'SUBMISSION UPDATE',
EXERCISE_SUBMISSION_UPDATE: 'EXERCISE SUBMISSION UPDATE'
}; };
const NAME_TO_PATH = { const NAME_TO_PATH = {
@@ -20,7 +21,8 @@ const NAME_TO_PATH = {
[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' [NOTIFICATION_NAME.SUBMISSION_UPDATE]: '/api/email/course/submission_update',
[NOTIFICATION_NAME.EXERCISE_SUBMISSION_UPDATE]: '/api/email/course/exercise_submission_update'
}; };
const getAccessToken = async () => { const getAccessToken = async () => {
+1
View File
@@ -11,6 +11,7 @@ export interface GroupPerson {
profile: Profile; profile: Profile;
profile_id: string; profile_id: string;
role_id: Role['id']; role_id: Role['id'];
fullname?: string;
} }
export interface CustomQuestionType { export interface CustomQuestionType {
@@ -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, exerciseTitle } = 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: `[Submitted]: ${exerciseTitle}`,
content
});
return json({
success: true,
message: 'Email sent'
});
}
@@ -27,7 +27,7 @@ export async function POST({ request }) {
await sendEmail({ await sendEmail({
from: `"${orgName} (via ClassroomIO.com)" <help@classroomio.com>`, from: `"${orgName} (via ClassroomIO.com)" <help@classroomio.com>`,
to, to,
subject: `Submission Update`, subject: 'Submission Update',
content content
}); });
+2 -3
View File
@@ -33,6 +33,7 @@
import { profile } from '$lib/utils/store/user'; import { profile } from '$lib/utils/store/user';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import Avatar from '$lib/components/Avatar/index.svelte'; import Avatar from '$lib/components/Avatar/index.svelte';
import type { GroupPerson } from '$lib/utils/types';
export let data; export let data;
const { courseId } = data; const { courseId } = data;
@@ -62,9 +63,7 @@
async function deletePerson() { async function deletePerson() {
$group.people = $group.people.filter((person: { id: string }) => person.id !== member.id); $group.people = $group.people.filter((person: { id: string }) => person.id !== member.id);
$group.tutors = $group.tutors.filter( $group.tutors = $group.tutors.filter((person: GroupPerson) => person.memberId !== member.id);
(person: { memberId: string }) => person.memberId !== member.id
);
await deleteGroupMember(member.id); await deleteGroupMember(member.id);
} }
@@ -114,12 +114,9 @@
${ ${
status_id == 3 status_id == 3
? `<p>Your score was ${totalMark}/${maxMark}</p> ? `<p>Your score was ${totalMark}/${maxMark}</p>
<div> <a class="button" href="${exerciseLink}">View your Result</a>
<a class="button" href="${exerciseLink}">View your Result</a> `
<div>` : `<a class="button" href="${exerciseLink}">Open Exercise</a>`
: `<div>
<a class="button" href="${exerciseLink}">Open Exercise</a>
<div>`
} }
<p>This exercise is for <strong>${ <p>This exercise is for <strong>${
lessonDetails.title lessonDetails.title
@@ -254,7 +251,7 @@
total: totalPoints total: totalPoints
}).then((res) => console.log('Updated submission', res)); }).then((res) => console.log('Updated submission', res));
snackbar.success(); snackbar.success('Grading saved and student notified');
await Promise.all(updates); await Promise.all(updates);
} }