Merge pull request #286 from learnhouse/feat/assignments

Assignments
This commit is contained in:
Badr B. 2024-08-09 19:46:23 +02:00 committed by GitHub
commit f4439a3368
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 8076 additions and 1621 deletions

116
apps/api/alembic.ini Normal file
View file

@ -0,0 +1,116 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = migrations
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
version_path_separator = os # Use os.pathsep. Default configuration used for new projects.
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = postgresql://learnhouse:learnhouse@localhost:5432/learnhouse
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

110
apps/api/migrations/env.py Normal file
View file

@ -0,0 +1,110 @@
import importlib
from logging.config import fileConfig
import os
import alembic_postgresql_enum # noqa: F401
from sqlalchemy import engine_from_config
from sqlalchemy import pool
from sqlmodel import SQLModel
from alembic import context
from config.config import get_learnhouse_config
# LearnHouse config
lh_config = get_learnhouse_config()
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
# IMPORTING ALL SCHEMAS
base_dir = 'src/db'
base_module_path = 'src.db'
# Recursively walk through the base directory
for root, dirs, files in os.walk(base_dir):
# Filter out __init__.py and non-Python files
module_files = [f for f in files if f.endswith('.py') and f != '__init__.py']
# Calculate the module's base path from its directory structure
path_diff = os.path.relpath(root, base_dir)
if path_diff == '.':
# Root of the base_dir, no additional path to add
current_module_base = base_module_path
else:
# Convert directory path to a module path
current_module_base = f"{base_module_path}.{path_diff.replace(os.sep, '.')}"
# Dynamically import each module
for file_name in module_files:
module_name = file_name[:-3] # Remove the '.py' extension
full_module_path = f"{current_module_base}.{module_name}"
importlib.import_module(full_module_path)
# IMPORTING ALL SCHEMAS
target_metadata = SQLModel.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View file

@ -0,0 +1,27 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa # noqa: F401
import sqlmodel # noqa: F401
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View file

@ -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 # noqa: F401
import sqlmodel # noqa: F401
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 ###

View file

@ -0,0 +1,31 @@
"""Add reference for AssignmentTasks
Revision ID: d8bc71595932
Revises: 6295e05ff7d0
Create Date: 2024-07-12 18:59:50.242716
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
import sqlmodel # noqa: F401
# revision identifiers, used by Alembic.
revision: str = 'd8bc71595932'
down_revision: Union[str, None] = '6295e05ff7d0'
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.add_column('assignmenttask', sa.Column('reference_file', sa.VARCHAR(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('assignmenttask', 'reference_file')
# ### end Alembic commands ###

View file

@ -0,0 +1,54 @@
"""Initial Migration
Revision ID: df2981bf24dd
Revises:
Create Date: 2024-07-11 19:33:37.993767
"""
from typing import Sequence, Union
from alembic import op
from grpc import server # noqa: F401
import sqlalchemy as sa
import sqlmodel # noqa: F401
# revision identifiers, used by Alembic.
revision: str = 'df2981bf24dd'
down_revision: Union[str, None] = None
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.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')
op.drop_column('assignmentusersubmission', 'assignment_user_uuid')
op.drop_constraint('trail_org_id_fkey', 'trail', type_='foreignkey')
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 ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('trail_org_id_fkey', 'trail', type_='foreignkey')
op.create_foreign_key('trail_org_id_fkey', 'trail', 'organization', ['org_id'], ['id'])
op.drop_constraint('trail_user_id_fkey', 'trail', type_='foreignkey')
op.create_foreign_key('trail_user_id_fkey', 'trail', 'user', ['user_id'], ['id'])
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.drop_column('activity', 'published')
# ### end Alembic commands ###

1154
apps/api/poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -38,6 +38,9 @@ tiktoken = "^0.7.0"
uvicorn = "0.30.1" uvicorn = "0.30.1"
typer = "^0.12.3" typer = "^0.12.3"
chromadb = "^0.5.3" chromadb = "^0.5.3"
alembic = "^1.13.2"
alembic-postgresql-enum = "^1.2.0"
sqlalchemy-utils = "^0.41.2"
[build-system] [build-system]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"

View file

@ -5,8 +5,12 @@ from sqlmodel import Field, SQLModel
class CollectionCourse(SQLModel, table=True): class CollectionCourse(SQLModel, table=True):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)
collection_id: int = Field(sa_column=Column(Integer, ForeignKey("collection.id", ondelete="CASCADE"))) collection_id: int = Field(
course_id: int = Field(sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))) 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") org_id: int = Field(default=None, foreign_key="organization.id")
creation_date: str creation_date: str
update_date: str update_date: str

View file

@ -8,7 +8,7 @@ class ActivityTypeEnum(str, Enum):
TYPE_VIDEO = "TYPE_VIDEO" TYPE_VIDEO = "TYPE_VIDEO"
TYPE_DOCUMENT = "TYPE_DOCUMENT" TYPE_DOCUMENT = "TYPE_DOCUMENT"
TYPE_DYNAMIC = "TYPE_DYNAMIC" TYPE_DYNAMIC = "TYPE_DYNAMIC"
TYPE_ASSESSMENT = "TYPE_ASSESSMENT" TYPE_ASSIGNMENT = "TYPE_ASSIGNMENT"
TYPE_CUSTOM = "TYPE_CUSTOM" TYPE_CUSTOM = "TYPE_CUSTOM"
@ -21,19 +21,18 @@ class ActivitySubTypeEnum(str, Enum):
# Document # Document
SUBTYPE_DOCUMENT_PDF = "SUBTYPE_DOCUMENT_PDF" SUBTYPE_DOCUMENT_PDF = "SUBTYPE_DOCUMENT_PDF"
SUBTYPE_DOCUMENT_DOC = "SUBTYPE_DOCUMENT_DOC" SUBTYPE_DOCUMENT_DOC = "SUBTYPE_DOCUMENT_DOC"
# Assessment # Assignment
SUBTYPE_ASSESSMENT_QUIZ = "SUBTYPE_ASSESSMENT_QUIZ" SUBTYPE_ASSIGNMENT_ANY = "SUBTYPE_ASSIGNMENT_ANY"
# Custom # Custom
SUBTYPE_CUSTOM = "SUBTYPE_CUSTOM" SUBTYPE_CUSTOM = "SUBTYPE_CUSTOM"
class ActivityBase(SQLModel): class ActivityBase(SQLModel):
name: str name: str
activity_type: ActivityTypeEnum = ActivityTypeEnum.TYPE_CUSTOM activity_type: ActivityTypeEnum
activity_sub_type: ActivitySubTypeEnum = ActivitySubTypeEnum.SUBTYPE_CUSTOM activity_sub_type: ActivitySubTypeEnum
content: dict = Field(default={}, sa_column=Column(JSON)) content: dict = Field(default={}, sa_column=Column(JSON))
published_version: int published: bool = False
version: int
class Activity(ActivityBase, table=True): class Activity(ActivityBase, table=True):
@ -52,20 +51,24 @@ class Activity(ActivityBase, table=True):
class ActivityCreate(ActivityBase): class ActivityCreate(ActivityBase):
chapter_id: int chapter_id: int
activity_type: ActivityTypeEnum = ActivityTypeEnum.TYPE_CUSTOM
activity_sub_type: ActivitySubTypeEnum = ActivitySubTypeEnum.SUBTYPE_CUSTOM
pass pass
class ActivityUpdate(ActivityBase): class ActivityUpdate(ActivityBase):
name: Optional[str] name: Optional[str]
content: dict = Field(default={}, sa_column=Column(JSON))
activity_type: Optional[ActivityTypeEnum] activity_type: Optional[ActivityTypeEnum]
activity_sub_type: Optional[ActivitySubTypeEnum] activity_sub_type: Optional[ActivitySubTypeEnum]
content: dict = Field(default={}, sa_column=Column(JSON))
published_version: Optional[int] published_version: Optional[int]
version: Optional[int] version: Optional[int]
class ActivityRead(ActivityBase): class ActivityRead(ActivityBase):
id: int id: int
org_id: int
course_id: int
activity_uuid: str activity_uuid: str
creation_date: str creation_date: str
update_date: str update_date: str

View file

@ -0,0 +1,308 @@
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
reference_file: Optional[str]
assignment_type: AssignmentTaskTypeEnum
contents: Dict = Field(default={}, sa_column=Column(JSON))
max_grade_value: int = 0 # Value is always between 0-100
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=None, sa_column=Column(JSON))
max_grade_value: 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_id: Optional[int]
assignment_task_submission_uuid: Optional[str]
task_submission: Optional[Dict] = Field(default=None, sa_column=Column(JSON))
grade: Optional[int]
task_submission_grade_feedback: Optional[str]
assignment_type: Optional[AssignmentTaskTypeEnum]
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.SUBMITTED
)
grade: int
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(SQLModel):
"""Model for creating a new assignment user submission."""
assignment_id: int
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
assignmentusersubmission_uuid: str
submission_status: AssignmentUserSubmissionStatus = (
AssignmentUserSubmissionStatus.SUBMITTED
)
grade: int
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")
)
)

View file

@ -35,7 +35,7 @@ class BlockCreate(BlockBase):
class BlockRead(BlockBase): class BlockRead(BlockBase):
id: int id: int = Field(default=None, primary_key=True)
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(default=None, foreign_key="organization.id")
course_id: int = Field(default=None, foreign_key="course.id") course_id: int = Field(default=None, foreign_key="course.id")
chapter_id: int = Field(default=None, foreign_key="chapter.id") chapter_id: int = Field(default=None, foreign_key="chapter.id")

View file

@ -2,7 +2,7 @@ from typing import Any, List, Optional
from pydantic import BaseModel from pydantic import BaseModel
from sqlalchemy import Column, ForeignKey from sqlalchemy import Column, ForeignKey
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from src.db.activities import ActivityRead from src.db.courses.activities import ActivityRead
class ChapterBase(SQLModel): class ChapterBase(SQLModel):
@ -33,10 +33,10 @@ class ChapterCreate(ChapterBase):
class ChapterUpdate(ChapterBase): class ChapterUpdate(ChapterBase):
name: Optional[str] name: Optional[str]
description: Optional[str] description: Optional[str] = ""
thumbnail_image: Optional[str] thumbnail_image: Optional[str] = ""
course_id: Optional[int] course_id: Optional[int]
org_id: Optional[int] org_id: Optional[int] # type: ignore
class ChapterRead(ChapterBase): class ChapterRead(ChapterBase):

View file

@ -3,7 +3,7 @@ from sqlalchemy import Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from src.db.users import UserRead from src.db.users import UserRead
from src.db.trails import TrailRead from src.db.trails import TrailRead
from src.db.chapters import ChapterRead from src.db.courses.chapters import ChapterRead
class CourseBase(SQLModel): class CourseBase(SQLModel):

View file

@ -6,12 +6,22 @@ from src.db.trail_runs import TrailRunRead
class TrailBase(SQLModel): class TrailBase(SQLModel):
org_id: int = Field(default=None, foreign_key="organization.id") org_id: int = Field(
user_id: int = Field(default=None, foreign_key="user.id") 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): class Trail(TrailBase, table=True):
id: Optional[int] = Field(default=None, primary_key=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 = "" trail_uuid: str = ""
creation_date: str = "" creation_date: str = ""
update_date: str = "" update_date: str = ""
@ -20,6 +30,7 @@ class Trail(TrailBase, table=True):
class TrailCreate(TrailBase): class TrailCreate(TrailBase):
pass pass
# TODO: This is a hacky way to get around the list[TrailRun] issue, find a better way to do this # TODO: This is a hacky way to get around the list[TrailRun] issue, find a better way to do this
class TrailRead(BaseModel): class TrailRead(BaseModel):
id: Optional[int] = Field(default=None, primary_key=True) id: Optional[int] = Field(default=None, primary_key=True)

View file

@ -1,8 +1,9 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from src.routers import usergroups 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.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.routers.install import install
from src.services.dev.dev import isDevModeEnabledOrRaise from src.services.dev.dev import isDevModeEnabledOrRaise
from src.services.install.install import isInstallModeEnabled 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(roles.router, prefix="/roles", tags=["roles"])
v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"]) v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"])
v1_router.include_router(courses.router, prefix="/courses", tags=["courses"]) 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(chapters.router, prefix="/chapters", tags=["chapters"])
v1_router.include_router(activities.router, prefix="/activities", tags=["activities"]) v1_router.include_router(activities.router, prefix="/activities", tags=["activities"])
v1_router.include_router(collections.router, prefix="/collections", tags=["collections"]) v1_router.include_router(collections.router, prefix="/collections", tags=["collections"])

View file

@ -1,12 +1,13 @@
from typing import List from typing import List
from fastapi import APIRouter, Depends, UploadFile, Form, Request 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.db.users import PublicUser
from src.core.events.database import get_db_session from src.core.events.database import get_db_session
from src.services.courses.activities.activities import ( from src.services.courses.activities.activities import (
create_activity, create_activity,
get_activity, get_activity,
get_activities, get_activities,
get_activityby_id,
update_activity, update_activity,
delete_activity, delete_activity,
) )
@ -34,8 +35,22 @@ async def api_create_activity(
return await create_activity(request, activity_object, current_user, db_session) return await create_activity(request, activity_object, current_user, db_session)
@router.get("/{activity_id}") @router.get("/{activity_uuid}")
async def api_get_activity( async def api_get_activity(
request: Request,
activity_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
) -> ActivityRead:
"""
Get single activity by activity_id
"""
return await get_activity(
request, activity_uuid, current_user=current_user, db_session=db_session
)
@router.get("/id/{activity_id}")
async def api_get_activityby_id(
request: Request, request: Request,
activity_id: str, activity_id: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
@ -44,11 +59,10 @@ async def api_get_activity(
""" """
Get single activity by activity_id Get single activity by activity_id
""" """
return await get_activity( return await get_activityby_id(
request, activity_id, current_user=current_user, db_session=db_session request, activity_id, current_user=current_user, db_session=db_session
) )
@router.get("/chapter/{chapter_id}") @router.get("/chapter/{chapter_id}")
async def api_get_chapter_activities( async def api_get_chapter_activities(
request: Request, request: Request,

View file

@ -1,5 +1,5 @@
from fastapi import APIRouter, Depends, UploadFile, Form, Request 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.core.events.database import get_db_session
from src.security.auth import get_current_user from src.security.auth import get_current_user
from src.services.blocks.block_types.imageBlock.imageBlock import ( from src.services.blocks.block_types.imageBlock.imageBlock import (

View file

@ -0,0 +1,495 @@
from fastapi import APIRouter, Depends, Request, UploadFile
from src.db.courses.assignments import (
AssignmentCreate,
AssignmentRead,
AssignmentTaskCreate,
AssignmentTaskSubmissionUpdate,
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,
delete_assignment,
delete_assignment_from_activity_uuid,
delete_assignment_submission,
delete_assignment_task,
delete_assignment_task_submission,
get_assignments_from_course,
get_grade_assignment_submission,
grade_assignment_submission,
handle_assignment_task_submission,
mark_activity_as_done_for_user,
put_assignment_task_reference_file,
put_assignment_task_submission_file,
read_assignment,
read_assignment_from_activity_uuid,
read_assignment_submissions,
read_assignment_task,
read_assignment_task_submissions,
read_assignment_tasks,
read_user_assignment_submissions,
read_user_assignment_submissions_me,
read_user_assignment_task_submissions,
read_user_assignment_task_submissions_me,
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.get("/activity/{activity_uuid}")
async def api_read_assignment_from_activity(
request: Request,
activity_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
) -> AssignmentRead:
"""
Read an assignment
"""
return await read_assignment_from_activity_uuid(
request, activity_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)
@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 ##
@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.get("/task/{assignment_task_uuid}")
async def api_read_assignment_task(
request: Request,
assignment_task_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Read task for an assignment
"""
return await read_assignment_task(
request, assignment_task_uuid, current_user, db_session
)
@router.put("/{assignment_uuid}/tasks/{assignment_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.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/ref_file")
async def api_put_assignment_task_ref_file(
request: Request,
assignment_task_uuid: str,
reference_file: UploadFile | None = None,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Update tasks for an assignment
"""
return await put_assignment_task_reference_file(
request, db_session, assignment_task_uuid, current_user, reference_file
)
@router.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/sub_file")
async def api_put_assignment_task_sub_file(
request: Request,
assignment_task_uuid: str,
sub_file: UploadFile | None = None,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Update tasks for an assignment
"""
return await put_assignment_task_submission_file(
request, db_session, assignment_task_uuid, current_user, sub_file
)
@router.delete("/{assignment_uuid}/tasks/{assignment_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.put("/{assignment_uuid}/tasks/{assignment_task_uuid}/submissions")
async def api_handle_assignment_task_submissions(
request: Request,
assignment_task_submission_object: AssignmentTaskSubmissionUpdate,
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 handle_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/{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/me")
async def api_read_user_assignment_task_submissions_me(
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_user_assignment_task_submissions_me(
request, assignment_task_uuid, 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,
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, 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/me")
async def api_read_user_assignment_submission_me(
request: Request,
assignment_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Read submissions for an assignment from the current user
"""
return await read_user_assignment_submissions_me(
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_uuid: 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, user_id, assignment_uuid, current_user, db_session
)
@router.get("/{assignment_uuid}/submissions/{user_id}/grade")
async def api_get_submission_grade(
request: Request,
assignment_uuid: str,
user_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Grade submissions for an assignment from a user
"""
return await get_grade_assignment_submission(
request, user_id, assignment_uuid, current_user, db_session
)
@router.post("/{assignment_uuid}/submissions/{user_id}/grade")
async def api_final_grade_submission(
request: Request,
assignment_uuid: str,
user_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Grade submissions for an assignment from a user
"""
return await grade_assignment_submission(
request, user_id, assignment_uuid, current_user, db_session
)
@router.post("/{assignment_uuid}/submissions/{user_id}/done")
async def api_submission_mark_as_done(
request: Request,
assignment_uuid: str,
user_id: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Grade submissions for an assignment from a user
"""
return await mark_activity_as_done_for_user(
request, user_id, assignment_uuid, current_user, db_session
)
@router.get("/course/{course_uuid}")
async def api_get_assignments(
request: Request,
course_uuid: str,
current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session),
):
"""
Get assignments for a course
"""
return await get_assignments_from_course(
request, course_uuid, current_user, db_session
)

View file

@ -1,7 +1,7 @@
from typing import List from typing import List
from fastapi import APIRouter, Depends, Request from fastapi import APIRouter, Depends, Request
from src.core.events.database import get_db_session from src.core.events.database import get_db_session
from src.db.chapters import ( from src.db.courses.chapters import (
ChapterCreate, ChapterCreate,
ChapterRead, ChapterRead,
ChapterUpdate, ChapterUpdate,

View file

@ -2,13 +2,13 @@ from typing import List
from fastapi import APIRouter, Depends, UploadFile, Form, Request from fastapi import APIRouter, Depends, UploadFile, Form, Request
from sqlmodel import Session from sqlmodel import Session
from src.core.events.database import get_db_session from src.core.events.database import get_db_session
from src.db.course_updates import ( from src.db.courses.course_updates import (
CourseUpdateCreate, CourseUpdateCreate,
CourseUpdateRead, CourseUpdateRead,
CourseUpdateUpdate, CourseUpdateUpdate,
) )
from src.db.users import PublicUser from src.db.users import PublicUser
from src.db.courses import ( from src.db.courses.courses import (
CourseCreate, CourseCreate,
CourseRead, CourseRead,
CourseUpdate, CourseUpdate,
@ -18,13 +18,19 @@ from src.security.auth import get_current_user
from src.services.courses.courses import ( from src.services.courses.courses import (
create_course, create_course,
get_course, get_course,
get_course_by_id,
get_course_meta, get_course_meta,
get_courses_orgslug, get_courses_orgslug,
update_course, update_course,
delete_course, delete_course,
update_course_thumbnail, update_course_thumbnail,
) )
from src.services.courses.updates import create_update, delete_update, get_updates_by_course_uuid, update_update from src.services.courses.updates import (
create_update,
delete_update,
get_updates_by_course_uuid,
update_update,
)
router = APIRouter() router = APIRouter()
@ -93,6 +99,21 @@ async def api_get_course(
) )
@router.get("/id/{course_id}")
async def api_get_course_by_id(
request: Request,
course_id: str,
db_session: Session = Depends(get_db_session),
current_user: PublicUser = Depends(get_current_user),
) -> CourseRead:
"""
Get single Course by id
"""
return await get_course_by_id(
request, course_id, current_user=current_user, db_session=db_session
)
@router.get("/{course_uuid}/meta") @router.get("/{course_uuid}/meta")
async def api_get_course_meta( async def api_get_course_meta(
request: Request, request: Request,
@ -154,7 +175,8 @@ async def api_delete_course(
return await delete_course(request, course_uuid, current_user, db_session) return await delete_course(request, course_uuid, current_user, db_session)
@ router.get("/{course_uuid}/updates")
@router.get("/{course_uuid}/updates")
async def api_get_course_updates( async def api_get_course_updates(
request: Request, request: Request,
course_uuid: str, course_uuid: str,
@ -165,7 +187,10 @@ async def api_get_course_updates(
Get Course Updates by course_uuid Get Course Updates by course_uuid
""" """
return await get_updates_by_course_uuid(request, course_uuid, current_user, db_session) return await get_updates_by_course_uuid(
request, course_uuid, current_user, db_session
)
@router.post("/{course_uuid}/updates") @router.post("/{course_uuid}/updates")
async def api_create_course_update( async def api_create_course_update(
@ -183,6 +208,7 @@ async def api_create_course_update(
request, course_uuid, update_object, current_user, db_session request, course_uuid, update_object, current_user, db_session
) )
@router.put("/{course_uuid}/update/{courseupdate_uuid}") @router.put("/{course_uuid}/update/{courseupdate_uuid}")
async def api_update_course_update( async def api_update_course_update(
request: Request, request: Request,
@ -200,6 +226,7 @@ async def api_update_course_update(
request, courseupdate_uuid, update_object, current_user, db_session request, courseupdate_uuid, update_object, current_user, db_session
) )
@router.delete("/{course_uuid}/update/{courseupdate_uuid}") @router.delete("/{course_uuid}/update/{courseupdate_uuid}")
async def api_delete_course_update( async def api_delete_course_update(
request: Request, request: Request,
@ -213,4 +240,3 @@ async def api_delete_course_update(
""" """
return await delete_update(request, courseupdate_uuid, current_user, db_session) return await delete_update(request, courseupdate_uuid, current_user, db_session)

View file

@ -3,7 +3,7 @@ from fastapi import HTTPException, status, Request
from sqlalchemy import null from sqlalchemy import null
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.collections import Collection 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.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.roles import Role from src.db.roles import Role
from src.db.user_organizations import UserOrganization from src.db.user_organizations import UserOrganization

View file

@ -3,10 +3,10 @@ from sqlmodel import Session, select
from src.db.organization_config import OrganizationConfig from src.db.organization_config import OrganizationConfig
from src.db.organizations import Organization from src.db.organizations import Organization
from src.services.ai.utils import check_limits_and_config, count_ai_ask 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.core.events.database import get_db_session
from src.db.users import PublicUser 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.security.auth import get_current_user
from src.services.ai.base import ask_ai, get_chat_session_history from src.services.ai.base import ask_ai, get_chat_session_history

View file

@ -3,9 +3,9 @@ from uuid import uuid4
from src.db.organizations import Organization from src.db.organizations import Organization
from fastapi import HTTPException, status, UploadFile, Request from fastapi import HTTPException, status, UploadFile, Request
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.activities import Activity from src.db.courses.activities import Activity
from src.db.blocks import Block, BlockRead, BlockTypeEnum from src.db.courses.blocks import Block, BlockRead, BlockTypeEnum
from src.db.courses import Course from src.db.courses.courses import Course
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser from src.services.users.users import PublicUser

View file

@ -3,9 +3,9 @@ from uuid import uuid4
from src.db.organizations import Organization from src.db.organizations import Organization
from fastapi import HTTPException, status, UploadFile, Request from fastapi import HTTPException, status, UploadFile, Request
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.activities import Activity from src.db.courses.activities import Activity
from src.db.blocks import Block, BlockRead, BlockTypeEnum from src.db.courses.blocks import Block, BlockRead, BlockTypeEnum
from src.db.courses import Course from src.db.courses.courses import Course
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser from src.services.users.users import PublicUser

View file

@ -3,9 +3,9 @@ from uuid import uuid4
from src.db.organizations import Organization from src.db.organizations import Organization
from fastapi import HTTPException, status, UploadFile, Request from fastapi import HTTPException, status, UploadFile, Request
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.activities import Activity from src.db.courses.activities import Activity
from src.db.blocks import Block, BlockRead, BlockTypeEnum from src.db.courses.blocks import Block, BlockRead, BlockTypeEnum
from src.db.courses import Course from src.db.courses.courses import Course
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
from src.services.users.users import PublicUser from src.services.users.users import PublicUser

View file

@ -1,14 +1,14 @@
from typing import Literal from typing import Literal
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.courses import Course from src.db.courses.courses import Course
from src.db.chapters import Chapter from src.db.courses.chapters import Chapter
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship_and_usergroups, authorization_verify_based_on_roles_and_authorship_and_usergroups,
authorization_verify_if_element_is_public, authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate from src.db.courses.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
from src.db.chapter_activities import ChapterActivity from src.db.courses.chapter_activities import ChapterActivity
from src.db.users import AnonymousUser, PublicUser from src.db.users import AnonymousUser, PublicUser
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from uuid import uuid4 from uuid import uuid4
@ -58,7 +58,7 @@ async def create_activity(
statement = ( statement = (
select(ChapterActivity) select(ChapterActivity)
.where(ChapterActivity.chapter_id == activity_object.chapter_id) .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() chapter_activities = db_session.exec(statement).all()
@ -116,6 +116,38 @@ async def get_activity(
return activity return activity
async def get_activityby_id(
request: Request,
activity_id: str,
current_user: PublicUser,
db_session: Session,
):
statement = select(Activity).where(Activity.id == activity_id)
activity = db_session.exec(statement).first()
if not activity:
raise HTTPException(
status_code=404,
detail="Activity not found",
)
# Get course from that activity
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",
)
# RBAC check
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
activity = ActivityRead.model_validate(activity)
return activity
async def update_activity( async def update_activity(
request: Request, request: Request,

File diff suppressed because it is too large Load diff

View file

@ -1,20 +1,20 @@
from typing import Literal from typing import Literal
from src.db.courses import Course from src.db.courses.courses import Course
from src.db.organizations import Organization from src.db.organizations import Organization
from sqlmodel import Session, select from sqlmodel import Session, select
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship_and_usergroups, authorization_verify_based_on_roles_and_authorship_and_usergroups,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.chapters import Chapter from src.db.courses.chapters import Chapter
from src.db.activities import ( from src.db.courses.activities import (
Activity, Activity,
ActivityRead, ActivityRead,
ActivitySubTypeEnum, ActivitySubTypeEnum,
ActivityTypeEnum, ActivityTypeEnum,
) )
from src.db.chapter_activities import ChapterActivity from src.db.courses.chapter_activities import ChapterActivity
from src.db.course_chapters import CourseChapter from src.db.courses.course_chapters import CourseChapter
from src.db.users import AnonymousUser, PublicUser from src.db.users import AnonymousUser, PublicUser
from src.services.courses.activities.uploads.pdfs import upload_pdf from src.services.courses.activities.uploads.pdfs import upload_pdf
from fastapi import HTTPException, status, UploadFile, Request from fastapi import HTTPException, status, UploadFile, Request

View file

@ -0,0 +1,23 @@
from src.services.utils.upload_content import upload_content
async def upload_submission_file(
file,
name_in_disk,
activity_uuid,
org_uuid,
course_uuid,
assignment_uuid,
assignment_task_uuid,
):
contents = file.file.read()
file.filename.split(".")[-1]
await upload_content(
f"courses/{course_uuid}/activities/{activity_uuid}/assignments/{assignment_uuid}/tasks/{assignment_task_uuid}/subs",
"orgs",
org_uuid,
contents,
f"{name_in_disk}",
["pdf", "docx", "mp4", "jpg", "jpeg", "png", "pptx"],
)

View file

@ -0,0 +1,23 @@
from src.services.utils.upload_content import upload_content
async def upload_reference_file(
file,
name_in_disk,
activity_uuid,
org_uuid,
course_uuid,
assignment_uuid,
assignment_task_uuid,
):
contents = file.file.read()
file.filename.split(".")[-1]
await upload_content(
f"courses/{course_uuid}/activities/{activity_uuid}/assignments/{assignment_uuid}/tasks/{assignment_task_uuid}",
"orgs",
org_uuid,
contents,
f"{name_in_disk}",
["pdf", "docx", "mp4", "jpg", "jpeg", "png", "pptx"],
)

View file

@ -1,5 +1,5 @@
from src.db.activities import ActivityRead from src.db.courses.activities import ActivityRead
from src.db.courses import CourseRead from src.db.courses.courses import CourseRead
def structure_activity_content_by_type(activity): def structure_activity_content_by_type(activity):

View file

@ -1,5 +1,5 @@
from typing import Literal from typing import Literal
from src.db.courses import Course from src.db.courses.courses import Course
from src.db.organizations import Organization from src.db.organizations import Organization
from pydantic import BaseModel 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_based_on_roles_and_authorship_and_usergroups,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.chapters import Chapter from src.db.courses.chapters import Chapter
from src.db.activities import ( from src.db.courses.activities import (
Activity, Activity,
ActivityRead, ActivityRead,
ActivitySubTypeEnum, ActivitySubTypeEnum,
ActivityTypeEnum, ActivityTypeEnum,
) )
from src.db.chapter_activities import ChapterActivity from src.db.courses.chapter_activities import ChapterActivity
from src.db.course_chapters import CourseChapter from src.db.courses.course_chapters import CourseChapter
from src.db.users import AnonymousUser, PublicUser from src.db.users import AnonymousUser, PublicUser
from src.services.courses.activities.uploads.videos import upload_video from src.services.courses.activities.uploads.videos import upload_video
from fastapi import HTTPException, status, UploadFile, Request from fastapi import HTTPException, status, UploadFile, Request

View file

@ -8,10 +8,10 @@ from src.security.rbac.rbac import (
authorization_verify_if_element_is_public, authorization_verify_if_element_is_public,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
) )
from src.db.course_chapters import CourseChapter from src.db.courses.course_chapters import CourseChapter
from src.db.activities import Activity, ActivityRead from src.db.courses.activities import Activity, ActivityRead
from src.db.chapter_activities import ChapterActivity from src.db.courses.chapter_activities import ChapterActivity
from src.db.chapters import ( from src.db.courses.chapters import (
Chapter, Chapter,
ChapterCreate, ChapterCreate,
ChapterRead, ChapterRead,

View file

@ -15,7 +15,7 @@ from src.db.collections import (
CollectionUpdate, CollectionUpdate,
) )
from src.db.collections_courses import CollectionCourse 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 src.services.users.users import PublicUser
from fastapi import HTTPException, status, Request from fastapi import HTTPException, status, Request

View file

@ -8,7 +8,7 @@ from src.db.organizations import Organization
from src.services.trail.trail import get_user_trail_with_orgid from src.services.trail.trail import get_user_trail_with_orgid
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.users import PublicUser, AnonymousUser, User, UserRead from src.db.users import PublicUser, AnonymousUser, User, UserRead
from src.db.courses import ( from src.db.courses.courses import (
Course, Course,
CourseCreate, CourseCreate,
CourseRead, CourseRead,
@ -58,6 +58,38 @@ async def get_course(
return course return course
async def get_course_by_id(
request: Request,
course_id: str,
current_user: PublicUser | AnonymousUser,
db_session: Session,
):
statement = select(Course).where(Course.id == 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)
# Get course authors
authors_statement = (
select(User)
.join(ResourceAuthor)
.where(ResourceAuthor.resource_uuid == course.course_uuid)
)
authors = db_session.exec(authors_statement).all()
# convert from User to UserRead
authors = [UserRead.model_validate(author) for author in authors]
course = CourseRead(**course.model_dump(), authors=authors)
return course
async def get_course_meta( async def get_course_meta(
request: Request, request: Request,

View file

@ -3,13 +3,13 @@ from typing import List
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, Request, status from fastapi import HTTPException, Request, status
from sqlmodel import Session, col, select from sqlmodel import Session, col, select
from src.db.course_updates import ( from src.db.courses.course_updates import (
CourseUpdate, CourseUpdate,
CourseUpdateCreate, CourseUpdateCreate,
CourseUpdateRead, CourseUpdateRead,
CourseUpdateUpdate, CourseUpdateUpdate,
) )
from src.db.courses import Course from src.db.courses.courses import Course
from src.db.organizations import Organization from src.db.organizations import Organization
from src.db.users import AnonymousUser, PublicUser from src.db.users import AnonymousUser, PublicUser
from src.services.courses.courses import rbac_check from src.services.courses.courses import rbac_check

View file

@ -1,10 +1,10 @@
from datetime import datetime from datetime import datetime
from uuid import uuid4 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 fastapi import HTTPException, Request, status
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.activities import Activity from src.db.courses.activities import Activity
from src.db.courses import Course from src.db.courses.courses import Course
from src.db.trail_runs import TrailRun, TrailRunRead from src.db.trail_runs import TrailRun, TrailRunRead
from src.db.trail_steps import TrailStep from src.db.trail_steps import TrailStep
from src.db.trails import Trail, TrailCreate, TrailRead from src.db.trails import Trail, TrailCreate, TrailRead
@ -244,7 +244,7 @@ async def add_activity_to_trail(
course_id=course.id if course.id is not None else 0, course_id=course.id if course.id is not None else 0,
trail_id=trail.id if trail.id is not None else 0, trail_id=trail.id if trail.id is not None else 0,
org_id=course.org_id, org_id=course.org_id,
complete=False, complete=True,
teacher_verified=False, teacher_verified=False,
grade="", grade="",
user_id=user.id, user_id=user.id,

View file

@ -1,8 +1,10 @@
from typing import Literal from typing import Literal, Optional
import boto3 import boto3
from botocore.exceptions import ClientError from botocore.exceptions import ClientError
import os import os
from fastapi import HTTPException
from config.config import get_learnhouse_config from config.config import get_learnhouse_config
@ -12,13 +14,24 @@ async def upload_content(
uuid: str, # org_uuid or user_uuid uuid: str, # org_uuid or user_uuid
file_binary: bytes, file_binary: bytes,
file_and_format: str, file_and_format: str,
allowed_formats: Optional[list[str]] = None,
): ):
# Get Learnhouse Config # Get Learnhouse Config
learnhouse_config = get_learnhouse_config() learnhouse_config = get_learnhouse_config()
file_format = file_and_format.split(".")[-1].strip().lower()
# Get content delivery method # Get content delivery method
content_delivery = learnhouse_config.hosting_config.content_delivery.type content_delivery = learnhouse_config.hosting_config.content_delivery.type
# Check if format file is allowed
if allowed_formats:
if file_format not in allowed_formats:
raise HTTPException(
status_code=400,
detail=f"File format {file_format} not allowed",
)
if content_delivery == "filesystem": if content_delivery == "filesystem":
# create folder for activity # create folder for activity
if not os.path.exists(f"content/{type_of_dir}/{uuid}/{directory}"): if not os.path.exists(f"content/{type_of_dir}/{uuid}/{directory}"):

View file

@ -17,9 +17,6 @@ function HomeClient() {
const { data: orgs } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, (url) => swrFetcher(url, access_token)) const { data: orgs } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, (url) => swrFetcher(url, access_token))
useEffect(() => { useEffect(() => {
console.log(orgs)
}, [session, orgs]) }, [session, orgs])
return ( return (
<div className='flex flex-col'> <div className='flex flex-col'>

View file

@ -1,14 +1,14 @@
'use client' 'use client'
import Link from 'next/link' import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config' import { getAPIUrl, getUriWithOrg } from '@services/config/config'
import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva' import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva'
import VideoActivity from '@components/Objects/Activities/Video/Video' import VideoActivity from '@components/Objects/Activities/Video/Video'
import { Check, MoreVertical } from 'lucide-react' import { BookOpenCheck, Check, CheckCircle, MoreVertical, UserRoundPen } from 'lucide-react'
import { markActivityAsComplete } from '@services/courses/activity' import { markActivityAsComplete } from '@services/courses/activity'
import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf' import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf'
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators' import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper' import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import { useRouter } from 'next/navigation' import { usePathname, useRouter } from 'next/navigation'
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement' import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
import { getCourseThumbnailMediaDirectory } from '@services/media/media' import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { useOrg } from '@components/Contexts/OrgContext' import { useOrg } from '@components/Contexts/OrgContext'
@ -16,6 +16,15 @@ import { CourseProvider } from '@components/Contexts/CourseContext'
import AIActivityAsk from '@components/Objects/Activities/AI/AIActivityAsk' import AIActivityAsk from '@components/Objects/Activities/AI/AIActivityAsk'
import AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext' import AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import React, { useEffect } from 'react'
import { getAssignmentFromActivityUUID, getFinalGrade, submitAssignmentForGrading } from '@services/courses/assignments'
import AssignmentStudentActivity from '@components/Objects/Activities/Assignment/AssignmentStudentActivity'
import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext'
import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext'
import AssignmentSubmissionProvider, { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext'
import toast from 'react-hot-toast'
import { mutate } from 'swr'
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
interface ActivityClientProps { interface ActivityClientProps {
activityid: string activityid: string
@ -32,6 +41,12 @@ function ActivityClient(props: ActivityClientProps) {
const activity = props.activity const activity = props.activity
const course = props.course const course = props.course
const org = useOrg() as any const org = useOrg() as any
const session = useLHSession() as any;
const pathname = usePathname()
const access_token = session?.data?.tokens?.access_token;
const [bgColor, setBgColor] = React.useState('bg-white')
const [assignment, setAssignment] = React.useState(null) as any;
const [markStatusButtonActive, setMarkStatusButtonActive] = React.useState(false);
function getChapterNameByActivityId(course: any, activity_id: any) { function getChapterNameByActivityId(course: any, activity_id: any) {
for (let i = 0; i < course.chapters.length; i++) { for (let i = 0; i < course.chapters.length; i++) {
@ -46,6 +61,26 @@ function ActivityClient(props: ActivityClientProps) {
return null // return null if no matching activity is found return null // return null if no matching activity is found
} }
async function getAssignmentUI() {
const assignment = await getAssignmentFromActivityUUID(activity.activity_uuid, access_token)
setAssignment(assignment.data)
}
useEffect(() => {
if (activity.activity_type == 'TYPE_DYNAMIC') {
setBgColor('bg-white nice-shadow');
}
else if (activity.activity_type == 'TYPE_ASSIGNMENT') {
setMarkStatusButtonActive(false);
setBgColor('bg-white nice-shadow');
getAssignmentUI();
}
else {
setBgColor('bg-zinc-950');
}
}
, [activity, pathname])
return ( return (
<> <>
<CourseProvider courseuuid={course?.course_uuid}> <CourseProvider courseuuid={course?.course_uuid}>
@ -92,7 +127,10 @@ function ActivityClient(props: ActivityClientProps) {
</h1> </h1>
</div> </div>
<div className="flex space-x-1 items-center"> <div className="flex space-x-1 items-center">
{activity && activity.published == true && (
<AuthenticatedClientElement checkMethod="authentication"> <AuthenticatedClientElement checkMethod="authentication">
{activity.activity_type != 'TYPE_ASSIGNMENT' &&
<>
<AIActivityAsk activity={activity} /> <AIActivityAsk activity={activity} />
<MoreVertical size={17} className="text-gray-300 " /> <MoreVertical size={17} className="text-gray-300 " />
<MarkStatus <MarkStatus
@ -101,16 +139,40 @@ function ActivityClient(props: ActivityClientProps) {
course={course} course={course}
orgslug={orgslug} orgslug={orgslug}
/> />
</AuthenticatedClientElement> </>
</div> }
</div> {activity.activity_type == 'TYPE_ASSIGNMENT' &&
<>
<MoreVertical size={17} className="text-gray-300 " />
<AssignmentSubmissionProvider assignment_uuid={assignment?.assignment_uuid}>
<AssignmentTools
assignment={assignment}
activity={activity}
activityid={activityid}
course={course}
orgslug={orgslug}
/>
</AssignmentSubmissionProvider>
</>
}
{activity ? ( </AuthenticatedClientElement>
)}
</div>
</div>
{activity && activity.published == false && (
<div className="p-7 drop-shadow-sm rounded-lg bg-gray-800">
<div className="text-white">
<h1 className="font-bold text-2xl">
This activity is not published yet
</h1>
</div>
</div>
)}
{activity && activity.published == true && (
<div <div
className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC' className={`p-7 drop-shadow-sm rounded-lg ${bgColor}`}
? 'bg-white'
: 'bg-zinc-950'
}`}
> >
<div> <div>
{activity.activity_type == 'TYPE_DYNAMIC' && ( {activity.activity_type == 'TYPE_DYNAMIC' && (
@ -126,11 +188,24 @@ function ActivityClient(props: ActivityClientProps) {
activity={activity} activity={activity}
/> />
)} )}
</div> {activity.activity_type == 'TYPE_ASSIGNMENT' && (
</div> <div>
{assignment ? (
<AssignmentProvider assignment_uuid={assignment?.assignment_uuid}>
<AssignmentsTaskProvider>
<AssignmentSubmissionProvider assignment_uuid={assignment?.assignment_uuid}>
<AssignmentStudentActivity />
</AssignmentSubmissionProvider>
</AssignmentsTaskProvider>
</AssignmentProvider>
) : ( ) : (
<div></div> <div></div>
)} )}
</div>
)}
</div>
</div>
)}
{<div style={{ height: '100px' }}></div>} {<div style={{ height: '100px' }}></div>}
</div> </div>
</GeneralWrapperStyled> </GeneralWrapperStyled>
@ -165,7 +240,7 @@ export function MarkStatus(props: {
) )
if (run) { if (run) {
return run.steps.find( return run.steps.find(
(step: any) => step.activity_id == props.activity.id (step: any) => (step.activity_id == props.activity.id) && (step.complete == true)
) )
} }
} }
@ -173,15 +248,15 @@ export function MarkStatus(props: {
return ( return (
<> <>
{isActivityCompleted() ? ( {isActivityCompleted() ? (
<div className="bg-teal-600 rounded-full px-5 drop-shadow-md flex items-center space-x-1 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out"> <div className="bg-teal-600 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out">
<i> <i>
<Check size={17}></Check> <Check size={17}></Check>
</i>{' '} </i>{' '}
<i className="not-italic text-xs font-bold">Already completed</i> <i className="not-italic text-xs font-bold">Complete</i>
</div> </div>
) : ( ) : (
<div <div
className="bg-gray-800 rounded-full px-5 drop-shadow-md flex items-center space-x-1 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" className="bg-gray-800 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out"
onClick={markActivityAsCompleteFront} onClick={markActivityAsCompleteFront}
> >
{' '} {' '}
@ -195,4 +270,120 @@ export function MarkStatus(props: {
) )
} }
function AssignmentTools(props: {
activity: any
activityid: string
course: any
orgslug: string
assignment: any
}) {
const submission = useAssignmentSubmission() as any
const session = useLHSession() as any;
const [finalGrade, setFinalGrade] = React.useState(null) as any;
const submitForGradingUI = async () => {
if (props.assignment) {
const res = await submitAssignmentForGrading(
props.assignment?.assignment_uuid,
session.data?.tokens?.access_token
)
if (res.success) {
toast.success('Assignment submitted for grading')
mutate(`${getAPIUrl()}assignments/${props.assignment?.assignment_uuid}/submissions/me`,)
}
else {
toast.error('Failed to submit assignment for grading')
}
}
}
const getGradingBasedOnMethod = async () => {
const res = await getFinalGrade(
session.data?.user?.id,
props.assignment?.assignment_uuid,
session.data?.tokens?.access_token
);
if (res.success) {
const { grade, max_grade, grading_type } = res.data;
let displayGrade;
switch (grading_type) {
case 'ALPHABET':
displayGrade = convertNumericToAlphabet(grade, max_grade);
break;
case 'NUMERIC':
displayGrade = `${grade}/${max_grade}`;
break;
case 'PERCENTAGE':
const percentage = (grade / max_grade) * 100;
displayGrade = `${percentage.toFixed(2)}%`;
break;
default:
displayGrade = 'Unknown grading type';
}
// Use displayGrade here, e.g., update state or display it
setFinalGrade(displayGrade);
} else {
}
};
// Helper function to convert numeric grade to alphabet grade
function convertNumericToAlphabet(grade: any, maxGrade: any) {
const percentage = (grade / maxGrade) * 100;
if (percentage >= 90) return 'A';
if (percentage >= 80) return 'B';
if (percentage >= 70) return 'C';
if (percentage >= 60) return 'D';
return 'F';
}
useEffect(() => {
if ( submission && submission.length > 0 && submission[0].submission_status === 'GRADED') {
getGradingBasedOnMethod();
}
}
, [submission, props.assignment])
if (!submission || submission.length === 0) {
return (
<ConfirmationModal
confirmationButtonText="Submit Assignment"
confirmationMessage="Are you sure you want to submit your assignment for grading? Once submitted, you will not be able to make any changes."
dialogTitle="Submit your assignment for grading"
dialogTrigger={
<div className="bg-cyan-800 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out">
<BookOpenCheck size={17} />
<span className="text-xs font-bold">Submit for grading</span>
</div>
}
functionToExecute={submitForGradingUI}
status="info"
/>
)
}
if (submission[0].submission_status === 'SUBMITTED') {
return (
<div className="bg-amber-800 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white transition delay-150 duration-300 ease-in-out">
<UserRoundPen size={17} />
<span className="text-xs font-bold">Grading in progress</span>
</div>
)
}
if (submission[0].submission_status === 'GRADED') {
return (
<div className="bg-teal-600 rounded-full px-5 drop-shadow-md flex items-center space-x-2 p-2.5 text-white transition delay-150 duration-300 ease-in-out">
<CheckCircle size={17} />
<span className="text-xs flex space-x-2 font-bold items-center"><span>Graded </span> <span className='bg-white text-teal-800 px-1 py-0.5 rounded-md'>{finalGrade}</span></span>
</div>
)
}
// Default return in case none of the conditions are met
return null
}
export default ActivityClient export default ActivityClient

View file

@ -12,7 +12,7 @@ import {
getCourseThumbnailMediaDirectory, getCourseThumbnailMediaDirectory,
getUserAvatarMediaDirectory, getUserAvatarMediaDirectory,
} from '@services/media/media' } from '@services/media/media'
import { ArrowRight, Check, File, Sparkles, Video } from 'lucide-react' import { ArrowRight, Backpack, Check, File, Sparkles, Video } from 'lucide-react'
import { useOrg } from '@components/Contexts/OrgContext' import { useOrg } from '@components/Contexts/OrgContext'
import UserAvatar from '@components/Objects/UserAvatar' import UserAvatar from '@components/Objects/UserAvatar'
import CourseUpdates from '@components/Objects/CourseUpdates/CourseUpdates' import CourseUpdates from '@components/Objects/CourseUpdates/CourseUpdates'
@ -185,6 +185,15 @@ const CourseClient = (props: any) => {
/> />
</div> </div>
)} )}
{activity.activity_type ===
'TYPE_ASSIGNMENT' && (
<div className="bg-gray-100 px-2 py-2 rounded-full">
<Backpack
className="text-gray-400"
size={13}
/>
</div>
)}
</div> </div>
<Link <Link
className="flex font-semibold grow pl-2 text-neutral-500" className="flex font-semibold grow pl-2 text-neutral-500"
@ -262,6 +271,27 @@ const CourseClient = (props: any) => {
</Link> </Link>
</> </>
)} )}
{activity.activity_type ===
'TYPE_ASSIGNMENT' && (
<>
<Link
className="flex grow pl-2 text-gray-500"
href={
getUriWithOrg(orgslug, '') +
`/course/${courseuuid}/activity/${activity.activity_uuid.replace(
'activity_',
''
)}`
}
rel="noopener noreferrer"
>
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
<p>Assignment</p>
<ArrowRight size={13} />
</div>
</Link>
</>
)}
</div> </div>
</div> </div>
</> </>

View file

@ -0,0 +1,80 @@
import { useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { getAPIUrl } from '@services/config/config';
import { createAssignmentTask } from '@services/courses/assignments'
import { AArrowUp, FileUp, ListTodo } from 'lucide-react'
import React from 'react'
import toast from 'react-hot-toast';
import { mutate } from 'swr';
function NewTaskModal({ closeModal, assignment_uuid }: any) {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const reminderShownRef = React.useRef(false);
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any
function showReminderToast() {
// Check if the reminder has already been shown using sessionStorage
if (sessionStorage.getItem("TasksReminderShown") !== "true") {
setTimeout(() => {
toast('When editing/adding your tasks, make sure to Unpublish your Assignment to avoid any issues with students, you can Publish it again when you are ready.',
{ icon: '✋', duration: 10000, style: { minWidth: 600 } });
// Mark the reminder as shown in sessionStorage
sessionStorage.setItem("TasksReminderShown", "true");
}, 3000);
}
}
async function createTask(type: string) {
const task_object = {
title: "Untitled Task",
description: "",
hint: "",
reference_file: "",
assignment_type: type,
contents: {},
max_grade_value: 100,
}
const res = await createAssignmentTask(task_object, assignment_uuid, access_token)
toast.success('Task created successfully')
showReminderToast()
mutate(`${getAPIUrl()}assignments/${assignment_uuid}/tasks`)
assignmentTaskStateHook({ type: 'setSelectedAssignmentTaskUUID', payload: res.data.assignment_task_uuid })
closeModal(false)
}
return (
<div className='flex space-x-6 mx-auto justify-center items-center'>
<div
onClick={() => createTask('QUIZ')}
className='flex flex-col space-y-2 justify-center text-center pt-10'>
<div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'>
<ListTodo size={30} />
</div>
<p className='text-xl text-gray-700 font-semibold'>Quiz</p>
<p className='text-sm text-gray-500 w-40'>Questions with multiple choice answers</p>
</div>
<div
onClick={() => createTask('FILE_SUBMISSION')}
className='flex flex-col space-y-2 justify-center text-center pt-10'>
<div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'>
<FileUp size={30} />
</div>
<p className='text-xl text-gray-700 font-semibold'>File submission</p>
<p className='text-sm text-gray-500 w-40'>Students can submit files for this task</p>
</div>
<div
onClick={() => toast.error('Forms are not yet supported')}
className='flex flex-col space-y-2 justify-center text-center pt-10 opacity-25'>
<div className='px-5 py-5 rounded-full nice-shadow w-fit mx-auto bg-gray-100/50 text-gray-500 cursor-pointer hover:bg-gray-100 transition-all ease-linear'>
<AArrowUp size={30} />
</div>
<p className='text-xl text-gray-700 font-semibold'>Form</p>
<p className='text-sm text-gray-500 w-40'>Forms for students to fill out</p>
</div>
</div>
)
}
export default NewTaskModal

View file

@ -0,0 +1,25 @@
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import React, { useEffect } from 'react'
import TaskQuizObject from './TaskTypes/TaskQuizObject';
import TaskFileObject from './TaskTypes/TaskFileObject';
function AssignmentTaskContentEdit() {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any
const assignment_task = useAssignmentsTask() as any
useEffect(() => {
}
, [assignment_task, assignmentTaskStateHook])
return (
<div>
{assignment_task?.assignmentTask.assignment_type === 'QUIZ' && <TaskQuizObject view='teacher' />}
{assignment_task?.assignmentTask.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject view='teacher' />}
</div>
)
}
export default AssignmentTaskContentEdit

View file

@ -0,0 +1,276 @@
'use client';
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { useOrg } from '@components/Contexts/OrgContext';
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
import * as Form from '@radix-ui/react-form';
import { getActivityByID } from '@services/courses/activities';
import { updateAssignmentTask, updateReferenceFile } from '@services/courses/assignments';
import { getTaskRefFileDir } from '@services/media/media';
import { useFormik } from 'formik';
import { Cloud, File, Info, Loader, UploadCloud } from 'lucide-react'
import Link from 'next/link';
import React, { useEffect } from 'react'
import toast from 'react-hot-toast';
export function AssignmentTaskGeneralEdit() {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const assignmentTaskState = useAssignmentsTask() as any
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any
const assignment = useAssignments() as any
const validate = (values: any) => {
const errors: any = {};
if (values.max_grade_value < 20 || values.max_grade_value > 100) {
errors.max_grade_value = 'Value should be between 20 and 100';
}
return errors;
};
const formik = useFormik({
initialValues: {
title: assignmentTaskState.assignmentTask.title,
description: assignmentTaskState.assignmentTask.description,
hint: assignmentTaskState.assignmentTask.hint,
max_grade_value: assignmentTaskState.assignmentTask.max_grade_value,
},
validate,
onSubmit: async values => {
const res = await updateAssignmentTask(values, assignmentTaskState.assignmentTask.assignment_task_uuid, assignment.assignment_object.assignment_uuid, access_token)
if (res) {
assignmentTaskStateHook({ type: 'reload' })
toast.success('Task updated successfully')
}
else {
toast.error('Error updating task, please retry later.')
}
},
enableReinitialize: true,
}) as any;
return (
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="title">
<FormLabelAndMessage label="Title" message={formik.errors.title} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.title}
type="text"
/>
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage label="Description" message={formik.errors.description} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.description}
type="text"
/>
</Form.Control>
</FormField>
<FormField name="hint">
<FormLabelAndMessage label="Hint" message={formik.errors.hint} />
<Form.Control asChild>
<Textarea
onChange={formik.handleChange}
value={formik.values.hint}
/>
</Form.Control>
</FormField>
<FormField name="hint">
<div className='flex space-x-3 justify-between items-center'>
<FormLabelAndMessage label="Reference file" message={formik.errors.hint} />
<div className='flex space-x-1.5 text-xs items-center text-gray-500 '>
<Info size={16} />
<p>Allowed formats : pdf, docx, mp4, jpg, jpeg, png, pptx</p>
</div>
</div>
<Form.Control asChild>
<UpdateTaskRef />
</Form.Control>
</FormField>
<FormField name="max_grade_value">
<FormLabelAndMessage label="Max Grade Value" message={formik.errors.max_grade_value} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.max_grade_value}
type="number"
/>
</Form.Control>
</FormField>
{/* Submit button */}
<Form.Submit >
<button
type="submit"
className="flex items-center justify-center w-full px-4 py-2 mt-4 font-semibold text-white bg-green-500 rounded-md hover:bg-green-600"
>
Submit
</button>
</Form.Submit>
</FormLayout>
)
}
function UpdateTaskRef() {
const session = useLHSession() as any;
const org = useOrg() as any;
const access_token = session?.data?.tokens?.access_token;
const assignmentTaskState = useAssignmentsTask() as any
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any
const assignment = useAssignments() as any
const [isLoading, setIsLoading] = React.useState(false)
const [error, setError] = React.useState('') as any
const [localRefFile, setLocalRefFile] = React.useState(null) as any
const [activity, setActivity] = React.useState('') as any
const handleFileChange = async (event: any) => {
const file = event.target.files[0]
setLocalRefFile(file)
setIsLoading(true)
const res = await updateReferenceFile(
file,
assignmentTaskState.assignmentTask.assignment_task_uuid,
assignment.assignment_object.assignment_uuid,
access_token
)
assignmentTaskStateHook({ type: 'reload' })
// wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) {
setError(res.data.detail)
setIsLoading(false)
} else {
toast.success('Reference file updated successfully')
setIsLoading(false)
setError('')
}
}
const getTaskRefDirUI = () => {
return getTaskRefFileDir(
org?.org_uuid,
assignment.course_object.course_uuid,
assignment.activity_object.activity_uuid,
assignment.assignment_object.assignment_uuid,
assignmentTaskState.assignmentTask.assignment_task_uuid,
assignmentTaskState.assignmentTask.reference_file
)
}
const deleteReferenceFile = async () => {
setIsLoading(true)
const res = await updateReferenceFile(
'',
assignmentTaskState.assignmentTask.assignment_task_uuid,
assignment.assignment_object.assignment_uuid,
access_token
)
assignmentTaskStateHook({ type: 'reload' })
// wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) {
setError(res.data.detail)
setIsLoading(false)
} else {
setIsLoading(false)
setError('')
}
}
async function getActivityUI() {
const res = await getActivityByID(assignment.assignment_object.activity_id, null, access_token)
setActivity(res.data)
}
useEffect(() => {
getActivityUI()
}
, [assignmentTaskState, org])
return (
<div className="w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow">
<div className="flex flex-col justify-center items-center h-full">
<div className="flex flex-col justify-center items-center">
<div className="flex flex-col justify-center items-center">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-2 transition-all shadow-sm">
<div className="text-sm font-semibold">{error}</div>
</div>
)}
</div>
{assignmentTaskState.assignmentTask.reference_file && !isLoading && (
<div className='flex flex-col rounded-lg bg-white text-gray-400 shadow-lg nice-shadow px-5 py-3 space-y-1 items-center relative'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-green-500 rounded-full px-1.5 py-1.5 text-white flex justify-center items-center'>
<Cloud size={15} />
</div>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{assignmentTaskState.assignmentTask.reference_file.split('.').pop()}
</div>
<div className='flex space-x-2 mt-2'>
<Link
href={getTaskRefDirUI()}
download
target='_blank'
className='bg-blue-500 text-white px-3 py-1 rounded-full text-xs font-semibold'>Download</Link>
{/** <button onClick={() => deleteReferenceFile()}
className='bg-red-500 text-white px-3 py-1 rounded-full text-xs font-semibold'>Delete</button> */}
</div>
</div>
)}
{isLoading ? (
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<div className="font-bold animate-pulse antialiased items-center bg-slate-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex">
<Loader size={16} className="mr-2" />
<span>Loading</span>
</div>
</div>
) : (
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<button
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
onClick={() => document.getElementById('fileInput')?.click()}
>
<UploadCloud size={16} className="mr-2" />
<span>Change Reference File</span>
</button>
</div>
)}
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,305 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { useOrg } from '@components/Contexts/OrgContext';
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'
import { getAssignmentTask, getAssignmentTaskSubmissionsMe, getAssignmentTaskSubmissionsUser, handleAssignmentTaskSubmission, updateSubFile } from '@services/courses/assignments';
import { getTaskFileSubmissionDir } from '@services/media/media';
import { Cloud, Download, File, Info, Loader, UploadCloud } from 'lucide-react'
import Link from 'next/link';
import React, { useEffect, useState } from 'react'
import toast from 'react-hot-toast';
type FileSchema = {
fileUUID: string;
};
type TaskFileObjectProps = {
view: 'teacher' | 'student' | 'grading' | 'custom-grading';
assignmentTaskUUID?: string;
user_id?: string;
};
export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: TaskFileObjectProps) {
const session = useLHSession() as any;
const org = useOrg() as any;
const access_token = session?.data?.tokens?.access_token;
const [isLoading, setIsLoading] = React.useState(false);
const [localUploadFile, setLocalUploadFile] = React.useState<File | null>(null);
const [error, setError] = React.useState<string | null>(null);
const [assignmentTask, setAssignmentTask] = React.useState<any>(null);
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
const assignment = useAssignments() as any;
/* TEACHER VIEW CODE */
/* TEACHER VIEW CODE */
/* STUDENT VIEW CODE */
const [showSavingDisclaimer, setShowSavingDisclaimer] = useState<boolean>(false);
const [userSubmissions, setUserSubmissions] = useState<FileSchema>({
fileUUID: '',
});
const [initialUserSubmissions, setInitialUserSubmissions] = useState<FileSchema>({
fileUUID: '',
});
const handleFileChange = async (event: any) => {
const file = event.target.files[0]
setLocalUploadFile(file)
setIsLoading(true)
const res = await updateSubFile(
file,
assignmentTask.assignment_task_uuid,
assignment.assignment_object.assignment_uuid,
access_token
)
// wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) {
setError(res.data.detail)
setIsLoading(false)
} else {
assignmentTaskStateHook({ type: 'reload' })
setUserSubmissions({
fileUUID: res.data.file_uuid,
})
setIsLoading(false)
setError('')
}
}
async function getAssignmentTaskSubmissionFromUserUI() {
if (assignmentTaskUUID) {
const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) {
setUserSubmissions(res.data.task_submission);
setInitialUserSubmissions(res.data.task_submission);
}
}
}
const submitFC = async () => {
// Save the quiz to the server
const values = {
task_submission: userSubmissions,
grade: 0,
task_submission_grade_feedback: '',
};
if (assignmentTaskUUID) {
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
assignmentTaskStateHook({
type: 'reload',
});
toast.success('Task saved successfully');
setShowSavingDisclaimer(false);
} else {
toast.error('Error saving task, please retry later.');
}
}
};
async function getAssignmentTaskUI() {
if (assignmentTaskUUID) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
if (res.success) {
setAssignmentTask(res.data);
setAssignmentTaskOutsideProvider(res.data);
}
}
}
// Detect changes between initial and current submissions
useEffect(() => {
if (userSubmissions.fileUUID !== initialUserSubmissions.fileUUID) {
setShowSavingDisclaimer(true);
} else {
setShowSavingDisclaimer(false);
}
}, [userSubmissions]);
/* STUDENT VIEW CODE */
/* GRADING VIEW CODE */
const [userSubmissionObject, setUserSubmissionObject] = useState<any>(null);
async function getAssignmentTaskSubmissionFromIdentifiedUserUI() {
if (assignmentTaskUUID && user_id) {
const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) {
setUserSubmissions(res.data.task_submission);
setUserSubmissionObject(res.data);
setInitialUserSubmissions(res.data.task_submission);
}
}
}
async function gradeCustomFC(grade: number) {
if (assignmentTaskUUID) {
if (grade > assignmentTaskOutsideProvider.max_grade_value) {
toast.error(`Grade cannot be more than ${assignmentTaskOutsideProvider.max_grade_value} points`);
return;
}
// Save the grade to the server
const values = {
task_submission: userSubmissions,
grade: grade,
task_submission_grade_feedback: 'Graded by teacher : @' + session.data.user.username,
};
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
getAssignmentTaskSubmissionFromIdentifiedUserUI();
toast.success(`Task graded successfully with ${grade} points`);
} else {
toast.error('Error grading task, please retry later.');
}
}
}
/* GRADING VIEW CODE */
const [assignmentTaskOutsideProvider, setAssignmentTaskOutsideProvider] = useState<any>(null);
useEffect(() => {
// Student area
if (view === 'student') {
getAssignmentTaskUI()
getAssignmentTaskSubmissionFromUserUI()
}
// Grading area
else if (view == 'custom-grading') {
getAssignmentTaskUI();
//setQuestions(assignmentTaskState.assignmentTask.contents.questions);
getAssignmentTaskSubmissionFromIdentifiedUserUI();
}
}
, [assignmentTaskUUID])
return (
<AssignmentBoxUI submitFC={submitFC} showSavingDisclaimer={showSavingDisclaimer} view={view} gradeCustomFC={gradeCustomFC} currentPoints={userSubmissionObject?.grade} maxPoints={assignmentTaskOutsideProvider?.max_grade_value} type="file">
{view === 'teacher' && (
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<Info size={20} />
<p>User will be able to submit a file for this task, you'll be able to review it in the Submissions Tab</p>
</div>
)}
{view === 'custom-grading' && (
<div className='flex flex-col space-y-1'>
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<Download size={20} />
<p>Please download the file and grade it manually, then input the grade above</p>
</div>
{userSubmissions.fileUUID && !isLoading && assignmentTaskUUID && (
<Link
href={getTaskFileSubmissionDir(org?.org_uuid, assignment.course_object.course_uuid, assignment.activity_object.activity_uuid, assignment.assignment_object.assignment_uuid, assignmentTaskUUID, userSubmissions.fileUUID)}
target='_blank'
className='flex flex-col rounded-lg bg-white text-gray-400 shadow-lg nice-shadow px-5 py-3 space-y-1 items-center relative'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-green-500 rounded-full px-1.5 py-1.5 text-white flex justify-center items-center'>
<Cloud size={15} />
</div>
<div
className='flex space-x-2 mt-2'>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{`${userSubmissions.fileUUID.slice(0, 8)}...${userSubmissions.fileUUID.slice(-4)}`}
</div>
</div>
</Link>
)}
</div>
)}
{view === 'student' && (
<>
<div className="w-auto bg-white rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow">
<div className="flex flex-col justify-center items-center h-full">
<div className="flex flex-col justify-center items-center">
<div className="flex flex-col justify-center items-center">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-2 transition-all shadow-sm">
<div className="text-sm font-semibold">{error}</div>
</div>
)}
</div>
{localUploadFile && !isLoading && (
<div className='flex flex-col rounded-lg bg-white text-gray-400 shadow-lg nice-shadow px-5 py-3 space-y-1 items-center relative'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-green-500 rounded-full px-1.5 py-1.5 text-white flex justify-center items-center'>
<Cloud size={15} />
</div>
<div className='flex space-x-2 mt-2'>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{localUploadFile.name}
</div>
</div>
</div>
)}
{userSubmissions.fileUUID && !isLoading && !localUploadFile && (
<div className='flex flex-col rounded-lg bg-white text-gray-400 shadow-lg nice-shadow px-5 py-3 space-y-1 items-center relative'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-green-500 rounded-full px-1.5 py-1.5 text-white flex justify-center items-center'>
<Cloud size={15} />
</div>
<div className='flex space-x-2 mt-2'>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{`${userSubmissions.fileUUID.slice(0, 8)}...${userSubmissions.fileUUID.slice(-4)}`}
</div>
</div>
</div>
)}
<div className='flex pt-4 font-semibold space-x-1.5 text-xs items-center text-gray-500 '>
<Info size={16} />
<p>Allowed formats : pdf, docx, mp4, jpg, jpeg, png, pptx</p>
</div>
{isLoading ? (
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<div className="font-bold animate-pulse antialiased items-center bg-slate-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex">
<Loader size={16} className="mr-2" />
<span>Loading</span>
</div>
</div>
) : (
<div className="flex justify-center items-center">
<input
type="file"
id={"fileInput_" + assignmentTaskUUID}
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<button
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
onClick={() => document.getElementById("fileInput_" + assignmentTaskUUID)?.click()}
>
<UploadCloud size={16} className="mr-2" />
<span>Submit File</span>
</button>
</div>
)}
</div>
</div>
</div>
</>
)}
</AssignmentBoxUI>
)
}

View file

@ -0,0 +1,452 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI';
import { getAssignmentTask, getAssignmentTaskSubmissionsMe, getAssignmentTaskSubmissionsUser, handleAssignmentTaskSubmission, updateAssignmentTask } from '@services/courses/assignments';
import { Check, Info, Minus, Plus, PlusCircle, X } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
import { v4 as uuidv4 } from 'uuid';
type QuizSchema = {
questionText: string;
questionUUID?: string;
options: {
optionUUID?: string;
text: string;
fileID: string;
type: 'text' | 'image' | 'audio' | 'video';
correct: boolean;
}[];
};
type QuizSubmitSchema = {
questions: QuizSchema[];
submissions: {
questionUUID: string;
optionUUID: string;
}[];
};
type TaskQuizObjectProps = {
view: 'teacher' | 'student' | 'grading';
user_id?: string; // Only for read-only view
assignmentTaskUUID?: string;
};
function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectProps) {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const assignmentTaskState = useAssignmentsTask() as any;
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
const assignment = useAssignments() as any;
/* TEACHER VIEW CODE */
const [questions, setQuestions] = useState<QuizSchema[]>([
{ questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] },
]);
const handleQuestionChange = (index: number, value: string) => {
const updatedQuestions = [...questions];
updatedQuestions[index].questionText = value;
setQuestions(updatedQuestions);
};
const handleOptionChange = (qIndex: number, oIndex: number, value: string) => {
const updatedQuestions = [...questions];
updatedQuestions[qIndex].options[oIndex].text = value;
setQuestions(updatedQuestions);
};
const addOption = (qIndex: number) => {
const updatedQuestions = [...questions];
updatedQuestions[qIndex].options.push({ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() });
setQuestions(updatedQuestions);
};
const removeOption = (qIndex: number, oIndex: number) => {
const updatedQuestions = [...questions];
updatedQuestions[qIndex].options.splice(oIndex, 1);
setQuestions(updatedQuestions);
};
const addQuestion = () => {
setQuestions([...questions, { questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] }]);
};
const removeQuestion = (qIndex: number) => {
const updatedQuestions = [...questions];
updatedQuestions.splice(qIndex, 1);
setQuestions(updatedQuestions);
};
const toggleCorrectOption = (qIndex: number, oIndex: number) => {
const updatedQuestions = [...questions];
// Find the option to toggle
const optionToToggle = updatedQuestions[qIndex].options[oIndex];
// Toggle the 'correct' property of the option
optionToToggle.correct = !optionToToggle.correct;
setQuestions(updatedQuestions);
};
const saveFC = async () => {
// Save the quiz to the server
const values = {
contents: {
questions,
},
};
const res = await updateAssignmentTask(values, assignmentTaskState.assignmentTask.assignment_task_uuid, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
assignmentTaskStateHook({
type: 'reload',
});
toast.success('Task saved successfully');
} else {
toast.error('Error saving task, please retry later.');
}
};
/* TEACHER VIEW CODE */
/* STUDENT VIEW CODE */
const [userSubmissions, setUserSubmissions] = useState<QuizSubmitSchema>({
questions: [],
submissions: [],
});
const [initialUserSubmissions, setInitialUserSubmissions] = useState<QuizSubmitSchema>({
questions: [],
submissions: [],
});
const [showSavingDisclaimer, setShowSavingDisclaimer] = useState<boolean>(false);
const [assignmentTaskOutsideProvider, setAssignmentTaskOutsideProvider] = useState<any>(null);
async function chooseOption(qIndex: number, oIndex: number) {
const updatedSubmissions = [...userSubmissions.submissions];
const questionUUID = questions[qIndex].questionUUID;
const optionUUID = questions[qIndex].options[oIndex].optionUUID;
// Check if this question already has a submission with the selected option
const existingSubmissionIndex = updatedSubmissions.findIndex(
(submission) => submission.questionUUID === questionUUID && submission.optionUUID === optionUUID
);
if (existingSubmissionIndex === -1 && optionUUID && questionUUID) {
// If the selected option is not already chosen, add it to the submissions
updatedSubmissions.push({ questionUUID, optionUUID });
} else {
// If the selected option is already chosen, remove it from the submissions
updatedSubmissions.splice(existingSubmissionIndex, 1);
}
setUserSubmissions({
...userSubmissions,
submissions: updatedSubmissions,
});
}
async function getAssignmentTaskUI() {
if (assignmentTaskUUID) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
if (res.success) {
setAssignmentTaskOutsideProvider(res.data);
setQuestions(res.data.contents.questions);
}
}
}
async function getAssignmentTaskSubmissionFromUserUI() {
if (assignmentTaskUUID) {
const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) {
setUserSubmissions(res.data.task_submission);
setInitialUserSubmissions(res.data.task_submission);
}
}
}
// Detect changes between initial and current submissions
useEffect(() => {
const hasChanges = JSON.stringify(initialUserSubmissions.submissions) !== JSON.stringify(userSubmissions.submissions);
setShowSavingDisclaimer(hasChanges);
}, [userSubmissions, initialUserSubmissions.submissions]);
const submitFC = async () => {
// Save the quiz to the server
const values = {
task_submission: userSubmissions,
grade: 0,
task_submission_grade_feedback: '',
};
if (assignmentTaskUUID) {
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
assignmentTaskStateHook({
type: 'reload',
});
toast.success('Task saved successfully');
setShowSavingDisclaimer(false);
} else {
toast.error('Error saving task, please retry later.');
}
}
};
/* STUDENT VIEW CODE */
/* GRADING VIEW CODE */
const [userSubmissionObject, setUserSubmissionObject] = useState<any>(null);
async function getAssignmentTaskSubmissionFromIdentifiedUserUI() {
if (assignmentTaskUUID && user_id) {
const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) {
setUserSubmissions(res.data.task_submission);
setUserSubmissionObject(res.data);
setInitialUserSubmissions(res.data.task_submission);
}
}
}
async function gradeFC() {
if (assignmentTaskUUID) {
// Ensure maxPoints is defined
const maxPoints = assignmentTaskOutsideProvider?.max_grade_value || 100; // Default to 100 if not defined
// Ensure userSubmissions.questions are set
const totalQuestions = questions.length;
let correctQuestions = 0;
let incorrectQuestions = 0;
userSubmissions.submissions.forEach((submission) => {
const question = questions.find((q) => q.questionUUID === submission.questionUUID);
const option = question?.options.find((o) => o.optionUUID === submission.optionUUID);
if (option?.correct) {
correctQuestions++;
} else {
incorrectQuestions++;
}
});
// Calculate grade with penalties for incorrect answers
const pointsPerQuestion = maxPoints / totalQuestions;
const rawGrade = (correctQuestions - incorrectQuestions) * pointsPerQuestion;
// Ensure the grade is within the valid range
const finalGrade = Math.max(0, Math.min(rawGrade, maxPoints));
// Save the grade to the server
const values = {
task_submission: userSubmissions,
grade: finalGrade,
task_submission_grade_feedback: 'Auto graded by system',
};
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
getAssignmentTaskSubmissionFromIdentifiedUserUI();
toast.success(`Task graded successfully with ${finalGrade} points`);
} else {
toast.error('Error grading task, please retry later.');
}
}
}
/* GRADING VIEW CODE */
useEffect(() => {
assignmentTaskStateHook({
setSelectedAssignmentTaskUUID: assignmentTaskUUID,
});
// Teacher area
if (view == 'teacher' && assignmentTaskState.assignmentTask.contents?.questions) {
setQuestions(assignmentTaskState.assignmentTask.contents.questions);
}
// Student area
else if (view == 'student') {
getAssignmentTaskUI();
getAssignmentTaskSubmissionFromUserUI();
}
// Grading area
else if (view == 'grading') {
getAssignmentTaskUI();
//setQuestions(assignmentTaskState.assignmentTask.contents.questions);
getAssignmentTaskSubmissionFromIdentifiedUserUI();
}
}, [assignmentTaskState, assignment, assignmentTaskStateHook, access_token]);
if (questions && questions.length >= 0) {
return (
<AssignmentBoxUI submitFC={submitFC} saveFC={saveFC} gradeFC={gradeFC} view={view} currentPoints={userSubmissionObject?.grade} maxPoints={assignmentTaskOutsideProvider?.max_grade_value} showSavingDisclaimer={showSavingDisclaimer} type="quiz">
<div className="flex flex-col space-y-6">
{questions && questions.map((question, qIndex) => (
<div key={qIndex} className="flex flex-col space-y-1.5">
<div className="flex space-x-2 items-center">
{view === 'teacher' ? (
<input
value={question.questionText}
onChange={(e) => handleQuestionChange(qIndex, e.target.value)}
placeholder="Question"
className="w-full px-3 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"
/>
) : (
<p className="w-full px-3 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold">
{question.questionText}
</p>
)}
{view === 'teacher' && (
<div
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200/60 text-slate-500 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
onClick={() => removeQuestion(qIndex)}
>
<Minus size={12} className="mx-auto" />
</div>
)}
</div>
<div className="flex flex-col space-y-2">
{question.options.map((option, oIndex) => (
<div className="flex" key={oIndex}>
<div
onClick={() => view === 'student' && chooseOption(qIndex, oIndex)}
className={"answer outline outline-3 outline-white pr-2 shadow w-full flex items-center space-x-2 h-[30px] hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white text-sm duration-150 cursor-pointer ease-linear nice-shadow " + (view == 'student' ? 'active:scale-110' : '')}
>
<div className="font-bold text-base flex items-center h-full w-[40px] rounded-l-md text-slate-800 bg-slate-100/80">
<p className="mx-auto font-bold text-sm">{String.fromCharCode(65 + oIndex)}</p>
</div>
{view === 'teacher' ? (
<input
type="text"
value={option.text}
onChange={(e) => handleOptionChange(qIndex, oIndex, e.target.value)}
placeholder="Option"
className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"
/>
) : (
<p className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] text-sm font-bold">
{option.text}
</p>
)}
{view === 'teacher' && (
<>
<div
className={`w-fit flex-none flex text-xs px-2 py-0.5 space-x-1 items-center h-fit rounded-lg ${option.correct ? 'bg-lime-200 text-lime-600' : 'bg-rose-200/60 text-rose-500'
} hover:bg-lime-300 text-sm transition-all ease-linear cursor-pointer`}
onClick={() => toggleCorrectOption(qIndex, oIndex)}
>
{option.correct ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
{option.correct ? (
<p className="mx-auto font-bold text-xs">Correct</p>
) : (
<p className="mx-auto font-bold text-xs">Incorrect</p>
)}
</div>
<div
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200/60 text-slate-500 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
onClick={() => removeOption(qIndex, oIndex)}
>
<Minus size={12} className="mx-auto" />
</div>
</>
)}
{view === 'grading' && (
<>
<div
className={`w-fit flex-none flex text-xs px-2 py-0.5 space-x-1 items-center h-fit rounded-lg ${option.correct ? 'bg-lime-200 text-lime-600' : 'bg-rose-200/60 text-rose-500'
} hover:bg-lime-300 text-sm transition-all ease-linear cursor-pointer`}
>
{option.correct ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
{option.correct ? (
<p className="mx-auto font-bold text-xs">Marked as Correct</p>
) : (
<p className="mx-auto font-bold text-xs">Marked as Incorrect</p>
)}
</div>
</>
)}
{view === 'student' && (
<div className={`w-[20px] flex-none flex items-center h-[20px] rounded-lg ${userSubmissions.submissions.find(
(submission) =>
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
)
? "bg-green-200/60 text-green-500 hover:bg-green-300" // Selected state colors
: "bg-slate-200/60 text-slate-500 hover:bg-slate-300" // Default state colors
} text-sm transition-all ease-linear cursor-pointer`}>
{userSubmissions.submissions.find(
(submission) =>
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
) ? (
<Check size={12} className="mx-auto" />
) : (
<X size={12} className="mx-auto" />
)}
</div>
)}
{view === 'grading' && (
<div className={`w-[20px] flex-none flex items-center h-[20px] rounded-lg ${userSubmissions.submissions.find(
(submission) =>
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
)
? "bg-green-200/60 text-green-500 hover:bg-green-300" // Selected state colors
: "bg-slate-200/60 text-slate-500 hover:bg-slate-300" // Default state colors
} text-sm transition-all ease-linear cursor-pointer`}>
{userSubmissions.submissions.find(
(submission) =>
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
) ? (
<Check size={12} className="mx-auto" />
) : (
<X size={12} className="mx-auto" />
)}
</div>
)}
</div>
{view === 'teacher' && oIndex === question.options.length - 1 && questions[qIndex].options.length <= 4 && (
<div className="flex justify-center mx-auto px-2">
<div
className="outline text-xs outline-3 outline-white px-2 shadow w-full flex items-center h-[30px] hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white duration-150 cursor-pointer ease-linear nice-shadow"
onClick={() => addOption(qIndex)}
>
<Plus size={14} className="inline-block" />
<span></span>
</div>
</div>
)}
</div>
))}
</div>
</div>
))}
</div>
{view === 'teacher' && questions.length <= 5 && (
<div className="flex justify-center mx-auto px-2">
<div
className="flex w-full my-2 py-2 px-4 bg-white text-slate text-xs rounded-md nice-shadow hover:shadow-sm cursor-pointer space-x-3 items-center transition duration-150 ease-linear"
onClick={addQuestion}
>
<PlusCircle size={14} className="inline-block" />
<span>Add Question</span>
</div>
</div>
)}
</AssignmentBoxUI>
);
}
else {
return <div className='flex flex-row space-x-2 text-sm items-center'>
<Info size={12} />
<p>No questions found</p>
</div>;
}
}
export default TaskQuizObject;

View file

@ -0,0 +1,119 @@
'use client';
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { getAPIUrl } from '@services/config/config';
import { deleteAssignmentTask } from '@services/courses/assignments';
import { GalleryVerticalEnd, Info, TentTree, Trash } from 'lucide-react'
import React, { useEffect } from 'react'
import toast from 'react-hot-toast';
import { mutate } from 'swr';
import dynamic from 'next/dynamic';
import { AssignmentTaskGeneralEdit } from './Subs/AssignmentTaskGeneralEdit';
const AssignmentTaskContentEdit = dynamic(() => import('./Subs/AssignmentTaskContentEdit'))
function AssignmentTaskEditor({ page }: any) {
const [selectedSubPage, setSelectedSubPage] = React.useState(page)
const assignment = useAssignments() as any
const assignmentTaskState = useAssignmentsTask() as any
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
async function deleteTaskUI() {
const res = await deleteAssignmentTask(assignmentTaskState.assignmentTask.assignment_task_uuid, assignment.assignment_object.assignment_uuid, access_token)
if (res) {
assignmentTaskStateHook({
type: 'SET_MULTIPLE_STATES',
payload: {
selectedAssignmentTaskUUID: null,
assignmentTask: {},
},
});
mutate(`${getAPIUrl()}assignments/${assignment.assignment_object.assignment_uuid}/tasks`)
mutate(`${getAPIUrl()}assignments/${assignment.assignment_object.assignment_uuid}`)
toast.success('Task deleted successfully')
} else {
toast.error('Error deleting task, please retry later.')
}
}
useEffect(() => {
// Switch back to general page if the selectedAssignmentTaskUUID is changed
if (assignmentTaskState.selectedAssignmentTaskUUID !== assignmentTaskState.assignmentTask.assignment_task_uuid) {
setSelectedSubPage('general')
}
}
, [assignmentTaskState, assignmentTaskStateHook, selectedSubPage, assignment])
return (
<div className="flex flex-col font-black text-sm w-full z-20">
{assignmentTaskState.assignmentTask && Object.keys(assignmentTaskState.assignmentTask).length > 0 && (
<div className='flex flex-col space-y-3'>
<div className='flex flex-col bg-white pl-10 pr-10 text-sm tracking-tight z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)] pt-5 mb-3 nice-shadow'>
<div className='flex py-1 justify-between items-center'>
<div className='font-semibold text-lg '>
{assignmentTaskState?.assignmentTask.title}
</div>
<div>
<div
onClick={() => deleteTaskUI()}
className='flex px-2 py-1.5 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-red-800 bg-rose-100 border border-rose-600/10 shadow-rose-900/10 shadow-lg'>
<Trash size={18} />
<p className='text-xs font-semibold'>Delete Task</p>
</div>
</div>
</div>
<div className='flex space-x-2 '>
<div
onClick={() => setSelectedSubPage('general')}
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'general'
? 'border-b-4'
: 'opacity-50'
} cursor-pointer`}
>
<div className="flex items-center space-x-2.5 mx-2">
<Info size={16} />
<div>General</div>
</div>
</div>
<div
onClick={() => setSelectedSubPage('content')}
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'content'
? 'border-b-4'
: 'opacity-50'
} cursor-pointer`}
>
<div className="flex items-center space-x-2.5 mx-2">
<GalleryVerticalEnd size={16} />
<div>Content</div>
</div>
</div>
</div>
</div>
<div className='ml-10 mr-10 mt-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5 nice-shadow'>
{selectedSubPage === 'general' && <AssignmentTaskGeneralEdit />}
{selectedSubPage === 'content' && <AssignmentTaskContentEdit />}
</div>
</div>
)}
{Object.keys(assignmentTaskState.assignmentTask).length == 0 && (
<div className='flex flex-col h-full bg-white pl-10 pr-10 text-sm tracking-tight z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)] pt-5'>
<div className='flex justify-center items-center h-full text-gray-300 antialiased'>
<div className='flex flex-col space-y-2 items-center'>
<TentTree size={60} />
<div className='font-semibold text-2xl py-1'>
No Task Selected
</div>
</div>
</div>
</div>
)}
</div>
)
}
export default AssignmentTaskEditor

View file

@ -0,0 +1,73 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext'
import Modal from '@components/StyledElements/Modal/Modal';
import { FileUp, ListTodo, PanelLeftOpen, Plus } from 'lucide-react';
import React, { useEffect } from 'react'
import NewTaskModal from './Modals/NewTaskModal';
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
function AssignmentTasks({ assignment_uuid }: any) {
const assignments = useAssignments() as any;
const assignmentTask = useAssignmentsTask() as any;
const assignmentTaskHook = useAssignmentsTaskDispatch() as any;
const [isNewTaskModalOpen, setIsNewTaskModalOpen] = React.useState(false)
async function setSelectTask(task_uuid: string) {
assignmentTaskHook({ type: 'setSelectedAssignmentTaskUUID', payload: task_uuid })
}
useEffect(() => {
}, [assignments])
return (
<div className='flex w-full'>
<div className='flex flex-col space-y-3 mx-auto'>
{assignments && assignments?.assignment_tasks?.length < 10 && (<Modal
isDialogOpen={isNewTaskModalOpen}
onOpenChange={setIsNewTaskModalOpen}
minHeight="sm"
minWidth='sm'
dialogContent={
<NewTaskModal assignment_uuid={assignment_uuid} closeModal={setIsNewTaskModalOpen} />
}
dialogTitle="Add an Assignment Task"
dialogDescription="Create a new task for this assignment"
dialogTrigger={
<div className='flex space-x-1.5 px-2 py-2 justify-center bg-black text-white text-xs rounded-md antialiased items-center font-semibold cursor-pointer'>
<Plus size={17} />
<p>Add Task</p>
</div>
}
/>)}
{assignments && assignments?.assignment_tasks?.map((task: any) => {
return (
<div
key={task.id}
className='flex flex-col w-[250px] nice-shadow bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.06)] p-3 rounded-md'
onClick={() => setSelectTask(task.assignment_task_uuid)}
>
<div className='flex items-center px-2 justify-between'>
<div className="flex space-x-3 items-center">
<div className='text-gray-500'>
{task.assignment_type === 'QUIZ' && <ListTodo size={15} />}
{task.assignment_type === 'FILE_SUBMISSION' && <FileUp size={15} />}
</div>
<div className='font-semibold text-sm'>{task.title}</div>
</div>
<button className={`outline outline-1 outline-gray-200 ${task.assignment_task_uuid == assignmentTask.selectedAssignmentTaskUUID ? 'bg-slate-100' : ''} hover:bg-slate-100/50 rounded-md text-gray-500 font-bold py-2 px-3 ease-linear transition-all`}>
<PanelLeftOpen size={16} />
</button>
</div>
</div>
)
})}
</div>
</div>
)
}
export default AssignmentTasks

View file

@ -0,0 +1,155 @@
'use client';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { BookOpen, BookX, EllipsisVertical, Eye, Layers2, UserRoundPen } from 'lucide-react'
import React, { useEffect } from 'react'
import { AssignmentProvider, useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import ToolTip from '@components/StyledElements/Tooltip/Tooltip';
import { updateAssignment } from '@services/courses/assignments';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { mutate } from 'swr';
import { getAPIUrl } from '@services/config/config';
import toast from 'react-hot-toast';
import Link from 'next/link';
import { useParams, useSearchParams } from 'next/navigation';
import { updateActivity } from '@services/courses/activities';
// Lazy Loading
import dynamic from 'next/dynamic';
import AssignmentEditorSubPage from './subpages/AssignmentEditorSubPage';
const AssignmentSubmissionsSubPage = dynamic(() => import('./subpages/AssignmentSubmissionsSubPage'))
function AssignmentEdit() {
const params = useParams<{ assignmentuuid: string; }>()
const searchParams = useSearchParams()
const [selectedSubPage, setSelectedSubPage] = React.useState( searchParams.get('subpage') || 'editor')
return (
<div className='flex w-full flex-col'>
<AssignmentProvider assignment_uuid={'assignment_' + params.assignmentuuid}>
<div className='flex flex-col bg-white z-50 shadow-[0px_4px_16px_rgba(0,0,0,0.06)] nice-shadow'>
<div className='flex justify-between mr-10 h-full'>
<div className="pl-10 mr-10 tracking-tighter">
<BrdCmpx />
<div className="w-100 flex justify-between">
<div className="flex font-bold text-2xl">Assignment Tools </div>
</div>
</div>
<div className='flex flex-col justify-center antialiased'>
<PublishingState />
</div>
</div>
<div className='flex space-x-2 pt-2 text-sm tracking-tight font-semibold pl-10 mr-10'>
<div
onClick={() => setSelectedSubPage('editor')}
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'editor'
? 'border-b-4'
: 'opacity-50'
} cursor-pointer`}
>
<div className="flex items-center space-x-2.5 mx-2">
<Layers2 size={16} />
<div>Editor</div>
</div>
</div>
<div
onClick={() => setSelectedSubPage('submissions')}
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${selectedSubPage === 'submissions'
? 'border-b-4'
: 'opacity-50'
} cursor-pointer`}
>
<div className="flex items-center space-x-2.5 mx-2">
<UserRoundPen size={16} />
<div>Submissions</div>
</div>
</div>
</div>
</div>
<div className="flex h-full w-full">
{selectedSubPage === 'editor' && <AssignmentEditorSubPage assignmentuuid={params.assignmentuuid} />}
{selectedSubPage === 'submissions' && <AssignmentSubmissionsSubPage assignment_uuid={params.assignmentuuid} />}
</div>
</AssignmentProvider>
</div>
)
}
export default AssignmentEdit
function BrdCmpx() {
const assignment = useAssignments() as any
useEffect(() => {
}, [assignment])
return (
<BreadCrumbs type="assignments" last_breadcrumb={assignment?.assignment_object?.title} />
)
}
function PublishingState() {
const assignment = useAssignments() as any;
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
async function updateAssignmentPublishState(assignmentUUID: string) {
const res = await updateAssignment({ published: !assignment?.assignment_object?.published }, assignmentUUID, access_token)
const res2 = await updateActivity({ published: !assignment?.assignment_object?.published }, assignment?.activity_object?.activity_uuid, access_token)
if (res.success && res2) {
mutate(`${getAPIUrl()}assignments/${assignmentUUID}`)
toast.success('The assignment has been updated successfully')
}
else {
toast.error('Error updating assignment, please retry later.')
}
}
useEffect(() => {
}, [assignment])
return (
<div className='flex mx-auto mt-5 items-center space-x-4'>
<div className={`flex text-xs rounded-full px-3.5 py-2 mx-auto font-bold outline outline-1 ${!assignment?.assignment_object?.published ? 'outline-gray-300 bg-gray-200/60' : 'outline-green-300 bg-green-200/60'}`}>
{assignment?.assignment_object?.published ? 'Published' : 'Unpublished'}
</div>
<div><EllipsisVertical className='text-gray-500' size={13} /></div>
<ToolTip
side='left'
slateBlack
sideOffset={10}
content="Preview the Assignment as a student" >
<Link
target='_blank'
href={`/course/${assignment?.course_object?.course_uuid.replace('course_', '')}/activity/${assignment?.activity_object?.activity_uuid.replace('activity_', '')}`}
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-cyan-800 font-medium from-sky-400/50 to-cyan-200/80 border border-cyan-600/10 shadow-cyan-900/10 shadow-lg'>
<Eye size={18} />
<p className=' text-sm font-bold'>Preview</p>
</Link>
</ToolTip>
{assignment?.assignment_object?.published && <ToolTip
side='left'
slateBlack
sideOffset={10}
content="Make your Assignment unavailable for students" >
<div
onClick={() => updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)}
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg'>
<BookX size={18} />
<p className='text-sm font-bold'>Unpublish</p>
</div>
</ToolTip>}
{!assignment?.assignment_object?.published &&
<ToolTip
side='left'
slateBlack
sideOffset={10}
content="Make your Assignment public and available for students" >
<div
onClick={() => updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)}
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-green-800 font-medium from-green-400/50 to-lime-200/80 border border-green-600/10 shadow-green-900/10 shadow-lg'>
<BookOpen size={18} />
<p className=' text-sm font-bold'>Publish</p>
</div>
</ToolTip>}
</div>
)
}

View file

@ -0,0 +1,29 @@
'use client';
import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext'
import { LayoutList } from 'lucide-react'
import React from 'react'
import AssignmentTasks from '../_components/Tasks'
import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext'
import dynamic from 'next/dynamic';
const AssignmentTaskEditor = dynamic(() => import('../_components/TaskEditor/TaskEditor'))
function AssignmentEditorSubPage({ assignmentuuid }: { assignmentuuid: string }) {
return (
<AssignmentsTaskProvider>
<div className='flex w-[400px] flex-col h-full custom-dots-bg'>
<div className='flex mx-auto px-3.5 py-1 bg-neutral-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'>
<LayoutList size={18} />
<p>Tasks</p>
</div>
<AssignmentTasks assignment_uuid={'assignment_' + assignmentuuid} />
</div>
<div className='flex flex-grow bg-[#fefcfe] nice-shadow h-full w-full'>
<AssignmentProvider assignment_uuid={'assignment_' + assignmentuuid}>
<AssignmentTaskEditor page='general' />
</AssignmentProvider>
</div>
</AssignmentsTaskProvider>
)
}
export default AssignmentEditorSubPage

View file

@ -0,0 +1,141 @@
import { useLHSession } from '@components/Contexts/LHSessionContext';
import UserAvatar from '@components/Objects/UserAvatar';
import Modal from '@components/StyledElements/Modal/Modal';
import { getAPIUrl } from '@services/config/config';
import { getUserAvatarMediaDirectory } from '@services/media/media';
import { swrFetcher } from '@services/utils/ts/requests';
import { Loader, SendHorizonal, UserCheck, X } from 'lucide-react';
import React, { useEffect } from 'react';
import useSWR from 'swr';
import EvaluateAssignment from './Modals/EvaluateAssignment';
import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext';
import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import AssignmentSubmissionProvider from '@components/Contexts/Assignments/AssignmentSubmissionContext';
function AssignmentSubmissionsSubPage({ assignment_uuid }: { assignment_uuid: string }) {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const { data: assignmentSubmission, error: assignmentError } = useSWR(
`${getAPIUrl()}assignments/assignment_${assignment_uuid}/submissions`,
(url) => swrFetcher(url, access_token)
);
useEffect(() => {
console.log(assignmentSubmission);
}, [session, assignmentSubmission]);
const renderSubmissions = (status: string) => {
return assignmentSubmission
?.filter((submission: any) => submission.submission_status === status)
.map((submission: any) => (
<SubmissionBox key={submission.submission_uuid} submission={submission} assignment_uuid={assignment_uuid} user_id={submission.user_id} />
));
};
return (
<div className='pl-10 mr-10 flex flex-col pt-3 w-full'>
<div className='flex flex-row w-full'>
<div className='flex-1'>
<div className='flex w-fit mx-auto px-3.5 py-1 bg-rose-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'>
<X size={18} />
<h3>Late</h3>
</div>
{renderSubmissions('LATE')}
</div>
<div className='flex-1'>
<div className='flex w-fit mx-auto px-3.5 py-1 bg-amber-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'>
<SendHorizonal size={18} />
<h3>Submitted</h3>
</div>
{renderSubmissions('SUBMITTED')}
</div>
<div className='flex-1'>
<div className='flex w-fit mx-auto px-3.5 py-1 bg-emerald-600/80 space-x-2 my-5 items-center text-sm font-bold text-white rounded-full'>
<UserCheck size={18} />
<h3>Graded</h3>
</div>
{renderSubmissions('GRADED')}
</div>
</div>
</div>
);
}
function SubmissionBox({ assignment_uuid, user_id, submission }: any) {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const [gradeSudmissionModal, setGradeSubmissionModal] = React.useState({
open: false,
submission_id: '',
});
const { data: user, error: userError } = useSWR(
`${getAPIUrl()}users/id/${user_id}`,
(url) => swrFetcher(url, access_token)
);
useEffect(() => {
console.log(user);
}
, [session, user]);
return (
<div className='flex flex-row bg-white shadow-[0px_4px_16px_rgba(0,0,0,0.06)] nice-shadow rounded-lg p-4 w-[350px] mx-auto'>
<div className='flex flex-col space-y-2 w-full'>
<div className='flex justify-between w-full'>
<h2 className='uppercase text-slate-400 text-xs tracking-tight font-semibold'>Submission</h2>
<p className='uppercase text-xs tracking-tight font-semibold'>
{new Date(submission.creation_date).toLocaleDateString('en-UK', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</p>
</div>
<div className='flex justify-between space-x-2'>
<div className='flex space-x-2'>
<UserAvatar
border="border-4"
avatar_url={getUserAvatarMediaDirectory(user?.user_uuid, user?.avatar_image)}
predefined_avatar={user?.avatar_image ? undefined : 'empty'}
width={40}
/>
<div className='flex flex-col'>
{user?.first_name && user?.last_name ? (<p className='text-sm font-semibold'>{user?.first_name} {user?.last_name}</p>) : (<p className='text-sm font-semibold'>@{user?.username}</p>)}
<p className='text-xs text-slate-400'>{user?.email}</p>
</div>
</div>
<div className='flex flex-col'>
<Modal
isDialogOpen={gradeSudmissionModal.open && gradeSudmissionModal.submission_id === submission.submission_uuid}
onOpenChange={(open: boolean) => setGradeSubmissionModal({ open, submission_id: submission.submission_uuid })}
minHeight="lg"
minWidth="lg"
dialogContent={
<AssignmentProvider assignment_uuid={"assignment_" + assignment_uuid}>
<AssignmentsTaskProvider>
<AssignmentSubmissionProvider assignment_uuid={"assignment_" + assignment_uuid}>
<EvaluateAssignment user_id={user_id} />
</AssignmentSubmissionProvider>
</AssignmentsTaskProvider>
</AssignmentProvider>
}
dialogTitle={`Evaluate @${user?.username}`}
dialogDescription="Evaluate the submission"
dialogTrigger={
<div className='bg-slate-800 hover:bg-slate-700 text-white font-bold py-2 px-4 rounded text-xs cursor-pointer'>
Evaluate
</div>
}
/>
</div>
</div>
</div>
</div>
);
}
export default AssignmentSubmissionsSubPage;

View file

@ -0,0 +1,115 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { Apple, ArrowRightFromLine, BookOpenCheck, Check, Download, Info, Medal, MoveRight, X } from 'lucide-react';
import Link from 'next/link';
import React from 'react'
import TaskQuizObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskQuizObject';
import TaskFileObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskFileObject';
import { useOrg } from '@components/Contexts/OrgContext';
import { getTaskRefFileDir } from '@services/media/media';
import { deleteUserSubmission, markActivityAsDoneForUser, putFinalGrade } from '@services/courses/assignments';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { useRouter } from 'next/navigation';
import toast from 'react-hot-toast';
function EvaluateAssignment({ user_id }: any) {
const assignments = useAssignments() as any;
const session = useLHSession() as any;
const org = useOrg() as any;
const router = useRouter();
async function gradeAssignment() {
const res = await putFinalGrade(user_id, assignments?.assignment_object.assignment_uuid, session.data?.tokens?.access_token);
if (res.success) {
toast.success(res.data.message)
}
else {
toast.error(res.data.message)
}
}
async function markActivityAsDone() {
const res = await markActivityAsDoneForUser(user_id, assignments?.assignment_object.assignment_uuid, session.data?.tokens?.access_token)
if (res.success) {
toast.success(res.data.message)
}
else {
toast.error(res.data.message)
}
}
async function rejectAssignment() {
const res = await deleteUserSubmission(user_id, assignments?.assignment_object.assignment_uuid, session.data?.tokens?.access_token)
toast.success('Assignment rejected successfully')
window.location.reload()
}
return (
<div className='flex-col space-y-4 px-3 py-3 overflow-y-auto min-h-fit'>
{assignments && assignments?.assignment_tasks?.sort((a: any, b: any) => a.id - b.id).map((task: any, index: number) => {
return (
<div className='flex flex-col space-y-2' key={task.assignment_task_uuid}>
<div className='flex justify-between py-2'>
<div className='flex space-x-2 font-semibold text-slate-800'>
<p>Task {index + 1} : </p>
<p className='text-slate-500'>{task.description}</p>
</div>
<div className='flex space-x-2'>
<div
onClick={() => alert(task.hint)}
className='px-3 py-1 flex items-center nice-shadow bg-amber-50/40 text-amber-900 rounded-full space-x-2 cursor-pointer'>
<Info size={13} />
<p className='text-xs font-semibold'>Hint</p>
</div>
<Link
href={getTaskRefFileDir(
org?.org_uuid,
assignments?.course_object.course_uuid,
assignments?.activity_object.activity_uuid,
assignments?.assignment_object.assignment_uuid,
task.assignment_task_uuid,
task.reference_file
)}
target='_blank'
download={true}
className='px-3 py-1 flex items-center nice-shadow bg-cyan-50/40 text-cyan-900 rounded-full space-x-2 cursor-pointer'>
<Download size={13} />
<div className='flex items-center space-x-2'>
{task.reference_file && (
<span className='relative'>
<span className='absolute right-0 top-0 block h-2 w-2 rounded-full ring-2 ring-white bg-green-400'></span>
</span>
)}
<p className='text-xs font-semibold'>Reference Document</p>
</div>
</Link>
</div>
</div>
<div className='min-h-full'>
{task.assignment_type === 'QUIZ' && <TaskQuizObject key={task.assignment_task_uuid} view='grading' user_id={user_id} assignmentTaskUUID={task.assignment_task_uuid} />}
{task.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject key={task.assignment_task_uuid} view='custom-grading' user_id={user_id} assignmentTaskUUID={task.assignment_task_uuid} />}
</div>
</div>
)
})}
<div className='flex space-x-4 font-semibold items-center justify-between'>
<button onClick={rejectAssignment} className='flex space-x-2 px-4 py-2 text-sm bg-rose-600/80 text-white rounded-lg nice-shadow items-center'>
<X size={18} />
<span>Reject Assignment</span>
</button>
<div className='flex space-x-3 items-center'>
<button onClick={gradeAssignment} className='flex space-x-2 px-4 py-2 text-sm bg-violet-600/80 text-white rounded-lg nice-shadow items-center'>
<BookOpenCheck size={18} />
<span>Set final grade</span>
</button>
<MoveRight className='text-gray-400' size={18} />
<button onClick={markActivityAsDone} className='flex space-x-2 px-4 py-2 text-sm bg-teal-600/80 text-white rounded-lg nice-shadow items-center'>
<Check size={18} />
<span>Mark Activity as Done for User</span>
</button>
</div>
</div>
</div>
)
}
export default EvaluateAssignment

View file

@ -0,0 +1,175 @@
'use client';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { useOrg } from '@components/Contexts/OrgContext';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import { getAssignmentsFromACourse } from '@services/courses/assignments';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import { swrFetcher } from '@services/utils/ts/requests';
import { Book, EllipsisVertical, GalleryVertical, GalleryVerticalEnd, Info, Layers2, PenBox, UserRoundPen } from 'lucide-react';
import Link from 'next/link';
import React from 'react'
import useSWR from 'swr';
function AssignmentsHome() {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const org = useOrg() as any;
const [courseAssignments, setCourseAssignments] = React.useState<any[]>([])
const { data: courses } = useSWR(`${getAPIUrl()}courses/org_slug/${org?.slug}/page/1/limit/50`, (url) => swrFetcher(url, access_token))
async function getAvailableAssignmentsForCourse(course_uuid: string) {
const res = await getAssignmentsFromACourse(course_uuid, access_token)
return res.data
}
function removeAssignmentPrefix(assignment_uuid: string) {
return assignment_uuid.replace('assignment_', '')
}
function removeCoursePrefix(course_uuid: string) {
return course_uuid.replace('course_', '')
}
React.useEffect(() => {
if (courses) {
const course_uuids = courses.map((course: any) => course.course_uuid)
const courseAssignmentsPromises = course_uuids.map((course_uuid: string) => getAvailableAssignmentsForCourse(course_uuid))
Promise.all(courseAssignmentsPromises).then((results) => {
setCourseAssignments(results)
})
}
}, [courses])
return (
<div className='flex w-full'>
<div className='pl-10 mr-10 tracking-tighter flex flex-col space-y-5 w-full'>
<div className='flex flex-col space-y-2'>
<BreadCrumbs type="assignments" />
<h1 className="pt-3 flex font-bold text-4xl">Assignments</h1>
</div>
<div className='flex flex-col space-y-3 w-full'>
{courseAssignments.map((assignments: any, index: number) => (
<div key={index} className='flex flex-col space-y-2 bg-white nice-shadow p-4 rounded-xl w-full'>
<div>
<div className='flex space-x-2 items-center justify-between w-full'>
<div className='flex space-x-2 items-center'>
<MiniThumbnail course={courses[index]} />
<div className='flex flex-col font-bold text-lg '>
<p className='bg-gray-200 text-gray-700 px-2 text-xs py-0.5 rounded-full w-fit'>Course</p>
<p>{courses[index].name}</p>
</div>
</div>
<Link
href={{
pathname: getUriWithOrg(org.slug, `/dash/courses/course/${removeCoursePrefix(courses[index].course_uuid)}/content`),
query: { subpage: 'editor' }
}}
prefetch
className='bg-black font-semibold text-sm text-zinc-100 rounded-md flex space-x-1.5 nice-shadow items-center px-3 py-1'>
<GalleryVerticalEnd size={15} />
<p>Course Editor</p>
</Link>
</div>
{assignments && assignments.map((assignment: any) => (
<div key={assignment.assignment_uuid} className='flex mt-3 p-3 rounded flex-row space-x-2 w-full light-shadow justify-between bg-gray-50 items-center'>
<div className='flex flex-row items-center space-x-2 '>
<div className='flex text-xs font-bold bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full h-fit'>
<p>Assignment</p>
</div>
<div className='flex font-semibold text-lg'>{assignment.title}</div>
<div className='flex font-semibold text-gray-600 px-2 py-0.5 rounded outline outline-gray-200/70'>{assignment.description}</div>
</div>
<div className='flex space-x-2 font-bold text-sm items-center'>
<EllipsisVertical className='text-gray-500' size={17} />
<Link
href={{
pathname: getUriWithOrg(org.slug, `/dash/assignments/${removeAssignmentPrefix(assignment.assignment_uuid)}`),
query: { subpage: 'editor' }
}}
prefetch
className='bg-white rounded-full flex space-x-2 nice-shadow items-center px-3 py-0.5'>
<Layers2 size={15} />
<p>Editor</p>
</Link>
<Link
href={{
pathname: getUriWithOrg(org.slug, `/dash/assignments/${removeAssignmentPrefix(assignment.assignment_uuid)}`),
query: { subpage: 'submissions' }
}}
prefetch
className='bg-white rounded-full flex space-x-2 nice-shadow items-center px-3 py-0.5'>
<UserRoundPen size={15} />
<p>Submissions</p>
</Link>
</div>
</div>
))}
{assignments.length === 0 && (
<>
<div className='flex mx-auto space-x-2 font-semibold mt-3 text-gray-600 items-center'>
<Info size={20} />
<p>No assignments available for this course, create course assignments from the course editor</p>
</div>
</>
)}
</div>
</div>
))}
</div>
</div>
</div>
)
}
const MiniThumbnail = (props: { course: any }) => {
const org = useOrg() as any
// function to remove "course_" from the course_uuid
function removeCoursePrefix(course_uuid: string) {
return course_uuid.replace('course_', '')
}
return (
<Link
href={getUriWithOrg(
org.orgslug,
'/course/' + removeCoursePrefix(props.course.course_uuid)
)}
>
{props.course.thumbnail_image ? (
<div
className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl w-[70px] h-[40px] bg-cover"
style={{
backgroundImage: `url(${getCourseThumbnailMediaDirectory(
org?.org_uuid,
props.course.course_uuid,
props.course.thumbnail_image
)})`,
}}
/>
) : (
<div
className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl w-[70px] h-[40px] bg-cover"
style={{
backgroundImage: `url('../empty_thumbnail.png')`,
backgroundSize: 'contain',
}}
/>
)}
</Link>
)
}
export default AssignmentsHome

View file

@ -82,7 +82,6 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
</div> </div>
</div> </div>
</Link> </Link>
</div> </div>
</div> </div>

View file

@ -0,0 +1,54 @@
'use client'
import { getAPIUrl } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests'
import React, { createContext, useContext, useEffect } from 'react'
import useSWR from 'swr'
import { useLHSession } from '@components/Contexts/LHSessionContext'
export const AssignmentContext = createContext({})
export function AssignmentProvider({ children, assignment_uuid }: { children: React.ReactNode, assignment_uuid: string }) {
const session = useLHSession() as any
const accessToken = session?.data?.tokens?.access_token
const [assignmentsFull, setAssignmentsFull] = React.useState({ assignment_object: null, assignment_tasks: null, course_object: null , activity_object: null})
const { data: assignment, error: assignmentError } = useSWR(
`${getAPIUrl()}assignments/${assignment_uuid}`,
(url) => swrFetcher(url, accessToken)
)
const { data: assignment_tasks, error: assignmentTasksError } = useSWR(
`${getAPIUrl()}assignments/${assignment_uuid}/tasks`,
(url) => swrFetcher(url, accessToken)
)
const course_id = assignment?.course_id
const { data: course_object, error: courseObjectError } = useSWR(
course_id ? `${getAPIUrl()}courses/id/${course_id}` : null,
(url) => swrFetcher(url, accessToken)
)
const activity_id = assignment?.activity_id
const { data: activity_object, error: activityObjectError } = useSWR(
activity_id ? `${getAPIUrl()}activities/id/${activity_id}` : null,
(url) => swrFetcher(url, accessToken)
)
useEffect(() => {
if (assignment && assignment_tasks && (!course_id || course_object) && (!activity_id || activity_object)) {
setAssignmentsFull({ assignment_object: assignment, assignment_tasks: assignment_tasks, course_object: course_object, activity_object: activity_object })
}
}, [assignment, assignment_tasks, course_object, activity_object, course_id, activity_id])
if (assignmentError || assignmentTasksError || courseObjectError || activityObjectError) return <div></div>
if (!assignment || !assignment_tasks || (course_id && !course_object) || (activity_id && !activity_object)) return <div></div>
return <AssignmentContext.Provider value={assignmentsFull}>{children}</AssignmentContext.Provider>
}
export function useAssignments() {
return useContext(AssignmentContext)
}

View file

@ -0,0 +1,28 @@
'use client'
import React from 'react'
import { useLHSession } from '../LHSessionContext'
import { getAPIUrl } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests'
import useSWR from 'swr'
export const AssignmentSubmissionContext = React.createContext({})
function AssignmentSubmissionProvider({ children, assignment_uuid }: { children: React.ReactNode, assignment_uuid: string }) {
const session = useLHSession() as any
const accessToken = session?.data?.tokens?.access_token
const { data: assignmentSubmission, error: assignmentError } = useSWR(
`${getAPIUrl()}assignments/${assignment_uuid}/submissions/me`,
(url) => swrFetcher(url, accessToken)
)
return (
<AssignmentSubmissionContext.Provider value={assignmentSubmission} >{children}</AssignmentSubmissionContext.Provider>
)
}
export function useAssignmentSubmission() {
return React.useContext(AssignmentSubmissionContext)
}
export default AssignmentSubmissionProvider

View file

@ -0,0 +1,94 @@
'use client'
import React, { createContext, useContext, useEffect, useReducer } from 'react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { getAssignmentTask } from '@services/courses/assignments'
import { useAssignments } from './AssignmentContext';
import { mutate } from 'swr';
import { getAPIUrl } from '@services/config/config';
interface State {
selectedAssignmentTaskUUID: string | null;
assignmentTask: Record<string, any>;
reloadTrigger: number;
}
interface Action {
type: string;
payload?: any;
}
const initialState: State = {
selectedAssignmentTaskUUID: null,
assignmentTask: {},
reloadTrigger: 0,
};
export const AssignmentsTaskContext = createContext<State | undefined>(undefined);
export const AssignmentsTaskDispatchContext = createContext<React.Dispatch<Action> | undefined>(undefined);
export function AssignmentsTaskProvider({ children }: { children: React.ReactNode }) {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
const assignment = useAssignments() as any
const [state, dispatch] = useReducer(assignmentstaskReducer, initialState);
async function fetchAssignmentTask(assignmentTaskUUID: string) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
if (res.success) {
dispatch({ type: 'setAssignmentTask', payload: res.data });
}
}
useEffect(() => {
if (state.selectedAssignmentTaskUUID) {
fetchAssignmentTask(state.selectedAssignmentTaskUUID);
mutate(`${getAPIUrl()}assignments/${assignment.assignment_object?.assignment_uuid}/tasks`);
}
}, [state.selectedAssignmentTaskUUID, state.reloadTrigger, assignment]);
return (
<AssignmentsTaskContext.Provider value={state}>
<AssignmentsTaskDispatchContext.Provider value={dispatch}>
{children}
</AssignmentsTaskDispatchContext.Provider>
</AssignmentsTaskContext.Provider>
);
}
export function useAssignmentsTask() {
const context = useContext(AssignmentsTaskContext);
if (context === undefined) {
throw new Error('useAssignmentsTask must be used within an AssignmentsTaskProvider');
}
return context;
}
export function useAssignmentsTaskDispatch() {
const context = useContext(AssignmentsTaskDispatchContext);
if (context === undefined) {
throw new Error('useAssignmentsTaskDispatch must be used within an AssignmentsTaskProvider');
}
return context;
}
function assignmentstaskReducer(state: State, action: Action): State {
switch (action.type) {
case 'setSelectedAssignmentTaskUUID':
return { ...state, selectedAssignmentTaskUUID: action.payload };
case 'setAssignmentTask':
return { ...state, assignmentTask: action.payload };
case 'reload':
return { ...state, reloadTrigger: state.reloadTrigger + 1 };
case 'SET_MULTIPLE_STATES':
return {
...state,
...action.payload,
};
default:
return state;
}
}

View file

@ -87,6 +87,7 @@ function NewActivityButton(props: NewActivityButtonProps) {
isDialogOpen={newActivityModal} isDialogOpen={newActivityModal}
onOpenChange={setNewActivityModal} onOpenChange={setNewActivityModal}
minHeight="no-min" minHeight="no-min"
minWidth='md'
addDefCloseButton={false} addDefCloseButton={false}
dialogContent={ dialogContent={
<NewActivityModal <NewActivityModal

View file

@ -3,9 +3,12 @@ import { getAPIUrl, getUriWithOrg } from '@services/config/config'
import { deleteActivity, updateActivity } from '@services/courses/activities' import { deleteActivity, updateActivity } from '@services/courses/activities'
import { revalidateTags } from '@services/utils/ts/requests' import { revalidateTags } from '@services/utils/ts/requests'
import { import {
Backpack,
Eye, Eye,
File, File,
FilePenLine, FilePenLine,
Globe,
Lock,
MoreVertical, MoreVertical,
Pencil, Pencil,
Save, Save,
@ -16,9 +19,12 @@ import {
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import React from 'react' import React, { useEffect, useState } from 'react'
import { Draggable } from 'react-beautiful-dnd' import { Draggable } from 'react-beautiful-dnd'
import { mutate } from 'swr' import { mutate } from 'swr'
import { deleteAssignmentUsingActivityUUID, getAssignmentFromActivityUUID } from '@services/courses/assignments'
import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext'
type ActivitiyElementProps = { type ActivitiyElementProps = {
orgslug: string orgslug: string
@ -45,12 +51,30 @@ function ActivityElement(props: ActivitiyElementProps) {
const activityUUID = props.activity.activity_uuid const activityUUID = props.activity.activity_uuid
async function deleteActivityUI() { 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) await deleteActivity(props.activity.activity_uuid, access_token)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`) mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug) await revalidateTags(['courses'], props.orgslug)
router.refresh() router.refresh()
} }
async function changePublicStatus() {
await updateActivity(
{
published: !props.activity.published,
},
props.activity.activity_uuid,
access_token
)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
}
async function updateActivityName(activityId: string) { async function updateActivityName(activityId: string) {
if ( if (
modifiedActivity?.activityId === activityId && modifiedActivity?.activityId === activityId &&
@ -60,8 +84,6 @@ function ActivityElement(props: ActivitiyElementProps) {
let modifiedActivityCopy = { let modifiedActivityCopy = {
name: modifiedActivity.activityName, name: modifiedActivity.activityName,
description: '', description: '',
type: props.activity.type,
content: props.activity.content,
} }
await updateActivity(modifiedActivityCopy, activityUUID, access_token) await updateActivity(modifiedActivityCopy, activityUUID, access_token)
@ -127,31 +149,27 @@ function ActivityElement(props: ActivitiyElementProps) {
className="text-neutral-400 hover:cursor-pointer" className="text-neutral-400 hover:cursor-pointer"
/> />
</div> </div>
{/* Edit and View Button */} {/* Edit and View Button */}
<div className="flex flex-row space-x-2"> <div className="flex flex-row space-x-2">
{props.activity.activity_type === 'TYPE_DYNAMIC' && ( <ActivityElementOptions activity={props.activity} />
<> {/* Publishing */}
<Link <div
href={ className={`hover:cursor-pointer p-1 px-3 border shadow-lg rounded-md font-bold text-xs flex items-center space-x-1 ${!props.activity.published
getUriWithOrg(props.orgslug, '') + ? 'bg-gradient-to-bl text-green-800 from-green-400/50 to-lime-200/80 border-green-600/10 shadow-green-900/10'
`/course/${props.course_uuid.replace( : 'bg-gradient-to-bl text-gray-800 from-gray-400/50 to-gray-200/80 border-gray-600/10 shadow-gray-900/10'
'course_', }`}
'' rel="noopener noreferrer"
)}/activity/${props.activity.activity_uuid.replace( onClick={() => changePublicStatus()}
'activity_',
''
)}/edit`
}
prefetch
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
target='_blank' // hotfix for an editor prosemirror bug
> >
<div className="text-sky-100 font-bold text-xs flex items-center space-x-1"> {!props.activity.published ? (
<FilePenLine size={12} /> <span>Edit Page</span> <Globe strokeWidth={2} size={12} className="text-green-600" />
</div> ) : (
</Link> <Lock strokeWidth={2} size={12} className="text-gray-600" />
</>
)} )}
<span>{!props.activity.published ? 'Publish' : 'Unpublish'}</span>
</div>
<Link <Link
href={ href={
getUriWithOrg(props.orgslug, '') + getUriWithOrg(props.orgslug, '') +
@ -164,10 +182,10 @@ function ActivityElement(props: ActivitiyElementProps) {
)}` )}`
} }
prefetch prefetch
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md font-bold text-xs flex items-center space-x-1" className=" hover:cursor-pointer p-1 px-3 bg-gradient-to-bl text-cyan-800 from-sky-400/50 to-cyan-200/80 border border-cyan-600/10 shadow-cyan-900/10 shadow-lg rounded-md font-bold text-xs flex items-center space-x-1"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<Eye strokeWidth={2} size={12} className="text-gray-600" /> <Eye strokeWidth={2} size={12} className="text-sky-600" />
<span>Preview</span> <span>Preview</span>
</Link> </Link>
</div> </div>
@ -221,6 +239,18 @@ const ActivityTypeIndicator = (props: { activityType: string }) => {
</div> </div>
</> </>
)} )}
{props.activityType === 'TYPE_ASSIGNMENT' && (
<>
<div className="flex space-x-2 items-center">
<div className="w-[30px]">
<Backpack size={16} />{' '}
</div>
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">
Assignment
</div>{' '}
</div>
</>
)}
{props.activityType === 'TYPE_DYNAMIC' && ( {props.activityType === 'TYPE_DYNAMIC' && (
<> <>
<div className="flex space-x-2 items-center"> <div className="flex space-x-2 items-center">
@ -234,4 +264,75 @@ const ActivityTypeIndicator = (props: { activityType: string }) => {
</div> </div>
) )
} }
const ActivityElementOptions = ({ activity }: any) => {
const [assignmentUUID, setAssignmentUUID] = useState('');
const org = useOrg() as any;
const course = useCourse() as any;
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
async function getAssignmentUUIDFromActivityUUID(activityUUID: string) {
const activity = await getAssignmentFromActivityUUID(activityUUID, access_token);
if (activity) {
return activity.data.assignment_uuid;
}
}
const fetchAssignmentUUID = async () => {
if (activity.activity_type === 'TYPE_ASSIGNMENT') {
const assignment_uuid = await getAssignmentUUIDFromActivityUUID(activity.activity_uuid);
setAssignmentUUID(assignment_uuid.replace('assignment_', ''));
}
};
useEffect(() => {
fetchAssignmentUUID();
}, [activity, course]);
return (
<>
{activity.activity_type === 'TYPE_DYNAMIC' && (
<>
<Link
href={
getUriWithOrg(org.slug, '') +
`/course/${course?.courseStructure.course_uuid.replace(
'course_',
''
)}/activity/${activity.activity_uuid.replace(
'activity_',
''
)}/edit`
}
prefetch
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
target='_blank' // hotfix for an editor prosemirror bug
>
<div className="text-sky-100 font-bold text-xs flex items-center space-x-1">
<FilePenLine size={12} /> <span>Edit Page</span>
</div>
</Link>
</>
)}
{activity.activity_type === 'TYPE_ASSIGNMENT' && assignmentUUID && (
<>
<Link
href={
getUriWithOrg(org.slug, '') +
`/dash/assignments/${assignmentUUID}`
}
prefetch
className=" hover:cursor-pointer p-1 px-3 bg-teal-700 rounded-md items-center"
>
<div className="text-sky-100 font-bold text-xs flex items-center space-x-1">
<FilePenLine size={12} /> <span>Edit Assignment</span>
</div>
</Link>
</>
)}
</>
);
};
export default ActivityElement export default ActivityElement

View file

@ -1,15 +1,16 @@
import { useCourse } from '@components/Contexts/CourseContext' 'use client';
import { Book, ChevronRight, School, User, Users } from 'lucide-react' import { useOrg } from '@components/Contexts/OrgContext';
import { Backpack, Book, ChevronRight, School, User, Users } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
import React from 'react' import React from 'react'
type BreadCrumbsProps = { type BreadCrumbsProps = {
type: 'courses' | 'user' | 'users' | 'org' | 'orgusers' type: 'courses' | 'user' | 'users' | 'org' | 'orgusers' | 'assignments'
last_breadcrumb?: string last_breadcrumb?: string
} }
function BreadCrumbs(props: BreadCrumbsProps) { function BreadCrumbs(props: BreadCrumbsProps) {
const course = useCourse() as any const org = useOrg() as any
return ( return (
<div> <div>
@ -25,6 +26,15 @@ function BreadCrumbs(props: BreadCrumbsProps) {
) : ( ) : (
'' ''
)} )}
{props.type == 'assignments' ? (
<div className="flex space-x-2 items-center">
{' '}
<Backpack className="text-gray" size={14}></Backpack>
<Link href="/dash/assignments">Assignments</Link>
</div>
) : (
''
)}
{props.type == 'user' ? ( {props.type == 'user' ? (
<div className="flex space-x-2 items-center"> <div className="flex space-x-2 items-center">
{' '} {' '}
@ -64,7 +74,6 @@ function BreadCrumbs(props: BreadCrumbsProps) {
</div> </div>
</div> </div>
</div> </div>
<div className="h-2"></div>
</div> </div>
) )
} }

View file

@ -3,7 +3,7 @@ import { useOrg } from '@components/Contexts/OrgContext'
import { signOut } from 'next-auth/react' import { signOut } from 'next-auth/react'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip' import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import LearnHouseDashboardLogo from '@public/dashLogo.png' import LearnHouseDashboardLogo from '@public/dashLogo.png'
import { BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react' import { Backpack, BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react'
import Image from 'next/image' import Image from 'next/image'
import Link from 'next/link' import Link from 'next/link'
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
@ -96,6 +96,14 @@ function LeftMenu() {
<BookCopy size={18} /> <BookCopy size={18} />
</Link> </Link>
</ToolTip> </ToolTip>
<ToolTip content={'Assignments'} slateBlack sideOffset={8} side="right">
<Link
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
href={`/dash/assignments`}
>
<Backpack size={18} />
</Link>
</ToolTip>
<ToolTip content={'Users'} slateBlack sideOffset={8} side="right"> <ToolTip content={'Users'} slateBlack sideOffset={8} side="right">
<Link <Link
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear" className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"

View file

@ -0,0 +1,126 @@
import { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext'
import { BookPlus, BookUser, EllipsisVertical, FileUp, Forward, InfoIcon, ListTodo, Save } from 'lucide-react'
import React, { useEffect } from 'react'
type AssignmentBoxProps = {
type: 'quiz' | 'file'
view?: 'teacher' | 'student' | 'grading' | 'custom-grading'
maxPoints?: number
currentPoints?: number
saveFC?: () => void
submitFC?: () => void
gradeFC?: () => void
gradeCustomFC?: (grade: number) => void
showSavingDisclaimer?: boolean
children: React.ReactNode
}
function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitFC, gradeFC, gradeCustomFC, showSavingDisclaimer, children }: AssignmentBoxProps) {
const [customGrade, setCustomGrade] = React.useState<number>(0)
const submission = useAssignmentSubmission() as any
useEffect(() => {
console.log(submission)
}
, [submission])
return (
<div className='flex flex-col px-6 py-4 nice-shadow rounded-md bg-slate-100/30'>
<div className='flex justify-between space-x-2 pb-2 text-slate-400 items-center'>
<div className='flex space-x-1 items-center'>
<div className='text-lg font-semibold'>
{type === 'quiz' &&
<div className='flex space-x-1.5 items-center'>
<ListTodo size={17} />
<p>Quiz</p>
</div>}
{type === 'file' &&
<div className='flex space-x-1.5 items-center'>
<FileUp size={17} />
<p>File Submission</p>
</div>}
</div>
<div className='flex items-center space-x-1'>
<EllipsisVertical size={15} />
</div>
{view === 'teacher' &&
<div className='flex bg-amber-200/20 text-xs rounded-full space-x-1 px-2 py-0.5 mx-auto font-bold outline items-center text-amber-600 outline-1 outline-amber-300/40'>
<BookUser size={12} />
<p>Teacher view</p>
</div>
}
{maxPoints &&
<div className='flex bg-emerald-200/20 text-xs rounded-full space-x-1 px-2 py-0.5 mx-auto font-bold outline items-center text-emerald-600 outline-1 outline-emerald-300/40'>
<BookPlus size={12} />
<p>{maxPoints} points</p>
</div>
}
</div>
<div className='flex px-1 py-1 rounded-md items-center'>
{showSavingDisclaimer &&
<div className='flex space-x-2 items-center font-semibold px-3 py-1 outline-dashed outline-red-200 text-red-400 mr-5 rounded-full'>
<InfoIcon size={14} />
<p className='text-xs'>Don't forget to save your progress</p>
</div>
}
{/* Teacher button */}
{view === 'teacher' &&
<div
onClick={() => saveFC && saveFC()}
className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-emerald-700 bg-emerald-300/20 hover:bg-emerald-300/10 hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-emerald-500/60'>
<Save size={14} />
<p className='text-xs font-semibold'>Save</p>
</div>
}
{/* Student button */}
{view === 'student' && submission && submission.length <= 0 &&
<div
onClick={() => submitFC && submitFC()}
className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-emerald-700 bg-emerald-300/20 hover:bg-emerald-300/10 hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-emerald-500/60'>
<Forward size={14} />
<p className='text-xs font-semibold'>Save your progress</p>
</div>
}
{/* Grading button */}
{view === 'grading' &&
<div
className='flex px-0.5 py-0.5 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'>
<p className='font-semibold px-2 text-xs text-orange-700'>Current points : {currentPoints}</p>
<div
onClick={() => gradeFC && gradeFC()}
className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2'>
<BookPlus size={14} />
<p className='text-xs font-semibold'>Grade</p>
</div>
</div>
}
{/* CustomGrading button */}
{view === 'custom-grading' && maxPoints &&
<div
className='flex px-0.5 py-0.5 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'>
<p className='font-semibold px-2 text-xs text-orange-700'>Current points : {currentPoints}</p>
<input
onChange={(e) => setCustomGrade(parseInt(e.target.value))}
placeholder={maxPoints.toString()} className='w-[100px] light-shadow text-sm py-0.5 outline outline-gray-200 rounded-lg px-2' type="number" />
<div
onClick={() => gradeCustomFC && gradeCustomFC(customGrade)}
className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2'>
<BookPlus size={14} />
<p className='text-xs font-semibold'>Grade</p>
</div>
</div>
}
</div>
</div>
{children}
</div>
)
}
export default AssignmentBoxUI

View file

@ -0,0 +1,95 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { useAssignmentsTask } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useCourse } from '@components/Contexts/CourseContext';
import { useOrg } from '@components/Contexts/OrgContext';
import { getTaskRefFileDir } from '@services/media/media';
import TaskFileObject from 'app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject';
import TaskQuizObject from 'app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject'
import { Backpack, Calendar, Download, EllipsisVertical, Info } from 'lucide-react';
import Link from 'next/link';
import React, { useEffect } from 'react'
function AssignmentStudentActivity() {
const assignments = useAssignments() as any;
const course = useCourse() as any;
const org = useOrg() as any;
useEffect(() => {
}, [assignments, org])
return (
<div className='flex flex-col space-y-6'>
<div className='flex flex-row justify-center space-x-3 items-center '>
<div className='text-xs h-fit flex space-x-3 items-center '>
<div className='flex space-x-2 py-2 px-5 h-fit text-sm text-slate-700 bg-slate-100/5 rounded-full nice-shadow'>
<Backpack size={18} />
<p className='font-semibold'>Assignment</p>
</div>
</div>
<div>
<div className='flex space-x-2 items-center'>
<EllipsisVertical className='text-slate-400' size={18} />
<div className='flex space-x-2 items-center'>
<div className='flex space-x-2 text-xs items-center text-slate-400'>
<Calendar size={14} />
<p className=' font-semibold'>Due Date</p>
<p className=' font-semibold'>{assignments?.assignment_object?.due_date}</p>
</div>
</div>
</div>
</div>
</div>
<div className='w-full rounded-full bg-slate-500/5 nice-shadow h-[2px]'></div>
{assignments && assignments?.assignment_tasks?.sort((a: any, b: any) => a.id - b.id).map((task: any, index: number) => {
return (
<div className='flex flex-col space-y-2' key={task.assignment_task_uuid}>
<div className='flex justify-between py-2'>
<div className='flex space-x-2 font-semibold text-slate-800'>
<p>Task {index + 1} : </p>
<p className='text-slate-500'>{task.description}</p>
</div>
<div className='flex space-x-2'>
<div
onClick={() => alert(task.hint)}
className='px-3 py-1 flex items-center nice-shadow bg-amber-50/40 text-amber-900 rounded-full space-x-2 cursor-pointer'>
<Info size={13} />
<p className='text-xs font-semibold'>Hint</p>
</div>
<Link
href={getTaskRefFileDir(
org?.org_uuid,
assignments?.course_object.course_uuid,
assignments?.activity_object.activity_uuid,
assignments?.assignment_object.assignment_uuid,
task.assignment_task_uuid,
task.reference_file
)}
target='_blank'
download={true}
className='px-3 py-1 flex items-center nice-shadow bg-cyan-50/40 text-cyan-900 rounded-full space-x-2 cursor-pointer'>
<Download size={13} />
<div className='flex items-center space-x-2'>
{task.reference_file && (
<span className='relative'>
<span className='absolute right-0 top-0 block h-2 w-2 rounded-full ring-2 ring-white bg-green-400'></span>
</span>
)}
<p className='text-xs font-semibold'>Reference Document</p>
</div>
</Link>
</div>
</div>
<div>
{task.assignment_type === 'QUIZ' && <TaskQuizObject key={task.assignment_task_uuid} view='student' assignmentTaskUUID={task.assignment_task_uuid} />}
{task.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject key={task.assignment_task_uuid} view='student' assignmentTaskUUID={task.assignment_task_uuid} />}
</div>
</div>
)
})}
</div>
)
}
export default AssignmentStudentActivity

View file

@ -2,11 +2,13 @@ import React, { useState } from 'react'
import DynamicPageActivityImage from 'public/activities_types/dynamic-page-activity.png' import DynamicPageActivityImage from 'public/activities_types/dynamic-page-activity.png'
import VideoPageActivityImage from 'public//activities_types/video-page-activity.png' import VideoPageActivityImage from 'public//activities_types/video-page-activity.png'
import DocumentPdfPageActivityImage from 'public//activities_types/documentpdf-page-activity.png' import DocumentPdfPageActivityImage from 'public//activities_types/documentpdf-page-activity.png'
import { styled } from '@stitches/react' import AssignmentActivityImage from 'public//activities_types/assignment-page-activity.png'
import DynamicCanvaModal from './NewActivityModal/DynamicCanva' import DynamicCanvaModal from './NewActivityModal/DynamicCanva'
import VideoModal from './NewActivityModal/Video' import VideoModal from './NewActivityModal/Video'
import Image from 'next/image' import Image from 'next/image'
import DocumentPdfModal from './NewActivityModal/DocumentPdf' import DocumentPdfModal from './NewActivityModal/DocumentPdf'
import Assignment from './NewActivityModal/Assignment'
function NewActivityModal({ function NewActivityModal({
closeModal, closeModal,
@ -19,43 +21,58 @@ function NewActivityModal({
const [selectedView, setSelectedView] = useState('home') const [selectedView, setSelectedView] = useState('home')
return ( return (
<div> <>
{selectedView === 'home' && ( {selectedView === 'home' && (
<ActivityChooserWrapper> <div className="flex flex-row space-x-2 justify-start mt-2.5 w-full">
<ActivityOption <ActivityOption
onClick={() => { onClick={() => {
setSelectedView('dynamic') setSelectedView('dynamic')
}} }}
> >
<ActivityTypeImage> <div className="h-20 rounded-lg m-0.5 flex flex-col items-center justify-end text-center bg-white hover:cursor-pointer">
<Image alt="Dynamic Page" src={DynamicPageActivityImage}></Image> <Image quality={100} alt="Dynamic Page" src={DynamicPageActivityImage}></Image>
</ActivityTypeImage> </div>
<ActivityTypeTitle>Dynamic Page</ActivityTypeTitle> <div className="flex text-sm h-5 font-medium text-gray-500 items-center justify-center text-center">
Dynamic Page
</div>
</ActivityOption> </ActivityOption>
<ActivityOption <ActivityOption
onClick={() => { onClick={() => {
setSelectedView('video') setSelectedView('video')
}} }}
> >
<ActivityTypeImage> <div className="h-20 rounded-lg m-0.5 flex flex-col items-center justify-end text-center bg-white hover:cursor-pointer">
<Image alt="Video Page" src={VideoPageActivityImage}></Image> <Image quality={100} alt="Video Page" src={VideoPageActivityImage}></Image>
</ActivityTypeImage> </div>
<ActivityTypeTitle>Video Page</ActivityTypeTitle> <div className="flex text-sm h-5 font-medium text-gray-500 items-center justify-center text-center">
Video
</div>
</ActivityOption> </ActivityOption>
<ActivityOption <ActivityOption
onClick={() => { onClick={() => {
setSelectedView('documentpdf') setSelectedView('documentpdf')
}} }}
> >
<ActivityTypeImage> <div className="h-20 rounded-lg m-0.5 flex flex-col items-center justify-end text-center bg-white hover:cursor-pointer">
<Image <Image quality={100} alt="Document PDF Page" src={DocumentPdfPageActivityImage}></Image>
alt="Document PDF Page" </div>
src={DocumentPdfPageActivityImage} <div className="flex text-sm h-5 font-medium text-gray-500 items-center justify-center text-center">
></Image> Document
</ActivityTypeImage> </div>
<ActivityTypeTitle>PDF Document Page</ActivityTypeTitle>
</ActivityOption> </ActivityOption>
</ActivityChooserWrapper> <ActivityOption
onClick={() => {
setSelectedView('assignments')
}}
>
<div className="h-20 rounded-lg m-0.5 flex flex-col items-center justify-end text-center bg-white hover:cursor-pointer">
<Image quality={100} alt="Assignment Page" src={AssignmentActivityImage}></Image>
</div>
<div className="flex text-sm h-5 font-medium text-gray-500 items-center justify-center text-center">
Assignments
</div>
</ActivityOption>
</div>
)} )}
{selectedView === 'dynamic' && ( {selectedView === 'dynamic' && (
@ -82,63 +99,26 @@ function NewActivityModal({
course={course} course={course}
/> />
)} )}
</div>
{selectedView === 'assignments' && (
<Assignment
submitActivity={submitActivity}
chapterId={chapterId}
course={course}
closeModal={closeModal}
/>)
}
</>
) )
} }
const ActivityChooserWrapper = styled('div', { const ActivityOption = ({ onClick, children }: any) => (
display: 'flex', <div
flexDirection: 'row', onClick={onClick}
justifyContent: 'start', className="w-full text-center rounded-xl bg-gray-100 border-4 border-gray-100 mx-auto hover:bg-gray-200 hover:border-gray-200 transition duration-200 ease-in-out cursor-pointer"
marginTop: 10, >
}) {children}
</div>
const ActivityOption = styled('div', { )
width: '180px',
textAlign: 'center',
borderRadius: 10,
background: '#F6F6F6',
border: '4px solid #F5F5F5',
margin: 'auto',
// hover
'&:hover': {
cursor: 'pointer',
background: '#ededed',
border: '4px solid #ededed',
transition: 'background 0.2s ease-in-out, border 0.2s ease-in-out',
},
})
const ActivityTypeImage = styled('div', {
height: 80,
borderRadius: 8,
margin: 2,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'end',
textAlign: 'center',
background: '#ffffff',
// hover
'&:hover': {
cursor: 'pointer',
},
})
const ActivityTypeTitle = styled('div', {
display: 'flex',
fontSize: 12,
height: '20px',
fontWeight: 500,
color: 'rgba(0, 0, 0, 0.38);',
// center text vertically
alignItems: 'center',
justifyContent: 'center',
textAlign: 'center',
})
export default NewActivityModal export default NewActivityModal

View file

@ -0,0 +1,151 @@
import React from 'react'
import FormLayout, {
ButtonBlack,
Flex,
FormField,
FormLabel,
FormMessage,
Input,
} from '@components/StyledElements/Form/Form'
import * as Form from '@radix-ui/react-form'
import { BarLoader } from 'react-spinners'
import { useOrg } from '@components/Contexts/OrgContext'
import { getAPIUrl } from '@services/config/config'
import { mutate } from 'swr'
import { createAssignment } from '@services/courses/assignments'
import { useLHSession } from '@components/Contexts/LHSessionContext'
import { createActivity } from '@services/courses/activities'
function NewAssignment({ submitActivity, chapterId, course, closeModal }: any) {
const org = useOrg() as any;
const session = useLHSession() as any
const [activityName, setActivityName] = React.useState('')
const [isSubmitting, setIsSubmitting] = React.useState(false)
const [activityDescription, setActivityDescription] = React.useState('')
const [dueDate, setDueDate] = React.useState('')
const [gradingType, setGradingType] = React.useState('ALPHABET')
const handleNameChange = (e: any) => {
setActivityName(e.target.value)
}
const handleDescriptionChange = (e: any) => {
setActivityDescription(e.target.value)
}
const handleDueDateChange = (e: any) => {
setDueDate(e.target.value)
}
const handleGradingTypeChange = (e: any) => {
setGradingType(e.target.value)
}
const handleSubmit = async (e: any) => {
e.preventDefault()
setIsSubmitting(true)
const activity = {
name: activityName,
chapter_id: chapterId,
activity_type: 'TYPE_ASSIGNMENT',
activity_sub_type: 'SUBTYPE_ASSIGNMENT_ANY',
published: false,
course_id: course?.courseStructure.id,
}
const activity_res = await createActivity(activity, chapterId, org?.id, session.data?.tokens?.access_token)
await createAssignment({
title: activityName,
description: activityDescription,
due_date: dueDate,
grading_type: gradingType,
course_id: course?.courseStructure.id,
org_id: org?.id,
chapter_id: chapterId,
activity_id: activity_res?.id,
}, session.data?.tokens?.access_token)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
setIsSubmitting(false)
closeModal()
}
return (
<FormLayout onSubmit={handleSubmit}>
<FormField name="assignment-activity-title">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Assignment Title</FormLabel>
<FormMessage match="valueMissing">
Please provide a name for your assignment
</FormMessage>
</Flex>
<Form.Control asChild>
<Input onChange={handleNameChange} type="text" required />
</Form.Control>
</FormField>
{/* Description */}
<FormField name="assignment-activity-description">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Assignment Description</FormLabel>
<FormMessage match="valueMissing">
Please provide a description for your assignment
</FormMessage>
</Flex>
<Form.Control asChild>
<Input onChange={handleDescriptionChange} type="text" required />
</Form.Control>
</FormField>
{/* Due date */}
<FormField name="assignment-activity-due-date">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Due Date</FormLabel>
<FormMessage match="valueMissing">
Please provide a due date for your assignment
</FormMessage>
</Flex>
<Form.Control asChild>
<Input onChange={handleDueDateChange} type="date" required />
</Form.Control>
</FormField>
{/* Grading type */}
<FormField name="assignment-activity-grading-type">
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
<FormLabel>Grading Type</FormLabel>
<FormMessage match="valueMissing">
Please provide a grading type for your assignment
</FormMessage>
</Flex>
<Form.Control asChild>
<select className='bg-gray-100/40 rounded-lg px-1 py-2 outline outline-1 outline-gray-100' onChange={handleGradingTypeChange} required>
<option value="ALPHABET">Alphabet</option>
<option value="NUMERIC">Numeric</option>
<option value="PERCENTAGE">Percentage</option>
</select>
</Form.Control>
</FormField>
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
<Form.Submit asChild>
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
{isSubmitting ? (
<BarLoader
cssOverride={{ borderRadius: 60 }}
width={60}
color="#ffffff"
/>
) : (
'Create activity'
)}
</ButtonBlack>
</Form.Submit>
</Flex>
</FormLayout>
)
}
export default NewAssignment

View file

@ -1,17 +1,12 @@
'use client' 'use client'
import FormLayout, { import FormLayout, {
ButtonBlack,
Flex,
FormField, FormField,
FormLabel,
FormLabelAndMessage, FormLabelAndMessage,
FormMessage,
Input, Input,
} from '@components/StyledElements/Form/Form' } from '@components/StyledElements/Form/Form'
import * as Form from '@radix-ui/react-form' import * as Form from '@radix-ui/react-form'
import { useOrg } from '@components/Contexts/OrgContext' import { useOrg } from '@components/Contexts/OrgContext'
import React from 'react' import React from 'react'
import { BarLoader } from 'react-spinners'
import { createUserGroup } from '@services/usergroups/usergroups' import { createUserGroup } from '@services/usergroups/usergroups'
import { mutate } from 'swr' import { mutate } from 'swr'
import { getAPIUrl } from '@services/config/config' import { getAPIUrl } from '@services/config/config'

View file

@ -6,7 +6,7 @@ import { getUriWithOrg } from '@services/config/config'
import { deleteCourseFromBackend } from '@services/courses/courses' import { deleteCourseFromBackend } from '@services/courses/courses'
import { getCourseThumbnailMediaDirectory } from '@services/media/media' import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { revalidateTags } from '@services/utils/ts/requests' import { revalidateTags } from '@services/utils/ts/requests'
import { BookMinus, FilePenLine, Settings, Settings2, X, EllipsisVertical } from 'lucide-react' import { BookMinus, FilePenLine, Settings2, EllipsisVertical } from 'lucide-react'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'

View file

@ -9,7 +9,7 @@ import OnBoardAI from '@public/onboarding/OnBoardAI.png';
import OnBoardUGs from '@public/onboarding/OnBoardUGs.png'; import OnBoardUGs from '@public/onboarding/OnBoardUGs.png';
import OnBoardAccess from '@public/onboarding/OnBoardAccess.png'; import OnBoardAccess from '@public/onboarding/OnBoardAccess.png';
import OnBoardMore from '@public/onboarding/OnBoardMore.png'; import OnBoardMore from '@public/onboarding/OnBoardMore.png';
import { ArrowRight, Book, Check, Globe, Info, PictureInPicture, Sparkle, Sprout, SquareUser, Users } from 'lucide-react'; import { ArrowRight, Book, Check, Globe, Info, PictureInPicture, Sparkle, Sprout, SquareUser } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { getUriWithOrg } from '@services/config/config'; import { getUriWithOrg } from '@services/config/config';
import { useOrg } from '@components/Contexts/OrgContext'; import { useOrg } from '@components/Contexts/OrgContext';

View file

@ -58,7 +58,7 @@ export const inputStyles = {
borderRadius: 4, borderRadius: 4,
fontSize: 15, fontSize: 15,
color: '#7c7c7c', color: '#7c7c7c',
background: '#F9FAFB', background: '#fbfdff',
boxShadow: `0 0 0 1px #edeeef`, boxShadow: `0 0 0 1px #edeeef`,
'&:hover': { boxShadow: `0 0 0 1px #edeeef` }, '&:hover': { boxShadow: `0 0 0 1px #edeeef` },
'&:focus': { boxShadow: `0 0 0 2px #edeeef` }, '&:focus': { boxShadow: `0 0 0 2px #edeeef` },

View file

@ -82,6 +82,7 @@ const contentClose = keyframes({
const DialogOverlay = styled(Dialog.Overlay, { const DialogOverlay = styled(Dialog.Overlay, {
backgroundColor: blackA.blackA9, backgroundColor: blackA.blackA9,
backdropFilter: 'blur(0.6px)',
position: 'fixed', position: 'fixed',
zIndex: 500, zIndex: 500,
inset: 0, inset: 0,

View file

@ -11,35 +11,35 @@
"lint:fix": "eslint --fix ." "lint:fix": "eslint --fix ."
}, },
"dependencies": { "dependencies": {
"@hocuspocus/provider": "^2.13.1", "@hocuspocus/provider": "^2.13.5",
"@radix-ui/colors": "^0.1.9", "@radix-ui/colors": "^0.1.9",
"@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-aspect-ratio": "^1.1.0",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-form": "^0.0.3", "@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-tooltip": "^1.1.2",
"@stitches/react": "^1.2.8", "@stitches/react": "^1.2.8",
"@tiptap/core": "^2.4.0", "@tiptap/core": "^2.5.8",
"@tiptap/extension-code-block-lowlight": "^2.4.0", "@tiptap/extension-code-block-lowlight": "^2.5.8",
"@tiptap/extension-collaboration": "^2.4.0", "@tiptap/extension-collaboration": "^2.5.8",
"@tiptap/extension-collaboration-cursor": "^2.4.0", "@tiptap/extension-collaboration-cursor": "^2.5.8",
"@tiptap/extension-youtube": "^2.4.0", "@tiptap/extension-youtube": "^2.5.8",
"@tiptap/html": "^2.4.0", "@tiptap/html": "^2.5.8",
"@tiptap/pm": "^2.4.0", "@tiptap/pm": "^2.5.8",
"@tiptap/react": "^2.4.0", "@tiptap/react": "^2.5.8",
"@tiptap/starter-kit": "^2.4.0", "@tiptap/starter-kit": "^2.5.8",
"@types/randomcolor": "^0.5.9", "@types/randomcolor": "^0.5.9",
"avvvatars-react": "^0.4.2", "avvvatars-react": "^0.4.2",
"dayjs": "^1.11.11", "dayjs": "^1.11.12",
"formik": "^2.4.6", "formik": "^2.4.6",
"framer-motion": "^10.18.0", "framer-motion": "^10.18.0",
"get-youtube-id": "^1.0.1", "get-youtube-id": "^1.0.1",
"highlight.js": "^11.9.0", "highlight.js": "^11.10.0",
"katex": "^0.16.10", "katex": "^0.16.11",
"lowlight": "^3.1.0", "lowlight": "^3.1.0",
"lucide-react": "^0.363.0", "lucide-react": "^0.424.0",
"next": "14.2.4", "next": "14.2.5",
"next-auth": "^4.24.7", "next-auth": "^4.24.7",
"nextjs-toploader": "^1.6.12", "nextjs-toploader": "^1.6.12",
"prosemirror-state": "^1.4.3", "prosemirror-state": "^1.4.3",
@ -54,15 +54,15 @@
"react-spinners": "^0.13.8", "react-spinners": "^0.13.8",
"react-youtube": "^10.1.0", "react-youtube": "^10.1.0",
"sharp": "^0.33.4", "sharp": "^0.33.4",
"styled-components": "^6.1.11", "styled-components": "^6.1.12",
"swr": "^2.2.5", "swr": "^2.2.5",
"tailwind-merge": "^2.3.0", "tailwind-merge": "^2.4.0",
"tailwind-scrollbar": "^3.1.0", "tailwind-scrollbar": "^3.1.0",
"uuid": "^9.0.1", "uuid": "^9.0.1",
"y-indexeddb": "^9.0.12", "y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.8", "y-prosemirror": "^1.2.11",
"y-webrtc": "^10.3.0", "y-webrtc": "^10.3.0",
"yjs": "^13.6.16" "yjs": "^13.6.18"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "20.12.2", "@types/node": "20.12.2",
@ -73,12 +73,12 @@
"@types/react-transition-group": "^4.4.10", "@types/react-transition-group": "^4.4.10",
"@types/styled-components": "^5.1.34", "@types/styled-components": "^5.1.34",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.20",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-next": "^14.2.3", "eslint-config-next": "^14.2.5",
"eslint-plugin-unused-imports": "^3.2.0", "eslint-plugin-unused-imports": "^3.2.0",
"postcss": "^8.4.38", "postcss": "^8.4.40",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.7",
"typescript": "5.4.4" "typescript": "5.4.4"
} }
} }

1837
apps/web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 941 B

View file

@ -74,12 +74,25 @@ export async function createExternalVideoActivity(
} }
export async function getActivity( export async function getActivity(
activity_uuid: any,
next: any,
access_token: string
) {
const result = await fetch(
`${getAPIUrl()}activities/${activity_uuid}`,
RequestBodyWithAuthHeader('GET', null, next, access_token)
)
const res = await result.json()
return res
}
export async function getActivityByID(
activity_id: any, activity_id: any,
next: any, next: any,
access_token: string access_token: string
) { ) {
const result = await fetch( const result = await fetch(
`${getAPIUrl()}activities/${activity_id}`, `${getAPIUrl()}activities/id/${activity_id}`,
RequestBodyWithAuthHeader('GET', null, next, access_token) RequestBodyWithAuthHeader('GET', null, next, access_token)
) )
const res = await result.json() const res = await result.json()

View file

@ -0,0 +1,292 @@
import { getAPIUrl } from '@services/config/config'
import {
RequestBodyFormWithAuthHeader,
RequestBodyWithAuthHeader,
getResponseMetadata,
} from '@services/utils/ts/requests'
export async function createAssignment(body: any, access_token: string) {
const result: any = await fetch(
`${getAPIUrl()}assignments/`,
RequestBodyWithAuthHeader('POST', body, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function updateAssignment(
body: any,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}`,
RequestBodyWithAuthHeader('PUT', body, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function getAssignmentFromActivityUUID(
activityUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/activity/${activityUUID}`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
// Delete an assignment
export async function deleteAssignment(
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}`,
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function deleteAssignmentUsingActivityUUID(
activityUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/activity/${activityUUID}`,
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
// tasks
export async function createAssignmentTask(
body: any,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks`,
RequestBodyWithAuthHeader('POST', body, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function getAssignmentTask(
assignmentTaskUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/task/${assignmentTaskUUID}`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function getAssignmentTaskSubmissionsMe(
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/submissions/me`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function getAssignmentTaskSubmissionsUser(
assignmentTaskUUID: string,
user_id: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/submissions/user/${user_id}`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function handleAssignmentTaskSubmission(
body: any,
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/submissions`,
RequestBodyWithAuthHeader('PUT', body, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function updateAssignmentTask(
body: any,
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}`,
RequestBodyWithAuthHeader('PUT', body, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function deleteAssignmentTask(
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}`,
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function updateReferenceFile(
file: any,
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
// Send file thumbnail as form data
const formData = new FormData()
if (file) {
formData.append('reference_file', file)
}
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/ref_file`,
RequestBodyFormWithAuthHeader('POST', formData, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function updateSubFile(
file: any,
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
// Send file thumbnail as form data
const formData = new FormData()
if (file) {
formData.append('sub_file', file)
}
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/sub_file`,
RequestBodyFormWithAuthHeader('POST', formData, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
// submissions
export async function submitAssignmentForGrading(
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/submissions`,
RequestBodyWithAuthHeader('POST', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function deleteUserSubmission(
user_id: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/submissions/${user_id}`,
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function putUserSubmission(
body: any,
user_id: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/submissions/${user_id}`,
RequestBodyWithAuthHeader('PUT', body, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function putFinalGrade(
user_id: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/submissions/${user_id}/grade`,
RequestBodyWithAuthHeader('POST', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function getFinalGrade(
user_id: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/submissions/${user_id}/grade`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function markActivityAsDoneForUser(
user_id: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/submissions/${user_id}/done`,
RequestBodyWithAuthHeader('POST', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function getAssignmentsFromACourse(
courseUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/course/${courseUUID}`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}

View file

@ -55,6 +55,15 @@ export async function getCourse(course_uuid: string, next: any, access_token:any
return res return res
} }
export async function getCourseById(course_id: string, next: any, access_token:any) {
const result: any = await fetch(
`${getAPIUrl()}courses/id/${course_id}`,
RequestBodyWithAuthHeader('GET', null, next,access_token)
)
const res = await errorHandling(result)
return res
}
export async function updateCourseThumbnail(course_uuid: any, thumbnail: any, access_token:any) { export async function updateCourseThumbnail(course_uuid: any, thumbnail: any, access_token:any) {
const formData = new FormData() const formData = new FormData()
formData.append('thumbnail', thumbnail) formData.append('thumbnail', thumbnail)

View file

@ -45,19 +45,44 @@ export function getActivityBlockMediaDirectory(
} }
} }
export function getTaskRefFileDir(
orgUUID: string,
courseUUID: string,
activityUUID: string,
assignmentUUID: string,
assignmentTaskUUID: string,
fileID : string
) {
let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseUUID}/activities/${activityUUID}/assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/${fileID}`
return uri
}
export function getTaskFileSubmissionDir(
orgUUID: string,
courseUUID: string,
activityUUID: string,
assignmentUUID: string,
assignmentTaskUUID: string,
fileSubID : string
) {
let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseUUID}/activities/${activityUUID}/assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/subs/${fileSubID}`
return uri
}
export function getActivityMediaDirectory( export function getActivityMediaDirectory(
orgUUID: string, orgUUID: string,
courseId: string, courseUUID: string,
activityId: string, activityUUID: string,
fileId: string, fileId: string,
activityType: string activityType: string
) { ) {
if (activityType == 'video') { if (activityType == 'video') {
let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseId}/activities/${activityId}/video/${fileId}` let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseUUID}/activities/${activityUUID}/video/${fileId}`
return uri return uri
} }
if (activityType == 'documentpdf') { if (activityType == 'documentpdf') {
let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseId}/activities/${activityId}/documentpdf/${fileId}` let uri = `${getMediaUrl()}content/orgs/${orgUUID}/courses/${courseUUID}/activities/${activityUUID}/documentpdf/${fileId}`
return uri return uri
} }
} }

View file

@ -9,6 +9,20 @@
@apply shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 @apply shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40
} }
.light-shadow {
@apply shadow-lg shadow-gray-300/15 outline outline-1 outline-neutral-200/30
}
.custom-dots-bg {
@apply bg-fixed;
background-image: radial-gradient(#4744446b 1px, transparent 1px),
radial-gradient(#4744446b 1px, transparent 1px);
background-position: 0 0, 25px 25px;
background-size: 50px 50px;
background-repeat: repeat;
}
html, html,
body { body {
padding: 0; padding: 0;