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:
committed by
Space Cloud
co-authored by
Rotimi Best
parent
bfd42a11e9
commit
b31fd5bcf2
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user