CLASSROOMIO-T-80: Replace sapper with sveltekit

This commit is contained in:
Rotimi Best
2023-08-02 19:35:27 +01:00
parent 8ffdf319b3
commit 7dfbb05451
320 changed files with 5207 additions and 18330 deletions
@@ -0,0 +1,5 @@
import { supabase } from '$lib/utils/functions/supabase';
export async function takeAttendance(update) {
return supabase.from('group_attendance').upsert(update);
}
+263
View File
@@ -0,0 +1,263 @@
import { get } from 'svelte/store';
import type { PostgrestError } from '@supabase/supabase-js';
import { supabase } from '$lib/utils/functions/supabase';
// @ts-ignore
import { addGroupMember } from './index';
// @ts-ignore
import { profile } from '$lib/utils/store/user';
import type { Course, Exercise, Lesson, Profile } from '$lib/utils/types';
// @ts-ignore
import { ROLE } from '$lib/utils/constants/roles';
// @ts-ignore
import { QUESTION_TYPE } from '$lib/components/Question/constants';
type GetCourseResponse = {
data: Course;
error: PostgrestError | null;
};
async function fetchCourseData(courseId: string): Promise<GetCourseResponse> {
const { data, error } = await supabase
.from('course')
.select(
`
*,
lessons:lesson(
*,
exercise:id(
id, title, description, due_by,
questions:question(
*,
options:option(*),
question_type:question_type_id(id, label)
)
)
)
`
)
.match({ id: courseId })
.single();
return { data, error };
}
async function cloneGroupAndBasicCourse(
course: Course,
newTitle: string
): Promise<{ newCourse: Course }> {
const { description } = course;
const { data: newGroup } = await supabase
.from('group')
.insert({ name: newTitle, description })
.select();
console.log(`newGroup`, newGroup);
if (!Array.isArray(newGroup)) {
throw new Error('Group wasnt created successfully');
}
const { id: group_id } = newGroup[0];
// 2. Create course with group_id
const { data: newCourse } = await supabase
.from('course')
.insert({
...course,
title: newTitle,
description,
group_id,
lessons: undefined,
id: undefined,
updated_at: undefined,
created_at: undefined,
})
.select();
console.log(`newCourse`, newCourse);
// 3. Add group members
const profileStoreValue: Profile = get(profile);
// Get profile of author and add as member
await addGroupMember({
profile_id: profileStoreValue.id,
email: profileStoreValue.email,
group_id,
role_id: ROLE.TUTOR,
});
console.log('profileStoreValue', profileStoreValue);
return {
newCourse: newCourse?.[0],
};
}
async function cloneLessons(lessons: Lesson[], courseId: Course['id']) {
const clonedLessons = lessons
.sort(
(a, b) =>
new Date(a.created_at || '').getTime() -
new Date(b.created_at || '').getTime()
)
.map((lesson, index) => ({
call_url: lesson.call_url,
course_id: courseId,
is_complete: lesson.is_complete,
lesson_at: new Date(),
note: lesson.note,
order: lesson.order || index + 1,
public: lesson.public,
slide_url: lesson.slide_url,
title: lesson.title,
video_url: lesson.video_url,
}));
const { data } = await supabase.from('lesson').insert(clonedLessons).select();
const newLessons = data?.map((dataItem, dataIndex) => {
return {
...dataItem,
// @ts-ignore
exercise: lessons[dataIndex].exercise,
};
});
console.log('newLessons', newLessons);
return { newLessons };
}
async function cloneExercises(lessons: Lesson[]) {
console.log('lessons', lessons);
// Lessons
for (const lesson of lessons) {
// @ts-ignore
const { id: lessonId, exercise = [] } = lesson;
// Exercises
for (const exerciseItem of exercise) {
const { title, description, questions = [] } = exerciseItem;
// Insert
const { data } = await supabase
.from('exercise')
.insert({
description,
due_by: new Date(),
lesson_id: lessonId,
title,
})
.select();
const newExercise = Array.isArray(data) ? data[0] : data;
console.log('newExercise', newExercise);
// Questions
for (const question of questions) {
const { options = [] } = question;
// Insert
const { data } = await supabase
.from('question')
.insert({
name: question.name,
title: question.title,
points: question.points,
order: question.order,
question_type_id: question.question_type_id,
exercise_id: newExercise.id,
})
.select();
const newQuestion = Array.isArray(data) ? data[0] : data;
console.log('newQuestion', newQuestion);
// Options: Don't map options for 'Paragraph' questions
if (QUESTION_TYPE.TEXTAREA !== newQuestion.question_type_id) {
// @ts-ignore
const newOptions = options.map((option) => ({
value: option.value,
label: option.label,
is_correct: option.is_correct,
question_id: newQuestion.id,
}));
// Insert[Batch]
await supabase.from('option').insert(newOptions).select();
}
}
}
}
return true;
}
export async function cloneCourse(
courseId: string,
newTitle: string
): Promise<Course> {
const { data: course, error } = await fetchCourseData(courseId);
console.log(`course`, course);
if (error) {
console.error('Error when cloning', error);
throw error;
}
/**
* 1. Copy course table
* 2. Copy lessons table
* 3. Copy exercises table
* 4. Copy questions table
* 5. Copy options table
*/
// tables: group, groupmember, course(group_id)
const { newCourse } = await cloneGroupAndBasicCourse(course, newTitle);
console.log('returned -> newCourse', newCourse);
// @ts-ignore
const { newLessons } = await cloneLessons(course.lessons, newCourse.id);
console.log('returned -> newLessons', newLessons);
const cloneExercisesRes = await cloneExercises(newLessons || []);
console.log('returned -> cloneExercisesRes', cloneExercisesRes);
return newCourse;
}
/*
1. Click on 3 dots to clone course
2. Show a modal with a new name "Copy of previous course name"
-> Note the following information will be copied
3. Things to copy
Overview
Lessons
Exercises - questions, options, everything basically.
Technical approach
- Fetch all the data to the client
- Manually call functions to create
- call a psql function with newname, course_id
- copy all in course table
- copy all in group table by course_id
- add only one group member as admin
- get all lessons in course
- copy each one with unique ids
- get all exercises by course id
- copy exercise
- get questions
- copy questions
- get options
- copy options
*/
+427
View File
@@ -0,0 +1,427 @@
import { supabase } from '$lib/utils/functions/supabase';
import { isUUID } from '$lib/utils/functions/isUUID';
import { QUESTION_TYPE } from '$lib/components/Question/constants';
import type {
Lesson,
Course,
Group,
Groupmember,
Exercise,
} from '$lib/utils/types';
export async function fetchCourse(
courseId?: Course['id'],
slug?: Course['slug']
) {
let match: { slug?: string; id?: string } = {};
if (slug) {
match.slug = slug;
} else {
match.id = courseId;
}
const { data, error } = await supabase
.from('course')
.select(
`
id,
title,
description,
overview,
logo,
group(*,
members:groupmember(*,
profile(*)
)
),
slug,
cost,
currency,
metadata,
is_certificate_downloadable,
certificate_theme,
lessons:lesson(
id, title,public, lesson_at, call_url, is_complete, order, created_at,
note, video_url, slide_url, call_url, totalExercises:exercise(count),
profile:teacher_id(id, avatar_url, fullname)
),
attendance:group_attendance(*)
`
)
.match(match)
.single();
console.log(`error`, error);
if (!data || error) {
console.log(`data`, data);
console.log(`fetchCourse => error`, error);
// return this.redirect(301, '/courses');
return { data, error };
}
return {
data,
error,
};
}
export async function fetchGroup(groupId: Group['id']) {
const { data, error } = await supabase
.from('group')
.select(`*,members:groupmember(*,profile(*))`)
.match({ id: groupId })
.single();
return {
data,
error,
};
}
export async function setProfileIdOfGroupMember(
email: string,
profileId: string
) {
const { data, error } = await supabase
.from('groupmember')
.update({
email: null,
profile_id: profileId,
})
.match({ email: email });
return {
data,
error,
};
}
export async function uploadAvatar(courseId: string, avatar: string) {
const filename = `course/${courseId + Date.now()}.webp`;
let logo;
const { data } = await supabase.storage
.from('avatars')
.upload(filename, avatar, {
cacheControl: '3600',
upsert: false,
});
if (data && data.Key) {
const { publicURL } = supabase.storage
.from('avatars')
.getPublicUrl(filename);
if (!publicURL) return;
logo = publicURL;
}
return logo;
}
export async function updateCourse(
courseId: Course['id'],
avatar: string | undefined,
course: Course
) {
if (avatar) {
const filename = `course/${courseId + Date.now()}.webp`;
const { data } = await supabase.storage
.from('avatars')
.upload(filename, avatar, {
cacheControl: '3600',
upsert: false,
});
if (data && data.Key) {
const { publicURL } = supabase.storage
.from('avatars')
.getPublicUrl(filename);
if (!publicURL) return;
course.logo = publicURL;
}
}
await supabase.from('course').update(course).match({ id: courseId });
return course.logo;
}
export async function deleteCourse(courseId: Course['id']) {
return await supabase.from('course').delete().match({ id: courseId });
}
export function addGroupMember(member: any) {
return supabase.from('groupmember').insert(member).select();
}
export function updatedGroupMember(update: any, match: any) {
return supabase.from('groupmember').update(update).match(match);
}
export function deleteGroupMember(groupMemberId: Groupmember['id']) {
return supabase.from('groupmember').delete().match({ id: groupMemberId });
}
export function fetchLesson(lessonId: Lesson['id']) {
return supabase
.from('lesson')
.select(
`id, note, video_url, slide_url, call_url, totalExercises:exercise(count)`
)
.eq('id', lessonId)
.single();
}
export function createLesson(lesson: any) {
return supabase.from('lesson').insert(lesson).select();
}
export function updateLesson(lesson: any, lessonId: Lesson['id']) {
return supabase
.from('lesson')
.update({ ...lesson, id: undefined })
.match({ id: lessonId });
}
export function deleteLesson(lessonId: Lesson['id']) {
// Need to implement soft delete
return supabase.from('lesson').delete().match({ id: lessonId });
}
export function createExercise(exercise: any) {
return supabase.from('exercise').insert(exercise).select();
}
export function fetchExercisesByMarks(courseId: Course['id']) {
return supabase.rpc('get_exercises').eq('course_id', courseId);
}
function isNew(item: any) {
return isNaN(item);
}
// TODO: Add questionnaire type
export async function upsertExercise(
questionnaire: any,
exerciseId: Exercise['id']
) {
const {
questions,
title,
description,
due_by,
is_title_dirty,
is_description_dirty,
is_due_by_dirty,
} = questionnaire;
if (is_description_dirty || is_title_dirty || is_due_by_dirty) {
await supabase
.from('exercise')
.update({
title,
description,
due_by,
})
.match({ id: exerciseId });
}
const updatedQuestions = [];
for (const question of questions) {
const {
title,
id,
name,
question_type,
options,
deleted_at,
order,
points,
is_dirty,
} = question;
// DELETE /delete/:questionId - Don't delete if answer already given
if (deleted_at) {
// Delete from server only if this question exists in the database
if (!isNew(id)) {
await supabase.from('option').delete().match({ question_id: id });
await supabase
.from('question_answer')
.delete()
.match({ question_id: id });
const { error } = await supabase
.from('question')
.delete()
.match({ id });
if (error) {
console.error('Cannot delete this question', error);
continue;
}
}
// Skip the remaining operation so we filter out this question from the new questions array
continue;
}
// INSERT or UPDATE /update/:questionId or /insert/:questionId
const newQuestion = {
id: isNew(id) ? undefined : id,
name: isNew(id) ? undefined : name,
title,
points,
order,
question_type_id: question_type.id,
exercise_id: exerciseId,
};
let questionSupabaseRes;
if (is_dirty || isNew(id)) {
const res = await supabase.from('question').upsert(newQuestion);
if (res.error) {
console.error(`Upsert question`, res.error);
}
questionSupabaseRes = Array.isArray(res.data) ? res.data[0] : null;
} else {
questionSupabaseRes = Object.assign(newQuestion);
}
if (questionSupabaseRes) {
const { question_type_id, id, name, order } = questionSupabaseRes;
// Delete cause this is not a field in the table
delete newQuestion.question_type_id;
// @ts-ignore
newQuestion.question_type = { id: question_type_id };
newQuestion.id = id;
newQuestion.name = name;
newQuestion.order = order;
// @ts-ignore
newQuestion.options = [];
// Don't map options for 'Paragraph' questions
if (QUESTION_TYPE.TEXTAREA !== question_type_id) {
for (const option of options) {
const { deleted_at, is_dirty } = option;
// DELETE /delete/:optionId
if (deleted_at) {
// if this option exists in the database
if (!isNew(option.id)) {
supabase.from('option').delete().match({ id: option.id });
}
// Skip the remaining operation so we filter out this option from the new option array
continue;
}
// INSERT and UPDATE
const newOption = {
...option,
is_dirty: undefined,
id: isNew(option.id) ? undefined : option.id,
value: isUUID(option.value) ? option.value : undefined, // this value is of UUID type
question_id: newQuestion.id,
};
if (is_dirty || isNew(option.id)) {
const { data } = await supabase.from('option').upsert(newOption);
if (Array.isArray(data)) {
// @ts-ignore
newQuestion.options.push(data[0]);
}
} else {
// @ts-ignore
newQuestion.options.push(newOption);
}
}
}
updatedQuestions.push(newQuestion);
}
}
return updatedQuestions;
}
interface LooseObject {
[key: string]: any;
}
export async function submitExercise(
answers: Array<string>,
questions: Array<{ name: string; id: string }>,
exerciseId: Exercise['id'],
courseId: Course['id'],
groupMemberId: Groupmember['id'] | undefined
) {
if (!groupMemberId) {
return;
}
const questionsByName = questions.reduce(
(acc, q) => ({ ...acc, [q.name]: q.id }),
{}
) as LooseObject;
const questionAnswers = [];
const { data: submission } = await supabase
.from('submission')
.insert({
submitted_by: groupMemberId,
exercise_id: exerciseId,
course_id: courseId,
})
.select();
for (const questionName in answers) {
const value = answers[questionName];
const questionAnswer = {
group_member_id: groupMemberId,
question_id: questionsByName[questionName],
open_answer: '',
answers: [],
submission_id: Array.isArray(submission) ? submission[0].id : null,
};
if (typeof value === 'string') {
questionAnswer.open_answer = value;
} else {
questionAnswer.answers = value;
}
questionAnswers.push(questionAnswer);
}
const res = await supabase
.from('question_answer')
.insert(questionAnswers)
.select();
console.log(`res`, res);
}
export async function deleteExercise(
questions: Array<{ id: string }>,
exerciseId: Exercise['id']
) {
for (const question of questions) {
const { id } = question;
await supabase.from('option').delete().match({ question_id: id });
await supabase.from('question_answer').delete().match({ question_id: id });
await supabase.from('question').delete().match({ id });
}
await supabase.from('submission').delete().match({ exercise_id: exerciseId });
await supabase.from('exercise').delete().match({ id: exerciseId });
}
+25
View File
@@ -0,0 +1,25 @@
import type { UserLessonDataType } from '$lib/utils/types';
import { supabase } from '$lib/utils/functions/supabase';
export async function fetchUserUpcomingData(
profileId: string | null
): Promise<UserLessonDataType[] | []> {
if (!profileId) {
return [];
}
const { data: userUpcomingData } = await supabase.rpc(
'get_user_upcoming_lessons',
{
profile_id_arg: profileId,
}
);
console.log(`userUpcomingData`, userUpcomingData);
if (!Array.isArray(userUpcomingData)) {
return [];
}
return userUpcomingData;
}
+7
View File
@@ -0,0 +1,7 @@
import { supabase } from '$lib/utils/functions/supabase';
export async function fetchMarks(courseId) {
const { data } = await supabase.rpc('get_marks').eq('course_id', courseId);
return { data };
}
+177
View File
@@ -0,0 +1,177 @@
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { goto } from '$app/navigation';
import { supabase } from '$lib/utils/functions/supabase';
import { orgs, currentOrg, orgAudience, orgTeam } from '$lib/utils/store/org';
import { ROLE, ROLE_LABEL } from '$lib/utils/constants/roles';
export async function getOrgTeam(orgId) {
const { data, error } = await supabase
.from('organizationmember')
.select(
`
id,
email,
verified,
role_id,
profile(
id,
fullname,
email
)
`
)
.eq('organization_id', orgId)
.neq('role_id', ROLE.STUDENT)
.order('id', { ascending: false });
const team = [];
if (data?.length) {
data.forEach((teamMember) => {
team.push({
id: teamMember.id,
email: teamMember?.profile?.email || teamMember?.email,
verified: teamMember?.verified,
profileId: teamMember?.profile?.id,
role: ROLE_LABEL[teamMember?.role_id] || '',
isAdmin: teamMember?.role_id === ROLE.ADMIN,
});
});
orgTeam.set(team);
}
return {
team,
error,
};
}
export async function getOrganizations(userId) {
const { data, error } = await supabase
.from('organizationmember')
.select(
`
id,
profile_id,
organization!organizationmember_organization_id_fkey (
id,
name,
siteName,
avatar_url
)
`
)
.eq('profile_id', userId);
const orgsArray = [];
if (Array.isArray(data) && data.length) {
data.forEach((orgMember) => {
orgsArray.push({
id: orgMember?.organization?.id,
name: orgMember?.organization?.name,
shortName:
orgMember?.organization?.name?.substring(0, 2)?.toUpperCase() || '',
siteName: orgMember?.organization?.siteName,
avatar_url: orgMember?.organization?.avatar_url,
memberId: orgMember?.id,
});
});
orgs.set(orgsArray);
currentOrg.set(orgsArray[0]);
}
return {
orgs: orgsArray,
currentOrg: orgsArray[0],
error,
};
}
export async function getOrgAudience(orgId) {
// get all students who are participants in any course belonging to an org
const { data, error } = await supabase
.from('profile')
.select(
`
id,
fullname,
email,
avatar_url,
created_at,
organizationmember!inner(
role_id,
organization_id
)
`
)
.eq('organizationmember.organization_id', orgId)
.eq('organizationmember.role_id', 3); // is a student, tutor is 2 and admin is 1
console.log('data', data);
const audience = (data || []).map((profile) => ({
id: profile.id,
name: profile.fullname,
email: profile.email,
avatar_url: profile.avatar_url,
date_joined: new Date(profile.created_at).toDateString(),
}));
orgAudience.set(audience);
return {
audience: audience,
error,
};
}
export async function getCourseBySiteName(siteName) {
const { data, error } = await supabase
.from('course')
.select(
`
*,
group!inner(
organization!inner(id, name, siteName, avatar_url)
)
`
)
.eq('group.organization.siteName', siteName)
.eq('is_published', true);
if (error) {
return [];
}
const org = get(data, '[0].group.organization', {
id: '',
name: 'ClassroomIO',
shortName: '',
siteName: '',
avatar_url: '/logo-192.png',
});
currentOrg.set(org);
return data;
}
export async function getCurrentOrg(siteName) {
const { data, error } = await supabase
.from('organization')
.select(
`
id,
name,
siteName,
avatar_url
`
)
.eq('siteName', siteName);
if (error || isEmpty(data)) {
console.error('Error getOrganization', error);
return goto('/404');
}
currentOrg.set(data[0]);
}
+26
View File
@@ -0,0 +1,26 @@
import { createQuizModal, deleteModal } from '$lib/utils/store/org';
export function onEdit(quiz) {}
export function onShare(quiz) {}
export function onReport(quiz) {}
export function onRename(quiz) {
createQuizModal.update((m) => ({
...m,
id: quiz.id,
title: quiz.title,
openEdit: true,
}));
}
export function onMakeCopy(quiz) {}
export function onDelete(quiz) {
deleteModal.update((m) => ({
...m,
open: true,
id: quiz.id,
}));
}
@@ -0,0 +1,67 @@
import { supabase } from '$lib/utils/functions/supabase';
export function fetchSubmissionStatus() {
return supabase.from('submissionstatus').select(`*`);
}
export function fetchSubmissions(course_id) {
return supabase
.from('submission')
.select(
`
id,
created_at,
answers:question_answer(*),
exercise:exercise_id(
id, title, due_by,
lesson:lesson_id(id, title),
questions:question(
*,
options:option(*),
question_type:question_type_id(id, label)
)
),
status_id,
course:course_id(*),
groupmember:submitted_by(
profile(*)
)
`
)
.match({
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 }, otherArgs) {
const toUpdate = {
status_id,
};
if (typeof total === 'number') {
toUpdate.total = total;
}
return supabase.from('submission').update(toUpdate, otherArgs).match({ id });
}
export async function updateQuestionAnswer(update, match) {
return supabase.from('question_answer').update(update).match(match);
}