Grade home works

This commit is contained in:
rotimi-best
2021-09-04 22:27:00 +03:00
parent 030fd24fe1
commit b94db946a9
17 changed files with 376 additions and 123 deletions
@@ -1,32 +1,35 @@
<script> <script>
import { onMount } from 'svelte';
import Modal from '../../../../Modal/index.svelte'; import Modal from '../../../../Modal/index.svelte';
import Select from '../../../../Form/Select.svelte'; import Select from '../../../../Form/Select.svelte';
import PrimaryButton from '../../../../PrimaryButton/index.svelte';
import { VARIANTS } from '../../../../PrimaryButton/constants';
import Preview from './Preview.svelte'; import Preview from './Preview.svelte';
import { SELECTABLE_STATUS } from './constants'; import { SELECTABLE_STATUS } from './constants';
import { snackbarStore } from '../../../../Snackbar/store'; import { snackbarStore } from '../../../../Snackbar/store';
import { onMount } from 'svelte';
export let open; export let open = false;
export let onClose; export let onClose = () => {};
export let handleSave = () => {};
// export let submissionId; // export let submissionId;
export let data = {}; export let data = {};
export let status = SELECTABLE_STATUS[0]; export let updateStatus = () => {};
let status = SELECTABLE_STATUS[0];
let grades = {};
let total = 0; let total = 0;
let gradeTotal = 0; let maxPoints = 0;
function setGrades(data) { function getMaxPoints(questions) {
grades = (data.questions || []).reduce( return (questions || []).reduce(
(acc, question) => ({ ...acc, [question.name]: question.points }), (acc, question) => acc + question.points,
{} 0
); );
gradeTotal = calculateTotal(grades);
} }
function calculateTotal(grades) { function calculateTotal(grades) {
if (!grades) return 0;
return Object.values(grades).reduce( return Object.values(grades).reduce(
(acc, grade) => acc + parseInt(grade), (acc, grade) => acc + parseInt(grade),
0 0
@@ -34,21 +37,48 @@
} }
function handleStatusChange() { function handleStatusChange() {
console.log(`status`, status); updateStatus({
submissionId: data.id,
prevStatusId: data.status_id,
nextStatusId: status.value,
total,
});
$snackbarStore.open = true; $snackbarStore.open = true;
$snackbarStore.message = `Submission updated to '${status.label}'`; $snackbarStore.message = `Submission updated to '${status.label}'`;
} }
function setStatus(data) {
status = SELECTABLE_STATUS.find(
(status) => status.value === data.status_id
);
}
function getStatusColor(status) {
// if (!status)
return '';
// switch (status.value) {
// case STATUS.SUBMITTED:
// return '';
// case STATUS.IN_PROGRESS:
// return 'text-white bg-blue-700';
// case STATUS.GRADED:
// return 'text-white bg-green-700';
// }
}
onMount(() => { onMount(() => {
console.log('mounting'); console.log('mounting');
}); });
$: setGrades(data); $: total = calculateTotal(data.questionAnswerByPoint);
$: total = calculateTotal(grades); $: maxPoints = getMaxPoints(data.questions);
$: setStatus(data);
</script> </script>
<Modal <Modal
modalHeading="Lesson 1: How to use props in React.js" modalHeading={data.title}
bind:open bind:open
{onClose} {onClose}
width="w-4/5" width="w-4/5"
@@ -61,16 +91,27 @@
answers: data.answers || {}, answers: data.answers || {},
isFinished: true, isFinished: true,
}} }}
bind:grades bind:grades={data.questionAnswerByPoint}
/> />
</div> </div>
<div class="ml-10 w-1/3"> <div class="ml-4 w-2/5 sticky top-0">
<Select <div class="flex items-center justify-between">
bind:value={status} <Select
options={SELECTABLE_STATUS} bind:value={status}
color="primary" options={SELECTABLE_STATUS}
onChange={handleStatusChange} selectClassName={getStatusColor(status)}
/> onChange={handleStatusChange}
/>
<PrimaryButton
onClick={() => {
handleSave(data);
}}
label="Save"
variant={VARIANTS.CONTAINED_SUCCESS}
className="py-2 px-8"
/>
</div>
<div class="border border-gray-300 rounded-md mt-2"> <div class="border border-gray-300 rounded-md mt-2">
<div <div
class="hover:bg-gray-100 border-b border-t-0 border-l-0 border-r-0 border-gray-300 p-3" class="hover:bg-gray-100 border-b border-t-0 border-l-0 border-r-0 border-gray-300 p-3"
@@ -79,24 +120,31 @@
</div> </div>
<div class="flex items-center text-sm p-3"> <div class="flex items-center text-sm p-3">
<p class="font-bold w-1/2">Total grade</p> <p class="font-bold w-2/5">Total grade</p>
<p>{total} / {gradeTotal}</p> <p>{total} / {maxPoints}</p>
</div> </div>
<div class="flex items-center text-sm p-3"> <!-- <div class="flex items-center text-sm p-3">
<p class="font-bold w-1/2">Status</p> <p class="font-bold w-1/2">Status</p>
<div class="flex items-center"> <div class="flex items-center">
<p class="rounded-full w-5 h-5 bg-yellow-300 mr-2" /> <p class="rounded-full w-5 h-5 bg-yellow-300 mr-2" />
<p>Grading</p> <p>Grading</p>
</div> </div>
</div> </div> -->
<div class="flex items-center text-sm p-3"> <div class="flex items-center text-sm p-3">
<p class="font-bold w-1/2">Student</p> <p class="font-bold w-2/5">Student</p>
<p>Jeremy Matt</p> {#if data.student}
<img
alt="Student avatar"
class="block rounded-full h-6 w-6"
src={data.student.avatar_url}
/>
<p class="ml-2 text-sm">{data.student.fullname}</p>
{/if}
</div> </div>
<div class="flex items-center text-sm p-3"> <!-- <div class="flex items-center text-sm p-3">
<p class="font-bold w-1/2">Teacher</p> <p class="font-bold w-1/2">Teacher</p>
<p>rotimi-best</p> <p>rotimi-best</p>
</div> </div> -->
</div> </div>
</div> </div>
</Modal> </Modal>
@@ -11,6 +11,7 @@
export let onSubmit = () => {}; export let onSubmit = () => {};
export let onPrevious = () => {}; export let onPrevious = () => {};
export let handleGrade = () => {}; export let handleGrade = () => {};
export let disableGrading = false;
</script> </script>
{#each questions as currentQuestion, currentQuestionIndex} {#each questions as currentQuestion, currentQuestionIndex}
@@ -25,9 +26,10 @@
onPrevious, onPrevious,
true true
)} )}
bind:grade={grades[currentQuestion.name]} bind:grade={grades[currentQuestion.id]}
gradeMax={currentQuestion.points} gradeMax={currentQuestion.points}
handleGrade={handleGrade(currentQuestion.name)} handleGrade={handleGrade(currentQuestion.id)}
{disableGrading}
/> />
{:else if QUESTION_TYPE.CHECKBOX === currentQuestion.question_type.id} {:else if QUESTION_TYPE.CHECKBOX === currentQuestion.question_type.id}
<CheckboxQuestion <CheckboxQuestion
@@ -40,9 +42,10 @@
onPrevious, onPrevious,
true true
)} )}
bind:grade={grades[currentQuestion.name]} bind:grade={grades[currentQuestion.id]}
gradeMax={currentQuestion.points} gradeMax={currentQuestion.points}
handleGrade={handleGrade(currentQuestion.name)} handleGrade={handleGrade(currentQuestion.id)}
{disableGrading}
/> />
{:else if QUESTION_TYPE.TEXTAREA === currentQuestion.question_type.id} {:else if QUESTION_TYPE.TEXTAREA === currentQuestion.question_type.id}
<TextareaQuestion <TextareaQuestion
@@ -55,9 +58,10 @@
onPrevious, onPrevious,
true true
)} )}
bind:grade={grades[currentQuestion.name]} bind:grade={grades[currentQuestion.id]}
gradeMax={currentQuestion.points} gradeMax={currentQuestion.points}
handleGrade={handleGrade(currentQuestion.name)} handleGrade={handleGrade(currentQuestion.id)}
{disableGrading}
/> />
{/if} {/if}
{/each} {/each}
@@ -13,7 +13,9 @@
import { QUESTION_TYPE } from '../../../../Question/constants'; import { QUESTION_TYPE } from '../../../../Question/constants';
import { STATUS } from './constants'; import { STATUS } from './constants';
import { getPropsForQuestion, filterOutDeleted } from './functions'; import { getPropsForQuestion, filterOutDeleted } from './functions';
import { formatAnswers } from '../../../function';
import { submitExercise } from '../../../../../utils/services/courses'; import { submitExercise } from '../../../../../utils/services/courses';
import { fetchSubmission } from '../../../../../utils/services/submissions';
import { profile } from '../../../../../utils/store/user'; import { profile } from '../../../../../utils/store/user';
export let preview; export let preview;
@@ -21,6 +23,15 @@
let currentQuestion = {}; let currentQuestion = {};
let renderProps = {}; let renderProps = {};
let submission;
function getGroupMemberId(people, profileId) {
const groupMember = people.find(
(person) => person.profile_id === profileId
);
return groupMember ? groupMember.id : null;
}
function handleStart() { function handleStart() {
$questionnaireMetaData.currentQuestionIndex += 1; $questionnaireMetaData.currentQuestionIndex += 1;
@@ -55,13 +66,13 @@
// If last question send to server // If last question send to server
if (isFinished) { if (isFinished) {
$questionnaireMetaData.status = 1;
submitExercise( submitExercise(
$questionnaireMetaData.answers, $questionnaireMetaData.answers,
questions, questions,
exerciseId, exerciseId,
$course.id, $course.id,
$group, getGroupMemberId($group.people, $profile.id)
$profile
); );
} }
} }
@@ -70,15 +81,6 @@
$questionnaireMetaData.currentQuestionIndex -= 1; $questionnaireMetaData.currentQuestionIndex -= 1;
} }
function getTotalScores() {
let total = 0;
for (const score in $questionnaireMetaData.scores) {
total += parseInt($questionnaireMetaData.scores[score]);
}
return total;
}
function getProgressValue(currentQuestionIndex) { function getProgressValue(currentQuestionIndex) {
if ($questionnaireMetaData.isFinished) { if ($questionnaireMetaData.isFinished) {
return 100; return 100;
@@ -91,6 +93,55 @@
); );
} }
function getTotalPossibleGrade() {
return $questionnaire.questions.reduce((acc, question) => {
acc += question.points;
return acc;
}, 0);
}
async function checkForSubmission(people, profileId, courseId) {
console.log('checkForSubmission');
if (!Array.isArray(people) || !profileId || !courseId || !!submission) {
return;
}
const args = {
exerciseId,
courseId,
submittedBy: getGroupMemberId(people, profileId),
};
const { data } = await fetchSubmission(args);
if (Array.isArray(data) && data.length) {
console.log(`submissions`, data);
submission = data[0];
$questionnaireMetaData.answers = formatAnswers({
questions: $questionnaire.questions,
answers: submission.answers,
});
$questionnaireMetaData.totalPossibleGrade = getTotalPossibleGrade();
$questionnaireMetaData.currentQuestionIndex =
$questionnaire.questions.length;
$questionnaireMetaData.isFinished = true;
$questionnaireMetaData.status = submission.status_id;
$questionnaireMetaData.finalTotalGrade = 0;
$questionnaireMetaData.grades = submission.answers.reduce(
(acc, answer) => {
acc[answer.question_id] = answer.point;
$questionnaireMetaData.finalTotalGrade += answer.point;
return acc;
},
{}
);
}
}
$: { $: {
currentQuestion = currentQuestion =
$questionnaire.questions[$questionnaireMetaData.currentQuestionIndex - 1]; $questionnaire.questions[$questionnaireMetaData.currentQuestionIndex - 1];
@@ -114,6 +165,8 @@
$: $questionnaireMetaData.progressValue = getProgressValue( $: $questionnaireMetaData.progressValue = getProgressValue(
$questionnaireMetaData.currentQuestionIndex $questionnaireMetaData.currentQuestionIndex
); );
$: checkForSubmission($group.people, $profile.id, $course.id);
</script> </script>
{#if !preview && $questionnaire.questions.length && !$questionnaireMetaData.isFinished} {#if !preview && $questionnaire.questions.length && !$questionnaireMetaData.isFinished}
@@ -137,7 +190,20 @@
{:else if $questionnaireMetaData.currentQuestionIndex === 0} {:else if $questionnaireMetaData.currentQuestionIndex === 0}
<div> <div>
<h2 class="my-1">{$questionnaire.title}</h2> <h2 class="my-1">{$questionnaire.title}</h2>
<p>{$questionnaire.description}</p>
<div class="flex items-center">
<p class="mx-2">
<strong>{$questionnaire.questions.length}</strong> questions
</p>
|
<p class="mx-2">
<strong>{getTotalPossibleGrade()}</strong> points.
</p>
|
<p class="mx-2">All required</p>
</div>
<p class="text-lg mt-3">{$questionnaire.description}</p>
<PrimaryButton <PrimaryButton
onClick={handleStart} onClick={handleStart}
@@ -150,20 +216,20 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex flex-col justify-between"> <div class="flex flex-col justify-between">
<h2 class="text-xl mb-2 mt-0">{$questionnaire.title}</h2> <h2 class="text-xl mb-2 mt-0">{$questionnaire.title}</h2>
{#if STATUS.PENDING === $questionnaireMetaData.status} {#if STATUS.GRADED === $questionnaireMetaData.status}
<span
class="bg-yellow-600 text-white rounded-full py-2 px-6 text-center"
title="Status: Pending Review"
>
Pending
</span>
{:else if STATUS.GRADED === $questionnaireMetaData.status && getTotalScores() > 0}
<span <span
class="bg-green-700 text-white rounded-full py-2 px-6 text-center" class="bg-green-700 text-white rounded-full py-2 px-6 text-center"
title="Status: Pending Review" title="Status: Pending Review"
> >
Graded Graded
</span> </span>
{:else}
<span
class="bg-yellow-600 text-white rounded-full py-2 px-6 text-center"
title="Status: Pending Review"
>
Pending
</span>
{/if} {/if}
</div> </div>
@@ -171,12 +237,14 @@
class="border-2 border-gray-700 rounded-full h-24 w-24 flex items-center justify-center text-2xl" class="border-2 border-gray-700 rounded-full h-24 w-24 flex items-center justify-center text-2xl"
title="Status: Pending Review" title="Status: Pending Review"
> >
{getTotalScores()}/100 {$questionnaireMetaData.finalTotalGrade}/{$questionnaireMetaData.totalPossibleGrade}
</span> </span>
</div> </div>
<Preview <Preview
questions={$questionnaire.questions} questions={$questionnaire.questions}
questionnaireMetaData={$questionnaireMetaData} questionnaireMetaData={$questionnaireMetaData}
grades={$questionnaireMetaData.grades}
disableGrading={true}
/> />
{:else if currentQuestion} {:else if currentQuestion}
{#key currentQuestion.id} {#key currentQuestion.id}
@@ -1,13 +1,13 @@
export const STATUS = { export const STATUS = {
PENDING: 0, SUBMITTED: 1,
IN_PROGRESS: 1, IN_PROGRESS: 2,
GRADED: 2, GRADED: 3,
}; };
export const SELECTABLE_STATUS = [ export const SELECTABLE_STATUS = [
{ {
label: 'Submitted', label: 'Submitted',
value: STATUS.PENDING, value: STATUS.SUBMITTED,
}, },
{ {
label: 'In progress', label: 'In progress',
@@ -13,6 +13,9 @@ import { STATUS } from '../Exercise/constants';
const initAnswerState = { const initAnswerState = {
answers: {}, answers: {},
scores: {}, scores: {},
grades: {},
totalPossibleGrade: 0,
finalTotalGrade: 0,
currentQuestionIndex: 0, currentQuestionIndex: 0,
isFinished: false, isFinished: false,
progressValue: 100, progressValue: 100,
+21 -1
View File
@@ -1,4 +1,4 @@
import { ROUTES } from "./constants"; import { ROUTES } from './constants';
export function getNavItemRoute(courseId, routeId) { export function getNavItemRoute(courseId, routeId) {
const path = `/${ROUTES.COURSES}/${courseId}`; const path = `/${ROUTES.COURSES}/${courseId}`;
@@ -27,3 +27,23 @@ export function getLectureNo(index) {
return `0${index}`; return `0${index}`;
} }
export function formatAnswers(data) {
const answers = {};
const questionByIdAndName = {};
for (const question of data.questions) {
questionByIdAndName[question.id] = question.name;
}
for (const answer of data.answers) {
const questionName = questionByIdAndName[answer.question_id];
answers[questionName] =
Array.isArray(answer.answers) && answer.answers.length
? answer.answers
: answer.open_answer;
}
return answers;
}
+6 -9
View File
@@ -7,6 +7,7 @@
export let labelKey = 'label'; export let labelKey = 'label';
export let isRequired = false; export let isRequired = false;
export let className = ''; export let className = '';
export let selectClassName = '';
export let color = 'default'; // primary export let color = 'default'; // primary
export let onChange = () => {}; // This is to know if element is 'dirty' export let onChange = () => {}; // This is to know if element is 'dirty'
@@ -19,10 +20,10 @@
{#if label} <span class="text-gray-700 mr-2">{label}</span> {/if} {#if label} <span class="text-gray-700 mr-2">{label}</span> {/if}
<select <select
bind:value bind:value
class="form-select block w-auto mt-1" class="form-select block w-auto mt-1 {selectClassName}"
required={isRequired} required={isRequired}
on:change={onChange} on:change={onChange}
class:primary={color === 'primary'} class:customColor={!!selectClassName}
> >
{#each options as option} {#each options as option}
<option class="bg-white text-black" value={option}> <option class="bg-white text-black" value={option}>
@@ -33,18 +34,14 @@
</label> </label>
<style> <style>
select.primary { select.customColor {
font-weight: 700; font-weight: 700;
cursor: pointer; cursor: pointer;
border-radius: 0.375rem; border-radius: 4px;
--tw-text-opacity: 1;
color: rgba(255, 255, 255, var(--tw-text-opacity));
--tw-bg-opacity: 1;
background-color: rgba(29, 78, 216, var(--tw-bg-opacity));
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' class='text-white' viewBox='0 0 20 20'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' class='text-white' viewBox='0 0 20 20'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
} }
select.primary option { select.customColor option {
background-color: #fff; background-color: #fff;
color: #000; color: #000;
} }
+1 -1
View File
@@ -5,7 +5,7 @@
export let className = ''; export let className = '';
</script> </script>
<div class="root mt-4 mx-auto {width} {className} relative pb-5"> <div class="root mt-4 mx-auto {width} {className} relative pb-5 px-4">
{#if $$slots.header} {#if $$slots.header}
<div <div
class="head flex items-center justify-between sticky right-0 w-full py-3" class="head flex items-center justify-between sticky right-0 w-full py-3"
@@ -1,11 +1,14 @@
export const VARIANTS = { export const VARIANTS = {
CONTAINED: 'CONTAINED', CONTAINED: 'CONTAINED',
CONTAINED_DANGER: 'CONTAINED_DANGER', CONTAINED_DANGER: 'CONTAINED_DANGER',
CONTAINED_SUCCESS: 'CONTAINED_SUCCESS',
OUTLINED: 'OUTLINED', OUTLINED: 'OUTLINED',
}; };
export const VARIANTS_CLASS = { export const VARIANTS_CLASS = {
[VARIANTS.CONTAINED]: 'bg-blue-700 hover:bg-blue-900 text-white font-bold', [VARIANTS.CONTAINED]: 'bg-blue-700 hover:bg-blue-900 text-white font-bold',
[VARIANTS.CONTAINED_SUCCESS]:
'bg-green-700 hover:bg-green-900 text-white font-bold',
[VARIANTS.CONTAINED_DANGER]: [VARIANTS.CONTAINED_DANGER]:
'bg-red-700 hover:bg-red-900 text-white font-bold', 'bg-red-700 hover:bg-red-900 text-white font-bold',
[VARIANTS.OUTLINED]: [VARIANTS.OUTLINED]:
@@ -18,6 +18,7 @@
export let nextButtonProps = {}; export let nextButtonProps = {};
export let grade; export let grade;
export let gradeMax = 0; export let gradeMax = 0;
export let disableGrading = false;
function getVal(form, name) { function getVal(form, name) {
let values = []; let values = [];
@@ -67,6 +68,7 @@
bind:value={grade} bind:value={grade}
type="number" type="number"
className="w-20" className="w-20"
isDisabled={disableGrading}
/> />
<p class="ml-2 text-lg">/ {gradeMax}</p> <p class="ml-2 text-lg">/ {gradeMax}</p>
</div> </div>
@@ -18,6 +18,7 @@
export let nextButtonProps = {}; export let nextButtonProps = {};
export let grade; export let grade;
export let gradeMax = 0; export let gradeMax = 0;
export let disableGrading = false;
function getRadioVal(form, name) { function getRadioVal(form, name) {
let val; let val;
@@ -67,6 +68,7 @@
bind:value={grade} bind:value={grade}
type="number" type="number"
className="w-20" className="w-20"
isDisabled={disableGrading}
/> />
<p class="ml-2 text-lg">/ {gradeMax}</p> <p class="ml-2 text-lg">/ {gradeMax}</p>
</div> </div>
@@ -18,6 +18,7 @@
export let disabled = false; export let disabled = false;
export let grade; export let grade;
export let gradeMax = 0; export let gradeMax = 0;
export let disableGrading = false;
function handleFormSubmit(event) { function handleFormSubmit(event) {
if (isPreview) return; if (isPreview) return;
@@ -42,6 +43,7 @@
bind:value={grade} bind:value={grade}
type="number" type="number"
className="w-20" className="w-20"
isDisabled={disableGrading}
/> />
<p class="ml-2 text-lg">/ {gradeMax}</p> <p class="ml-2 text-lg">/ {gradeMax}</p>
</div> </div>
+2
View File
@@ -1,6 +1,7 @@
export const SNACKBAR_SEVERITY = { export const SNACKBAR_SEVERITY = {
ERROR: 'ERROR', ERROR: 'ERROR',
WARNING: 'WARNING', WARNING: 'WARNING',
DEFAULT: 'DEFAULT',
INFO: 'INFO', INFO: 'INFO',
SUCCESS: 'SUCCESS', SUCCESS: 'SUCCESS',
}; };
@@ -9,5 +10,6 @@ export const SNACKBAR_SEVERITY_COLOR = {
ERROR: 'bg-red-600', ERROR: 'bg-red-600',
WARNING: 'bg-yellow-600', WARNING: 'bg-yellow-600',
INFO: 'bg-blue-600', INFO: 'bg-blue-600',
DEFAULT: 'bg-gray-600',
SUCCESS: 'bg-green-600', SUCCESS: 'bg-green-600',
}; };
+1 -1
View File
@@ -4,7 +4,7 @@ import { SNACKBAR_SEVERITY } from './constants';
export const snackbarStore = writable({ export const snackbarStore = writable({
open: false, open: false,
message: '', message: '',
severity: SNACKBAR_SEVERITY.INFO, severity: SNACKBAR_SEVERITY.DEFAULT,
handleClose: () => {}, handleClose: () => {},
autoHideDuration: 6000, autoHideDuration: 6000,
}); });
+113 -39
View File
@@ -26,7 +26,14 @@
import PageBody from '../../../components/PageBody/index.svelte'; import PageBody from '../../../components/PageBody/index.svelte';
import CourseContainer from '../../../components/CourseContainer/index.svelte'; import CourseContainer from '../../../components/CourseContainer/index.svelte';
import { setCourseData, course } from '../../../components/Course/store'; import { setCourseData, course } from '../../../components/Course/store';
import { fetchSubmissions } from '../../../utils/services/submissions'; import {
fetchSubmissions,
updateSubmission,
updateQuestionAnswer,
} from '../../../utils/services/submissions';
import { formatAnswers } from '../../../components/Course/function';
import { snackbarStore } from '../../../components/Snackbar/store';
const flipDurationMs = 300; const flipDurationMs = 300;
export let courseData = {}; export let courseData = {};
@@ -59,18 +66,41 @@
const { page } = stores(); const { page } = stores();
function handleItemFinalize(columnIdx, newItems) { function handleItemFinalize(columnIdx, newItems) {
console.log('handleItemFinalize columnIdx=>', columnIdx, newItems); let itemToWithNewStatus;
sections[columnIdx].items = newItems;
// onFinalUpdate([...sections]); const { id } = sections[columnIdx];
// Set column in the UI
sections[columnIdx].items = newItems.map((item) => {
if (item.statusId !== id) {
itemToWithNewStatus = item;
item.statusId = id;
}
return item;
});
// Update backend
if (itemToWithNewStatus) {
// Update key mapping for each submission also
submissionIdData[itemToWithNewStatus.id] = {
...submissionIdData[itemToWithNewStatus.id],
status_id: itemToWithNewStatus.statusId,
};
console.log(
`submissionIdData[itemToWithNewStatus.id]`,
submissionIdData[itemToWithNewStatus.id]
);
updateSubmission({
id: itemToWithNewStatus.id,
status_id: itemToWithNewStatus.statusId,
}).then((res) => console.log('Updated submission', res));
}
} }
function handleDndConsiderCards(columnIdx) { function handleDndConsiderCards(columnIdx) {
return (e) => { return function (e) {
console.warn(
'handleDndConsiderCards columnIdx',
columnIdx,
e.detail.items
);
sections[columnIdx].items = e.detail.items; sections[columnIdx].items = e.detail.items;
}; };
} }
@@ -83,30 +113,66 @@
goto($page.path); goto($page.path);
} }
function formatAnswers(data) { // Via dialog
const answers = {}; function updateStatus({ submissionId, prevStatusId, nextStatusId, total }) {
const questionByIdAndName = {}; let itemToWithNewStatus;
for (const question of data.questions) { // Remove from current column
questionByIdAndName[question.id] = question.name; const { items } = sections[prevStatusId - 1];
sections[prevStatusId - 1].items = items.filter((item) => {
if (item.id === submissionId) {
itemToWithNewStatus = Object.assign(item);
itemToWithNewStatus.statusId = nextStatusId;
return false;
}
return true;
});
console.log(`itemToWithNewStatus`, itemToWithNewStatus);
// Move to right column
sections[nextStatusId - 1].items = [
...sections[nextStatusId - 1].items,
itemToWithNewStatus,
];
// If something changed
if (itemToWithNewStatus) {
// Update key mapping for each submission also
submissionIdData[itemToWithNewStatus.id] = {
...submissionIdData[itemToWithNewStatus.id],
status_id: itemToWithNewStatus.statusId,
};
// Update backend
updateSubmission({
id: itemToWithNewStatus.id,
status_id: itemToWithNewStatus.statusId,
total,
}).then((res) => console.log('Updated submission', res));
} }
}
for (const answer of data.answers) { async function handleSave(submission) {
const questionName = questionByIdAndName[answer.question_id]; const { questionAnswerByPoint, questionAnswers } = submission;
answers[questionName] = const updates = Object.keys(questionAnswerByPoint).map((questionId) => {
Array.isArray(answer.answers) && answer.answers.length const questionAnswer = questionAnswers.find(
? answer.answers (answer) => answer.question_id == questionId
: answer.open_answer; );
}
return answers; const point = questionAnswerByPoint[questionId];
return updateQuestionAnswer({ point }, { id: questionAnswer.id });
});
await Promise.all(updates);
$snackbarStore.open = true;
$snackbarStore.message = `Saved successfully`;
} }
onMount(async () => { onMount(async () => {
setCourseData(courseData); setCourseData(courseData);
console.log(`courseData`, courseData);
console.log(`$course`, $course);
const { data: submissions } = await fetchSubmissions( const { data: submissions } = await fetchSubmissions(
courseData.id || $course.id courseData.id || $course.id
); );
@@ -118,6 +184,7 @@
const submissionItem = { const submissionItem = {
id, id,
statusId: status_id,
exercise: { exercise: {
id: exercise.id, id: exercise.id,
title: exercise.title, title: exercise.title,
@@ -129,10 +196,20 @@
title: exercise.lesson.title, title: exercise.lesson.title,
}, },
}; };
console.log(`answers`, answers);
submissionIdData[id] = { submissionIdData[id] = {
id,
status_id,
title: exercise.title,
student: submissionItem.student,
questions: exercise.questions, questions: exercise.questions,
answers: formatAnswers({ questions: exercise.questions, answers }), answers: formatAnswers({ questions: exercise.questions, answers }),
questionAnswers: answers,
questionAnswerByPoint: answers.reduce((acc, answer) => {
acc[answer.question_id] = answer.point;
return acc;
}, {}),
}; };
if (Array.isArray(sectionById[status_id])) { if (Array.isArray(sectionById[status_id])) {
@@ -151,13 +228,11 @@
? sectionById[index + 1] ? sectionById[index + 1]
: [], : [],
})); }));
console.log(`submissionIdData`, submissionIdData);
}); });
$: { $: {
submissionId = $page.query.submissionId; submissionId = $page.query.submissionId;
openExercise = !!submissionId; openExercise = !!submissionId && submissionIdData[submissionId];
} }
</script> </script>
@@ -165,6 +240,8 @@
bind:open={openExercise} bind:open={openExercise}
onClose={handleModalClose} onClose={handleModalClose}
data={submissionIdData[submissionId] || {}} data={submissionIdData[submissionId] || {}}
{handleSave}
{updateStatus}
{submissionId} {submissionId}
/> />
@@ -172,14 +249,14 @@
<PageNav title="Submitted Exercises" /> <PageNav title="Submitted Exercises" />
<PageBody width="w-11/12 overflow-x-auto overflow-y-hidden"> <PageBody width="w-11/12 overflow-x-auto overflow-y-hidden">
<div class="flex items-center justify-between w-full"> <div class="flex items-center w-full">
{#each sections as { id, title, items }, idx (id)} {#each sections as { id, title, items }, idx (id)}
<div <div
class="section rounded-md bg-gray-100 border border-gray-50 p-3 h-80 mr-3 overflow-hidden" class="section rounded-md bg-gray-100 border border-gray-50 p-3 h-80 mr-3 overflow-hidden"
animate:flip={{ duration: flipDurationMs }} animate:flip={{ duration: flipDurationMs }}
> >
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<Chip value={items.length} /> <Chip value={items.length} class="bg-set" />
<p class="ml-2 font-bold">{title}</p> <p class="ml-2 font-bold">{title}</p>
</div> </div>
<div <div
@@ -197,19 +274,16 @@
class="border border-grey-700 w-full my-2 mx-0 rounded-md bg-white py-2 px-3" class="border border-grey-700 w-full my-2 mx-0 rounded-md bg-white py-2 px-3"
animate:flip={{ duration: flipDurationMs }} animate:flip={{ duration: flipDurationMs }}
> >
<a <div
class="flex items-center no-underline hover:underline text-black mb-2" class="flex items-center no-underline hover:underline text-black mb-2"
href="{$page.path}?submissionId={item.student.id}"
> >
<img <img
alt="Placeholder" alt="Student avatar"
class="block rounded-full" class="block rounded-full h-6 w-6"
width="24"
height="20"
src={item.student.avatar_url} src={item.student.avatar_url}
/> />
<p class="ml-2 text-sm">{item.student.username}</p> <p class="ml-2 text-sm">{item.student.username}</p>
</a> </div>
<a <a
class="text-blue-700 text-md font-bold" class="text-blue-700 text-md font-bold"
href="{$page.path}?submissionId={item.id}" href="{$page.path}?submissionId={item.id}"
@@ -219,7 +293,7 @@
<a <a
class="flex items-center no-underline hover:underline text-black my-2" class="flex items-center no-underline hover:underline text-black my-2"
href="{$page.path.replace('submissions', 'lessons')}/{item href="{$page.path.replace('submissions', 'lessons')}/{item
.lesson.id}/exercises" .lesson.id}/exercises/{item.exercise.id}"
> >
<p class="text-grey text-sm"> <p class="text-grey text-sm">
#{item.lesson.title} #{item.lesson.title}
+4 -9
View File
@@ -302,14 +302,9 @@ export async function submitExercise(
questions, questions,
exerciseId, exerciseId,
courseId, courseId,
group, groupMemberId
profile
) { ) {
const groupMember = group.people.find( if (!groupMemberId) {
(person) => person.profile_id === profile.id
);
console.log(`groupMember`, groupMember);
if (!groupMember) {
return; return;
} }
@@ -320,7 +315,7 @@ export async function submitExercise(
const questionAnswers = []; const questionAnswers = [];
const { data: submission } = await supabase.from('submission').insert({ const { data: submission } = await supabase.from('submission').insert({
submitted_by: groupMember.id, submitted_by: groupMemberId,
exercise_id: exerciseId, exercise_id: exerciseId,
course_id: courseId, course_id: courseId,
}); });
@@ -329,7 +324,7 @@ export async function submitExercise(
const value = answers[questionName]; const value = answers[questionName];
const questionAnswer = { const questionAnswer = {
group_member_id: groupMember.id, group_member_id: groupMemberId,
question_id: questionsByName[questionName], question_id: questionsByName[questionName],
open_answer: '', open_answer: '',
answers: [], answers: [],
+33
View File
@@ -31,3 +31,36 @@ export function fetchSubmissions(course_id) {
course_id, course_id,
}); });
} }
export async function fetchSubmission({ courseId, exerciseId, submittedBy }) {
return supabase
.from('submission')
.select(
`
id,
answers:question_answer(*),
status_id
`
)
.match({
submitted_by: submittedBy,
exercise_id: exerciseId,
course_id: courseId,
});
}
export async function updateSubmission({ id, status_id, total }) {
const toUpdate = {
status_id,
};
if (typeof total === 'number') {
toUpdate.total = total;
}
return supabase.from('submission').update(toUpdate).match({ id });
}
export async function updateQuestionAnswer(update, match) {
return supabase.from('question_answer').update(update).match(match);
}