diff --git a/apps/api/migrations/env.py b/apps/api/migrations/env.py
index 41302d3d..08a2dd30 100644
--- a/apps/api/migrations/env.py
+++ b/apps/api/migrations/env.py
@@ -1,7 +1,7 @@
import importlib
from logging.config import fileConfig
import os
-
+import alembic_postgresql_enum
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from sqlmodel import SQLModel
diff --git a/apps/api/migrations/versions/6295e05ff7d0_enum_updates.py b/apps/api/migrations/versions/6295e05ff7d0_enum_updates.py
new file mode 100644
index 00000000..4ff2406e
--- /dev/null
+++ b/apps/api/migrations/versions/6295e05ff7d0_enum_updates.py
@@ -0,0 +1,41 @@
+"""Enum updates
+
+Revision ID: 6295e05ff7d0
+Revises: df2981bf24dd
+Create Date: 2024-07-11 20:46:26.582170
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+import sqlmodel
+from alembic_postgresql_enum import TableReference # type: ignore
+
+# revision identifiers, used by Alembic.
+revision: str = '6295e05ff7d0'
+down_revision: Union[str, None] = 'df2981bf24dd'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.sync_enum_values('public', 'activitytypeenum', ['TYPE_VIDEO', 'TYPE_DOCUMENT', 'TYPE_DYNAMIC', 'TYPE_ASSIGNMENT', 'TYPE_CUSTOM'],
+ [TableReference(table_schema='public', table_name='activity', column_name='activity_type')],
+ enum_values_to_rename=[])
+ op.sync_enum_values('public', 'activitysubtypeenum', ['SUBTYPE_DYNAMIC_PAGE', 'SUBTYPE_VIDEO_YOUTUBE', 'SUBTYPE_VIDEO_HOSTED', 'SUBTYPE_DOCUMENT_PDF', 'SUBTYPE_DOCUMENT_DOC', 'SUBTYPE_ASSIGNMENT_ANY', 'SUBTYPE_CUSTOM'],
+ [TableReference(table_schema='public', table_name='activity', column_name='activity_sub_type')],
+ enum_values_to_rename=[])
+ # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.sync_enum_values('public', 'activitysubtypeenum', ['SUBTYPE_DYNAMIC_PAGE', 'SUBTYPE_VIDEO_YOUTUBE', 'SUBTYPE_VIDEO_HOSTED', 'SUBTYPE_DOCUMENT_PDF', 'SUBTYPE_DOCUMENT_DOC', 'SUBTYPE_ASSESSMENT_QUIZ', 'SUBTYPE_CUSTOM'],
+ [TableReference(table_schema='public', table_name='activity', column_name='activity_sub_type')],
+ enum_values_to_rename=[])
+ op.sync_enum_values('public', 'activitytypeenum', ['TYPE_VIDEO', 'TYPE_DOCUMENT', 'TYPE_DYNAMIC', 'TYPE_ASSESSMENT', 'TYPE_CUSTOM'],
+ [TableReference(table_schema='public', table_name='activity', column_name='activity_type')],
+ enum_values_to_rename=[])
+ # ### end Alembic commands ###
diff --git a/apps/api/migrations/versions/df2981bf24dd_initial_migration.py b/apps/api/migrations/versions/df2981bf24dd_initial_migration.py
index e27b5c33..a6a5be8a 100644
--- a/apps/api/migrations/versions/df2981bf24dd_initial_migration.py
+++ b/apps/api/migrations/versions/df2981bf24dd_initial_migration.py
@@ -23,6 +23,9 @@ depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('activity', sa.Column('published', sa.Boolean(), nullable=False, server_default=sa.true()))
+ # If you need to rename columns instead of dropping them, use the rename_column command
+ # For example, if we are changing the name 'published_version' to 'published', we would use:
+ # op.alter_column('activity', 'published_version', new_column_name='published', existing_type=sa.Boolean())
op.drop_column('activity', 'published_version')
op.drop_column('activity', 'version')
@@ -32,7 +35,6 @@ def upgrade() -> None:
op.create_foreign_key('trail_org_id_fkey', 'trail', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.drop_constraint('trail_user_id_fkey', 'trail', type_='foreignkey')
op.create_foreign_key('trail_user_id_fkey', 'trail', 'user', ['user_id'], ['id'], ondelete='CASCADE')
-
# ### end Alembic commands ###
@@ -45,7 +47,8 @@ def downgrade() -> None:
op.add_column('assignmentusersubmission', sa.Column('assignment_user_uuid', sa.VARCHAR(), autoincrement=False, nullable=False))
- op.add_column('activity', sa.Column('version', sa.INTEGER(), autoincrement=False, nullable=False , server_default=sa.text('1')))
- op.add_column('activity', sa.Column('published_version', sa.INTEGER(), autoincrement=False, nullable=False , server_default=sa.text('1')) )
+ op.add_column('activity', sa.Column('version', sa.INTEGER(), autoincrement=False, nullable=False, server_default=sa.text('1')))
+ op.add_column('activity', sa.Column('published_version', sa.INTEGER(), autoincrement=False, nullable=False, server_default=sa.text('1')))
op.drop_column('activity', 'published')
# ### end Alembic commands ###
+
diff --git a/apps/api/poetry.lock b/apps/api/poetry.lock
index 5aae9088..2b49d1cf 100644
--- a/apps/api/poetry.lock
+++ b/apps/api/poetry.lock
@@ -128,6 +128,21 @@ typing-extensions = ">=4"
[package.extras]
tz = ["backports.zoneinfo"]
+[[package]]
+name = "alembic-postgresql-enum"
+version = "1.2.0"
+description = "Alembic autogenerate support for creation, alteration and deletion of enums"
+optional = false
+python-versions = "<4.0,>=3.7"
+files = [
+ {file = "alembic_postgresql_enum-1.2.0-py3-none-any.whl", hash = "sha256:bd156e882a10c680fc88ebad25cfe78ccf9f826dec89670f8aeb28e5359e502b"},
+ {file = "alembic_postgresql_enum-1.2.0.tar.gz", hash = "sha256:971bd3a4c35ea38869bb5e263ea79e5b4a9c4a02f174a3dd7ddcb29d41260cba"},
+]
+
+[package.dependencies]
+alembic = ">=1.7"
+SQLAlchemy = ">=1.4"
+
[[package]]
name = "anyio"
version = "4.4.0"
@@ -3018,6 +3033,34 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"]
pymysql = ["pymysql"]
sqlcipher = ["sqlcipher3_binary"]
+[[package]]
+name = "sqlalchemy-utils"
+version = "0.41.2"
+description = "Various utility functions for SQLAlchemy."
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990"},
+ {file = "SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e"},
+]
+
+[package.dependencies]
+SQLAlchemy = ">=1.3"
+
+[package.extras]
+arrow = ["arrow (>=0.3.4)"]
+babel = ["Babel (>=1.3)"]
+color = ["colour (>=0.0.4)"]
+encrypted = ["cryptography (>=0.6)"]
+intervals = ["intervals (>=0.7.1)"]
+password = ["passlib (>=1.6,<2.0)"]
+pendulum = ["pendulum (>=2.0.5)"]
+phone = ["phonenumbers (>=5.9.2)"]
+test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
+test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
+timezone = ["python-dateutil"]
+url = ["furl (>=0.4.1)"]
+
[[package]]
name = "sqlmodel"
version = "0.0.19"
@@ -3871,4 +3914,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
-content-hash = "76c4defc807fe83375766ac085982a2edf16e57b2d092a4494021f00a0424a4c"
+content-hash = "49d72c6871e3ecffae3b55ccad3a6b140f9a1ebbca84d7632dafd54e1d2b7f9d"
diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml
index 0632d60b..a742eca1 100644
--- a/apps/api/pyproject.toml
+++ b/apps/api/pyproject.toml
@@ -39,6 +39,8 @@ uvicorn = "0.30.1"
typer = "^0.12.3"
chromadb = "^0.5.3"
alembic = "^1.13.2"
+alembic-postgresql-enum = "^1.2.0"
+sqlalchemy-utils = "^0.41.2"
[build-system]
build-backend = "poetry.core.masonry.api"
diff --git a/apps/api/src/db/collections_courses.py b/apps/api/src/db/collections_courses.py
index 4e0fc270..9ea829d8 100644
--- a/apps/api/src/db/collections_courses.py
+++ b/apps/api/src/db/collections_courses.py
@@ -5,8 +5,12 @@ from sqlmodel import Field, SQLModel
class CollectionCourse(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
- collection_id: int = Field(sa_column=Column(Integer, ForeignKey("collection.id", ondelete="CASCADE")))
- course_id: int = Field(sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE")))
+ collection_id: int = Field(
+ sa_column=Column(Integer, ForeignKey("collection.id", ondelete="CASCADE"))
+ )
+ course_id: int = Field(
+ sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
+ )
org_id: int = Field(default=None, foreign_key="organization.id")
creation_date: str
update_date: str
diff --git a/apps/api/src/db/courses/assignments.py b/apps/api/src/db/courses/assignments.py
index 78632aa2..67b1f872 100644
--- a/apps/api/src/db/courses/assignments.py
+++ b/apps/api/src/db/courses/assignments.py
@@ -87,7 +87,7 @@ class Assignment(AssignmentBase, table=True):
class AssignmentTaskTypeEnum(str, Enum):
FILE_SUBMISSION = "FILE_SUBMISSION"
QUIZ = "QUIZ"
- FORM = "FORM" # soon to be implemented
+ FORM = "FORM" # soon to be implemented
OTHER = "OTHER"
diff --git a/apps/api/src/db/trails.py b/apps/api/src/db/trails.py
index afce3b7d..9b0430ba 100644
--- a/apps/api/src/db/trails.py
+++ b/apps/api/src/db/trails.py
@@ -16,6 +16,12 @@ class TrailBase(SQLModel):
class Trail(TrailBase, table=True):
id: Optional[int] = Field(default=None, primary_key=True)
+ 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"))
+ )
trail_uuid: str = ""
creation_date: str = ""
update_date: str = ""
diff --git a/apps/api/src/routers/courses/assignments.py b/apps/api/src/routers/courses/assignments.py
index 6bcd334e..1dba87ce 100644
--- a/apps/api/src/routers/courses/assignments.py
+++ b/apps/api/src/routers/courses/assignments.py
@@ -17,6 +17,7 @@ from src.services.courses.activities.assignments import (
create_assignment_task,
create_assignment_task_submission,
delete_assignment,
+ delete_assignment_from_activity_uuid,
delete_assignment_submission,
delete_assignment_task,
delete_assignment_task_submission,
@@ -90,6 +91,18 @@ async def api_delete_assignment(
"""
return await delete_assignment(request, assignment_uuid, current_user, db_session)
+@router.delete("/activity/{activity_uuid}")
+async def api_delete_assignment_from_activity(
+ request: Request,
+ activity_uuid: str,
+ current_user: PublicUser = Depends(get_current_user),
+ db_session=Depends(get_db_session),
+):
+ """
+ Delete an assignment
+ """
+ return await delete_assignment_from_activity_uuid(request, activity_uuid, current_user, db_session)
+
## ASSIGNMENTS Tasks ##
diff --git a/apps/api/src/services/courses/activities/assignments.py b/apps/api/src/services/courses/activities/assignments.py
index 9bbd5717..cb133190 100644
--- a/apps/api/src/services/courses/activities/assignments.py
+++ b/apps/api/src/services/courses/activities/assignments.py
@@ -8,6 +8,7 @@ from uuid import uuid4
from fastapi import HTTPException, Request
from sqlmodel import Session, select
+from src.db.courses.activities import Activity
from src.db.courses.assignments import (
Assignment,
AssignmentCreate,
@@ -184,6 +185,53 @@ async def delete_assignment(
return {"message": "Assignment deleted"}
+async def delete_assignment_from_activity_uuid(
+ request: Request,
+ activity_uuid: str,
+ current_user: PublicUser | AnonymousUser,
+ db_session: Session,
+):
+ # Check if activity exists
+ statement = select(Activity).where(Activity.activity_uuid == activity_uuid)
+
+ activity = db_session.exec(statement).first()
+
+ if not activity:
+ raise HTTPException(
+ status_code=404,
+ detail="Activity not found",
+ )
+
+ # Check if course exists
+ statement = select(Course).where(Course.id == activity.course_id)
+ course = db_session.exec(statement).first()
+
+ if not course:
+ raise HTTPException(
+ status_code=404,
+ detail="Course not found",
+ )
+
+ # Check if assignment exists
+ statement = select(Assignment).where(Assignment.activity_id == activity.id)
+ assignment = db_session.exec(statement).first()
+
+ if not assignment:
+ raise HTTPException(
+ status_code=404,
+ detail="Assignment 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
diff --git a/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx b/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx
index a76c72e7..a631adcc 100644
--- a/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx
+++ b/apps/web/components/Dashboard/Course/EditCourseStructure/DraggableElements/ActivityElement.tsx
@@ -19,6 +19,7 @@ import { useRouter } from 'next/navigation'
import React from 'react'
import { Draggable } from 'react-beautiful-dnd'
import { mutate } from 'swr'
+import { deleteAssignment, deleteAssignmentUsingActivityUUID } from '@services/courses/assignments'
type ActivitiyElementProps = {
orgslug: string
@@ -45,6 +46,11 @@ function ActivityElement(props: ActivitiyElementProps) {
const activityUUID = props.activity.activity_uuid
async function deleteActivityUI() {
+ // Assignments
+ if(props.activity.activity_type === 'TYPE_ASSIGNMENT') {
+ await deleteAssignmentUsingActivityUUID(props.activity.activity_uuid, access_token)
+ }
+
await deleteActivity(props.activity.activity_uuid, access_token)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
diff --git a/apps/web/components/Objects/Modals/Activities/Create/NewActivity.tsx b/apps/web/components/Objects/Modals/Activities/Create/NewActivity.tsx
index 797a2c8f..16201245 100644
--- a/apps/web/components/Objects/Modals/Activities/Create/NewActivity.tsx
+++ b/apps/web/components/Objects/Modals/Activities/Create/NewActivity.tsx
@@ -102,9 +102,10 @@ function NewActivityModal({
{selectedView === 'assignments' && (