CLASSROOMIO-T-226: Reorganise exercise page + fetch exercise submission

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


Merge-request: CLASSROOMIO-MR-108
Merged-by: Best Admin <[email protected]>
This commit is contained in:
Rotimi Best
2023-10-19 08:31:57 +00:00
committed by Space Cloud
co-authored by Rotimi Best
parent bfd42a11e9
commit b31fd5bcf2
13 changed files with 193 additions and 149 deletions
@@ -0,0 +1,31 @@
<script lang="ts">
import { fetchSubmission } from '$lib/utils/services/submissions';
import type { ExerciseSubmissions } from '$lib/utils/types';
export let exerciseId: string;
let isLoading = false;
let analyticsData: ExerciseSubmissions[] = [];
async function fetchAnalyticsData(id: string | undefined) {
console.log('id', id);
if (!id) return;
const { data } = await fetchSubmission({
exerciseId: id
});
if (!data) return;
// This is the data you need, look at the typescript interface
analyticsData = data;
}
$: fetchAnalyticsData(exerciseId);
</script>
{#if isLoading}
Loading data
{:else}
Analytics data
{/if}
@@ -17,11 +17,11 @@
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte'; import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte'; import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import Modal from '$lib/components/Modal/index.svelte'; import Modal from '$lib/components/Modal/index.svelte';
import TextArea from '$lib/components/Form/TextArea.svelte'; import TextArea from '$lib/components/Form/TextArea.svelte';
import Checkbox from '$lib/components/Form/Checkbox.svelte'; import Checkbox from '$lib/components/Form/Checkbox.svelte';
import RadioItem from '$lib/components/Form/RadioItem.svelte'; import RadioItem from '$lib/components/Form/RadioItem.svelte';
import AutoGrowTextField from '$lib/components/Form/AutoGrowTextField.svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import ErrorMessage from '$lib/components/ErrorMessage/index.svelte'; import ErrorMessage from '$lib/components/ErrorMessage/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
@@ -34,10 +34,9 @@
import { deleteExercise } from '$lib/utils/services/courses'; import { deleteExercise } from '$lib/utils/services/courses';
import { lesson } from '../store/lessons'; import { lesson } from '../store/lessons';
const extraActions = ['Code Snippets', 'Upload image'];
const initialQuestionsLength = $questionnaire.questions.length; const initialQuestionsLength = $questionnaire.questions.length;
export let editDescription = false; export let shouldDeleteExercise = false;
export let exerciseId; export let exerciseId;
export let goBack = () => {}; export let goBack = () => {};
@@ -65,21 +64,6 @@
handleRemoveQuestion(questionIdToDelete)(); handleRemoveQuestion(questionIdToDelete)();
} }
function handleOptionClick(questionIndex) {
return (actionIndex) => {
switch (actionIndex) {
case 0:
handleCode(questionIndex, true);
break;
case 1:
// uploadImage(questionIndex);
break;
default:
console.log('No option');
}
};
}
function getQuestionErrorMsg(errors, question, errorKey) { function getQuestionErrorMsg(errors, question, errorKey) {
return errors[question.id] ? errors[question.id][errorKey] : null; return errors[question.id] ? errors[question.id][errorKey] : null;
} }
@@ -108,13 +92,13 @@
<OrderModal /> <OrderModal />
<Modal <Modal
onClose={() => (editDescription = false)} onClose={() => (shouldDeleteExercise = false)}
bind:open={editDescription} bind:open={shouldDeleteExercise}
width="w-2/4" width="w-2/4"
modalHeading="Update description" modalHeading="Delete Modal"
> >
<form on:submit|preventDefault> <form on:submit|preventDefault>
<h1 class="dark:text-white text-2xl">Are you sure?</h1> <h1 class="dark:text-white text-xl">Are you sure you want to delete this exercise?</h1>
<div class="mt-5 flex items-center justify-between"> <div class="mt-5 flex items-center justify-between">
<PrimaryButton <PrimaryButton
@@ -122,7 +106,7 @@
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
label="No, cancel" label="No, cancel"
type="submit" type="submit"
onClick={() => (editDescription = false)} onClick={() => (shouldDeleteExercise = false)}
/> />
<PrimaryButton <PrimaryButton
className="px-6 py-3" className="px-6 py-3"
@@ -153,15 +137,15 @@
question.is_dirty = true; question.is_dirty = true;
}} }}
> >
<div class="flex justify-between"> <div class="flex justify-between items-center">
<div class="mr-5 w-3/5"> <div class="mr-5 w-3/5">
<AutoGrowTextField <TextField
placeholder="Question"
bind:value={question.title} bind:value={question.title}
onInput={(e) => { isRequired={true}
question.title = e.target?.textContent; onChange={() => {
question.is_dirty = true; question.is_dirty = true;
}} }}
placeholder="Question"
/> />
</div> </div>
@@ -7,7 +7,7 @@
export let preview: boolean; export let preview: boolean;
function getTotalPossibleGrade(questions) { function getTotalPossibleGrade(questions: { points: string }[]) {
return questions.reduce((acc, question) => { return questions.reduce((acc, question) => {
acc += parseFloat(question.points, 10); acc += parseFloat(question.points, 10);
return acc; return acc;
@@ -1,4 +1,4 @@
<script> <script lang="ts">
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { group, course } from '$lib/components/Course/store'; import { group, course } from '$lib/components/Course/store';
import { questionnaire } from '../store/exercise'; import { questionnaire } from '../store/exercise';
@@ -19,16 +19,15 @@
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 { exerciseMode } from './store';
export let preview; export let preview = false;
export let exerciseId; export let exerciseId = '';
let currentQuestion = {}; let currentQuestion = {};
let renderProps = {}; let renderProps = {};
let submission; let submission;
let hasSubmission = null; let hasSubmission = false;
function handleStart() { function handleStart() {
$questionnaireMetaData.currentQuestionIndex += 1; $questionnaireMetaData.currentQuestionIndex += 1;
@@ -92,7 +91,7 @@
}, 0); }, 0);
} }
async function checkForSubmission(people, profileId, courseId) { async function checkForSubmission(people, profileId: string, courseId: string) {
if (!Array.isArray(people) || !profileId || !courseId || !!submission) { if (!Array.isArray(people) || !profileId || !courseId || !!submission) {
return; return;
} }
@@ -163,12 +162,12 @@
{/if} {/if}
{#if preview} {#if preview}
{#if $exerciseMode.editMode} <RoleBasedSecurity allowedRoles={[1, 2]}>
<Preview <Preview
questions={filterOutDeleted($questionnaire.questions)} questions={filterOutDeleted($questionnaire.questions)}
questionnaireMetaData={$questionnaireMetaData} questionnaireMetaData={$questionnaireMetaData}
/> />
{/if} </RoleBasedSecurity>
{:else if !$questionnaire.questions.length} {:else if !$questionnaire.questions.length}
<Box> <Box>
<img src="/images/empty-exercise-icon.svg" alt="Exercise svg" class="my-2.5" /> <img src="/images/empty-exercise-icon.svg" alt="Exercise svg" class="my-2.5" />
@@ -1,7 +1,6 @@
<script> <script>
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import AddAltIcon from 'carbon-icons-svelte/lib/AddAlt.svelte'; import AddAltIcon from 'carbon-icons-svelte/lib/AddAlt.svelte';
import TableIcon from 'carbon-icons-svelte/lib/Table.svelte';
import ViewIcon from 'carbon-icons-svelte/lib/View.svelte'; import ViewIcon from 'carbon-icons-svelte/lib/View.svelte';
import ViewFilledIcon from 'carbon-icons-svelte/lib/ViewFilled.svelte'; import ViewFilledIcon from 'carbon-icons-svelte/lib/ViewFilled.svelte';
import { import {
@@ -20,44 +19,46 @@
import { VARIANTS } from '$lib/components/PrimaryButton/constants'; import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { upsertExercise } from '$lib/utils/services/courses'; import { upsertExercise } from '$lib/utils/services/courses';
import UpdateDescription from './UpdateDescription.svelte'; import UpdateDescription from './UpdateDescription.svelte';
import { TrashCan } from 'carbon-icons-svelte';
import { exerciseMode } from './store';
import { snackbar } from '$lib/components/Snackbar/store'; import { snackbar } from '$lib/components/Snackbar/store';
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
import Analytics from './Analytics/index.svelte';
import { ContentSwitcher, Switch } from 'carbon-components-svelte';
export let exerciseId; export let exerciseId = '';
export let goBack = () => {}; export let goBack = () => {};
export let isStudent = true; export let isStudent = true;
let preview = false; let preview = false;
let editDescription = false; let shouldDeleteExercise = false;
let isSaving = false; let isSaving = false;
let selectedIndex = 0;
async function handleSave() { async function handleSave() {
if ($exerciseMode.editMode) { if (isStudent) return;
const errors = validateQuestionnaire($questionnaire.questions);
if (Object.values(errors).length > 0) {
return;
}
isSaving = true;
// $exerciseMode.mode = MODES.view;
reset(); const errors = validateQuestionnaire($questionnaire.questions);
try { if (Object.values(errors).length > 0) {
const updatedQuestions = await upsertExercise($questionnaire, exerciseId); return;
questionnaire.update((q) => ({
...q,
is_title_dirty: false,
is_description_dirty: false,
questions: updatedQuestions
}));
snackbar.success('Saved Successfully');
} catch (error) {
console.error(error);
snackbar.error();
}
isSaving = false;
} }
isSaving = true;
reset();
try {
const updatedQuestions = await upsertExercise($questionnaire, exerciseId);
questionnaire.update((q) => ({
...q,
is_title_dirty: false,
is_description_dirty: false,
// @ts-ignore
questions: updatedQuestions
}));
snackbar.success('Saved Successfully');
} catch (error) {
console.error(error);
snackbar.error();
}
isSaving = false;
} }
onDestroy(() => { onDestroy(() => {
@@ -67,30 +68,33 @@
$: $questionnaire.questions.length < 1 && handleAddQuestion(); $: $questionnaire.questions.length < 1 && handleAddQuestion();
</script> </script>
<PageBody <PageBody bind:isPageNavHidden={isStudent} padding="px-4 overflow-x-hidden">
bind:isPageNavHidden={isStudent} <ContentSwitcher bind:selectedIndex class="mb-2">
padding="px-4 overflow-x-hidden" <Switch text={`Questions ${0}`} />
headerClassName="bg-gray-100 rounded-md mb-3" <Switch text="Analytics" />
> </ContentSwitcher>
<svelte:fragment slot="header">
<RoleBasedSecurity allowedRoles={[1, 2]}> {#if selectedIndex === 0}
<div class="flex items-center py-2 w-"> <div
{#if $exerciseMode.editMode} class="top-0 z-10 bg-gray-100 rounded-md mb-3 dark:bg-neutral-800 flex items-center justify-end sticky right-0 w-full px-3"
>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="flex items-center">
<PrimaryButton <PrimaryButton
className="mr-2" className="mr-2"
variant={VARIANTS.CONTAINED} variant={VARIANTS.CONTAINED}
label={'Save'} label="Save"
onClick={handleSave} onClick={handleSave}
isLoading={isSaving} isLoading={isSaving}
/> />
{/if}
{#if $exerciseMode.editMode}
<IconButton <IconButton
onClick={() => (preview = !preview)} onClick={() => (preview = !preview)}
toolTipProps={{ title: 'Toggle preview', direction: 'right' }} contained={preview}
selected={preview} toolTipProps={{
contained={true} title: 'Preview',
size="small" direction: 'bottom',
hotkeys: []
}}
> >
{#if preview} {#if preview}
<ViewFilledIcon size={24} class="carbon-icon dark:text-white" /> <ViewFilledIcon size={24} class="carbon-icon dark:text-white" />
@@ -98,50 +102,32 @@
<ViewIcon size={24} class="carbon-icon dark:text-white" /> <ViewIcon size={24} class="carbon-icon dark:text-white" />
{/if} {/if}
</IconButton> </IconButton>
{/if} <IconButton onClick={() => handleAddQuestion()} size="small">
</div> <AddAltIcon size={24} class="carbon-icon dark:text-white" />
</IconButton>
<OverflowMenu flipped>
<OverflowMenuItem
text="Reorder Questions"
on:click={() => ($questionnaireOrder.open = true)}
/>
<OverflowMenuItem
danger
text="Delete Exercise"
on:click={() => (shouldDeleteExercise = true)}
/>
</OverflowMenu>
</div>
</RoleBasedSecurity>
</div>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<UpdateDescription {preview} />
</RoleBasedSecurity> </RoleBasedSecurity>
{#if !isStudent && !preview}
{#if $exerciseMode.editMode} <EditMode bind:shouldDeleteExercise {exerciseId} {goBack} />
<div class="flex items-center"> {:else}
<IconButton <ViewMode {preview} {exerciseId} />
onClick={() => handleAddQuestion()}
toolTipProps={{ title: 'Add question', direction: 'bottom' }}
size="small"
>
<AddAltIcon size={24} class="carbon-icon dark:text-white" />
</IconButton>
<IconButton
onClick={() => ($questionnaireOrder.open = true)}
toolTipProps={{ title: 'Order questionnaire', direction: 'bottom' }}
size="small"
>
<TableIcon size={24} class="carbon-icon dark:text-white" />
</IconButton>
<IconButton
onClick={() => (editDescription = true)}
toolTipProps={{ title: 'Delete Exercise', direction: 'bottom' }}
size="small"
>
<TrashCan size={24} class="carbon-icon dark:text-white" />
</IconButton>
<!-- <IconButton
onClick={handleDeleteExercise}
toolTipProps={{ title: 'Delete questionnaire', direction: 'bottom' }}
>
<Delete32 class="carbon-icon dark:text-white" />
</IconButton> -->
</div>
{/if} {/if}
</svelte:fragment> {:else if selectedIndex === 1}
<Analytics bind:exerciseId />
{#if $exerciseMode.editMode}
<UpdateDescription {preview} {exerciseId} />
{/if}
{#if $exerciseMode.editMode && !preview}
<EditMode bind:editDescription {exerciseId} {goBack} />
{:else}
<ViewMode {preview} {exerciseId} />
{/if} {/if}
</PageBody> </PageBody>
@@ -1,5 +0,0 @@
import { writable } from 'svelte/store';
export const exerciseMode = writable({
editMode: true
});
@@ -13,7 +13,6 @@
import { QUESTION_TYPES } from '$lib/components/Question/constants'; import { QUESTION_TYPES } from '$lib/components/Question/constants';
import { lesson } from '../store/lessons'; import { lesson } from '../store/lessons';
import { questionnaire, isQuestionnaireFetching } from '../store/exercise'; import { questionnaire, isQuestionnaireFetching } from '../store/exercise';
import { exerciseMode } from '../Exercise/store';
import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte'; import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte';
import type { ExerciseTemplate } from '$lib/utils/types'; import type { ExerciseTemplate } from '$lib/utils/types';
@@ -124,7 +123,6 @@
data.is_due_by_dirty = false; data.is_due_by_dirty = false;
questionnaire.set(data); questionnaire.set(data);
console.log('getExercise', exerciseId, $questionnaire);
isQuestionnaireFetching.update(() => false); isQuestionnaireFetching.update(() => false);
isFetching = false; isFetching = false;
@@ -139,8 +137,6 @@
}); });
$: getExercise(exerciseId); $: getExercise(exerciseId);
$: $exerciseMode.editMode = !isStudent;
</script> </script>
{#if isFetching} {#if isFetching}
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import ToolTip from '../ToolTip/index.svelte'; import ToolTip from '../ToolTip/index.svelte';
export let onClick = (v: string) => {}; export let onClick = (v: string) => {};
export let stopPropagation = false; export let stopPropagation = false;
export let disabled = false; export let disabled = false;
+1 -1
View File
@@ -19,7 +19,7 @@
> >
{#if $$slots.header} {#if $$slots.header}
<div <div
class="head dark:bg-black flex items-center justify-between sticky right-0 w-full px-3 {headerClassName}" class="head dark:bg-neutral-800 flex items-center justify-between sticky right-0 w-full px-3 {headerClassName}"
> >
<slot name="header" /> <slot name="header" />
</div> </div>
@@ -66,7 +66,7 @@
<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} {!isNaN(grade) && 'w-3/4'}">
{title} {@html title}
</h3> </h3>
</svelte:fragment> </svelte:fragment>
</HtmlRender> </HtmlRender>
+4 -4
View File
@@ -1,7 +1,7 @@
<script> <script lang="ts">
export let title; export let title = '';
export let hotkeys = []; export let hotkeys: string[] = [];
export let direction; export let direction = '';
</script> </script>
{#if !!title} {#if !!title}
+42 -10
View File
@@ -1,10 +1,12 @@
import type { PostgrestSingleResponse } from '@supabase/supabase-js';
import { supabase } from '$lib/utils/functions/supabase'; import { supabase } from '$lib/utils/functions/supabase';
import type { ExerciseSubmissions } from '$lib/utils/types';
export function fetchSubmissionStatus() { export function fetchSubmissionStatus() {
return supabase.from('submissionstatus').select(`*`); return supabase.from('submissionstatus').select(`*`);
} }
export function fetchSubmissions(course_id) { export function fetchSubmissions(course_id: string) {
return supabase return supabase
.from('submission') .from('submission')
.select( .select(
@@ -33,28 +35,58 @@ export function fetchSubmissions(course_id) {
}); });
} }
export async function fetchSubmission({ courseId, exerciseId, submittedBy }) { export async function fetchSubmission({
courseId,
exerciseId,
submittedBy
}: {
exerciseId: string;
courseId?: string;
submittedBy?: string;
}) {
const query: {
exercise_id: string;
course_id?: string;
submitted_by?: string;
} = {
exercise_id: exerciseId
};
if (courseId) {
query.course_id = courseId;
}
if (submittedBy) {
query.submitted_by = submittedBy;
}
return supabase return supabase
.from('submission') .from('submission')
.select( .select(
` `
id, id,
answers:question_answer(*), answers:question_answer(*),
status_id status_id,
submitted_by:groupmember!inner(
profile!inner(
id,
fullname,
avatar_url
)
)
` `
) )
.match({ .match(query)
submitted_by: submittedBy, .returns<ExerciseSubmissions[]>();
exercise_id: exerciseId,
course_id: courseId
});
} }
export async function updateSubmission( export async function updateSubmission(
{ id, status_id, total }: { id?: number; status_id?: number; total?: number }, { id, status_id, total }: { id?: number; status_id?: number; total?: number },
otherArgs?: any otherArgs?: any
) { ) {
const toUpdate = { const toUpdate: {
status_id?: number;
total?: number;
} = {
status_id status_id
}; };
@@ -65,6 +97,6 @@ export async function updateSubmission(
return supabase.from('submission').update(toUpdate, otherArgs).match({ id }); return supabase.from('submission').update(toUpdate, otherArgs).match({ id });
} }
export async function updateQuestionAnswer(update, match) { export async function updateQuestionAnswer(update: any, match: any) {
return supabase.from('question_answer').update(update).match(match); return supabase.from('question_answer').update(update).match(match);
} }
+20
View File
@@ -35,6 +35,26 @@ export interface ExerciseTemplate {
}[]; }[];
}; };
} }
export interface ExerciseSubmissions {
id: string;
status_id: number;
submitted_by: {
profile: {
id: string;
fullname: string;
avatar_url: string;
};
};
answers: {
answers: string[];
group_member_id: string;
id: number;
open_answer: string;
point: number;
question_id: number;
submission_id: string;
}[];
}
//=========================================== //===========================================
interface CourseMetadata { interface CourseMetadata {