From 47782b57bc224a194eb1e0f34cc38a791f3bf198 Mon Sep 17 00:00:00 2001 From: swve Date: Wed, 10 Jul 2024 23:35:32 +0200 Subject: [PATCH] feat: add Assignments, Tasks, Submissions CRUD --- apps/api/poetry.lock | 61 +- apps/api/src/db/assignments.py | 133 --- apps/api/src/db/{ => courses}/activities.py | 0 apps/api/src/db/courses/assignments.py | 317 ++++++ apps/api/src/db/{ => courses}/blocks.py | 2 +- .../db/{ => courses}/chapter_activities.py | 0 apps/api/src/db/{ => courses}/chapters.py | 8 +- .../src/db/{ => courses}/course_chapters.py | 0 .../src/db/{ => courses}/course_updates.py | 0 apps/api/src/db/{ => courses}/courses.py | 4 +- apps/api/src/db/trails.py | 9 +- apps/api/src/router.py | 6 +- .../courses/{ => activities}/activities.py | 2 +- .../{ => courses/activities}/blocks.py | 2 +- apps/api/src/routers/courses/assignments.py | 310 ++++++ apps/api/src/routers/courses/chapters.py | 2 +- apps/api/src/routers/courses/courses.py | 4 +- apps/api/src/security/rbac/rbac.py | 2 +- apps/api/src/services/ai/ai.py | 4 +- .../block_types/imageBlock/imageBlock.py | 6 +- .../blocks/block_types/pdfBlock/pdfBlock.py | 6 +- .../block_types/videoBlock/videoBlock.py | 6 +- .../services/courses/activities/activities.py | 10 +- .../courses/activities/assignments.py | 997 ++++++++++++++++++ .../src/services/courses/activities/pdf.py | 10 +- .../src/services/courses/activities/utils.py | 4 +- .../src/services/courses/activities/video.py | 10 +- apps/api/src/services/courses/chapters.py | 8 +- apps/api/src/services/courses/collections.py | 2 +- apps/api/src/services/courses/courses.py | 2 +- apps/api/src/services/courses/updates.py | 4 +- apps/api/src/services/trail/trail.py | 6 +- 32 files changed, 1719 insertions(+), 218 deletions(-) delete mode 100644 apps/api/src/db/assignments.py rename apps/api/src/db/{ => courses}/activities.py (100%) create mode 100644 apps/api/src/db/courses/assignments.py rename apps/api/src/db/{ => courses}/blocks.py (96%) rename apps/api/src/db/{ => courses}/chapter_activities.py (100%) rename apps/api/src/db/{ => courses}/chapters.py (90%) rename apps/api/src/db/{ => courses}/course_chapters.py (100%) rename apps/api/src/db/{ => courses}/course_updates.py (100%) rename apps/api/src/db/{ => courses}/courses.py (95%) rename apps/api/src/routers/courses/{ => activities}/activities.py (97%) rename apps/api/src/routers/{ => courses/activities}/blocks.py (98%) create mode 100644 apps/api/src/routers/courses/assignments.py create mode 100644 apps/api/src/services/courses/activities/assignments.py diff --git a/apps/api/poetry.lock b/apps/api/poetry.lock index 85625d58..deb41715 100644 --- a/apps/api/poetry.lock +++ b/apps/api/poetry.lock @@ -215,17 +215,17 @@ typecheck = ["mypy"] [[package]] name = "boto3" -version = "1.34.137" +version = "1.34.143" description = "The AWS SDK for Python" optional = false python-versions = ">=3.8" files = [ - {file = "boto3-1.34.137-py3-none-any.whl", hash = "sha256:7cb697d67fd138ceebc6f789919ae370c092a50c6b0ccc4ef483027935502eab"}, - {file = "boto3-1.34.137.tar.gz", hash = "sha256:0b21b84db4619b3711a6f643d465a5a25e81231ee43615c55a20ff6b89c6cc3c"}, + {file = "boto3-1.34.143-py3-none-any.whl", hash = "sha256:0d16832f23e6bd3ae94e35ea8e625529850bfad9baccd426de96ad8f445d8e03"}, + {file = "boto3-1.34.143.tar.gz", hash = "sha256:b590ce80c65149194def43ebf0ea1cf0533945502507837389a8d22e3ecbcf05"}, ] [package.dependencies] -botocore = ">=1.34.137,<1.35.0" +botocore = ">=1.34.143,<1.35.0" jmespath = ">=0.7.1,<2.0.0" s3transfer = ">=0.10.0,<0.11.0" @@ -234,13 +234,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] [[package]] name = "botocore" -version = "1.34.137" +version = "1.34.143" description = "Low-level, data-driven core of boto 3." optional = false python-versions = ">=3.8" files = [ - {file = "botocore-1.34.137-py3-none-any.whl", hash = "sha256:a980fa4adec4bfa23fff70a3512622e9412c69c791898a52cafc2458b0be6040"}, - {file = "botocore-1.34.137.tar.gz", hash = "sha256:e29c8e9bfda0b20a1997792968e85868bfce42fefad9730f633a81adcff3f2ef"}, + {file = "botocore-1.34.143-py3-none-any.whl", hash = "sha256:094aea179e8aaa1bc957ad49cc27d93b189dd3a1f3075d8b0ca7c445a2a88430"}, + {file = "botocore-1.34.143.tar.gz", hash = "sha256:059f032ec05733a836e04e869c5a15534420102f93116f3bc9a5b759b0651caf"}, ] [package.dependencies] @@ -844,13 +844,13 @@ tqdm = ["tqdm"] [[package]] name = "google-auth" -version = "2.31.0" +version = "2.32.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google-auth-2.31.0.tar.gz", hash = "sha256:87805c36970047247c8afe614d4e3af8eceafc1ebba0c679fe75ddd1d575e871"}, - {file = "google_auth-2.31.0-py2.py3-none-any.whl", hash = "sha256:042c4702efa9f7d3c48d3a69341c209381b125faa6dbf3ebe56bc7e40ae05c23"}, + {file = "google_auth-2.32.0-py2.py3-none-any.whl", hash = "sha256:53326ea2ebec768070a94bee4e1b9194c9646ea0c2bd72422785bd0f9abfad7b"}, + {file = "google_auth-2.32.0.tar.gz", hash = "sha256:49315be72c55a6a37d62819e3573f6b416aca00721f7e3e31a008d928bf64022"}, ] [package.dependencies] @@ -1877,13 +1877,13 @@ sympy = "*" [[package]] name = "openai" -version = "1.35.8" +version = "1.35.13" description = "The official Python library for the openai API" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.35.8-py3-none-any.whl", hash = "sha256:69d5b0f47f0c806d5da83fb0f84c147661395226d7f79acc78aa1d9b8c635887"}, - {file = "openai-1.35.8.tar.gz", hash = "sha256:3f6101888bb516647edade74c503f2b937b8bab73408e799d58f2aba68bbe51c"}, + {file = "openai-1.35.13-py3-none-any.whl", hash = "sha256:36ec3e93e0d1f243f69be85c89b9221a471c3e450dfd9df16c9829e3cdf63e60"}, + {file = "openai-1.35.13.tar.gz", hash = "sha256:c684f3945608baf7d2dcc0ef3ee6f3e27e4c66f21076df0b47be45d57e6ae6e4"}, ] [package.dependencies] @@ -2732,13 +2732,13 @@ rsa = ["oauthlib[signedtoken] (>=3.0.0)"] [[package]] name = "resend" -version = "2.1.0" +version = "2.2.0" description = "Resend Python SDK" optional = false python-versions = ">=3.7" files = [ - {file = "resend-2.1.0-py2.py3-none-any.whl", hash = "sha256:7f2a221983fab74a09f669c0c14a75daf547ffa4b4930141626d9cca55bca767"}, - {file = "resend-2.1.0.tar.gz", hash = "sha256:92dc8e035c2ce8cf8210c1c322e86b0a4f509e0c82a80932d3323cd2f3a43d2d"}, + {file = "resend-2.2.0-py2.py3-none-any.whl", hash = "sha256:be420762885de25c816497f09207da1cd54d253c44d9f81f441367893a42d099"}, + {file = "resend-2.2.0.tar.gz", hash = "sha256:f44976e4a37bb66445280bd8ef201eaac536b07bbe7c4da8f1717f4fcc63da7e"}, ] [package.dependencies] @@ -2796,13 +2796,13 @@ crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] [[package]] name = "sentry-sdk" -version = "2.7.1" +version = "2.9.0" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" files = [ - {file = "sentry_sdk-2.7.1-py2.py3-none-any.whl", hash = "sha256:ef1b3d54eb715825657cd4bb3cb42bb4dc85087bac14c56b0fd8c21abd968c9a"}, - {file = "sentry_sdk-2.7.1.tar.gz", hash = "sha256:25006c7e68b75aaa5e6b9c6a420ece22e8d7daec4b7a906ffd3a8607b67c037b"}, + {file = "sentry_sdk-2.9.0-py2.py3-none-any.whl", hash = "sha256:0bea5fa8b564cc0d09f2e6f55893e8f70286048b0ffb3a341d5b695d1af0e6ee"}, + {file = "sentry_sdk-2.9.0.tar.gz", hash = "sha256:4c85bad74df9767976afb3eeddc33e0e153300e887d637775a753a35ef99bee6"}, ] [package.dependencies] @@ -2847,13 +2847,13 @@ tornado = ["tornado (>=6)"] [[package]] name = "setuptools" -version = "70.2.0" +version = "70.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-70.2.0-py3-none-any.whl", hash = "sha256:b8b8060bb426838fbe942479c90296ce976249451118ef566a5a0b7d8b78fb05"}, - {file = "setuptools-70.2.0.tar.gz", hash = "sha256:bd63e505105011b25c3c11f753f7e3b8465ea739efddaccef8f0efac2137bac1"}, + {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, + {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, ] [package.extras] @@ -3014,27 +3014,30 @@ full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7 [[package]] name = "sympy" -version = "1.12.1" +version = "1.13.0" description = "Computer algebra system (CAS) in Python" optional = false python-versions = ">=3.8" files = [ - {file = "sympy-1.12.1-py3-none-any.whl", hash = "sha256:9b2cbc7f1a640289430e13d2a56f02f867a1da0190f2f99d8968c2f74da0e515"}, - {file = "sympy-1.12.1.tar.gz", hash = "sha256:2877b03f998cd8c08f07cd0de5b767119cd3ef40d09f41c30d722f6686b0fb88"}, + {file = "sympy-1.13.0-py3-none-any.whl", hash = "sha256:6b0b32a4673fb91bd3cac3b55406c8e01d53ae22780be467301cc452f6680c92"}, + {file = "sympy-1.13.0.tar.gz", hash = "sha256:3b6af8f4d008b9a1a6a4268b335b984b23835f26d1d60b0526ebc71d48a25f57"}, ] [package.dependencies] -mpmath = ">=1.1.0,<1.4.0" +mpmath = ">=1.1.0,<1.4" + +[package.extras] +dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] [[package]] name = "tenacity" -version = "8.4.2" +version = "8.5.0" description = "Retry code until it succeeds" optional = false python-versions = ">=3.8" files = [ - {file = "tenacity-8.4.2-py3-none-any.whl", hash = "sha256:9e6f7cf7da729125c7437222f8a522279751cdfbe6b67bfe64f75d3a348661b2"}, - {file = "tenacity-8.4.2.tar.gz", hash = "sha256:cd80a53a79336edba8489e767f729e4f391c896956b57140b5d7511a64bbd3ef"}, + {file = "tenacity-8.5.0-py3-none-any.whl", hash = "sha256:b594c2a5945830c267ce6b79a166228323ed52718f30302c1359836112346687"}, + {file = "tenacity-8.5.0.tar.gz", hash = "sha256:8bc6c0c8a09b31e6cad13c47afbed1a567518250a9a171418582ed8d9c20ca78"}, ] [package.extras] diff --git a/apps/api/src/db/assignments.py b/apps/api/src/db/assignments.py deleted file mode 100644 index 9d8788d5..00000000 --- a/apps/api/src/db/assignments.py +++ /dev/null @@ -1,133 +0,0 @@ -from typing import Optional -from openai import BaseModel -from sqlalchemy import JSON, Column, ForeignKey -from sqlmodel import Field, SQLModel -from enum import Enum - - -class AssignmentTypeEnum(str, Enum): - FILE_SUBMISSION = "FILE_SUBMISSION" - QUIZ = "QUIZ" - OTHER = "OTHER" - - -class Assignment(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - assignment_uuid: str - title: str - description: str - due_date: str - - org_id: int = Field( - sa_column=Column("org_id", ForeignKey("organization.id", ondelete="CASCADE")) - ) - course_id: int = Field( - sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE")) - ) - chapter_id: int = Field( - sa_column=Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")) - ) - activity_id: int = Field( - sa_column=Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")) - ) - - creation_date: str - update_date: str - - -class AssignmentTask(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - assignment_task_uuid: str = "" - title: str = "" - description: str = "" - hint: str = "" - assignment_type: AssignmentTypeEnum - contents: dict = Field(default={}, sa_column=Column(JSON)) - max_grade_value: int = ( - 0 # Value is always between 0-100 and is used as the maximum grade for the task - ) - - # Foreign keys - assignment_id: int = Field( - sa_column=Column( - "assignment_id", ForeignKey("assignment.id", ondelete="CASCADE") - ) - ) - org_id: int = Field( - sa_column=Column("org_id", ForeignKey("organization.id", ondelete="CASCADE")) - ) - course_id: int = Field( - sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE")) - ) - chapter_id: int = Field( - sa_column=Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")) - ) - activity_id: int = Field( - sa_column=Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")) - ) - creation_date: str - update_date: str - - -class AssignmentTaskSubmission(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - assignment_task_submission_uuid: str = "" - task_submission: dict = Field(default={}, sa_column=Column(JSON)) - grade: int = ( - 0 # Value is always between 0-100 depending on the questions, this is used to calculate the final grade on the AssignmentUser model - ) - task_submission_grade_feedback: str = "" # Feedback given by the teacher - - # Foreign keys - user_id: int = Field( - sa_column=Column("user_id", ForeignKey("user.id", ondelete="CASCADE")) - ) - activity_id: int = Field( - sa_column=Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")) - ) - course_id: int = Field( - sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE")) - ) - chapter_id: int = Field( - sa_column=Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")) - ) - assignment_task_id: int = Field( - sa_column=Column( - "assignment_task_id", ForeignKey("assignment_task.id", ondelete="CASCADE") - ) - ) - creation_date: str = "" - update_date: str = "" - - -# Note on grading : -# To calculate the final grade : - - - - -class AssignmentUserSubmissionStatus(str, Enum): - PENDING = "PENDING" - SUBMITTED = "SUBMITTED" - GRADED = "GRADED" - LATE = "LATE" - NOT_SUBMITTED = "NOT_SUBMITTED" - - -class AssignmentUserSubmission(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - assignment_user_uuid: str = "" - submission_status: AssignmentUserSubmissionStatus = ( - AssignmentUserSubmissionStatus.PENDING - ) - grading: str = "" - user_id: int = Field( - sa_column=Column("user_id", ForeignKey("user.id", ondelete="CASCADE")) - ) - assignment_id: int = Field( - sa_column=Column( - "assignment_id", ForeignKey("assignment.id", ondelete="CASCADE") - ) - ) - creation_date: str = "" - update_date: str = "" diff --git a/apps/api/src/db/activities.py b/apps/api/src/db/courses/activities.py similarity index 100% rename from apps/api/src/db/activities.py rename to apps/api/src/db/courses/activities.py diff --git a/apps/api/src/db/courses/assignments.py b/apps/api/src/db/courses/assignments.py new file mode 100644 index 00000000..78632aa2 --- /dev/null +++ b/apps/api/src/db/courses/assignments.py @@ -0,0 +1,317 @@ +from typing import Optional, Dict +from sqlalchemy import JSON, Column, ForeignKey +from sqlmodel import Field, SQLModel +from enum import Enum + + + +## Assignment ## +class GradingTypeEnum(str, Enum): + ALPHABET = "ALPHABET" + NUMERIC = "NUMERIC" + PERCENTAGE = "PERCENTAGE" + + +class AssignmentBase(SQLModel): + """Represents the common fields for an assignment.""" + + title: str + description: str + due_date: str + published: Optional[bool] = False + grading_type: GradingTypeEnum + + org_id: int + course_id: int + chapter_id: int + activity_id: int + + +class AssignmentCreate(AssignmentBase): + """Model for creating a new assignment.""" + + pass # Inherits all fields from AssignmentBase + + +class AssignmentRead(AssignmentBase): + """Model for reading an assignment.""" + + id: int + assignment_uuid: str + creation_date: Optional[str] + update_date: Optional[str] + + +class AssignmentUpdate(SQLModel): + """Model for updating an assignment.""" + + title: Optional[str] + description: Optional[str] + due_date: Optional[str] + published: Optional[bool] + grading_type: Optional[GradingTypeEnum] + org_id: Optional[int] + course_id: Optional[int] + chapter_id: Optional[int] + activity_id: Optional[int] + update_date: Optional[str] + + +class Assignment(AssignmentBase, table=True): + """Represents an assignment with relevant details and foreign keys.""" + + id: Optional[int] = Field(default=None, primary_key=True) + creation_date: Optional[str] + update_date: Optional[str] + assignment_uuid: str + + org_id: int = Field( + sa_column=Column("org_id", ForeignKey("organization.id", ondelete="CASCADE")) + ) + course_id: int = Field( + sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE")) + ) + chapter_id: int = Field( + sa_column=Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")) + ) + activity_id: int = Field( + sa_column=Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")) + ) + + +## Assignment ## + +## AssignmentTask ## + + +class AssignmentTaskTypeEnum(str, Enum): + FILE_SUBMISSION = "FILE_SUBMISSION" + QUIZ = "QUIZ" + FORM = "FORM" # soon to be implemented + OTHER = "OTHER" + + +class AssignmentTaskBase(SQLModel): + """Represents the common fields for an assignment task.""" + + title: str + description: str + hint: str + assignment_type: AssignmentTaskTypeEnum + contents: Dict = Field(default={}, sa_column=Column(JSON)) + max_grade_value: int = 0 # Value is always between 0-100 + + assignment_id: int + org_id: int + course_id: int + chapter_id: int + activity_id: int + + +class AssignmentTaskCreate(AssignmentTaskBase ): + """Model for creating a new assignment task.""" + + pass # Inherits all fields from AssignmentTaskBase + + +class AssignmentTaskRead(AssignmentTaskBase): + """Model for reading an assignment task.""" + + id: int + assignment_task_uuid: str + + +class AssignmentTaskUpdate(SQLModel): + """Model for updating an assignment task.""" + + title: Optional[str] + description: Optional[str] + hint: Optional[str] + assignment_type: Optional[AssignmentTaskTypeEnum] + contents: Optional[Dict] = Field(default={}, sa_column=Column(JSON)) + max_grade_value: Optional[int] + assignment_id: Optional[int] + org_id: Optional[int] + course_id: Optional[int] + chapter_id: Optional[int] + activity_id: Optional[int] + + +class AssignmentTask(AssignmentTaskBase, table=True): + """Represents a task within an assignment with various attributes and foreign keys.""" + + id: Optional[int] = Field(default=None, primary_key=True) + + assignment_task_uuid: str + creation_date: str + update_date: str + + assignment_id: int = Field( + sa_column=Column( + "assignment_id", ForeignKey("assignment.id", ondelete="CASCADE") + ) + ) + org_id: int = Field( + sa_column=Column("org_id", ForeignKey("organization.id", ondelete="CASCADE")) + ) + course_id: int = Field( + sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE")) + ) + chapter_id: int = Field( + sa_column=Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")) + ) + activity_id: int = Field( + sa_column=Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")) + ) + + +## AssignmentTask ## + + +## AssignmentTaskSubmission ## + + +class AssignmentTaskSubmissionBase(SQLModel): + """Represents the common fields for an assignment task submission.""" + + task_submission: Dict = Field(default={}, sa_column=Column(JSON)) + grade: int = 0 # Value is always between 0-100 + task_submission_grade_feedback: str + assignment_type: AssignmentTaskTypeEnum + + user_id: int + activity_id: int + course_id: int + chapter_id: int + assignment_task_id: int + + +class AssignmentTaskSubmissionCreate(AssignmentTaskSubmissionBase): + """Model for creating a new assignment task submission.""" + + pass # Inherits all fields from AssignmentTaskSubmissionBase + + +class AssignmentTaskSubmissionRead(AssignmentTaskSubmissionBase): + """Model for reading an assignment task submission.""" + + id: int + creation_date: str + update_date: str + + +class AssignmentTaskSubmissionUpdate(SQLModel): + """Model for updating an assignment task submission.""" + + assignment_task_submission_uuid: Optional[str] + task_submission: Optional[Dict] = Field(default={}, sa_column=Column(JSON)) + grade: Optional[int] + task_submission_grade_feedback: Optional[str] + assignment_type: Optional[AssignmentTaskTypeEnum] + user_id: Optional[int] + activity_id: Optional[int] + course_id: Optional[int] + chapter_id: Optional[int] + assignment_task_id: Optional[int] + + +class AssignmentTaskSubmission(AssignmentTaskSubmissionBase, table=True): + """Represents a submission for a specific assignment task with grade and feedback.""" + + id: Optional[int] = Field(default=None, primary_key=True) + assignment_task_submission_uuid: str + task_submission: Dict = Field(default={}, sa_column=Column(JSON)) + grade: int = 0 # Value is always between 0-100 + task_submission_grade_feedback: str + assignment_type: AssignmentTaskTypeEnum + + user_id: int = Field( + sa_column=Column("user_id", ForeignKey("user.id", ondelete="CASCADE")) + ) + activity_id: int = Field( + sa_column=Column("activity_id", ForeignKey("activity.id", ondelete="CASCADE")) + ) + course_id: int = Field( + sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE")) + ) + chapter_id: int = Field( + sa_column=Column("chapter_id", ForeignKey("chapter.id", ondelete="CASCADE")) + ) + assignment_task_id: int = Field( + sa_column=Column( + "assignment_task_id", ForeignKey("assignmenttask.id", ondelete="CASCADE") + ) + ) + + creation_date: str + update_date: str + +## AssignmentTaskSubmission ## + +## AssignmentUserSubmission ## + +class AssignmentUserSubmissionStatus(str, Enum): + PENDING = "PENDING" + SUBMITTED = "SUBMITTED" + GRADED = "GRADED" + LATE = "LATE" + NOT_SUBMITTED = "NOT_SUBMITTED" + + +class AssignmentUserSubmissionBase(SQLModel): + """Represents the submission status of an assignment for a user.""" + + + submission_status: AssignmentUserSubmissionStatus = ( + AssignmentUserSubmissionStatus.PENDING + ) + grade: str + user_id: int = Field( + sa_column=Column("user_id", ForeignKey("user.id", ondelete="CASCADE")) + ) + assignment_id: int = Field( + sa_column=Column( + "assignment_id", ForeignKey("assignment.id", ondelete="CASCADE") + ) + ) + +class AssignmentUserSubmissionCreate(AssignmentUserSubmissionBase): + """Model for creating a new assignment user submission.""" + + pass # Inherits all fields from AssignmentUserSubmissionBase + +class AssignmentUserSubmissionRead(AssignmentUserSubmissionBase): + """Model for reading an assignment user submission.""" + + id: int + creation_date: str + update_date: str + +class AssignmentUserSubmissionUpdate(SQLModel): + """Model for updating an assignment user submission.""" + + submission_status: Optional[AssignmentUserSubmissionStatus] + grade: Optional[str] + user_id: Optional[int] + assignment_id: Optional[int] + +class AssignmentUserSubmission(AssignmentUserSubmissionBase, table=True): + """Represents the submission status of an assignment for a user.""" + + id: Optional[int] = Field(default=None, primary_key=True) + creation_date: str + update_date: str + + submission_status: AssignmentUserSubmissionStatus = ( + AssignmentUserSubmissionStatus.PENDING + ) + grade: str + user_id: int = Field( + sa_column=Column("user_id", ForeignKey("user.id", ondelete="CASCADE")) + ) + assignment_id: int = Field( + sa_column=Column( + "assignment_id", ForeignKey("assignment.id", ondelete="CASCADE") + ) + ) + diff --git a/apps/api/src/db/blocks.py b/apps/api/src/db/courses/blocks.py similarity index 96% rename from apps/api/src/db/blocks.py rename to apps/api/src/db/courses/blocks.py index 453429f7..c8723cf9 100644 --- a/apps/api/src/db/blocks.py +++ b/apps/api/src/db/courses/blocks.py @@ -35,7 +35,7 @@ class BlockCreate(BlockBase): class BlockRead(BlockBase): - id: int + id: int = Field(default=None, primary_key=True) org_id: int = Field(default=None, foreign_key="organization.id") course_id: int = Field(default=None, foreign_key="course.id") chapter_id: int = Field(default=None, foreign_key="chapter.id") diff --git a/apps/api/src/db/chapter_activities.py b/apps/api/src/db/courses/chapter_activities.py similarity index 100% rename from apps/api/src/db/chapter_activities.py rename to apps/api/src/db/courses/chapter_activities.py diff --git a/apps/api/src/db/chapters.py b/apps/api/src/db/courses/chapters.py similarity index 90% rename from apps/api/src/db/chapters.py rename to apps/api/src/db/courses/chapters.py index 4e94dc62..d5c30b3d 100644 --- a/apps/api/src/db/chapters.py +++ b/apps/api/src/db/courses/chapters.py @@ -2,7 +2,7 @@ from typing import Any, List, Optional from pydantic import BaseModel from sqlalchemy import Column, ForeignKey from sqlmodel import Field, SQLModel -from src.db.activities import ActivityRead +from src.db.courses.activities import ActivityRead class ChapterBase(SQLModel): @@ -33,10 +33,10 @@ class ChapterCreate(ChapterBase): class ChapterUpdate(ChapterBase): name: Optional[str] - description: Optional[str] - thumbnail_image: Optional[str] + description: Optional[str] = "" + thumbnail_image: Optional[str] = "" course_id: Optional[int] - org_id: Optional[int] + org_id: Optional[int] # type: ignore class ChapterRead(ChapterBase): diff --git a/apps/api/src/db/course_chapters.py b/apps/api/src/db/courses/course_chapters.py similarity index 100% rename from apps/api/src/db/course_chapters.py rename to apps/api/src/db/courses/course_chapters.py diff --git a/apps/api/src/db/course_updates.py b/apps/api/src/db/courses/course_updates.py similarity index 100% rename from apps/api/src/db/course_updates.py rename to apps/api/src/db/courses/course_updates.py diff --git a/apps/api/src/db/courses.py b/apps/api/src/db/courses/courses.py similarity index 95% rename from apps/api/src/db/courses.py rename to apps/api/src/db/courses/courses.py index 31586d25..bbc97af6 100644 --- a/apps/api/src/db/courses.py +++ b/apps/api/src/db/courses/courses.py @@ -3,7 +3,7 @@ from sqlalchemy import Column, ForeignKey, Integer from sqlmodel import Field, SQLModel from src.db.users import UserRead from src.db.trails import TrailRead -from src.db.chapters import ChapterRead +from src.db.courses.chapters import ChapterRead class CourseBase(SQLModel): @@ -21,7 +21,7 @@ class Course(CourseBase, table=True): org_id: int = Field( sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE")) ) - course_uuid: str = "" + course_uuid: str = "" creation_date: str = "" update_date: str = "" diff --git a/apps/api/src/db/trails.py b/apps/api/src/db/trails.py index da6238c0..afce3b7d 100644 --- a/apps/api/src/db/trails.py +++ b/apps/api/src/db/trails.py @@ -6,8 +6,12 @@ from src.db.trail_runs import TrailRunRead class TrailBase(SQLModel): - org_id: int = Field(default=None, foreign_key="organization.id") - user_id: int = Field(default=None, foreign_key="user.id") + org_id: int = Field( + sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE")) + ) + user_id: int = Field( + sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE")) + ) class Trail(TrailBase, table=True): @@ -20,6 +24,7 @@ class Trail(TrailBase, table=True): class TrailCreate(TrailBase): pass + # TODO: This is a hacky way to get around the list[TrailRun] issue, find a better way to do this class TrailRead(BaseModel): id: Optional[int] = Field(default=None, primary_key=True) diff --git a/apps/api/src/router.py b/apps/api/src/router.py index fd171096..cc4d908c 100644 --- a/apps/api/src/router.py +++ b/apps/api/src/router.py @@ -1,8 +1,9 @@ from fastapi import APIRouter, Depends from src.routers import usergroups -from src.routers import blocks, dev, trail, users, auth, orgs, roles +from src.routers import dev, trail, users, auth, orgs, roles from src.routers.ai import ai -from src.routers.courses import chapters, collections, courses, activities +from src.routers.courses import chapters, collections, courses, assignments +from src.routers.courses.activities import activities, blocks from src.routers.install import install from src.services.dev.dev import isDevModeEnabledOrRaise from src.services.install.install import isInstallModeEnabled @@ -19,6 +20,7 @@ v1_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"]) v1_router.include_router(roles.router, prefix="/roles", tags=["roles"]) v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"]) v1_router.include_router(courses.router, prefix="/courses", tags=["courses"]) +v1_router.include_router(assignments.router, prefix="/assignments", tags=["assignments"]) v1_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"]) v1_router.include_router(activities.router, prefix="/activities", tags=["activities"]) v1_router.include_router(collections.router, prefix="/collections", tags=["collections"]) diff --git a/apps/api/src/routers/courses/activities.py b/apps/api/src/routers/courses/activities/activities.py similarity index 97% rename from apps/api/src/routers/courses/activities.py rename to apps/api/src/routers/courses/activities/activities.py index 8afad228..d7c69035 100644 --- a/apps/api/src/routers/courses/activities.py +++ b/apps/api/src/routers/courses/activities/activities.py @@ -1,6 +1,6 @@ from typing import List from fastapi import APIRouter, Depends, UploadFile, Form, Request -from src.db.activities import ActivityCreate, ActivityRead, ActivityUpdate +from src.db.courses.activities import ActivityCreate, ActivityRead, ActivityUpdate from src.db.users import PublicUser from src.core.events.database import get_db_session from src.services.courses.activities.activities import ( diff --git a/apps/api/src/routers/blocks.py b/apps/api/src/routers/courses/activities/blocks.py similarity index 98% rename from apps/api/src/routers/blocks.py rename to apps/api/src/routers/courses/activities/blocks.py index 19f4b03b..fea2f20d 100644 --- a/apps/api/src/routers/blocks.py +++ b/apps/api/src/routers/courses/activities/blocks.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, Depends, UploadFile, Form, Request -from src.db.blocks import BlockRead +from src.db.courses.blocks import BlockRead from src.core.events.database import get_db_session from src.security.auth import get_current_user from src.services.blocks.block_types.imageBlock.imageBlock import ( diff --git a/apps/api/src/routers/courses/assignments.py b/apps/api/src/routers/courses/assignments.py new file mode 100644 index 00000000..6bcd334e --- /dev/null +++ b/apps/api/src/routers/courses/assignments.py @@ -0,0 +1,310 @@ +from fastapi import APIRouter, Depends, Request +from src.db.courses.assignments import ( + AssignmentCreate, + AssignmentRead, + AssignmentTaskCreate, + AssignmentTaskSubmissionCreate, + AssignmentTaskUpdate, + AssignmentUpdate, + AssignmentUserSubmissionCreate, +) +from src.db.users import PublicUser +from src.core.events.database import get_db_session +from src.security.auth import get_current_user +from src.services.courses.activities.assignments import ( + create_assignment, + create_assignment_submission, + create_assignment_task, + create_assignment_task_submission, + delete_assignment, + delete_assignment_submission, + delete_assignment_task, + delete_assignment_task_submission, + read_assignment, + read_assignment_submissions, + read_assignment_task_submissions, + read_assignment_tasks, + read_user_assignment_submissions, + read_user_assignment_task_submissions, + update_assignment, + update_assignment_submission, + update_assignment_task, +) + +router = APIRouter() + +## ASSIGNMENTS ## + + +@router.post("/") +async def api_create_assignments( + request: Request, + assignment_object: AssignmentCreate, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +) -> AssignmentRead: + """ + Create new activity + """ + return await create_assignment(request, assignment_object, current_user, db_session) + + +@router.get("/{assignment_uuid}") +async def api_read_assignment( + request: Request, + assignment_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +) -> AssignmentRead: + """ + Read an assignment + """ + return await read_assignment(request, assignment_uuid, current_user, db_session) + + +@router.put("/{assignment_uuid}") +async def api_update_assignment( + request: Request, + assignment_uuid: str, + assignment_object: AssignmentUpdate, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +) -> AssignmentRead: + """ + Update an assignment + """ + return await update_assignment( + request, assignment_uuid, assignment_object, current_user, db_session + ) + + +@router.delete("/{assignment_uuid}") +async def api_delete_assignment( + request: Request, + assignment_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Delete an assignment + """ + return await delete_assignment(request, assignment_uuid, current_user, db_session) + + +## ASSIGNMENTS Tasks ## + + +@router.post("/{assignment_uuid}/tasks") +async def api_create_assignment_tasks( + request: Request, + assignment_uuid: str, + assignment_task_object: AssignmentTaskCreate, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Create new tasks for an assignment + """ + return await create_assignment_task( + request, assignment_uuid, assignment_task_object, current_user, db_session + ) + + +@router.get("/{assignment_uuid}/tasks") +async def api_read_assignment_tasks( + request: Request, + assignment_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Read tasks for an assignment + """ + return await read_assignment_tasks( + request, assignment_uuid, current_user, db_session + ) + + +@router.put("/{assignment_uuid}/tasks/{task_uuid}") +async def api_update_assignment_tasks( + request: Request, + assignment_task_uuid: str, + assignment_task_object: AssignmentTaskUpdate, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Update tasks for an assignment + """ + return await update_assignment_task( + request, assignment_task_uuid, assignment_task_object, current_user, db_session + ) + + +@router.delete("/{assignment_uuid}/tasks/{task_uuid}") +async def api_delete_assignment_tasks( + request: Request, + assignment_task_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Delete tasks for an assignment + """ + return await delete_assignment_task( + request, assignment_task_uuid, current_user, db_session + ) + + +## ASSIGNMENTS Tasks Submissions ## + + +@router.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions") +async def api_create_assignment_task_submissions( + request: Request, + assignment_task_submission_object: AssignmentTaskSubmissionCreate, + assignment_task_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Create new task submissions for an assignment + """ + return await create_assignment_task_submission( + request, + assignment_task_uuid, + assignment_task_submission_object, + current_user, + db_session, + ) + + +@router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions/{user_id}") +async def api_read_user_assignment_task_submissions( + request: Request, + assignment_task_uuid: str, + user_id: int, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Read task submissions for an assignment from a user + """ + return await read_user_assignment_task_submissions( + request, assignment_task_uuid, user_id, current_user, db_session + ) + + +@router.get("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions") +async def api_read_assignment_task_submissions( + request: Request, + assignment_task_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Read task submissions for an assignment from a user + """ + return await read_assignment_task_submissions( + request, assignment_task_uuid, current_user, db_session + ) + + +@router.delete( + "/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions/{assignment_task_submission_uuid}" +) +async def api_delete_assignment_task_submissions( + request: Request, + assignment_task_submission_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Delete task submissions for an assignment from a user + """ + return await delete_assignment_task_submission( + request, assignment_task_submission_uuid, current_user, db_session + ) + + +## ASSIGNMENTS Submissions ## + + +@router.post("/{assignment_uuid}/submissions") +async def api_create_assignment_submissions( + request: Request, + assignment_uuid: str, + assignment_submission: AssignmentUserSubmissionCreate, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Create new submissions for an assignment + """ + return await create_assignment_submission( + request, assignment_uuid, assignment_submission, current_user, db_session + ) + + +@router.get("/{assignment_uuid}/submissions") +async def api_read_assignment_submissions( + request: Request, + assignment_uuid: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Read submissions for an assignment + """ + return await read_assignment_submissions( + request, assignment_uuid, current_user, db_session + ) + + +@router.get("/{assignment_uuid}/submissions/{user_id}") +async def api_read_user_assignment_submissions( + request: Request, + assignment_uuid: str, + user_id: int, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Read submissions for an assignment from a user + """ + return await read_user_assignment_submissions( + request, assignment_uuid, user_id, current_user, db_session + ) + + +@router.put("/{assignment_uuid}/submissions/{user_id}") +async def api_update_user_assignment_submissions( + request: Request, + assignment_uuid: str, + user_id: str, + assignment_submission: AssignmentUserSubmissionCreate, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Update submissions for an assignment from a user + """ + return await update_assignment_submission( + request, user_id, assignment_submission, current_user, db_session + ) + + +@router.delete("/{assignment_uuid}/submissions/{user_id}") +async def api_delete_user_assignment_submissions( + request: Request, + assignment_id: str, + user_id: str, + current_user: PublicUser = Depends(get_current_user), + db_session=Depends(get_db_session), +): + """ + Delete submissions for an assignment from a user + """ + return await delete_assignment_submission( + request, assignment_id, user_id, current_user, db_session + ) diff --git a/apps/api/src/routers/courses/chapters.py b/apps/api/src/routers/courses/chapters.py index f6cf42db..0d33e6ec 100644 --- a/apps/api/src/routers/courses/chapters.py +++ b/apps/api/src/routers/courses/chapters.py @@ -1,7 +1,7 @@ from typing import List from fastapi import APIRouter, Depends, Request from src.core.events.database import get_db_session -from src.db.chapters import ( +from src.db.courses.chapters import ( ChapterCreate, ChapterRead, ChapterUpdate, diff --git a/apps/api/src/routers/courses/courses.py b/apps/api/src/routers/courses/courses.py index 38057069..e81bf9ae 100644 --- a/apps/api/src/routers/courses/courses.py +++ b/apps/api/src/routers/courses/courses.py @@ -2,13 +2,13 @@ from typing import List from fastapi import APIRouter, Depends, UploadFile, Form, Request from sqlmodel import Session from src.core.events.database import get_db_session -from src.db.course_updates import ( +from src.db.courses.course_updates import ( CourseUpdateCreate, CourseUpdateRead, CourseUpdateUpdate, ) from src.db.users import PublicUser -from src.db.courses import ( +from src.db.courses.courses import ( CourseCreate, CourseRead, CourseUpdate, diff --git a/apps/api/src/security/rbac/rbac.py b/apps/api/src/security/rbac/rbac.py index d7055a79..04b64ca1 100644 --- a/apps/api/src/security/rbac/rbac.py +++ b/apps/api/src/security/rbac/rbac.py @@ -3,7 +3,7 @@ from fastapi import HTTPException, status, Request from sqlalchemy import null from sqlmodel import Session, select from src.db.collections import Collection -from src.db.courses import Course +from src.db.courses.courses import Course from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum from src.db.roles import Role from src.db.user_organizations import UserOrganization diff --git a/apps/api/src/services/ai/ai.py b/apps/api/src/services/ai/ai.py index 8afc7f51..17516d76 100644 --- a/apps/api/src/services/ai/ai.py +++ b/apps/api/src/services/ai/ai.py @@ -3,10 +3,10 @@ from sqlmodel import Session, select from src.db.organization_config import OrganizationConfig from src.db.organizations import Organization from src.services.ai.utils import check_limits_and_config, count_ai_ask -from src.db.courses import Course, CourseRead +from src.db.courses.courses import Course, CourseRead from src.core.events.database import get_db_session from src.db.users import PublicUser -from src.db.activities import Activity, ActivityRead +from src.db.courses.activities import Activity, ActivityRead from src.security.auth import get_current_user from src.services.ai.base import ask_ai, get_chat_session_history diff --git a/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py b/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py index 63fabcc7..3b4dc539 100644 --- a/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py +++ b/apps/api/src/services/blocks/block_types/imageBlock/imageBlock.py @@ -3,9 +3,9 @@ from uuid import uuid4 from src.db.organizations import Organization from fastapi import HTTPException, status, UploadFile, Request from sqlmodel import Session, select -from src.db.activities import Activity -from src.db.blocks import Block, BlockRead, BlockTypeEnum -from src.db.courses import Course +from src.db.courses.activities import Activity +from src.db.courses.blocks import Block, BlockRead, BlockTypeEnum +from src.db.courses.courses import Course from src.services.blocks.utils.upload_files import upload_file_and_return_file_object from src.services.users.users import PublicUser diff --git a/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py b/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py index 4d69cc89..46f4d004 100644 --- a/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py +++ b/apps/api/src/services/blocks/block_types/pdfBlock/pdfBlock.py @@ -3,9 +3,9 @@ from uuid import uuid4 from src.db.organizations import Organization from fastapi import HTTPException, status, UploadFile, Request from sqlmodel import Session, select -from src.db.activities import Activity -from src.db.blocks import Block, BlockRead, BlockTypeEnum -from src.db.courses import Course +from src.db.courses.activities import Activity +from src.db.courses.blocks import Block, BlockRead, BlockTypeEnum +from src.db.courses.courses import Course from src.services.blocks.utils.upload_files import upload_file_and_return_file_object from src.services.users.users import PublicUser diff --git a/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py b/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py index 2e05ec01..481b5162 100644 --- a/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py +++ b/apps/api/src/services/blocks/block_types/videoBlock/videoBlock.py @@ -3,9 +3,9 @@ from uuid import uuid4 from src.db.organizations import Organization from fastapi import HTTPException, status, UploadFile, Request from sqlmodel import Session, select -from src.db.activities import Activity -from src.db.blocks import Block, BlockRead, BlockTypeEnum -from src.db.courses import Course +from src.db.courses.activities import Activity +from src.db.courses.blocks import Block, BlockRead, BlockTypeEnum +from src.db.courses.courses import Course from src.services.blocks.utils.upload_files import upload_file_and_return_file_object from src.services.users.users import PublicUser diff --git a/apps/api/src/services/courses/activities/activities.py b/apps/api/src/services/courses/activities/activities.py index e976f8c3..f3a93d2c 100644 --- a/apps/api/src/services/courses/activities/activities.py +++ b/apps/api/src/services/courses/activities/activities.py @@ -1,14 +1,14 @@ from typing import Literal from sqlmodel import Session, select -from src.db.courses import Course -from src.db.chapters import Chapter +from src.db.courses.courses import Course +from src.db.courses.chapters import Chapter from src.security.rbac.rbac import ( authorization_verify_based_on_roles_and_authorship_and_usergroups, authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, ) -from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate -from src.db.chapter_activities import ChapterActivity +from src.db.courses.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate +from src.db.courses.chapter_activities import ChapterActivity from src.db.users import AnonymousUser, PublicUser from fastapi import HTTPException, Request from uuid import uuid4 @@ -58,7 +58,7 @@ async def create_activity( statement = ( select(ChapterActivity) .where(ChapterActivity.chapter_id == activity_object.chapter_id) - .order_by(ChapterActivity.order) + .order_by(ChapterActivity.order) # type: ignore ) chapter_activities = db_session.exec(statement).all() diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py new file mode 100644 index 00000000..9bbd5717 --- /dev/null +++ b/apps/api/src/services/courses/activities/assignments.py @@ -0,0 +1,997 @@ +#################################################### +# CRUD +#################################################### + +from datetime import datetime +from typing import Literal +from uuid import uuid4 +from fastapi import HTTPException, Request +from sqlmodel import Session, select + +from src.db.courses.assignments import ( + Assignment, + AssignmentCreate, + AssignmentRead, + AssignmentTask, + AssignmentTaskCreate, + AssignmentTaskRead, + AssignmentTaskSubmission, + AssignmentTaskSubmissionCreate, + AssignmentTaskSubmissionRead, + AssignmentTaskUpdate, + AssignmentUpdate, + AssignmentUserSubmission, + AssignmentUserSubmissionCreate, + AssignmentUserSubmissionRead, +) +from src.db.courses.courses import Course +from src.db.users import AnonymousUser, PublicUser +from src.security.rbac.rbac import ( + authorization_verify_based_on_roles_and_authorship_and_usergroups, + authorization_verify_if_element_is_public, + authorization_verify_if_user_is_anon, +) + +## > Assignments CRUD + + +async def create_assignment( + request: Request, + assignment_object: AssignmentCreate, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if org exists + statement = select(Course).where(Course.id == assignment_object.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "create", db_session) + + # Create Assignment + assignment = Assignment(**assignment_object.model_dump()) + + assignment.assignment_uuid = str(f"assignment_{uuid4()}") + assignment.creation_date = str(datetime.now()) + assignment.update_date = str(datetime.now()) + assignment.org_id = course.org_id + + # Insert Assignment in DB + db_session.add(assignment) + db_session.commit() + db_session.refresh(assignment) + + # return assignment read + return AssignmentRead.model_validate(assignment) + + +async def read_assignment( + request: Request, + assignment_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment exists + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + + # return assignment read + return AssignmentRead.model_validate(assignment) + + +async def update_assignment( + request: Request, + assignment_uuid: str, + assignment_object: AssignmentUpdate, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment exists + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "update", db_session) + + # Update only the fields that were passed in + for var, value in vars(assignment_object).items(): + if value is not None: + setattr(assignment, var, value) + assignment.update_date = str(datetime.now()) + + # Insert Assignment in DB + db_session.add(assignment) + db_session.commit() + db_session.refresh(assignment) + + # return assignment read + return AssignmentRead.model_validate(assignment) + + +async def delete_assignment( + request: Request, + assignment_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment exists + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "delete", db_session) + + # Delete Assignment + db_session.delete(assignment) + db_session.commit() + + return {"message": "Assignment deleted"} + + +## > Assignments Tasks CRUD + + +async def create_assignment_task( + request: Request, + assignment_uuid: str, + assignment_task_object: AssignmentTaskCreate, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment exists + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "create", db_session) + + # Create Assignment Task + assignment_task = AssignmentTask(**assignment_task_object.model_dump()) + + assignment_task.assignment_task_uuid = str(f"assignmenttask_{uuid4()}") + assignment_task.creation_date = str(datetime.now()) + assignment_task.update_date = str(datetime.now()) + assignment_task.org_id = course.org_id + + # Insert Assignment Task in DB + db_session.add(assignment_task) + db_session.commit() + db_session.refresh(assignment_task) + + # return assignment task read + return AssignmentTaskRead.model_validate(assignment_task) + + +async def read_assignment_tasks( + request: Request, + assignment_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Find assignment + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # Find assignments tasks for an assignment + statement = select(AssignmentTask).where( + assignment.assignment_uuid == assignment_uuid + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + + # return assignment tasks read + return [ + AssignmentTaskRead.model_validate(assignment_task) + for assignment_task in db_session.exec(statement).all() + ] + + +async def update_assignment_task( + request: Request, + assignment_task_uuid: str, + assignment_task_object: AssignmentTaskUpdate, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment task exists + statement = select(AssignmentTask).where( + AssignmentTask.assignment_task_uuid == assignment_task_uuid + ) + assignment_task = db_session.exec(statement).first() + + if not assignment_task: + raise HTTPException( + status_code=404, + detail="Assignment Task not found", + ) + + # Check if assignment exists + statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "update", db_session) + + # Update only the fields that were passed in + for var, value in vars(assignment_task_object).items(): + if value is not None: + setattr(assignment_task, var, value) + assignment_task.update_date = str(datetime.now()) + + # Insert Assignment Task in DB + db_session.add(assignment_task) + db_session.commit() + db_session.refresh(assignment_task) + + # return assignment task read + return AssignmentTaskRead.model_validate(assignment_task) + + +async def delete_assignment_task( + request: Request, + assignment_task_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment task exists + statement = select(AssignmentTask).where( + AssignmentTask.assignment_task_uuid == assignment_task_uuid + ) + assignment_task = db_session.exec(statement).first() + + if not assignment_task: + raise HTTPException( + status_code=404, + detail="Assignment Task not found", + ) + + # Check if assignment exists + statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "delete", db_session) + + # Delete Assignment Task + db_session.delete(assignment_task) + db_session.commit() + + return {"message": "Assignment Task deleted"} + + +## > Assignments Tasks Submissions CRUD + + +async def create_assignment_task_submission( + request: Request, + assignment_task_uuid: str, + assignment_task_submission_object: AssignmentTaskSubmissionCreate, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment task exists + statement = select(AssignmentTask).where( + AssignmentTask.assignment_task_uuid == assignment_task_uuid + ) + assignment_task = db_session.exec(statement).first() + + if not assignment_task: + raise HTTPException( + status_code=404, + detail="Assignment Task not found", + ) + + # Check if assignment exists + statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "create", db_session) + + # Create Assignment Task Submission + assignment_task_submission = AssignmentTaskSubmission( + **assignment_task_submission_object.model_dump() + ) + + assignment_task_submission.assignment_task_submission_uuid = str( + f"assignmenttasksubmission_{uuid4()}" + ) + assignment_task_submission.creation_date = str(datetime.now()) + assignment_task_submission.update_date = str(datetime.now()) + assignment_task_submission.org_id = course.org_id + + # Insert Assignment Task Submission in DB + db_session.add(assignment_task_submission) + db_session.commit() + db_session.refresh(assignment_task_submission) + + # return assignment task submission read + return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission) + + +async def read_user_assignment_task_submissions( + request: Request, + assignment_task_submission_uuid: str, + user_id: int, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment task submission exists + statement = select(AssignmentTaskSubmission).where( + AssignmentTaskSubmission.assignment_task_submission_uuid + == assignment_task_submission_uuid, + AssignmentTaskSubmission.user_id == user_id, + ) + assignment_task_submission = db_session.exec(statement).first() + + if not assignment_task_submission: + raise HTTPException( + status_code=404, + detail="Assignment Task Submission not found", + ) + + # Check if assignment task exists + statement = select(AssignmentTask).where( + AssignmentTask.id == assignment_task_submission.assignment_task_id + ) + assignment_task = db_session.exec(statement).first() + + if not assignment_task: + raise HTTPException( + status_code=404, + detail="Assignment Task not found", + ) + + # Check if assignment exists + statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + + # return assignment task submission read + return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission) + + +async def read_assignment_task_submissions( + request: Request, + assignment_task_submission_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment task submission exists + statement = select(AssignmentTaskSubmission).where( + AssignmentTaskSubmission.assignment_task_submission_uuid + == assignment_task_submission_uuid, + ) + assignment_task_submission = db_session.exec(statement).first() + + if not assignment_task_submission: + raise HTTPException( + status_code=404, + detail="Assignment Task Submission not found", + ) + + # Check if assignment task exists + statement = select(AssignmentTask).where( + AssignmentTask.id == assignment_task_submission.assignment_task_id + ) + assignment_task = db_session.exec(statement).first() + + if not assignment_task: + raise HTTPException( + status_code=404, + detail="Assignment Task not found", + ) + + # Check if assignment exists + statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + + # return assignment task submission read + return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission) + + +async def update_assignment_task_submission( + request: Request, + assignment_task_submission_uuid: str, + assignment_task_submission_object: AssignmentTaskSubmissionCreate, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment task submission exists + statement = select(AssignmentTaskSubmission).where( + AssignmentTaskSubmission.assignment_task_submission_uuid + == assignment_task_submission_uuid + ) + assignment_task_submission = db_session.exec(statement).first() + + if not assignment_task_submission: + raise HTTPException( + status_code=404, + detail="Assignment Task Submission not found", + ) + + # Check if assignment task exists + statement = select(AssignmentTask).where( + AssignmentTask.id == assignment_task_submission.assignment_task_id + ) + assignment_task = db_session.exec(statement).first() + + if not assignment_task: + raise HTTPException( + status_code=404, + detail="Assignment Task not found", + ) + + # Check if assignment exists + statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "update", db_session) + + # Update only the fields that were passed in + for var, value in vars(assignment_task_submission_object).items(): + if value is not None: + setattr(assignment_task_submission, var, value) + assignment_task_submission.update_date = str(datetime.now()) + + # Insert Assignment Task Submission in DB + db_session.add(assignment_task_submission) + db_session.commit() + db_session.refresh(assignment_task_submission) + + # return assignment task submission read + return AssignmentTaskSubmissionRead.model_validate(assignment_task_submission) + + +async def delete_assignment_task_submission( + request: Request, + assignment_task_submission_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment task submission exists + statement = select(AssignmentTaskSubmission).where( + AssignmentTaskSubmission.assignment_task_submission_uuid + == assignment_task_submission_uuid + ) + assignment_task_submission = db_session.exec(statement).first() + + if not assignment_task_submission: + raise HTTPException( + status_code=404, + detail="Assignment Task Submission not found", + ) + + # Check if assignment task exists + statement = select(AssignmentTask).where( + AssignmentTask.id == assignment_task_submission.assignment_task_id + ) + assignment_task = db_session.exec(statement).first() + + if not assignment_task: + raise HTTPException( + status_code=404, + detail="Assignment Task not found", + ) + + # Check if assignment exists + statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "delete", db_session) + + # Delete Assignment Task Submission + db_session.delete(assignment_task_submission) + db_session.commit() + + return {"message": "Assignment Task Submission deleted"} + + +## > Assignments Submissions CRUD + + +async def create_assignment_submission( + request: Request, + assignment_uuid: str, + assignment_user_submission_object: AssignmentUserSubmissionCreate, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment exists + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if the submission has already been made + statement = select(AssignmentUserSubmission).where( + AssignmentUserSubmission.assignment_id == assignment.id, + AssignmentUserSubmission.user_id == assignment_user_submission_object.user_id, + ) + + assignment_user_submission = db_session.exec(statement).first() + + if assignment_user_submission: + raise HTTPException( + status_code=400, + detail="Assignment User Submission already exists", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "create", db_session) + + # Create Assignment User Submission + assignment_user_submission = AssignmentUserSubmission( + **assignment_user_submission_object.model_dump() + ) + + assignment_user_submission.assignment_user_submission_uuid = str( + f"assignmentusersubmission_{uuid4()}" + ) + assignment_user_submission.creation_date = str(datetime.now()) + assignment_user_submission.update_date = str(datetime.now()) + assignment_user_submission.org_id = course.org_id + + # Insert Assignment User Submission in DB + db_session.add(assignment_user_submission) + db_session.commit() + + # return assignment user submission read + return AssignmentUserSubmissionRead.model_validate(assignment_user_submission) + + +async def read_assignment_submissions( + request: Request, + assignment_uuid: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Find assignment + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # Find assignments tasks for an assignment + statement = select(AssignmentUserSubmission).where( + assignment.assignment_uuid == assignment_uuid + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + + # return assignment tasks read + return [ + AssignmentUserSubmissionRead.model_validate(assignment_user_submission) + for assignment_user_submission in db_session.exec(statement).all() + ] + + +async def read_user_assignment_submissions( + request: Request, + assignment_uuid: str, + user_id: int, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Find assignment + statement = select(Assignment).where(Assignment.assignment_uuid == assignment_uuid) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # Find assignments tasks for an assignment + statement = select(AssignmentUserSubmission).where( + assignment.assignment_uuid == assignment_uuid, + AssignmentUserSubmission.user_id == user_id, + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "read", db_session) + + # return assignment tasks read + return [ + AssignmentUserSubmissionRead.model_validate(assignment_user_submission) + for assignment_user_submission in db_session.exec(statement).all() + ] + + +async def update_assignment_submission( + request: Request, + user_id: str, + assignment_user_submission_object: AssignmentUserSubmissionCreate, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment user submission exists + statement = select(AssignmentUserSubmission).where( + AssignmentUserSubmission.user_id == user_id + ) + assignment_user_submission = db_session.exec(statement).first() + + if not assignment_user_submission: + raise HTTPException( + status_code=404, + detail="Assignment User Submission not found", + ) + + # Check if assignment exists + statement = select(Assignment).where( + Assignment.id == assignment_user_submission.assignment_id + ) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "update", db_session) + + # Update only the fields that were passed in + for var, value in vars(assignment_user_submission_object).items(): + if value is not None: + setattr(assignment_user_submission, var, value) + assignment_user_submission.update_date = str(datetime.now()) + + # Insert Assignment User Submission in DB + db_session.add(assignment_user_submission) + db_session.commit() + db_session.refresh(assignment_user_submission) + + # return assignment user submission read + return AssignmentUserSubmissionRead.model_validate(assignment_user_submission) + + +async def delete_assignment_submission( + request: Request, + user_id: str, + assignment_id: str, + current_user: PublicUser | AnonymousUser, + db_session: Session, +): + # Check if assignment user submission exists + statement = select(AssignmentUserSubmission).where( + AssignmentUserSubmission.user_id == user_id, + AssignmentUserSubmission.assignment_id == assignment_id, + ) + assignment_user_submission = db_session.exec(statement).first() + + if not assignment_user_submission: + raise HTTPException( + status_code=404, + detail="Assignment User Submission not found", + ) + + # Check if assignment exists + statement = select(Assignment).where( + Assignment.id == assignment_user_submission.assignment_id + ) + assignment = db_session.exec(statement).first() + + if not assignment: + raise HTTPException( + status_code=404, + detail="Assignment not found", + ) + + # Check if course exists + statement = select(Course).where(Course.id == assignment.course_id) + course = db_session.exec(statement).first() + + if not course: + raise HTTPException( + status_code=404, + detail="Course not found", + ) + + # RBAC check + await rbac_check(request, course.course_uuid, current_user, "delete", db_session) + + # Delete Assignment User Submission + db_session.delete(assignment_user_submission) + db_session.commit() + + return {"message": "Assignment User Submission deleted"} + + +## 🔒 RBAC Utils ## + + +async def rbac_check( + request: Request, + course_uuid: str, + current_user: PublicUser | AnonymousUser, + action: Literal["create", "read", "update", "delete"], + db_session: Session, +): + + if action == "read": + if current_user.id == 0: # Anonymous user + res = await authorization_verify_if_element_is_public( + request, course_uuid, action, db_session + ) + return res + else: + res = ( + await authorization_verify_based_on_roles_and_authorship_and_usergroups( + request, current_user.id, action, course_uuid, db_session + ) + ) + return res + else: + await authorization_verify_if_user_is_anon(current_user.id) + + await authorization_verify_based_on_roles_and_authorship_and_usergroups( + request, + current_user.id, + action, + course_uuid, + db_session, + ) + + +## 🔒 RBAC Utils ## diff --git a/apps/api/src/services/courses/activities/pdf.py b/apps/api/src/services/courses/activities/pdf.py index bb97da54..30b4db9d 100644 --- a/apps/api/src/services/courses/activities/pdf.py +++ b/apps/api/src/services/courses/activities/pdf.py @@ -1,20 +1,20 @@ from typing import Literal -from src.db.courses import Course +from src.db.courses.courses import Course from src.db.organizations import Organization from sqlmodel import Session, select from src.security.rbac.rbac import ( authorization_verify_based_on_roles_and_authorship_and_usergroups, authorization_verify_if_user_is_anon, ) -from src.db.chapters import Chapter -from src.db.activities import ( +from src.db.courses.chapters import Chapter +from src.db.courses.activities import ( Activity, ActivityRead, ActivitySubTypeEnum, ActivityTypeEnum, ) -from src.db.chapter_activities import ChapterActivity -from src.db.course_chapters import CourseChapter +from src.db.courses.chapter_activities import ChapterActivity +from src.db.courses.course_chapters import CourseChapter from src.db.users import AnonymousUser, PublicUser from src.services.courses.activities.uploads.pdfs import upload_pdf from fastapi import HTTPException, status, UploadFile, Request diff --git a/apps/api/src/services/courses/activities/utils.py b/apps/api/src/services/courses/activities/utils.py index c2904d18..d3fee6d4 100644 --- a/apps/api/src/services/courses/activities/utils.py +++ b/apps/api/src/services/courses/activities/utils.py @@ -1,5 +1,5 @@ -from src.db.activities import ActivityRead -from src.db.courses import CourseRead +from src.db.courses.activities import ActivityRead +from src.db.courses.courses import CourseRead def structure_activity_content_by_type(activity): diff --git a/apps/api/src/services/courses/activities/video.py b/apps/api/src/services/courses/activities/video.py index 629a76c7..3396607c 100644 --- a/apps/api/src/services/courses/activities/video.py +++ b/apps/api/src/services/courses/activities/video.py @@ -1,5 +1,5 @@ from typing import Literal -from src.db.courses import Course +from src.db.courses.courses import Course from src.db.organizations import Organization from pydantic import BaseModel @@ -8,15 +8,15 @@ from src.security.rbac.rbac import ( authorization_verify_based_on_roles_and_authorship_and_usergroups, authorization_verify_if_user_is_anon, ) -from src.db.chapters import Chapter -from src.db.activities import ( +from src.db.courses.chapters import Chapter +from src.db.courses.activities import ( Activity, ActivityRead, ActivitySubTypeEnum, ActivityTypeEnum, ) -from src.db.chapter_activities import ChapterActivity -from src.db.course_chapters import CourseChapter +from src.db.courses.chapter_activities import ChapterActivity +from src.db.courses.course_chapters import CourseChapter from src.db.users import AnonymousUser, PublicUser from src.services.courses.activities.uploads.videos import upload_video from fastapi import HTTPException, status, UploadFile, Request diff --git a/apps/api/src/services/courses/chapters.py b/apps/api/src/services/courses/chapters.py index ee8ce1b5..b960e2c1 100644 --- a/apps/api/src/services/courses/chapters.py +++ b/apps/api/src/services/courses/chapters.py @@ -8,10 +8,10 @@ from src.security.rbac.rbac import ( authorization_verify_if_element_is_public, authorization_verify_if_user_is_anon, ) -from src.db.course_chapters import CourseChapter -from src.db.activities import Activity, ActivityRead -from src.db.chapter_activities import ChapterActivity -from src.db.chapters import ( +from src.db.courses.course_chapters import CourseChapter +from src.db.courses.activities import Activity, ActivityRead +from src.db.courses.chapter_activities import ChapterActivity +from src.db.courses.chapters import ( Chapter, ChapterCreate, ChapterRead, diff --git a/apps/api/src/services/courses/collections.py b/apps/api/src/services/courses/collections.py index ac9627b6..9c5f8412 100644 --- a/apps/api/src/services/courses/collections.py +++ b/apps/api/src/services/courses/collections.py @@ -15,7 +15,7 @@ from src.db.collections import ( CollectionUpdate, ) from src.db.collections_courses import CollectionCourse -from src.db.courses import Course +from src.db.courses.courses import Course from src.services.users.users import PublicUser from fastapi import HTTPException, status, Request diff --git a/apps/api/src/services/courses/courses.py b/apps/api/src/services/courses/courses.py index 6c09ea9c..b3d2ed33 100644 --- a/apps/api/src/services/courses/courses.py +++ b/apps/api/src/services/courses/courses.py @@ -8,7 +8,7 @@ from src.db.organizations import Organization from src.services.trail.trail import get_user_trail_with_orgid from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum from src.db.users import PublicUser, AnonymousUser, User, UserRead -from src.db.courses import ( +from src.db.courses.courses import ( Course, CourseCreate, CourseRead, diff --git a/apps/api/src/services/courses/updates.py b/apps/api/src/services/courses/updates.py index b4460664..f3fea858 100644 --- a/apps/api/src/services/courses/updates.py +++ b/apps/api/src/services/courses/updates.py @@ -3,13 +3,13 @@ from typing import List from uuid import uuid4 from fastapi import HTTPException, Request, status from sqlmodel import Session, col, select -from src.db.course_updates import ( +from src.db.courses.course_updates import ( CourseUpdate, CourseUpdateCreate, CourseUpdateRead, CourseUpdateUpdate, ) -from src.db.courses import Course +from src.db.courses.courses import Course from src.db.organizations import Organization from src.db.users import AnonymousUser, PublicUser from src.services.courses.courses import rbac_check diff --git a/apps/api/src/services/trail/trail.py b/apps/api/src/services/trail/trail.py index 6dd5c15b..3101ceca 100644 --- a/apps/api/src/services/trail/trail.py +++ b/apps/api/src/services/trail/trail.py @@ -1,10 +1,10 @@ from datetime import datetime from uuid import uuid4 -from src.db.chapter_activities import ChapterActivity +from src.db.courses.chapter_activities import ChapterActivity from fastapi import HTTPException, Request, status from sqlmodel import Session, select -from src.db.activities import Activity -from src.db.courses import Course +from src.db.courses.activities import Activity +from src.db.courses.courses import Course from src.db.trail_runs import TrailRun, TrailRunRead from src.db.trail_steps import TrailStep from src.db.trails import Trail, TrailCreate, TrailRead