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 <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>


Merge-request: CLASSROOMIO-MR-109
Merged-by: Best Admin <[email protected]>
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 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="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>
{/if}
</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">
<p class="dark:text-white font-bold w-1/2">Teacher</p>
<p class="dark:text-white">rotimi-best</p>
@@ -19,9 +19,14 @@
import { submitExercise } from '$lib/utils/services/courses';
import { fetchSubmission } from '$lib/utils/services/submissions';
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 = '';
let currentQuestion = {};
@@ -33,10 +38,40 @@
$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) {
const { answers } = $questionnaireMetaData;
const { questions } = $questionnaire;
const prevAnswer = answers[id] || [];
const formattedAnswer =
typeof value === 'string' ? value : removeDuplicate([...prevAnswer, ...(value || [])]);
@@ -69,6 +104,7 @@
$course.id,
getGroupMemberId($group.people, $profile.id)
);
sendEmail();
}
}
@@ -153,7 +189,6 @@
$: $questionnaireMetaData.progressValue = getProgressValue(
$questionnaireMetaData.currentQuestionIndex
);
$: checkForSubmission($group.people, $profile.id, $course.id);
</script>
@@ -69,10 +69,12 @@
</script>
<PageBody bind:isPageNavHidden={isStudent} padding="px-4 overflow-x-hidden">
<ContentSwitcher bind:selectedIndex class="mb-2">
<Switch text={`Questions ${0}`} />
<Switch text="Analytics" />
</ContentSwitcher>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<ContentSwitcher bind:selectedIndex class="mb-2">
<Switch text="Questions" />
<Switch text="Analytics" />
</ContentSwitcher>
</RoleBasedSecurity>
{#if selectedIndex === 0}
<div
@@ -56,7 +56,7 @@
}
function handleCursor(event) {
if (!resize) {
if (!resize && sidebarRef) {
const isNearLeftBorder = event.clientX - sidebarRef.getBoundingClientRect().left < 8;
const isNearRightBorder = sidebarRef.getBoundingClientRect().right - event.clientX < 8;
@@ -69,7 +69,7 @@
}
function startDragging(event) {
if (event.button === 0) {
if (event.button === 0 && sidebarRef) {
event.preventDefault();
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 PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
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 Grade from '$lib/components/Question/Grade.svelte';
export let title = '';
export let code;
@@ -17,7 +17,7 @@
export let disabled = false;
export let isPreview = false;
export let nextButtonProps = {};
export let grade;
export let grade: number | undefined;
export let gradeMax = 0;
export let disableGrading = false;
@@ -63,21 +63,10 @@
<div class="flex items-center justify-between">
<HtmlRender className="mt-4">
<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>
</HtmlRender>
{#if !isNaN(grade)}
<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}
<Grade {gradeMax} {grade} {disableGrading} />
</div>
{#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 PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
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 Grade from '$lib/components/Question/Grade.svelte';
export let title = '';
export let code = '';
@@ -17,7 +17,7 @@
export let disabled = false;
export let isPreview = false;
export let nextButtonProps = {};
export let grade = undefined;
export let grade: number;
export let gradeMax = 0;
export let disableGrading = false;
export let labelClassName = '';
@@ -65,23 +65,12 @@
<div class="flex items-center justify-between">
<HtmlRender className="mt-4">
<svelte:fragment slot="content">
<h3 class="dark:text-white {labelClassName} {!isNaN(grade) && 'w-3/4'}">
{@html title}
<h3 class="dark:text-white {labelClassName} {typeof grade === 'number' && 'w-3/4'}">
{title}
</h3>
</svelte:fragment>
</HtmlRender>
{#if !isNaN(grade)}
<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}
<Grade {gradeMax} {grade} {disableGrading} />
</div>
{#if code}
@@ -1,13 +1,13 @@
<script>
<script lang="ts">
import CodeSnippet from '$lib/components/CodeSnippet/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
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 Grade from '$lib/components/Question/Grade.svelte';
export let title = '';
export let code;
export let code = '';
export let name = '';
export let onSubmit = () => {};
export let onPrevious = () => {};
@@ -16,7 +16,7 @@
export let isLast = false;
export let isPreview = false;
export let disabled = false;
export let grade;
export let grade: number | undefined;
export let gradeMax = 0;
export let disableGrading = false;
@@ -34,24 +34,13 @@
</script>
<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">
<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>
</HtmlRender>
{#if !isNaN(grade)}
<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}
<Grade {gradeMax} {grade} {disableGrading} />
</div>
{#if code}
+3
View File
@@ -1,3 +1,4 @@
import { browser } from '$app/environment';
import type { Course } from '../types';
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 => {
if (!browser) return html;
const dummyDiv = document.createElement('div');
dummyDiv.innerHTML = html;
@@ -8,7 +8,8 @@ export const NOTIFICATION_NAME = {
STUDENT_PROOVE_COURSE_PAYMENT: 'STUDENT PROOVE COURSE PAYMENT',
STUDENT_COURSE_WELCOME: 'STUDENT COURSE WELCOME',
TEACHER_STUDENT_JOINED: 'TEACHER STUDENT JOINED',
SUBMISSION_UPDATE: 'SUBMISSION UPDATE'
SUBMISSION_UPDATE: 'SUBMISSION UPDATE',
EXERCISE_SUBMISSION_UPDATE: 'EXERCISE SUBMISSION UPDATE'
};
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_COURSE_WELCOME]: '/api/email/course/student_welcome',
[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 () => {
+1
View File
@@ -11,6 +11,7 @@ export interface GroupPerson {
profile: Profile;
profile_id: string;
role_id: Role['id'];
fullname?: string;
}
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)" <[email protected]>`,
to,
subject: `[Submitted]: ${exerciseTitle}`,
content
});
return json({
success: true,
message: 'Email sent'
});
}
@@ -27,7 +27,7 @@ export async function POST({ request }) {
await sendEmail({
from: `"${orgName} (via ClassroomIO.com)" <[email protected]>`,
to,
subject: `Submission Update`,
subject: 'Submission Update',
content
});
+2 -3
View File
@@ -33,6 +33,7 @@
import { profile } from '$lib/utils/store/user';
import { snackbar } from '$lib/components/Snackbar/store';
import Avatar from '$lib/components/Avatar/index.svelte';
import type { GroupPerson } from '$lib/utils/types';
export let data;
const { courseId } = data;
@@ -62,9 +63,7 @@
async function deletePerson() {
$group.people = $group.people.filter((person: { id: string }) => person.id !== member.id);
$group.tutors = $group.tutors.filter(
(person: { memberId: string }) => person.memberId !== member.id
);
$group.tutors = $group.tutors.filter((person: GroupPerson) => person.memberId !== member.id);
await deleteGroupMember(member.id);
}
@@ -114,12 +114,9 @@
${
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>`
<a class="button" href="${exerciseLink}">View your Result</a>
`
: `<a class="button" href="${exerciseLink}">Open Exercise</a>`
}
<p>This exercise is for <strong>${
lessonDetails.title
@@ -254,7 +251,7 @@
total: totalPoints
}).then((res) => console.log('Updated submission', res));
snackbar.success();
snackbar.success('Grading saved and student notified');
await Promise.all(updates);
}