mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: introduce trail api and depreciate old api
This commit is contained in:
parent
ecf4e4d6d9
commit
e14ba02f97
13 changed files with 315 additions and 271 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { EyeOpenIcon, Pencil2Icon } from "@radix-ui/react-icons";
|
import { EyeOpenIcon, Pencil2Icon } from "@radix-ui/react-icons";
|
||||||
import { closeActivity, createActivity } from "@services/courses/activity";
|
import { removeCourse, startCourse } from "@services/courses/activity";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
@ -13,21 +13,19 @@ const CourseIdPage = (params: any) => {
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug;
|
||||||
const { data: course, error: error } = useSWR(`${getAPIUrl()}courses/meta/course_${courseid}`, swrFetcher);
|
const { data: course, error: error } = useSWR(`${getAPIUrl()}courses/meta/course_${courseid}`, swrFetcher);
|
||||||
|
|
||||||
async function startActivity() {
|
async function startCourseUI() {
|
||||||
// Create activity
|
// Create activity
|
||||||
await createActivity("course_" + courseid);
|
await startCourse("course_" + courseid, orgslug);
|
||||||
|
|
||||||
// Mutate course
|
// Mutate course
|
||||||
mutate(`${getAPIUrl()}courses/meta/course_${courseid}`);
|
mutate(`${getAPIUrl()}courses/meta/course_${courseid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function quitActivity() {
|
async function quitCourse() {
|
||||||
// Get activity id and org id
|
|
||||||
let activity_id = course.activity.activity_id;
|
|
||||||
let org_id = course.activity.org_id;
|
|
||||||
|
|
||||||
// Close activity
|
// Close activity
|
||||||
let activity = await closeActivity(activity_id, org_id);
|
let activity = await removeCourse("course_" + courseid, orgslug);
|
||||||
console.log(activity);
|
console.log(activity);
|
||||||
|
|
||||||
// Mutate course
|
// Mutate course
|
||||||
|
|
@ -111,12 +109,12 @@ const CourseIdPage = (params: any) => {
|
||||||
</BoxWrapper>
|
</BoxWrapper>
|
||||||
</CourseMetaLeft>
|
</CourseMetaLeft>
|
||||||
<CourseMetaRight>
|
<CourseMetaRight>
|
||||||
{course.activity.status == "ongoing" ? (
|
{course.trail.status == "ongoing" ? (
|
||||||
<button style={{ backgroundColor: "red" }} onClick={quitActivity}>
|
<button style={{ backgroundColor: "red" }} onClick={quitCourse}>
|
||||||
Quit Activity
|
Quit Course
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button onClick={startActivity}>Start Activity</button>
|
<button onClick={startCourseUI}>Start Course</button>
|
||||||
)}
|
)}
|
||||||
</CourseMetaRight>
|
</CourseMetaRight>
|
||||||
</CourseMetaWrapper>
|
</CourseMetaWrapper>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ export default function RootLayout({ children, params }: { children: React.React
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Menu></Menu>
|
<Menu orgslug={params.orgslug}></Menu>
|
||||||
{children}
|
{children}
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -5,42 +5,43 @@ import React from "react";
|
||||||
import { styled } from "styled-components";
|
import { styled } from "styled-components";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
function Activity(params: any) {
|
function Trail(params: any) {
|
||||||
let orgslug = params.params.orgslug;
|
let orgslug = params.params.orgslug;
|
||||||
const { data: activities, error: error } = useSWR(`${getAPIUrl()}activity/org_slug/${orgslug}/activities`, swrFetcher);
|
const { data: trail, error: error } = useSWR(`${getAPIUrl()}trail/org_slug/${orgslug}/trail`, swrFetcher);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActivityLayout>
|
<TrailLayout>
|
||||||
<h1>Activity</h1>
|
<h1>Trail</h1>
|
||||||
<br />
|
<br />
|
||||||
{error && <p>Failed to load</p>}
|
{error && <p>Failed to load</p>}
|
||||||
{!activities ? (
|
{!trail ? (
|
||||||
<div>Loading...</div>
|
<div>Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{activities.map((activity: any) => (
|
{trail.courses.map((course: any) => (
|
||||||
<ActivityBox key={activity.activity_id}>
|
<TrailBox key={trail.trail_id}>
|
||||||
<ActivityMetadata>
|
<TrailMetadata>
|
||||||
<ActivityThumbnail>
|
<TrailThumbnail>
|
||||||
<img src={`${getBackendUrl()}content/uploads/img/${activity.course.thumbnail}`}></img>
|
<img src={`${getBackendUrl()}content/uploads/img/${course.course_object.thumbnail}`}></img>
|
||||||
</ActivityThumbnail>
|
</TrailThumbnail>
|
||||||
<ActivityInfo>
|
<TrailInfo>
|
||||||
<h2>Course</h2>
|
<h2>Course</h2>
|
||||||
<h3>{activity.course.name}</h3>
|
<h3>{course.course_object.name}</h3>
|
||||||
</ActivityInfo>
|
</TrailInfo>
|
||||||
</ActivityMetadata>
|
</TrailMetadata>
|
||||||
<ActivityProgress progress={activity.progression} />
|
<TrailProgress progress={course.progress} />
|
||||||
</ActivityBox>
|
</TrailBox>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ActivityLayout>
|
</TrailLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Activity;
|
export default Trail;
|
||||||
|
|
||||||
const ActivityLayout = styled.div`
|
const TrailLayout = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
width: 1300px;
|
width: 1300px;
|
||||||
|
|
@ -48,13 +49,13 @@ const ActivityLayout = styled.div`
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ActivityMetadata = styled.div`
|
const TrailMetadata = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
const ActivityBox = styled.div`
|
const TrailBox = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
@ -67,7 +68,7 @@ const ActivityBox = styled.div`
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ActivityThumbnail = styled.div`
|
const TrailThumbnail = styled.div`
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 7px 0px 0px 7px;
|
border-radius: 7px 0px 0px 7px;
|
||||||
|
|
@ -78,7 +79,7 @@ const ActivityThumbnail = styled.div`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ActivityInfo = styled.div`
|
const TrailInfo = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
|
@ -99,7 +100,7 @@ const ActivityInfo = styled.div`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ActivityProgress = styled.div`
|
const TrailProgress = styled.div`
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
|
@ -10,10 +10,11 @@ import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { getOrgFromUri, getUriWithOrg } from "@services/config/config";
|
import { getOrgFromUri, getUriWithOrg } from "@services/config/config";
|
||||||
|
|
||||||
export const Menu = (params : any) => {
|
export const Menu = (props : any ) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const orgslug = getOrgFromUri();
|
const orgslug = props.orgslug;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GlobalHeader>
|
<GlobalHeader>
|
||||||
|
|
@ -41,7 +42,7 @@ export const Menu = (params : any) => {
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{" "}
|
{" "}
|
||||||
<Link href={getUriWithOrg(orgslug, "/activity")}>Activity</Link>
|
<Link href={getUriWithOrg(orgslug, "/trail")}>Trail</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>More</li>
|
<li>More</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ export const getOrgFromUri = () => {
|
||||||
} else {
|
} else {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const hostname = window.location.hostname;
|
const hostname = window.location.hostname;
|
||||||
return hostname.replace(".localhost:3000", "");
|
|
||||||
|
return hostname.replace(".localhost", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,15 @@ import { getAPIUrl } from "@services/config/config";
|
||||||
GET requests are called from the frontend using SWR (https://swr.vercel.app/)
|
GET requests are called from the frontend using SWR (https://swr.vercel.app/)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export async function createActivity(course_id: string) {
|
export async function startCourse(course_id: string, org_slug: string) {
|
||||||
let data = {
|
const result: any = await fetch(`${getAPIUrl()}trail/${org_slug}/add_course/${course_id}`, RequestBody("POST", null))
|
||||||
course_id: course_id,
|
|
||||||
};
|
|
||||||
const result: any = await fetch(`${getAPIUrl()}activity/start`, RequestBody("POST", data))
|
|
||||||
.then((result) => result.json())
|
.then((result) => result.json())
|
||||||
.catch((error) => console.log("error", error));
|
.catch((error) => console.log("error", error));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function closeActivity(org_id: string, activity_id: string) {
|
export async function removeCourse(course_id: string, org_slug: string) {
|
||||||
const result: any = await fetch(`${getAPIUrl()}activity/${org_id}/close_activity/${activity_id}"`, RequestBody("PATCH", null))
|
const result: any = await fetch(`${getAPIUrl()}trail/${org_slug}/remove_course/${course_id}`, RequestBody("POST", null))
|
||||||
.then((result) => result.json())
|
.then((result) => result.json())
|
||||||
.catch((error) => console.log("error", error));
|
.catch((error) => console.log("error", error));
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -29,4 +26,3 @@ export async function maskActivityAsComplete(org_id: string, course_id: string,
|
||||||
.catch((error) => console.log("error", error));
|
.catch((error) => console.log("error", error));
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from src.routers import activity, blocks, users, auth, orgs, roles
|
from src.routers import blocks, trail, users, auth, orgs, roles
|
||||||
from src.routers.courses import chapters, collections, courses,activities
|
from src.routers.courses import chapters, collections, courses,activities
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,6 +16,6 @@ global_router.include_router(courses.router, prefix="/courses", tags=["courses"]
|
||||||
global_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
|
global_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
|
||||||
global_router.include_router(activities.router, prefix="/activities", tags=["activities"])
|
global_router.include_router(activities.router, prefix="/activities", tags=["activities"])
|
||||||
global_router.include_router(collections.router, prefix="/collections", tags=["collections"])
|
global_router.include_router(collections.router, prefix="/collections", tags=["collections"])
|
||||||
global_router.include_router(activity.router, prefix="/activity", tags=["activity"])
|
global_router.include_router(trail.router, prefix="/trail", tags=["trail"])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
from fastapi import APIRouter, Depends, Request
|
|
||||||
from src.security.auth import get_current_user
|
|
||||||
from src.services.activity import Activity, add_activity_to_activity, close_activity, create_activity, get_user_activities, get_user_activities_orgslug
|
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/start")
|
|
||||||
async def api_start_activity(request: Request, activity_object: Activity, user=Depends(get_current_user)):
|
|
||||||
"""
|
|
||||||
Start activity
|
|
||||||
"""
|
|
||||||
return await create_activity(request, user, activity_object)
|
|
||||||
|
|
||||||
# TODO : get activity by user_is and org_id and course_id
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/org_id/{org_id}/activities")
|
|
||||||
async def api_get_activity_by_orgid(request: Request, org_id: str, user=Depends(get_current_user)):
|
|
||||||
"""
|
|
||||||
Get a user activities
|
|
||||||
"""
|
|
||||||
return await get_user_activities(request, user, org_id=org_id)
|
|
||||||
|
|
||||||
@router.get("/org_slug/{org_slug}/activities")
|
|
||||||
async def api_get_activity_by_orgslug(request: Request, org_slug: str, user=Depends(get_current_user)):
|
|
||||||
"""
|
|
||||||
Get a user activities using org slug
|
|
||||||
"""
|
|
||||||
return await get_user_activities_orgslug(request, user, org_slug=org_slug)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{org_id}/add_activity/{course_id}/{activity_id}")
|
|
||||||
async def api_add_activity_to_activity(request: Request, org_id: str, course_id: str, activity_id: str, user=Depends(get_current_user)):
|
|
||||||
"""
|
|
||||||
Add activity to activity
|
|
||||||
"""
|
|
||||||
return await add_activity_to_activity(request, user, org_id, course_id, activity_id)
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{org_id}/close_activity/{activity_id}")
|
|
||||||
async def api_close_activity(request: Request, org_id: str, activity_id: str, user=Depends(get_current_user)):
|
|
||||||
"""
|
|
||||||
Close activity
|
|
||||||
"""
|
|
||||||
return await close_activity(request, user, org_id, activity_id)
|
|
||||||
47
src/routers/trail.py
Normal file
47
src/routers/trail.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from typing import Optional
|
||||||
|
from fastapi import APIRouter, Depends, Request
|
||||||
|
from src.security.auth import get_current_user
|
||||||
|
from src.services.trail import Trail, add_activity_to_trail, add_course_to_trail, create_trail, get_user_trail_with_orgslug, get_user_trail, remove_course_from_trail
|
||||||
|
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/start")
|
||||||
|
async def api_start_trail(request: Request, trail_object: Trail, org_id: str, user=Depends(get_current_user)) -> Trail:
|
||||||
|
"""
|
||||||
|
Start trail
|
||||||
|
"""
|
||||||
|
return await create_trail(request, user, org_id, trail_object)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/org_id/{org_id}/trail")
|
||||||
|
async def api_get_trail_by_orgid(request: Request, org_slug: str, user=Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Get a user trails
|
||||||
|
"""
|
||||||
|
return await get_user_trail(request, user=user, org_slug=org_slug)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/org_slug/{org_slug}/trail")
|
||||||
|
async def api_get_trail_by_orgslug(request: Request, org_slug: str, user=Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Get a user trails using org slug
|
||||||
|
"""
|
||||||
|
return await get_user_trail_with_orgslug(request, user, org_slug=org_slug)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{org_slug}/add_course/{course_id}")
|
||||||
|
async def api_add_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Add Course to trail
|
||||||
|
"""
|
||||||
|
return await add_course_to_trail(request, user, org_slug, course_id)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{org_slug}/remove_course/{course_id}")
|
||||||
|
async def api_remove_course_to_trail(request: Request, course_id: str, org_slug: str, user=Depends(get_current_user)):
|
||||||
|
"""
|
||||||
|
Remove Course from trail
|
||||||
|
"""
|
||||||
|
return await remove_course_from_trail(request, user, org_slug, course_id)
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
from cmath import log
|
|
||||||
from datetime import datetime
|
|
||||||
import json
|
|
||||||
from typing import List, Literal, Optional
|
|
||||||
from uuid import uuid4
|
|
||||||
from fastapi import HTTPException, Request, status
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from src.services.courses.chapters import get_coursechapters_meta
|
|
||||||
|
|
||||||
from src.services.users.users import PublicUser
|
|
||||||
|
|
||||||
#### Classes ####################################################
|
|
||||||
|
|
||||||
|
|
||||||
class Activity(BaseModel):
|
|
||||||
course_id: str
|
|
||||||
status: Optional[Literal['ongoing', 'done', 'closed']] = 'ongoing'
|
|
||||||
masked: Optional[bool] = False
|
|
||||||
activities_marked_complete: Optional[List[str]] = []
|
|
||||||
activities_data: Optional[List[dict]] = []
|
|
||||||
|
|
||||||
|
|
||||||
class ActivityInDB(Activity):
|
|
||||||
activity_id: str
|
|
||||||
user_id: str
|
|
||||||
org_id: str
|
|
||||||
creationDate: str = datetime.now().isoformat()
|
|
||||||
updateDate: str = datetime.now().isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
#### Classes ####################################################
|
|
||||||
|
|
||||||
|
|
||||||
async def create_activity(request: Request, user: PublicUser, activity_object: Activity):
|
|
||||||
activities = request.app.db["activities"]
|
|
||||||
|
|
||||||
# find if the user has already started the course
|
|
||||||
isActivityAlreadCreated = await activities.find_one(
|
|
||||||
{"course_id": activity_object.course_id, "user_id": user.user_id})
|
|
||||||
|
|
||||||
if isActivityAlreadCreated:
|
|
||||||
if isActivityAlreadCreated['status'] == 'closed':
|
|
||||||
activity_object.status = 'ongoing'
|
|
||||||
await activities.update_one(
|
|
||||||
{"activity_id": isActivityAlreadCreated['activity_id']}, {"$set": activity_object.dict()})
|
|
||||||
return activity_object
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Activity already created")
|
|
||||||
# create activity id
|
|
||||||
activity_id = f"activity_{uuid4()}"
|
|
||||||
|
|
||||||
# create activity
|
|
||||||
activity = ActivityInDB(**activity_object.dict(),activity_id=activity_id,
|
|
||||||
user_id=user.user_id, org_id=activity_object.course_id)
|
|
||||||
|
|
||||||
await activities.insert_one(activity.dict())
|
|
||||||
|
|
||||||
return activity
|
|
||||||
|
|
||||||
|
|
||||||
async def get_user_activities(request: Request, user: PublicUser, org_id: str):
|
|
||||||
activities = request.app.db["activities"]
|
|
||||||
courses = request.app.db["courses"]
|
|
||||||
coursechapters = request.app.db["coursechapters"]
|
|
||||||
|
|
||||||
activities_metadata = []
|
|
||||||
|
|
||||||
user_activities = activities.find(
|
|
||||||
{"user_id": user.user_id}, {'_id': 0})
|
|
||||||
|
|
||||||
if not user_activities:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="No activities found")
|
|
||||||
|
|
||||||
for activity in await user_activities.to_list(length=100):
|
|
||||||
# get number of activities in the course
|
|
||||||
coursechapters = await get_coursechapters_meta(request, activity['course_id'], user)
|
|
||||||
|
|
||||||
# calculate progression using the number of activities marked complete and the total number of activities
|
|
||||||
progression = round(
|
|
||||||
len(activity['activities_marked_complete']) / len(coursechapters['activities']) * 100, 2)
|
|
||||||
|
|
||||||
course = await courses.find_one({"course_id": activity['course_id']}, {'_id': 0})
|
|
||||||
|
|
||||||
# add progression to the activity
|
|
||||||
one_activity = {"course": course, "activitydata": activity, "progression": progression}
|
|
||||||
activities_metadata.append(one_activity)
|
|
||||||
|
|
||||||
return activities_metadata
|
|
||||||
|
|
||||||
async def get_user_activities_orgslug(request: Request, user: PublicUser, org_slug: str):
|
|
||||||
activities = request.app.db["activities"]
|
|
||||||
courses = request.app.db["courses"]
|
|
||||||
coursechapters = request.app.db["coursechapters"]
|
|
||||||
|
|
||||||
activities_metadata = []
|
|
||||||
|
|
||||||
user_activities = activities.find(
|
|
||||||
{"user_id": user.user_id}, {'_id': 0})
|
|
||||||
|
|
||||||
if not user_activities:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="No activities found")
|
|
||||||
|
|
||||||
for activity in await user_activities.to_list(length=100):
|
|
||||||
# get number of activities in the course
|
|
||||||
coursechapters = await get_coursechapters_meta(request, activity['course_id'], user)
|
|
||||||
|
|
||||||
# calculate progression using the number of activities marked complete and the total number of activities
|
|
||||||
progression = round(
|
|
||||||
len(activity['activities_marked_complete']) / len(coursechapters['activities']) * 100, 2)
|
|
||||||
|
|
||||||
course = await courses.find_one({"course_id": activity['course_id']}, {'_id': 0})
|
|
||||||
|
|
||||||
# add progression to the activity
|
|
||||||
one_activity = {"course": course, "activitydata": activity, "progression": progression}
|
|
||||||
activities_metadata.append(one_activity)
|
|
||||||
|
|
||||||
return activities_metadata
|
|
||||||
|
|
||||||
|
|
||||||
async def add_activity_to_activity(request: Request, user: PublicUser, org_id: str, course_id: str, activity_id: str):
|
|
||||||
activities = request.app.db["activities"]
|
|
||||||
course_id = f"course_{course_id}"
|
|
||||||
activity_id = f"activity_{activity_id}"
|
|
||||||
|
|
||||||
activity = await activities.find_one(
|
|
||||||
{"course_id": course_id,
|
|
||||||
"user_id": user.user_id
|
|
||||||
}, {'_id': 0})
|
|
||||||
|
|
||||||
if activity_id not in activity['activities_marked_complete']:
|
|
||||||
activity['activities_marked_complete'].append(str(activity_id))
|
|
||||||
await activities.update_one(
|
|
||||||
{"activity_id": activity['activity_id']}, {"$set": activity})
|
|
||||||
return activity
|
|
||||||
|
|
||||||
if not activity:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Activity not found")
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Activity already marked complete")
|
|
||||||
|
|
||||||
|
|
||||||
async def close_activity(request: Request, user: PublicUser, activity_id: str, org_id: str,):
|
|
||||||
activities = request.app.db["activities"]
|
|
||||||
activity = await activities.find_one(
|
|
||||||
{"activity_id": activity_id, "user_id": user.user_id})
|
|
||||||
|
|
||||||
if not activity:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=status.HTTP_409_CONFLICT, detail="Activity not found")
|
|
||||||
|
|
||||||
activity['status'] = 'closed'
|
|
||||||
|
|
||||||
await activities.update_one(
|
|
||||||
{"activity_id": activity['activity_id']}, {"$set": activity})
|
|
||||||
|
|
||||||
activity = ActivityInDB(**activity)
|
|
||||||
|
|
||||||
return activity
|
|
||||||
|
|
@ -72,7 +72,7 @@ async def get_course(request: Request, course_id: str, current_user: PublicUser)
|
||||||
async def get_course_meta(request: Request, course_id: str, current_user: PublicUser):
|
async def get_course_meta(request: Request, course_id: str, current_user: PublicUser):
|
||||||
courses = request.app.db["courses"]
|
courses = request.app.db["courses"]
|
||||||
coursechapters = request.app.db["coursechapters"]
|
coursechapters = request.app.db["coursechapters"]
|
||||||
activities = request.app.db["activities"]
|
trails = request.app.db["trails"]
|
||||||
|
|
||||||
course = await courses.find_one({"course_id": course_id})
|
course = await courses.find_one({"course_id": course_id})
|
||||||
activities = request.app.db["activities"]
|
activities = request.app.db["activities"]
|
||||||
|
|
@ -119,17 +119,20 @@ async def get_course_meta(request: Request, course_id: str, current_user: Public
|
||||||
course = Course(**course)
|
course = Course(**course)
|
||||||
|
|
||||||
# Get activity by user
|
# Get activity by user
|
||||||
activity = await activities.find_one(
|
trail = await trails.find_one(
|
||||||
{"course_id": course_id, "user_id": current_user.user_id})
|
{"courses.course_id": course_id, "user_id": current_user.user_id})
|
||||||
if activity:
|
print(trail)
|
||||||
activity = json.loads(json.dumps(activity, default=str))
|
if trail:
|
||||||
|
# get only the course where course_id == course_id
|
||||||
|
trail_course = next(
|
||||||
|
(course for course in trail["courses"] if course["course_id"] == course_id), None)
|
||||||
else:
|
else:
|
||||||
activity = ""
|
trail_course = ""
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"course": course,
|
"course": course,
|
||||||
"chapters": chapters_list_with_activities,
|
"chapters": chapters_list_with_activities,
|
||||||
"activity": activity
|
"trail": trail_course
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ class PublicOrganization(Organization):
|
||||||
slug: str
|
slug: str
|
||||||
org_id: str
|
org_id: str
|
||||||
|
|
||||||
|
def __getitem__(self, item):
|
||||||
|
return getattr(self, item)
|
||||||
|
|
||||||
|
|
||||||
#### Classes ####################################################
|
#### Classes ####################################################
|
||||||
|
|
||||||
|
|
|
||||||
205
src/services/trail.py
Normal file
205
src/services/trail.py
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
from cmath import log
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
from typing import List, Literal, Optional
|
||||||
|
from uuid import uuid4
|
||||||
|
from fastapi import HTTPException, Request, status
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from src.services.courses.chapters import get_coursechapters_meta
|
||||||
|
from src.services.orgs import PublicOrganization
|
||||||
|
|
||||||
|
from src.services.users.users import PublicUser
|
||||||
|
|
||||||
|
#### Classes ####################################################
|
||||||
|
|
||||||
|
|
||||||
|
class ActivityData(BaseModel):
|
||||||
|
activity_id: str
|
||||||
|
activity_type: str
|
||||||
|
data: Optional[dict]
|
||||||
|
|
||||||
|
|
||||||
|
class TrailCourse(BaseModel):
|
||||||
|
course_id: str
|
||||||
|
elements_type: Optional[Literal['course']] = 'course'
|
||||||
|
status: Optional[Literal['ongoing', 'done', 'closed']] = 'ongoing'
|
||||||
|
course_object: dict
|
||||||
|
masked: Optional[bool] = False
|
||||||
|
activities_marked_complete: Optional[List[str]]
|
||||||
|
activities_data: Optional[List[ActivityData]]
|
||||||
|
progress: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
class Trail(BaseModel):
|
||||||
|
status: Optional[Literal['ongoing', 'done', 'closed']] = 'ongoing'
|
||||||
|
masked: Optional[bool] = False
|
||||||
|
courses: Optional[List[TrailCourse]]
|
||||||
|
|
||||||
|
|
||||||
|
class TrailInDB(Trail):
|
||||||
|
trail_id: str
|
||||||
|
org_id: str
|
||||||
|
user_id: str
|
||||||
|
creationDate: str = datetime.now().isoformat()
|
||||||
|
updateDate: str = datetime.now().isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
#### Classes ####################################################
|
||||||
|
|
||||||
|
|
||||||
|
async def create_trail(request: Request, user: PublicUser, org_id: str, trail_object: Trail) -> Trail:
|
||||||
|
trails = request.app.db["trails"]
|
||||||
|
|
||||||
|
# get list of courses
|
||||||
|
if trail_object.courses:
|
||||||
|
courses = trail_object.courses
|
||||||
|
# get course ids
|
||||||
|
course_ids = [course.course_id for course in courses]
|
||||||
|
|
||||||
|
# find if the user has already started the course
|
||||||
|
existing_trail = await trails.find_one({'user_id': user.user_id, 'courses.course_id': {'$in': course_ids}})
|
||||||
|
if existing_trail:
|
||||||
|
# update the status of the element with the matching course_id to "ongoing"
|
||||||
|
for element in existing_trail['courses']:
|
||||||
|
if element['course_id'] in course_ids:
|
||||||
|
element['status'] = 'ongoing'
|
||||||
|
# update the existing trail in the database
|
||||||
|
await trails.replace_one({'trail_id': existing_trail['trail_id']}, existing_trail)
|
||||||
|
|
||||||
|
# create trail id
|
||||||
|
trail_id = f"trail_{uuid4()}"
|
||||||
|
|
||||||
|
# create trail
|
||||||
|
trail = TrailInDB(**trail_object.dict(), trail_id=trail_id,
|
||||||
|
user_id=user.user_id, org_id=org_id)
|
||||||
|
|
||||||
|
await trails.insert_one(trail.dict())
|
||||||
|
|
||||||
|
return trail
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_trail(request: Request, org_slug: str, user: PublicUser) -> Trail:
|
||||||
|
trails = request.app.db["trails"]
|
||||||
|
trail = await trails.find_one({"user_id": user.user_id})
|
||||||
|
if not trail:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found")
|
||||||
|
for element in trail["courses"]:
|
||||||
|
course_id = element["course_id"]
|
||||||
|
chapters_meta = await get_coursechapters_meta(request, course_id, user)
|
||||||
|
activities = chapters_meta["activities"]
|
||||||
|
num_activities = len(activities)
|
||||||
|
|
||||||
|
num_completed_activities = len(
|
||||||
|
element.get("activities_marked_complete", []))
|
||||||
|
element["progress"] = round(
|
||||||
|
(num_completed_activities / num_activities) * 100, 2) if num_activities > 0 else 0
|
||||||
|
|
||||||
|
return Trail(**trail)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_user_trail_with_orgslug(request: Request, user: PublicUser, org_slug: str) -> Trail:
|
||||||
|
trails = request.app.db["trails"]
|
||||||
|
orgs = request.app.db["organizations"]
|
||||||
|
courses_mongo = request.app.db["courses"]
|
||||||
|
|
||||||
|
# get org_id from orgslug
|
||||||
|
org = await orgs.find_one({"slug": org_slug})
|
||||||
|
|
||||||
|
trail = await trails.find_one({"user_id": user.user_id, "org_id": org["org_id"]})
|
||||||
|
if not trail:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found")
|
||||||
|
for courses in trail["courses"]:
|
||||||
|
course_id = courses["course_id"]
|
||||||
|
|
||||||
|
chapters_meta = await get_coursechapters_meta(request, course_id, user)
|
||||||
|
activities = chapters_meta["activities"]
|
||||||
|
|
||||||
|
# get course object without _id
|
||||||
|
course_object = await courses_mongo.find_one({"course_id": course_id}, {"_id": 0})
|
||||||
|
|
||||||
|
courses["course_object"] = course_object
|
||||||
|
num_activities = len(activities)
|
||||||
|
|
||||||
|
num_completed_activities = len(
|
||||||
|
courses.get("activities_marked_complete", []))
|
||||||
|
courses["progress"] = round(
|
||||||
|
(num_completed_activities / num_activities) * 100, 2) if num_activities > 0 else 0
|
||||||
|
|
||||||
|
return Trail(**trail)
|
||||||
|
|
||||||
|
|
||||||
|
async def add_activity_to_trail(request: Request, user: PublicUser, trail_id: str, course_id: str, activity_id: str) -> Trail:
|
||||||
|
trails = request.app.db["trails"]
|
||||||
|
trail = await trails.find_one({"trail_id": trail_id, "user_id": user.user_id})
|
||||||
|
if not trail:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found")
|
||||||
|
for element in trail["courses"]:
|
||||||
|
if element["course_id"] == course_id:
|
||||||
|
if activity_id not in element["activities_marked_complete"]:
|
||||||
|
element["activities_marked_complete"].append(activity_id)
|
||||||
|
break
|
||||||
|
await trails.replace_one({"trail_id": trail_id}, trail)
|
||||||
|
return Trail(**trail)
|
||||||
|
|
||||||
|
|
||||||
|
async def add_course_to_trail(request: Request, user: PublicUser, orgslug: str, course_id: str) -> Trail:
|
||||||
|
trails = request.app.db["trails"]
|
||||||
|
orgs = request.app.db["organizations"]
|
||||||
|
|
||||||
|
|
||||||
|
org = await orgs.find_one({"slug": orgslug})
|
||||||
|
|
||||||
|
org = PublicOrganization(**org)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
trail = await trails.find_one(
|
||||||
|
{"user_id": user.user_id, "org_id": org["org_id"]})
|
||||||
|
|
||||||
|
if not trail:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found")
|
||||||
|
|
||||||
|
# check if course is already present in the trail
|
||||||
|
|
||||||
|
for element in trail["courses"]:
|
||||||
|
if element["course_id"] == course_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Course already present in the trail")
|
||||||
|
|
||||||
|
updated_trail = TrailCourse(course_id=course_id, activities_data=[
|
||||||
|
], activities_marked_complete=[], progress=0, course_object={}, status="ongoing", masked=False)
|
||||||
|
trail["courses"].append(updated_trail.dict())
|
||||||
|
await trails.replace_one({"trail_id": trail['trail_id']}, trail)
|
||||||
|
return Trail(**trail)
|
||||||
|
|
||||||
|
async def remove_course_from_trail(request: Request, user: PublicUser, orgslug: str, course_id: str) -> Trail:
|
||||||
|
trails = request.app.db["trails"]
|
||||||
|
orgs = request.app.db["organizations"]
|
||||||
|
|
||||||
|
|
||||||
|
org = await orgs.find_one({"slug": orgslug})
|
||||||
|
|
||||||
|
org = PublicOrganization(**org)
|
||||||
|
|
||||||
|
print(org)
|
||||||
|
|
||||||
|
trail = await trails.find_one(
|
||||||
|
{"user_id": user.user_id, "org_id": org["org_id"]})
|
||||||
|
|
||||||
|
if not trail:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="Trail not found")
|
||||||
|
|
||||||
|
# check if course is already present in the trail
|
||||||
|
|
||||||
|
for element in trail["courses"]:
|
||||||
|
if element["course_id"] == course_id:
|
||||||
|
trail["courses"].remove(element)
|
||||||
|
break
|
||||||
|
|
||||||
|
await trails.replace_one({"trail_id": trail['trail_id']}, trail)
|
||||||
|
return Trail(**trail)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue