diff --git a/apps/api/src/db/trail_runs.py b/apps/api/src/db/trail_runs.py index ce177589..e160a790 100644 --- a/apps/api/src/db/trail_runs.py +++ b/apps/api/src/db/trail_runs.py @@ -46,8 +46,12 @@ class TrailRunRead(BaseModel): course_id: int = Field(default=None, foreign_key="course.id") org_id: int = Field(default=None, foreign_key="organization.id") user_id: int = Field(default=None, foreign_key="user.id") + # course object + course: dict # timestamps creation_date: str update_date: str + # number of activities in course + course_total_steps: int steps: list[TrailStep] pass diff --git a/apps/api/src/db/trail_steps.py b/apps/api/src/db/trail_steps.py index 3afba947..5ec5c017 100644 --- a/apps/api/src/db/trail_steps.py +++ b/apps/api/src/db/trail_steps.py @@ -2,6 +2,8 @@ from enum import Enum from typing import Optional from sqlalchemy import JSON, Column from sqlmodel import Field, SQLModel +from sqlalchemy import BigInteger, Column, ForeignKey +from sqlmodel import Field, SQLModel class TrailStepTypeEnum(str, Enum): @@ -17,7 +19,9 @@ class TrailStep(SQLModel, table=True): grade: str data: dict = Field(default={}, sa_column=Column(JSON)) # foreign keys - trailrun_id: int = Field(default=None, foreign_key="trailrun.id") + trailrun_id: int = Field( + sa_column=Column(BigInteger, ForeignKey("trailrun.id", ondelete="CASCADE")) + ) trail_id: int = Field(default=None, foreign_key="trail.id") activity_id: int = Field(default=None, foreign_key="activity.id") course_id: int = Field(default=None, foreign_key="course.id") diff --git a/apps/api/src/routers/trail.py b/apps/api/src/routers/trail.py index 1c1c2fa5..cb9b3d48 100644 --- a/apps/api/src/routers/trail.py +++ b/apps/api/src/routers/trail.py @@ -85,7 +85,7 @@ async def api_remove_course_to_trail( @router.post("/add_activity/{activity_uuid}") async def api_add_activity_to_trail( request: Request, - activity_id: int, + activity_uuid: str, user=Depends(get_current_user), db_session=Depends(get_db_session), ) -> TrailRead: @@ -93,5 +93,5 @@ async def api_add_activity_to_trail( Add Course to trail """ return await add_activity_to_trail( - request, user, activity_id, db_session + request, user, activity_uuid, db_session ) diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 51a81384..369757e6 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -1,5 +1,6 @@ from datetime import datetime from uuid import uuid4 +from src.db.chapter_activities import ChapterActivity from fastapi import HTTPException, Request, status from sqlmodel import Session, select from src.db.activities import Activity @@ -57,9 +58,24 @@ async def get_user_trails( trail_runs = db_session.exec(statement).all() trail_runs = [ - TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0) + for trail_run in trail_runs ] + # Add course object and total activities in a course to trail runs + for trail_run in trail_runs: + statement = select(Course).where(Course.id == trail_run.course_id) + course = db_session.exec(statement).first() + trail_run.course = course + + # Add number of activities (steps) in a course + statement = select(ChapterActivity).where( + ChapterActivity.course_id == trail_run.course_id + ) + course_total_steps = db_session.exec(statement) + # count number of activities in a this list + trail_run.course_total_steps = len(course_total_steps.all()) + for trail_run in trail_runs: statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) trail_steps = db_session.exec(statement).all() @@ -95,9 +111,24 @@ async def get_user_trail_with_orgid( trail_runs = db_session.exec(statement).all() trail_runs = [ - TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0) + for trail_run in trail_runs ] + # Add course object and total activities in a course to trail runs + for trail_run in trail_runs: + statement = select(Course).where(Course.id == trail_run.course_id) + course = db_session.exec(statement).first() + trail_run.course = course + + # Add number of activities (steps) in a course + statement = select(ChapterActivity).where( + ChapterActivity.course_id == trail_run.course_id + ) + course_total_steps = db_session.exec(statement) + # count number of activities in a this list + trail_run.course_total_steps = len(course_total_steps.all()) + for trail_run in trail_runs: statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) trail_steps = db_session.exec(statement).all() @@ -121,11 +152,11 @@ async def get_user_trail_with_orgid( async def add_activity_to_trail( request: Request, user: PublicUser, - activity_id: int, + activity_uuid: str, db_session: Session, ) -> TrailRead: # Look for the activity - statement = select(Activity).where(Activity.id == activity_id) + statement = select(Activity).where(Activity.activity_uuid == activity_uuid) activity = db_session.exec(statement).first() if not activity: @@ -133,15 +164,6 @@ async def add_activity_to_trail( status_code=status.HTTP_404_NOT_FOUND, detail="Activity not found" ) - # check if run already exists - statement = select(TrailRun).where(TrailRun.course_id == activity.course_id) - trailrun = db_session.exec(statement).first() - - if trailrun: - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, detail="TrailRun already exists" - ) - statement = select(Course).where(Course.id == activity.course_id) course = db_session.exec(statement).first() @@ -179,15 +201,16 @@ async def add_activity_to_trail( db_session.refresh(trailrun) statement = select(TrailStep).where( - TrailStep.trailrun_id == trailrun.id, TrailStep.activity_id == activity_id + TrailStep.trailrun_id == trailrun.id, TrailStep.activity_id == activity.id ) trailstep = db_session.exec(statement).first() if not trailstep: trailstep = TrailStep( trailrun_id=trailrun.id if trailrun.id is not None else 0, - activity_id=activity_id, + activity_id=activity.id, course_id=course.id if course.id is not None else 0, + trail_id=trail.id if trail.id is not None else 0, org_id=course.org_id, complete=False, teacher_verified=False, @@ -204,7 +227,8 @@ async def add_activity_to_trail( trail_runs = db_session.exec(statement).all() trail_runs = [ - TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0) + for trail_run in trail_runs ] for trail_run in trail_runs: @@ -282,7 +306,7 @@ async def add_course_to_trail( trail_runs = db_session.exec(statement).all() trail_runs = [ - TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0 ) for trail_run in trail_runs ] for trail_run in trail_runs: @@ -338,12 +362,21 @@ async def remove_course_from_trail( db_session.delete(trail_run) db_session.commit() + # Delete all trail steps for this course + statement = select(TrailStep).where(TrailStep.course_id == course.id) + trail_steps = db_session.exec(statement).all() + + for trail_step in trail_steps: + db_session.delete(trail_step) + db_session.commit() + statement = select(TrailRun).where(TrailRun.trail_id == trail.id) trail_runs = db_session.exec(statement).all() trail_runs = [ - TrailRunRead(**trail_run.__dict__, steps=[]) for trail_run in trail_runs + TrailRunRead(**trail_run.__dict__, course={}, steps=[], course_total_steps=0 ) for trail_run in trail_runs ] + for trail_run in trail_runs: statement = select(TrailStep).where(TrailStep.trailrun_id == trail_run.id) trail_steps = db_session.exec(statement).all() diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx index 86ae0018..ee4890b7 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/collections/page.tsx @@ -8,7 +8,7 @@ import { Metadata } from "next"; import { cookies } from "next/headers"; import Link from "next/link"; import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth"; -import CollectionThumbnail from "@components/Objects/Other/CollectionThumbnail"; +import CollectionThumbnail from "@components/Objects/Thumbnails/CollectionThumbnail"; import NewCollectionButton from "@components/StyledElements/Buttons/NewCollectionButton"; type MetadataProps = { diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx index 6d74749b..e2f76e49 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/course/[courseuuid]/activity/[activityid]/activity.tsx @@ -57,7 +57,7 @@ function ActivityClient(props: ActivityClientProps) {

{course.name}

- +
@@ -66,7 +66,7 @@ function ActivityClient(props: ActivityClientProps) {
- +
@@ -91,23 +91,26 @@ function ActivityClient(props: ActivityClientProps) { -export function MarkStatus(props: { activityid: string, course: any, orgslug: string, courseid: string }) { +export function MarkStatus(props: { activity: any, activityid: string, course: any, orgslug: string }) { const router = useRouter(); - + console.log(props.course.trail) async function markActivityAsCompleteFront() { - const trail = await markActivityAsComplete(props.orgslug, props.courseid, props.activityid); + const trail = await markActivityAsComplete(props.orgslug, props.course.course_uuid, 'activity_' + props.activityid); router.refresh(); - - // refresh page (FIX for Next.js BUG) - //window.location.reload(); - } + const isActivityCompleted = () => { + let run = props.course.trail.runs.find((run: any) => run.course_id == props.course.id); + if (run) { + return run.steps.find((step: any) => step.activity_id == props.activity.id); + } + } + + console.log('isActivityCompleted', isActivityCompleted()); + return ( - <>{props.course.trail.activities_marked_complete && - props.course.trail.activities_marked_complete.includes("activity_" + props.activityid) && - props.course.trail.status == "ongoing" ? ( + <>{ isActivityCompleted() ? (
diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx index 3b786301..5c1348ba 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx @@ -6,7 +6,7 @@ import { useSearchParams } from 'next/navigation'; import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'; import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'; import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; -import CourseThumbnail from '@components/Objects/Other/CourseThumbnail'; +import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'; import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'; interface CourseProps { diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/page.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/page.tsx index 177cbcbc..aaed6f07 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/page.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/page.tsx @@ -9,8 +9,8 @@ import { cookies } from 'next/headers'; import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'; import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'; import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'; -import CourseThumbnail from '@components/Objects/Other/CourseThumbnail'; -import CollectionThumbnail from '@components/Objects/Other/CollectionThumbnail'; +import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'; +import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail'; import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; import { Plus, PlusCircle } from 'lucide-react'; import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'; diff --git a/apps/web/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx b/apps/web/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx index d19a8c78..fa1bf139 100644 --- a/apps/web/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx +++ b/apps/web/app/orgs/[orgslug]/(withmenu)/trail/trail.tsx @@ -1,4 +1,5 @@ "use client"; +import { useOrg } from "@components/Contexts/OrgContext"; import PageLoading from "@components/Objects/Loaders/PageLoading"; import TrailCourseElement from "@components/Pages/Trail/TrailCourseElement"; import TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle"; @@ -6,13 +7,18 @@ import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWra import { getAPIUrl } from "@services/config/config"; import { removeCourse } from "@services/courses/activity"; import { revalidateTags, swrFetcher } from "@services/utils/ts/requests"; -import React from "react"; +import React, { useEffect } from "react"; import useSWR, { mutate } from "swr"; function Trail(params: any) { let orgslug = params.orgslug; - const { data: trail, error: error } = useSWR(`${getAPIUrl()}trail/org_slug/${orgslug}/trail`, swrFetcher); + const org = useOrg() as any; + const orgID = org?.id; + const { data: trail, error: error } = useSWR(`${getAPIUrl()}trail/org/${orgID}/trail`, swrFetcher); + useEffect(() => { + } + , [trail,org]); return ( @@ -21,12 +27,10 @@ function Trail(params: any) { ) : (
- {trail.courses.map((course: any) => ( - !course.masked ? ( - - ) : ( - <> - ) + {trail.runs.map((run: any) => ( + <> + + ))} diff --git a/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx b/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx index cbde78ac..541368fa 100644 --- a/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx +++ b/apps/web/app/orgs/[orgslug]/dash/courses/client.tsx @@ -1,7 +1,7 @@ 'use client'; import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs' import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse'; -import CourseThumbnail from '@components/Objects/Other/CourseThumbnail'; +import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'; import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'; import Modal from '@components/StyledElements/Modal/Modal'; diff --git a/apps/web/components/Dashboard/Org/OrgEditGeneral/OrgEditGeneral.tsx b/apps/web/components/Dashboard/Org/OrgEditGeneral/OrgEditGeneral.tsx index f7e232c1..35793f9c 100644 --- a/apps/web/components/Dashboard/Org/OrgEditGeneral/OrgEditGeneral.tsx +++ b/apps/web/components/Dashboard/Org/OrgEditGeneral/OrgEditGeneral.tsx @@ -69,7 +69,6 @@ function OrgEditGeneral(props: any) { initialValues={orgValues} onSubmit={(values, { setSubmitting }) => { setTimeout(() => { - alert(JSON.stringify(values, null, 2)); setSubmitting(false); updateOrg(values) }, 400); diff --git a/apps/web/components/Dashboard/UI/LeftMenu.tsx b/apps/web/components/Dashboard/UI/LeftMenu.tsx index e0d8e511..1a4d2ca4 100644 --- a/apps/web/components/Dashboard/UI/LeftMenu.tsx +++ b/apps/web/components/Dashboard/UI/LeftMenu.tsx @@ -4,7 +4,7 @@ import { useAuth } from '@components/Security/AuthContext'; import ToolTip from '@components/StyledElements/Tooltip/Tooltip' import LearnHouseDashboardLogo from '@public/dashLogo.png'; import Avvvatars from 'avvvatars-react'; -import { ArrowLeft, Book, Home, School, Settings } from 'lucide-react' +import { ArrowLeft, Book, BookCopy, Home, School, Settings } from 'lucide-react' import Image from 'next/image'; import Link from 'next/link' import React, { use, useEffect } from 'react' @@ -52,7 +52,7 @@ function LeftMenu() { - + diff --git a/apps/web/components/Objects/Other/CollectionThumbnail.tsx b/apps/web/components/Objects/Thumbnails/CollectionThumbnail.tsx similarity index 100% rename from apps/web/components/Objects/Other/CollectionThumbnail.tsx rename to apps/web/components/Objects/Thumbnails/CollectionThumbnail.tsx diff --git a/apps/web/components/Objects/Other/CourseThumbnail.tsx b/apps/web/components/Objects/Thumbnails/CourseThumbnail.tsx similarity index 90% rename from apps/web/components/Objects/Other/CourseThumbnail.tsx rename to apps/web/components/Objects/Thumbnails/CourseThumbnail.tsx index 4d78c53c..2bc6692a 100644 --- a/apps/web/components/Objects/Other/CourseThumbnail.tsx +++ b/apps/web/components/Objects/Thumbnails/CourseThumbnail.tsx @@ -2,11 +2,12 @@ import { useOrg } from '@components/Contexts/OrgContext'; import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'; import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'; +import { DotsHorizontalIcon } from '@radix-ui/react-icons'; import { getUriWithOrg } from '@services/config/config'; import { deleteCourseFromBackend } from '@services/courses/courses'; import { getCourseThumbnailMediaDirectory } from '@services/media/media'; import { revalidateTags } from '@services/utils/ts/requests'; -import { FileEdit, X } from 'lucide-react'; +import { FileEdit, MoreHorizontal, Settings, X } from 'lucide-react'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import React, { use, useEffect } from 'react' @@ -55,12 +56,12 @@ const AdminEditsArea = (props: { orgSlug: string, courseId: string, course: any, action="update" ressourceType="course" checkMethod='roles' orgId={props.course.org_id}> -
+
- +
run.course_id == props.course.id); + if (run) { + return run.steps.find((step: any) => step.activity_id == activity.id); } - return false; + } function isActivityCurrent(activity: any) { diff --git a/apps/web/components/Pages/Trail/TrailCourseElement.tsx b/apps/web/components/Pages/Trail/TrailCourseElement.tsx index 9f955ea1..78b0f64d 100644 --- a/apps/web/components/Pages/Trail/TrailCourseElement.tsx +++ b/apps/web/components/Pages/Trail/TrailCourseElement.tsx @@ -1,21 +1,29 @@ 'use client'; +import { useOrg } from '@components/Contexts/OrgContext'; import { getAPIUrl, getBackendUrl, getUriWithOrg } from '@services/config/config'; import { removeCourse } from '@services/courses/activity'; import { getCourseThumbnailMediaDirectory } from '@services/media/media'; import { revalidateTags } from '@services/utils/ts/requests'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; +import { use, useEffect } from 'react'; import { mutate } from 'swr'; interface TrailCourseElementProps { course: any + run: any orgslug: string } function TrailCourseElement(props: TrailCourseElementProps) { + const org = useOrg() as any; const courseid = props.course.course_uuid.replace("course_", "") const course = props.course const router = useRouter(); + const course_total_steps = props.run.course_total_steps + const course_completed_steps = props.run.steps.length + const orgID = org?.id; + const course_progress = Math.round((course_completed_steps / course_total_steps) * 100) async function quitCourse(course_uuid: string) { // Close activity @@ -25,14 +33,18 @@ function TrailCourseElement(props: TrailCourseElementProps) { router.refresh(); // Mutate - mutate(`${getAPIUrl()}trail/org_slug/${props.orgslug}/trail`); + mutate(`${getAPIUrl()}trail/org/${orgID}/trail`); } + useEffect(() => { + } + , [props.course, org]); + return (
-
+
@@ -40,19 +52,19 @@ function TrailCourseElement(props: TrailCourseElementProps) {

Course

-

{course.course_object.name}

+

{course.name}

-

{course.progress}%

+

{course_progress}%

- +
-
+
diff --git a/apps/web/services/courses/activity.ts b/apps/web/services/courses/activity.ts index 79dae9f8..bdcf3368 100644 --- a/apps/web/services/courses/activity.ts +++ b/apps/web/services/courses/activity.ts @@ -7,19 +7,19 @@ import { getAPIUrl } from "@services/config/config"; */ export async function startCourse(course_uuid: string, org_slug: string) { - const result: any = await fetch(`${getAPIUrl()}trail/add_course/${course_uuid}`, RequestBody("POST", null, null)) + const result: any = await fetch(`${getAPIUrl()}trail/add_course/${course_uuid}`, RequestBody("POST", null, null)); const res = await errorHandling(result); return res; } export async function removeCourse(course_uuid: string, org_slug: string) { - const result: any = await fetch(`${getAPIUrl()}trail/remove_course/${course_uuid}`, RequestBody("DELETE", null, null)) + const result: any = await fetch(`${getAPIUrl()}trail/remove_course/${course_uuid}`, RequestBody("DELETE", null, null)); const res = await errorHandling(result); return res; } -export async function markActivityAsComplete(org_slug: string, course_uuid: string, activity_id: string) { - const result: any = await fetch(`${getAPIUrl()}trail/add_activity/${activity_id}`, RequestBody("POST", null, null)) +export async function markActivityAsComplete(org_slug: string, course_uuid: string, activity_uuid: string) { + const result: any = await fetch(`${getAPIUrl()}trail/add_activity/${activity_uuid}`, RequestBody("POST", null, null)); const res = await errorHandling(result); return res; }