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

Co-authored-by: Rotimi Best <rotimiibitoyeemma@gmail.com>


Merge-request: CLASSROOMIO-MR-108
Merged-by: Best Admin <best@digdippa.com>
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 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 TextArea from '$lib/components/Form/TextArea.svelte';
import Checkbox from '$lib/components/Form/Checkbox.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 ErrorMessage from '$lib/components/ErrorMessage/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
@@ -34,10 +34,9 @@
import { deleteExercise } from '$lib/utils/services/courses';
import { lesson } from '../store/lessons';
const extraActions = ['Code Snippets', 'Upload image'];
const initialQuestionsLength = $questionnaire.questions.length;
export let editDescription = false;
export let shouldDeleteExercise = false;
export let exerciseId;
export let goBack = () => {};
@@ -65,21 +64,6 @@
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) {
return errors[question.id] ? errors[question.id][errorKey] : null;
}
@@ -108,13 +92,13 @@
<OrderModal />
<Modal
onClose={() => (editDescription = false)}
bind:open={editDescription}
onClose={() => (shouldDeleteExercise = false)}
bind:open={shouldDeleteExercise}
width="w-2/4"
modalHeading="Update description"
modalHeading="Delete Modal"
>
<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">
<PrimaryButton
@@ -122,7 +106,7 @@
variant={VARIANTS.OUTLINED}
label="No, cancel"
type="submit"
onClick={() => (editDescription = false)}
onClick={() => (shouldDeleteExercise = false)}
/>
<PrimaryButton
className="px-6 py-3"
@@ -153,15 +137,15 @@
question.is_dirty = true;
}}
>
<div class="flex justify-between">
<div class="flex justify-between items-center">
<div class="mr-5 w-3/5">
<AutoGrowTextField
<TextField
placeholder="Question"
bind:value={question.title}
onInput={(e) => {
question.title = e.target?.textContent;
isRequired={true}
onChange={() => {
question.is_dirty = true;
}}
placeholder="Question"
/>
</div>
@@ -7,7 +7,7 @@
export let preview: boolean;
function getTotalPossibleGrade(questions) {
function getTotalPossibleGrade(questions: { points: string }[]) {
return questions.reduce((acc, question) => {
acc += parseFloat(question.points, 10);
return acc;
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { fly } from 'svelte/transition';
import { group, course } from '$lib/components/Course/store';
import { questionnaire } from '../store/exercise';
@@ -19,16 +19,15 @@
import { submitExercise } from '$lib/utils/services/courses';
import { fetchSubmission } from '$lib/utils/services/submissions';
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 renderProps = {};
let submission;
let hasSubmission = null;
let hasSubmission = false;
function handleStart() {
$questionnaireMetaData.currentQuestionIndex += 1;
@@ -92,7 +91,7 @@
}, 0);
}
async function checkForSubmission(people, profileId, courseId) {
async function checkForSubmission(people, profileId: string, courseId: string) {
if (!Array.isArray(people) || !profileId || !courseId || !!submission) {
return;
}
@@ -163,12 +162,12 @@
{/if}
{#if preview}
{#if $exerciseMode.editMode}
<RoleBasedSecurity allowedRoles={[1, 2]}>
<Preview
questions={filterOutDeleted($questionnaire.questions)}
questionnaireMetaData={$questionnaireMetaData}
/>
{/if}
</RoleBasedSecurity>
{:else if !$questionnaire.questions.length}
<Box>
<img src="/images/empty-exercise-icon.svg" alt="Exercise svg" class="my-2.5" />
@@ -1,7 +1,6 @@
<script>
import { onDestroy } from '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 ViewFilledIcon from 'carbon-icons-svelte/lib/ViewFilled.svelte';
import {
@@ -20,44 +19,46 @@
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { upsertExercise } from '$lib/utils/services/courses';
import UpdateDescription from './UpdateDescription.svelte';
import { TrashCan } from 'carbon-icons-svelte';
import { exerciseMode } from './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 isStudent = true;
let preview = false;
let editDescription = false;
let shouldDeleteExercise = false;
let isSaving = false;
let selectedIndex = 0;
async function handleSave() {
if ($exerciseMode.editMode) {
const errors = validateQuestionnaire($questionnaire.questions);
if (Object.values(errors).length > 0) {
return;
}
isSaving = true;
// $exerciseMode.mode = MODES.view;
if (isStudent) return;
reset();
try {
const updatedQuestions = await upsertExercise($questionnaire, exerciseId);
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;
const errors = validateQuestionnaire($questionnaire.questions);
if (Object.values(errors).length > 0) {
return;
}
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(() => {
@@ -67,30 +68,33 @@
$: $questionnaire.questions.length < 1 && handleAddQuestion();
</script>
<PageBody
bind:isPageNavHidden={isStudent}
padding="px-4 overflow-x-hidden"
headerClassName="bg-gray-100 rounded-md mb-3"
>
<svelte:fragment slot="header">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="flex items-center py-2 w-">
{#if $exerciseMode.editMode}
<PageBody bind:isPageNavHidden={isStudent} padding="px-4 overflow-x-hidden">
<ContentSwitcher bind:selectedIndex class="mb-2">
<Switch text={`Questions ${0}`} />
<Switch text="Analytics" />
</ContentSwitcher>
{#if selectedIndex === 0}
<div
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
className="mr-2"
variant={VARIANTS.CONTAINED}
label={'Save'}
label="Save"
onClick={handleSave}
isLoading={isSaving}
/>
{/if}
{#if $exerciseMode.editMode}
<IconButton
onClick={() => (preview = !preview)}
toolTipProps={{ title: 'Toggle preview', direction: 'right' }}
selected={preview}
contained={true}
size="small"
contained={preview}
toolTipProps={{
title: 'Preview',
direction: 'bottom',
hotkeys: []
}}
>
{#if preview}
<ViewFilledIcon size={24} class="carbon-icon dark:text-white" />
@@ -98,50 +102,32 @@
<ViewIcon size={24} class="carbon-icon dark:text-white" />
{/if}
</IconButton>
{/if}
</div>
<IconButton onClick={() => handleAddQuestion()} size="small">
<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>
{#if $exerciseMode.editMode}
<div class="flex items-center">
<IconButton
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 !isStudent && !preview}
<EditMode bind:shouldDeleteExercise {exerciseId} {goBack} />
{:else}
<ViewMode {preview} {exerciseId} />
{/if}
</svelte:fragment>
{#if $exerciseMode.editMode}
<UpdateDescription {preview} {exerciseId} />
{/if}
{#if $exerciseMode.editMode && !preview}
<EditMode bind:editDescription {exerciseId} {goBack} />
{:else}
<ViewMode {preview} {exerciseId} />
{:else if selectedIndex === 1}
<Analytics bind:exerciseId />
{/if}
</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 { lesson } from '../store/lessons';
import { questionnaire, isQuestionnaireFetching } from '../store/exercise';
import { exerciseMode } from '../Exercise/store';
import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte';
import type { ExerciseTemplate } from '$lib/utils/types';
@@ -124,7 +123,6 @@
data.is_due_by_dirty = false;
questionnaire.set(data);
console.log('getExercise', exerciseId, $questionnaire);
isQuestionnaireFetching.update(() => false);
isFetching = false;
@@ -139,8 +137,6 @@
});
$: getExercise(exerciseId);
$: $exerciseMode.editMode = !isStudent;
</script>
{#if isFetching}
@@ -1,5 +1,6 @@
<script lang="ts">
import ToolTip from '../ToolTip/index.svelte';
export let onClick = (v: string) => {};
export let stopPropagation = false;
export let disabled = false;
+1 -1
View File
@@ -19,7 +19,7 @@
>
{#if $$slots.header}
<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" />
</div>
@@ -66,7 +66,7 @@
<HtmlRender className="mt-4">
<svelte:fragment slot="content">
<h3 class="dark:text-white {labelClassName} {!isNaN(grade) && 'w-3/4'}">
{title}
{@html title}
</h3>
</svelte:fragment>
</HtmlRender>
+4 -4
View File
@@ -1,7 +1,7 @@
<script>
export let title;
export let hotkeys = [];
export let direction;
<script lang="ts">
export let title = '';
export let hotkeys: string[] = [];
export let direction = '';
</script>
{#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 type { ExerciseSubmissions } from '$lib/utils/types';
export function fetchSubmissionStatus() {
return supabase.from('submissionstatus').select(`*`);
}
export function fetchSubmissions(course_id) {
export function fetchSubmissions(course_id: string) {
return supabase
.from('submission')
.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
.from('submission')
.select(
`
id,
answers:question_answer(*),
status_id
status_id,
submitted_by:groupmember!inner(
profile!inner(
id,
fullname,
avatar_url
)
)
`
)
.match({
submitted_by: submittedBy,
exercise_id: exerciseId,
course_id: courseId
});
.match(query)
.returns<ExerciseSubmissions[]>();
}
export async function updateSubmission(
{ id, status_id, total }: { id?: number; status_id?: number; total?: number },
otherArgs?: any
) {
const toUpdate = {
const toUpdate: {
status_id?: number;
total?: number;
} = {
status_id
};
@@ -65,6 +97,6 @@ export async function updateSubmission(
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);
}
+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 {