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:
committed by
Space Cloud
co-authored by
Rotimi Best
parent
91f28a345f
commit
0f2d60534d
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user