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;
}