mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
commit
f4439a3368
82 changed files with 8076 additions and 1621 deletions
116
apps/api/alembic.ini
Normal file
116
apps/api/alembic.ini
Normal 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
110
apps/api/migrations/env.py
Normal 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()
|
||||
27
apps/api/migrations/script.py.mako
Normal file
27
apps/api/migrations/script.py.mako
Normal 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"}
|
||||
41
apps/api/migrations/versions/6295e05ff7d0_enum_updates.py
Normal file
41
apps/api/migrations/versions/6295e05ff7d0_enum_updates.py
Normal 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 ###
|
||||
|
|
@ -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 ###
|
||||
|
|
@ -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
1154
apps/api/poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -38,6 +38,9 @@ tiktoken = "^0.7.0"
|
|||
uvicorn = "0.30.1"
|
||||
typer = "^0.12.3"
|
||||
chromadb = "^0.5.3"
|
||||
alembic = "^1.13.2"
|
||||
alembic-postgresql-enum = "^1.2.0"
|
||||
sqlalchemy-utils = "^0.41.2"
|
||||
|
||||
[build-system]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
|
|
|||
|
|
@ -5,8 +5,12 @@ from sqlmodel import Field, SQLModel
|
|||
|
||||
class CollectionCourse(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
collection_id: int = Field(sa_column=Column(Integer, ForeignKey("collection.id", ondelete="CASCADE")))
|
||||
course_id: int = Field(sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE")))
|
||||
collection_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("collection.id", ondelete="CASCADE"))
|
||||
)
|
||||
course_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
|
||||
)
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
creation_date: str
|
||||
update_date: str
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ class ActivityTypeEnum(str, Enum):
|
|||
TYPE_VIDEO = "TYPE_VIDEO"
|
||||
TYPE_DOCUMENT = "TYPE_DOCUMENT"
|
||||
TYPE_DYNAMIC = "TYPE_DYNAMIC"
|
||||
TYPE_ASSESSMENT = "TYPE_ASSESSMENT"
|
||||
TYPE_ASSIGNMENT = "TYPE_ASSIGNMENT"
|
||||
TYPE_CUSTOM = "TYPE_CUSTOM"
|
||||
|
||||
|
||||
|
|
@ -21,19 +21,18 @@ class ActivitySubTypeEnum(str, Enum):
|
|||
# Document
|
||||
SUBTYPE_DOCUMENT_PDF = "SUBTYPE_DOCUMENT_PDF"
|
||||
SUBTYPE_DOCUMENT_DOC = "SUBTYPE_DOCUMENT_DOC"
|
||||
# Assessment
|
||||
SUBTYPE_ASSESSMENT_QUIZ = "SUBTYPE_ASSESSMENT_QUIZ"
|
||||
# Assignment
|
||||
SUBTYPE_ASSIGNMENT_ANY = "SUBTYPE_ASSIGNMENT_ANY"
|
||||
# Custom
|
||||
SUBTYPE_CUSTOM = "SUBTYPE_CUSTOM"
|
||||
|
||||
|
||||
class ActivityBase(SQLModel):
|
||||
name: str
|
||||
activity_type: ActivityTypeEnum = ActivityTypeEnum.TYPE_CUSTOM
|
||||
activity_sub_type: ActivitySubTypeEnum = ActivitySubTypeEnum.SUBTYPE_CUSTOM
|
||||
activity_type: ActivityTypeEnum
|
||||
activity_sub_type: ActivitySubTypeEnum
|
||||
content: dict = Field(default={}, sa_column=Column(JSON))
|
||||
published_version: int
|
||||
version: int
|
||||
published: bool = False
|
||||
|
||||
|
||||
class Activity(ActivityBase, table=True):
|
||||
|
|
@ -52,20 +51,24 @@ class Activity(ActivityBase, table=True):
|
|||
|
||||
class ActivityCreate(ActivityBase):
|
||||
chapter_id: int
|
||||
activity_type: ActivityTypeEnum = ActivityTypeEnum.TYPE_CUSTOM
|
||||
activity_sub_type: ActivitySubTypeEnum = ActivitySubTypeEnum.SUBTYPE_CUSTOM
|
||||
pass
|
||||
|
||||
|
||||
class ActivityUpdate(ActivityBase):
|
||||
name: Optional[str]
|
||||
content: dict = Field(default={}, sa_column=Column(JSON))
|
||||
activity_type: Optional[ActivityTypeEnum]
|
||||
activity_sub_type: Optional[ActivitySubTypeEnum]
|
||||
content: dict = Field(default={}, sa_column=Column(JSON))
|
||||
published_version: Optional[int]
|
||||
version: Optional[int]
|
||||
|
||||
|
||||
class ActivityRead(ActivityBase):
|
||||
id: int
|
||||
org_id: int
|
||||
course_id: int
|
||||
activity_uuid: str
|
||||
creation_date: str
|
||||
update_date: str
|
||||
308
apps/api/src/db/courses/assignments.py
Normal file
308
apps/api/src/db/courses/assignments.py
Normal 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")
|
||||
)
|
||||
)
|
||||
|
|
@ -35,7 +35,7 @@ class BlockCreate(BlockBase):
|
|||
|
||||
|
||||
class BlockRead(BlockBase):
|
||||
id: int
|
||||
id: int = Field(default=None, primary_key=True)
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
course_id: int = Field(default=None, foreign_key="course.id")
|
||||
chapter_id: int = Field(default=None, foreign_key="chapter.id")
|
||||
|
|
@ -2,7 +2,7 @@ from typing import Any, List, Optional
|
|||
from pydantic import BaseModel
|
||||
from sqlalchemy import Column, ForeignKey
|
||||
from sqlmodel import Field, SQLModel
|
||||
from src.db.activities import ActivityRead
|
||||
from src.db.courses.activities import ActivityRead
|
||||
|
||||
|
||||
class ChapterBase(SQLModel):
|
||||
|
|
@ -33,10 +33,10 @@ class ChapterCreate(ChapterBase):
|
|||
|
||||
class ChapterUpdate(ChapterBase):
|
||||
name: Optional[str]
|
||||
description: Optional[str]
|
||||
thumbnail_image: Optional[str]
|
||||
description: Optional[str] = ""
|
||||
thumbnail_image: Optional[str] = ""
|
||||
course_id: Optional[int]
|
||||
org_id: Optional[int]
|
||||
org_id: Optional[int] # type: ignore
|
||||
|
||||
|
||||
class ChapterRead(ChapterBase):
|
||||
|
|
@ -3,7 +3,7 @@ from sqlalchemy import Column, ForeignKey, Integer
|
|||
from sqlmodel import Field, SQLModel
|
||||
from src.db.users import UserRead
|
||||
from src.db.trails import TrailRead
|
||||
from src.db.chapters import ChapterRead
|
||||
from src.db.courses.chapters import ChapterRead
|
||||
|
||||
|
||||
class CourseBase(SQLModel):
|
||||
|
|
@ -6,12 +6,22 @@ from src.db.trail_runs import TrailRunRead
|
|||
|
||||
|
||||
class TrailBase(SQLModel):
|
||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
||||
user_id: int = Field(default=None, foreign_key="user.id")
|
||||
org_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
user_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
|
||||
)
|
||||
|
||||
|
||||
class Trail(TrailBase, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
org_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||
)
|
||||
user_id: int = Field(
|
||||
sa_column=Column(Integer, ForeignKey("user.id", ondelete="CASCADE"))
|
||||
)
|
||||
trail_uuid: str = ""
|
||||
creation_date: str = ""
|
||||
update_date: str = ""
|
||||
|
|
@ -20,6 +30,7 @@ class Trail(TrailBase, table=True):
|
|||
class TrailCreate(TrailBase):
|
||||
pass
|
||||
|
||||
|
||||
# TODO: This is a hacky way to get around the list[TrailRun] issue, find a better way to do this
|
||||
class TrailRead(BaseModel):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from src.routers import usergroups
|
||||
from src.routers import blocks, dev, trail, users, auth, orgs, roles
|
||||
from src.routers import dev, trail, users, auth, orgs, roles
|
||||
from src.routers.ai import ai
|
||||
from src.routers.courses import chapters, collections, courses, activities
|
||||
from src.routers.courses import chapters, collections, courses, assignments
|
||||
from src.routers.courses.activities import activities, blocks
|
||||
from src.routers.install import install
|
||||
from src.services.dev.dev import isDevModeEnabledOrRaise
|
||||
from src.services.install.install import isInstallModeEnabled
|
||||
|
|
@ -19,6 +20,7 @@ v1_router.include_router(orgs.router, prefix="/orgs", tags=["orgs"])
|
|||
v1_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
||||
v1_router.include_router(blocks.router, prefix="/blocks", tags=["blocks"])
|
||||
v1_router.include_router(courses.router, prefix="/courses", tags=["courses"])
|
||||
v1_router.include_router(assignments.router, prefix="/assignments", tags=["assignments"])
|
||||
v1_router.include_router(chapters.router, prefix="/chapters", tags=["chapters"])
|
||||
v1_router.include_router(activities.router, prefix="/activities", tags=["activities"])
|
||||
v1_router.include_router(collections.router, prefix="/collections", tags=["collections"])
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
from typing import List
|
||||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
||||
from src.db.activities import ActivityCreate, ActivityRead, ActivityUpdate
|
||||
from src.db.courses.activities import ActivityCreate, ActivityRead, ActivityUpdate
|
||||
from src.db.users import PublicUser
|
||||
from src.core.events.database import get_db_session
|
||||
from src.services.courses.activities.activities import (
|
||||
create_activity,
|
||||
get_activity,
|
||||
get_activities,
|
||||
get_activityby_id,
|
||||
update_activity,
|
||||
delete_activity,
|
||||
)
|
||||
|
|
@ -34,8 +35,22 @@ async def api_create_activity(
|
|||
return await create_activity(request, activity_object, current_user, db_session)
|
||||
|
||||
|
||||
@router.get("/{activity_id}")
|
||||
@router.get("/{activity_uuid}")
|
||||
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,
|
||||
activity_id: str,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
|
|
@ -44,11 +59,10 @@ async def api_get_activity(
|
|||
"""
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@router.get("/chapter/{chapter_id}")
|
||||
async def api_get_chapter_activities(
|
||||
request: Request,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
||||
from src.db.blocks import BlockRead
|
||||
from src.db.courses.blocks import BlockRead
|
||||
from src.core.events.database import get_db_session
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.blocks.block_types.imageBlock.imageBlock import (
|
||||
495
apps/api/src/routers/courses/assignments.py
Normal file
495
apps/api/src/routers/courses/assignments.py
Normal 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
|
||||
)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
from typing import List
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from src.core.events.database import get_db_session
|
||||
from src.db.chapters import (
|
||||
from src.db.courses.chapters import (
|
||||
ChapterCreate,
|
||||
ChapterRead,
|
||||
ChapterUpdate,
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ from typing import List
|
|||
from fastapi import APIRouter, Depends, UploadFile, Form, Request
|
||||
from sqlmodel import Session
|
||||
from src.core.events.database import get_db_session
|
||||
from src.db.course_updates import (
|
||||
from src.db.courses.course_updates import (
|
||||
CourseUpdateCreate,
|
||||
CourseUpdateRead,
|
||||
CourseUpdateUpdate,
|
||||
)
|
||||
from src.db.users import PublicUser
|
||||
from src.db.courses import (
|
||||
from src.db.courses.courses import (
|
||||
CourseCreate,
|
||||
CourseRead,
|
||||
CourseUpdate,
|
||||
|
|
@ -18,13 +18,19 @@ from src.security.auth import get_current_user
|
|||
from src.services.courses.courses import (
|
||||
create_course,
|
||||
get_course,
|
||||
get_course_by_id,
|
||||
get_course_meta,
|
||||
get_courses_orgslug,
|
||||
update_course,
|
||||
delete_course,
|
||||
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()
|
||||
|
|
@ -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")
|
||||
async def api_get_course_meta(
|
||||
request: Request,
|
||||
|
|
@ -154,7 +175,8 @@ async def api_delete_course(
|
|||
|
||||
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(
|
||||
request: Request,
|
||||
course_uuid: str,
|
||||
|
|
@ -165,7 +187,10 @@ async def api_get_course_updates(
|
|||
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")
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{course_uuid}/update/{courseupdate_uuid}")
|
||||
async def api_update_course_update(
|
||||
request: Request,
|
||||
|
|
@ -200,6 +226,7 @@ async def api_update_course_update(
|
|||
request, courseupdate_uuid, update_object, current_user, db_session
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{course_uuid}/update/{courseupdate_uuid}")
|
||||
async def api_delete_course_update(
|
||||
request: Request,
|
||||
|
|
@ -213,4 +240,3 @@ async def api_delete_course_update(
|
|||
"""
|
||||
|
||||
return await delete_update(request, courseupdate_uuid, current_user, db_session)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from fastapi import HTTPException, status, Request
|
|||
from sqlalchemy import null
|
||||
from sqlmodel import Session, select
|
||||
from src.db.collections import Collection
|
||||
from src.db.courses import Course
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
|
||||
from src.db.roles import Role
|
||||
from src.db.user_organizations import UserOrganization
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ from sqlmodel import Session, select
|
|||
from src.db.organization_config import OrganizationConfig
|
||||
from src.db.organizations import Organization
|
||||
from src.services.ai.utils import check_limits_and_config, count_ai_ask
|
||||
from src.db.courses import Course, CourseRead
|
||||
from src.db.courses.courses import Course, CourseRead
|
||||
from src.core.events.database import get_db_session
|
||||
from src.db.users import PublicUser
|
||||
from src.db.activities import Activity, ActivityRead
|
||||
from src.db.courses.activities import Activity, ActivityRead
|
||||
from src.security.auth import get_current_user
|
||||
from src.services.ai.base import ask_ai, get_chat_session_history
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ from uuid import uuid4
|
|||
from src.db.organizations import Organization
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from sqlmodel import Session, select
|
||||
from src.db.activities import Activity
|
||||
from src.db.blocks import Block, BlockRead, BlockTypeEnum
|
||||
from src.db.courses import Course
|
||||
from src.db.courses.activities import Activity
|
||||
from src.db.courses.blocks import Block, BlockRead, BlockTypeEnum
|
||||
from src.db.courses.courses import Course
|
||||
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||
from src.services.users.users import PublicUser
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ from uuid import uuid4
|
|||
from src.db.organizations import Organization
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from sqlmodel import Session, select
|
||||
from src.db.activities import Activity
|
||||
from src.db.blocks import Block, BlockRead, BlockTypeEnum
|
||||
from src.db.courses import Course
|
||||
from src.db.courses.activities import Activity
|
||||
from src.db.courses.blocks import Block, BlockRead, BlockTypeEnum
|
||||
from src.db.courses.courses import Course
|
||||
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||
|
||||
from src.services.users.users import PublicUser
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ from uuid import uuid4
|
|||
from src.db.organizations import Organization
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
from sqlmodel import Session, select
|
||||
from src.db.activities import Activity
|
||||
from src.db.blocks import Block, BlockRead, BlockTypeEnum
|
||||
from src.db.courses import Course
|
||||
from src.db.courses.activities import Activity
|
||||
from src.db.courses.blocks import Block, BlockRead, BlockTypeEnum
|
||||
from src.db.courses.courses import Course
|
||||
from src.services.blocks.utils.upload_files import upload_file_and_return_file_object
|
||||
|
||||
from src.services.users.users import PublicUser
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
from typing import Literal
|
||||
from sqlmodel import Session, select
|
||||
from src.db.courses import Course
|
||||
from src.db.chapters import Chapter
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.courses.chapters import Chapter
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
|
||||
from src.db.chapter_activities import ChapterActivity
|
||||
from src.db.courses.activities import ActivityCreate, Activity, ActivityRead, ActivityUpdate
|
||||
from src.db.courses.chapter_activities import ChapterActivity
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from fastapi import HTTPException, Request
|
||||
from uuid import uuid4
|
||||
|
|
@ -58,7 +58,7 @@ async def create_activity(
|
|||
statement = (
|
||||
select(ChapterActivity)
|
||||
.where(ChapterActivity.chapter_id == activity_object.chapter_id)
|
||||
.order_by(ChapterActivity.order)
|
||||
.order_by(ChapterActivity.order) # type: ignore
|
||||
)
|
||||
chapter_activities = db_session.exec(statement).all()
|
||||
|
||||
|
|
@ -116,6 +116,38 @@ async def get_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(
|
||||
request: Request,
|
||||
|
|
|
|||
1670
apps/api/src/services/courses/activities/assignments.py
Normal file
1670
apps/api/src/services/courses/activities/assignments.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,20 +1,20 @@
|
|||
from typing import Literal
|
||||
from src.db.courses import Course
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.organizations import Organization
|
||||
from sqlmodel import Session, select
|
||||
from src.security.rbac.rbac import (
|
||||
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.chapters import Chapter
|
||||
from src.db.activities import (
|
||||
from src.db.courses.chapters import Chapter
|
||||
from src.db.courses.activities import (
|
||||
Activity,
|
||||
ActivityRead,
|
||||
ActivitySubTypeEnum,
|
||||
ActivityTypeEnum,
|
||||
)
|
||||
from src.db.chapter_activities import ChapterActivity
|
||||
from src.db.course_chapters import CourseChapter
|
||||
from src.db.courses.chapter_activities import ChapterActivity
|
||||
from src.db.courses.course_chapters import CourseChapter
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.services.courses.activities.uploads.pdfs import upload_pdf
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
|
|
|
|||
23
apps/api/src/services/courses/activities/uploads/sub_file.py
Normal file
23
apps/api/src/services/courses/activities/uploads/sub_file.py
Normal 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"],
|
||||
)
|
||||
|
|
@ -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"],
|
||||
)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
from src.db.activities import ActivityRead
|
||||
from src.db.courses import CourseRead
|
||||
from src.db.courses.activities import ActivityRead
|
||||
from src.db.courses.courses import CourseRead
|
||||
|
||||
|
||||
def structure_activity_content_by_type(activity):
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from typing import Literal
|
||||
from src.db.courses import Course
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.organizations import Organization
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
|
@ -8,15 +8,15 @@ from src.security.rbac.rbac import (
|
|||
authorization_verify_based_on_roles_and_authorship_and_usergroups,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.chapters import Chapter
|
||||
from src.db.activities import (
|
||||
from src.db.courses.chapters import Chapter
|
||||
from src.db.courses.activities import (
|
||||
Activity,
|
||||
ActivityRead,
|
||||
ActivitySubTypeEnum,
|
||||
ActivityTypeEnum,
|
||||
)
|
||||
from src.db.chapter_activities import ChapterActivity
|
||||
from src.db.course_chapters import CourseChapter
|
||||
from src.db.courses.chapter_activities import ChapterActivity
|
||||
from src.db.courses.course_chapters import CourseChapter
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.services.courses.activities.uploads.videos import upload_video
|
||||
from fastapi import HTTPException, status, UploadFile, Request
|
||||
|
|
|
|||
|
|
@ -8,10 +8,10 @@ from src.security.rbac.rbac import (
|
|||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.db.course_chapters import CourseChapter
|
||||
from src.db.activities import Activity, ActivityRead
|
||||
from src.db.chapter_activities import ChapterActivity
|
||||
from src.db.chapters import (
|
||||
from src.db.courses.course_chapters import CourseChapter
|
||||
from src.db.courses.activities import Activity, ActivityRead
|
||||
from src.db.courses.chapter_activities import ChapterActivity
|
||||
from src.db.courses.chapters import (
|
||||
Chapter,
|
||||
ChapterCreate,
|
||||
ChapterRead,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from src.db.collections import (
|
|||
CollectionUpdate,
|
||||
)
|
||||
from src.db.collections_courses import CollectionCourse
|
||||
from src.db.courses import Course
|
||||
from src.db.courses.courses import Course
|
||||
from src.services.users.users import PublicUser
|
||||
from fastapi import HTTPException, status, Request
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from src.db.organizations import Organization
|
|||
from src.services.trail.trail import get_user_trail_with_orgid
|
||||
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
|
||||
from src.db.users import PublicUser, AnonymousUser, User, UserRead
|
||||
from src.db.courses import (
|
||||
from src.db.courses.courses import (
|
||||
Course,
|
||||
CourseCreate,
|
||||
CourseRead,
|
||||
|
|
@ -58,6 +58,38 @@ async def get_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(
|
||||
request: Request,
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ from typing import List
|
|||
from uuid import uuid4
|
||||
from fastapi import HTTPException, Request, status
|
||||
from sqlmodel import Session, col, select
|
||||
from src.db.course_updates import (
|
||||
from src.db.courses.course_updates import (
|
||||
CourseUpdate,
|
||||
CourseUpdateCreate,
|
||||
CourseUpdateRead,
|
||||
CourseUpdateUpdate,
|
||||
)
|
||||
from src.db.courses import Course
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.organizations import Organization
|
||||
from src.db.users import AnonymousUser, PublicUser
|
||||
from src.services.courses.courses import rbac_check
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
from src.db.chapter_activities import ChapterActivity
|
||||
from src.db.courses.chapter_activities import ChapterActivity
|
||||
from fastapi import HTTPException, Request, status
|
||||
from sqlmodel import Session, select
|
||||
from src.db.activities import Activity
|
||||
from src.db.courses import Course
|
||||
from src.db.courses.activities import Activity
|
||||
from src.db.courses.courses import Course
|
||||
from src.db.trail_runs import TrailRun, TrailRunRead
|
||||
from src.db.trail_steps import TrailStep
|
||||
from src.db.trails import Trail, TrailCreate, TrailRead
|
||||
|
|
@ -244,7 +244,7 @@ async def add_activity_to_trail(
|
|||
course_id=course.id if course.id is not None else 0,
|
||||
trail_id=trail.id if trail.id is not None else 0,
|
||||
org_id=course.org_id,
|
||||
complete=False,
|
||||
complete=True,
|
||||
teacher_verified=False,
|
||||
grade="",
|
||||
user_id=user.id,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,37 @@
|
|||
from typing import Literal
|
||||
from typing import Literal, Optional
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from config.config import get_learnhouse_config
|
||||
|
||||
|
||||
async def upload_content(
|
||||
directory: str,
|
||||
type_of_dir: Literal["orgs", "users"],
|
||||
uuid: str, # org_uuid or user_uuid
|
||||
uuid: str, # org_uuid or user_uuid
|
||||
file_binary: bytes,
|
||||
file_and_format: str,
|
||||
allowed_formats: Optional[list[str]] = None,
|
||||
):
|
||||
# Get Learnhouse Config
|
||||
learnhouse_config = get_learnhouse_config()
|
||||
|
||||
file_format = file_and_format.split(".")[-1].strip().lower()
|
||||
|
||||
# Get content delivery method
|
||||
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":
|
||||
# create folder for activity
|
||||
if not os.path.exists(f"content/{type_of_dir}/{uuid}/{directory}"):
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@ function HomeClient() {
|
|||
const { data: orgs } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, (url) => swrFetcher(url, access_token))
|
||||
|
||||
useEffect(() => {
|
||||
console.log(orgs)
|
||||
|
||||
|
||||
}, [session, orgs])
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
'use client'
|
||||
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 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 DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf'
|
||||
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
|
||||
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 { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||
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 AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext'
|
||||
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 {
|
||||
activityid: string
|
||||
|
|
@ -32,6 +41,12 @@ function ActivityClient(props: ActivityClientProps) {
|
|||
const activity = props.activity
|
||||
const course = props.course
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<CourseProvider courseuuid={course?.course_uuid}>
|
||||
|
|
@ -92,25 +127,52 @@ function ActivityClient(props: ActivityClientProps) {
|
|||
</h1>
|
||||
</div>
|
||||
<div className="flex space-x-1 items-center">
|
||||
<AuthenticatedClientElement checkMethod="authentication">
|
||||
<AIActivityAsk activity={activity} />
|
||||
<MoreVertical size={17} className="text-gray-300 " />
|
||||
<MarkStatus
|
||||
activity={activity}
|
||||
activityid={activityid}
|
||||
course={course}
|
||||
orgslug={orgslug}
|
||||
/>
|
||||
</AuthenticatedClientElement>
|
||||
{activity && activity.published == true && (
|
||||
<AuthenticatedClientElement checkMethod="authentication">
|
||||
{activity.activity_type != 'TYPE_ASSIGNMENT' &&
|
||||
<>
|
||||
<AIActivityAsk activity={activity} />
|
||||
<MoreVertical size={17} className="text-gray-300 " />
|
||||
<MarkStatus
|
||||
activity={activity}
|
||||
activityid={activityid}
|
||||
course={course}
|
||||
orgslug={orgslug}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
{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>
|
||||
</>
|
||||
}
|
||||
|
||||
</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 && activity.published == true && (
|
||||
<div
|
||||
className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC'
|
||||
? 'bg-white'
|
||||
: 'bg-zinc-950'
|
||||
}`}
|
||||
className={`p-7 drop-shadow-sm rounded-lg ${bgColor}`}
|
||||
>
|
||||
<div>
|
||||
{activity.activity_type == 'TYPE_DYNAMIC' && (
|
||||
|
|
@ -126,10 +188,23 @@ function ActivityClient(props: ActivityClientProps) {
|
|||
activity={activity}
|
||||
/>
|
||||
)}
|
||||
{activity.activity_type == 'TYPE_ASSIGNMENT' && (
|
||||
<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>
|
||||
|
|
@ -165,7 +240,7 @@ export function MarkStatus(props: {
|
|||
)
|
||||
if (run) {
|
||||
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 (
|
||||
<>
|
||||
{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>
|
||||
<Check size={17}></Check>
|
||||
</i>{' '}
|
||||
<i className="not-italic text-xs font-bold">Already completed</i>
|
||||
<i className="not-italic text-xs font-bold">Complete</i>
|
||||
</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}
|
||||
>
|
||||
{' '}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
getCourseThumbnailMediaDirectory,
|
||||
getUserAvatarMediaDirectory,
|
||||
} 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 UserAvatar from '@components/Objects/UserAvatar'
|
||||
import CourseUpdates from '@components/Objects/CourseUpdates/CourseUpdates'
|
||||
|
|
@ -185,6 +185,15 @@ const CourseClient = (props: any) => {
|
|||
/>
|
||||
</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>
|
||||
<Link
|
||||
className="flex font-semibold grow pl-2 text-neutral-500"
|
||||
|
|
@ -262,6 +271,27 @@ const CourseClient = (props: any) => {
|
|||
</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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
175
apps/web/app/orgs/[orgslug]/dash/assignments/page.tsx
Normal file
175
apps/web/app/orgs/[orgslug]/dash/assignments/page.tsx
Normal 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
|
||||
|
|
@ -82,7 +82,6 @@ function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
|||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -87,6 +87,7 @@ function NewActivityButton(props: NewActivityButtonProps) {
|
|||
isDialogOpen={newActivityModal}
|
||||
onOpenChange={setNewActivityModal}
|
||||
minHeight="no-min"
|
||||
minWidth='md'
|
||||
addDefCloseButton={false}
|
||||
dialogContent={
|
||||
<NewActivityModal
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
|||
import { deleteActivity, updateActivity } from '@services/courses/activities'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import {
|
||||
Backpack,
|
||||
Eye,
|
||||
File,
|
||||
FilePenLine,
|
||||
Globe,
|
||||
Lock,
|
||||
MoreVertical,
|
||||
Pencil,
|
||||
Save,
|
||||
|
|
@ -16,9 +19,12 @@ import {
|
|||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Draggable } from 'react-beautiful-dnd'
|
||||
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 = {
|
||||
orgslug: string
|
||||
|
|
@ -45,12 +51,30 @@ function ActivityElement(props: ActivitiyElementProps) {
|
|||
const activityUUID = props.activity.activity_uuid
|
||||
|
||||
async function deleteActivityUI() {
|
||||
// Assignments
|
||||
if (props.activity.activity_type === 'TYPE_ASSIGNMENT') {
|
||||
await deleteAssignmentUsingActivityUUID(props.activity.activity_uuid, access_token)
|
||||
}
|
||||
|
||||
await deleteActivity(props.activity.activity_uuid, access_token)
|
||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
|
||||
await revalidateTags(['courses'], props.orgslug)
|
||||
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) {
|
||||
if (
|
||||
modifiedActivity?.activityId === activityId &&
|
||||
|
|
@ -60,8 +84,6 @@ function ActivityElement(props: ActivitiyElementProps) {
|
|||
let modifiedActivityCopy = {
|
||||
name: modifiedActivity.activityName,
|
||||
description: '',
|
||||
type: props.activity.type,
|
||||
content: props.activity.content,
|
||||
}
|
||||
|
||||
await updateActivity(modifiedActivityCopy, activityUUID, access_token)
|
||||
|
|
@ -127,31 +149,27 @@ function ActivityElement(props: ActivitiyElementProps) {
|
|||
className="text-neutral-400 hover:cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Edit and View Button */}
|
||||
<div className="flex flex-row space-x-2">
|
||||
{props.activity.activity_type === 'TYPE_DYNAMIC' && (
|
||||
<>
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(props.orgslug, '') +
|
||||
`/course/${props.course_uuid.replace(
|
||||
'course_',
|
||||
''
|
||||
)}/activity/${props.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>
|
||||
</>
|
||||
)}
|
||||
<ActivityElementOptions activity={props.activity} />
|
||||
{/* Publishing */}
|
||||
<div
|
||||
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
|
||||
? 'bg-gradient-to-bl text-green-800 from-green-400/50 to-lime-200/80 border-green-600/10 shadow-green-900/10'
|
||||
: 'bg-gradient-to-bl text-gray-800 from-gray-400/50 to-gray-200/80 border-gray-600/10 shadow-gray-900/10'
|
||||
}`}
|
||||
rel="noopener noreferrer"
|
||||
onClick={() => changePublicStatus()}
|
||||
>
|
||||
{!props.activity.published ? (
|
||||
<Globe strokeWidth={2} size={12} className="text-green-600" />
|
||||
) : (
|
||||
<Lock strokeWidth={2} size={12} className="text-gray-600" />
|
||||
)}
|
||||
<span>{!props.activity.published ? 'Publish' : 'Unpublish'}</span>
|
||||
</div>
|
||||
<Link
|
||||
href={
|
||||
getUriWithOrg(props.orgslug, '') +
|
||||
|
|
@ -164,10 +182,10 @@ function ActivityElement(props: ActivitiyElementProps) {
|
|||
)}`
|
||||
}
|
||||
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"
|
||||
>
|
||||
<Eye strokeWidth={2} size={12} className="text-gray-600" />
|
||||
<Eye strokeWidth={2} size={12} className="text-sky-600" />
|
||||
<span>Preview</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
|
@ -221,6 +239,18 @@ const ActivityTypeIndicator = (props: { activityType: string }) => {
|
|||
</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' && (
|
||||
<>
|
||||
<div className="flex space-x-2 items-center">
|
||||
|
|
@ -234,4 +264,75 @@ const ActivityTypeIndicator = (props: { activityType: string }) => {
|
|||
</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
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
import { useCourse } from '@components/Contexts/CourseContext'
|
||||
import { Book, ChevronRight, School, User, Users } from 'lucide-react'
|
||||
'use client';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import { Backpack, Book, ChevronRight, School, User, Users } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import React from 'react'
|
||||
|
||||
type BreadCrumbsProps = {
|
||||
type: 'courses' | 'user' | 'users' | 'org' | 'orgusers'
|
||||
type: 'courses' | 'user' | 'users' | 'org' | 'orgusers' | 'assignments'
|
||||
last_breadcrumb?: string
|
||||
}
|
||||
|
||||
function BreadCrumbs(props: BreadCrumbsProps) {
|
||||
const course = useCourse() as any
|
||||
const org = useOrg() as any
|
||||
|
||||
return (
|
||||
<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' ? (
|
||||
<div className="flex space-x-2 items-center">
|
||||
{' '}
|
||||
|
|
@ -64,7 +74,6 @@ function BreadCrumbs(props: BreadCrumbsProps) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-2"></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useOrg } from '@components/Contexts/OrgContext'
|
|||
import { signOut } from 'next-auth/react'
|
||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||
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 Link from 'next/link'
|
||||
import React, { useEffect } from 'react'
|
||||
|
|
@ -96,6 +96,14 @@ function LeftMenu() {
|
|||
<BookCopy size={18} />
|
||||
</Link>
|
||||
</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">
|
||||
<Link
|
||||
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -2,11 +2,13 @@ import React, { useState } from 'react'
|
|||
import DynamicPageActivityImage from 'public/activities_types/dynamic-page-activity.png'
|
||||
import VideoPageActivityImage from 'public//activities_types/video-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 VideoModal from './NewActivityModal/Video'
|
||||
import Image from 'next/image'
|
||||
import DocumentPdfModal from './NewActivityModal/DocumentPdf'
|
||||
import Assignment from './NewActivityModal/Assignment'
|
||||
|
||||
function NewActivityModal({
|
||||
closeModal,
|
||||
|
|
@ -19,43 +21,58 @@ function NewActivityModal({
|
|||
const [selectedView, setSelectedView] = useState('home')
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{selectedView === 'home' && (
|
||||
<ActivityChooserWrapper>
|
||||
<div className="flex flex-row space-x-2 justify-start mt-2.5 w-full">
|
||||
<ActivityOption
|
||||
onClick={() => {
|
||||
setSelectedView('dynamic')
|
||||
}}
|
||||
>
|
||||
<ActivityTypeImage>
|
||||
<Image alt="Dynamic Page" src={DynamicPageActivityImage}></Image>
|
||||
</ActivityTypeImage>
|
||||
<ActivityTypeTitle>Dynamic Page</ActivityTypeTitle>
|
||||
<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="Dynamic Page" src={DynamicPageActivityImage}></Image>
|
||||
</div>
|
||||
<div className="flex text-sm h-5 font-medium text-gray-500 items-center justify-center text-center">
|
||||
Dynamic Page
|
||||
</div>
|
||||
</ActivityOption>
|
||||
<ActivityOption
|
||||
onClick={() => {
|
||||
setSelectedView('video')
|
||||
}}
|
||||
>
|
||||
<ActivityTypeImage>
|
||||
<Image alt="Video Page" src={VideoPageActivityImage}></Image>
|
||||
</ActivityTypeImage>
|
||||
<ActivityTypeTitle>Video Page</ActivityTypeTitle>
|
||||
<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="Video Page" src={VideoPageActivityImage}></Image>
|
||||
</div>
|
||||
<div className="flex text-sm h-5 font-medium text-gray-500 items-center justify-center text-center">
|
||||
Video
|
||||
</div>
|
||||
</ActivityOption>
|
||||
<ActivityOption
|
||||
onClick={() => {
|
||||
setSelectedView('documentpdf')
|
||||
}}
|
||||
>
|
||||
<ActivityTypeImage>
|
||||
<Image
|
||||
alt="Document PDF Page"
|
||||
src={DocumentPdfPageActivityImage}
|
||||
></Image>
|
||||
</ActivityTypeImage>
|
||||
<ActivityTypeTitle>PDF Document Page</ActivityTypeTitle>
|
||||
<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="Document PDF Page" src={DocumentPdfPageActivityImage}></Image>
|
||||
</div>
|
||||
<div className="flex text-sm h-5 font-medium text-gray-500 items-center justify-center text-center">
|
||||
Document
|
||||
</div>
|
||||
</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' && (
|
||||
|
|
@ -82,63 +99,26 @@ function NewActivityModal({
|
|||
course={course}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedView === 'assignments' && (
|
||||
<Assignment
|
||||
submitActivity={submitActivity}
|
||||
chapterId={chapterId}
|
||||
course={course}
|
||||
closeModal={closeModal}
|
||||
/>)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ActivityChooserWrapper = styled('div', {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'start',
|
||||
marginTop: 10,
|
||||
})
|
||||
|
||||
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',
|
||||
})
|
||||
const ActivityOption = ({ onClick, children }: any) => (
|
||||
<div
|
||||
onClick={onClick}
|
||||
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"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default NewActivityModal
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -1,17 +1,12 @@
|
|||
'use client'
|
||||
import FormLayout, {
|
||||
ButtonBlack,
|
||||
Flex,
|
||||
FormField,
|
||||
FormLabel,
|
||||
FormLabelAndMessage,
|
||||
FormMessage,
|
||||
Input,
|
||||
} from '@components/StyledElements/Form/Form'
|
||||
import * as Form from '@radix-ui/react-form'
|
||||
import { useOrg } from '@components/Contexts/OrgContext'
|
||||
import React from 'react'
|
||||
import { BarLoader } from 'react-spinners'
|
||||
import { createUserGroup } from '@services/usergroups/usergroups'
|
||||
import { mutate } from 'swr'
|
||||
import { getAPIUrl } from '@services/config/config'
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { getUriWithOrg } from '@services/config/config'
|
|||
import { deleteCourseFromBackend } from '@services/courses/courses'
|
||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||
import { revalidateTags } from '@services/utils/ts/requests'
|
||||
import { BookMinus, FilePenLine, Settings, Settings2, X, EllipsisVertical } from 'lucide-react'
|
||||
import { BookMinus, FilePenLine, Settings2, EllipsisVertical } from 'lucide-react'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/navigation'
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import OnBoardAI from '@public/onboarding/OnBoardAI.png';
|
|||
import OnBoardUGs from '@public/onboarding/OnBoardUGs.png';
|
||||
import OnBoardAccess from '@public/onboarding/OnBoardAccess.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 { getUriWithOrg } from '@services/config/config';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ export const inputStyles = {
|
|||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
color: '#7c7c7c',
|
||||
background: '#F9FAFB',
|
||||
background: '#fbfdff',
|
||||
boxShadow: `0 0 0 1px #edeeef`,
|
||||
'&:hover': { boxShadow: `0 0 0 1px #edeeef` },
|
||||
'&:focus': { boxShadow: `0 0 0 2px #edeeef` },
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ const contentClose = keyframes({
|
|||
|
||||
const DialogOverlay = styled(Dialog.Overlay, {
|
||||
backgroundColor: blackA.blackA9,
|
||||
backdropFilter: 'blur(0.6px)',
|
||||
position: 'fixed',
|
||||
zIndex: 500,
|
||||
inset: 0,
|
||||
|
|
|
|||
|
|
@ -11,35 +11,35 @@
|
|||
"lint:fix": "eslint --fix ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@hocuspocus/provider": "^2.13.1",
|
||||
"@hocuspocus/provider": "^2.13.5",
|
||||
"@radix-ui/colors": "^0.1.9",
|
||||
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
||||
"@radix-ui/react-dialog": "^1.0.5",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.0",
|
||||
"@radix-ui/react-dialog": "^1.1.1",
|
||||
"@radix-ui/react-form": "^0.0.3",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "^1.1.2",
|
||||
"@stitches/react": "^1.2.8",
|
||||
"@tiptap/core": "^2.4.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.4.0",
|
||||
"@tiptap/extension-collaboration": "^2.4.0",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.4.0",
|
||||
"@tiptap/extension-youtube": "^2.4.0",
|
||||
"@tiptap/html": "^2.4.0",
|
||||
"@tiptap/pm": "^2.4.0",
|
||||
"@tiptap/react": "^2.4.0",
|
||||
"@tiptap/starter-kit": "^2.4.0",
|
||||
"@tiptap/core": "^2.5.8",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.5.8",
|
||||
"@tiptap/extension-collaboration": "^2.5.8",
|
||||
"@tiptap/extension-collaboration-cursor": "^2.5.8",
|
||||
"@tiptap/extension-youtube": "^2.5.8",
|
||||
"@tiptap/html": "^2.5.8",
|
||||
"@tiptap/pm": "^2.5.8",
|
||||
"@tiptap/react": "^2.5.8",
|
||||
"@tiptap/starter-kit": "^2.5.8",
|
||||
"@types/randomcolor": "^0.5.9",
|
||||
"avvvatars-react": "^0.4.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"dayjs": "^1.11.12",
|
||||
"formik": "^2.4.6",
|
||||
"framer-motion": "^10.18.0",
|
||||
"get-youtube-id": "^1.0.1",
|
||||
"highlight.js": "^11.9.0",
|
||||
"katex": "^0.16.10",
|
||||
"highlight.js": "^11.10.0",
|
||||
"katex": "^0.16.11",
|
||||
"lowlight": "^3.1.0",
|
||||
"lucide-react": "^0.363.0",
|
||||
"next": "14.2.4",
|
||||
"lucide-react": "^0.424.0",
|
||||
"next": "14.2.5",
|
||||
"next-auth": "^4.24.7",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"prosemirror-state": "^1.4.3",
|
||||
|
|
@ -54,15 +54,15 @@
|
|||
"react-spinners": "^0.13.8",
|
||||
"react-youtube": "^10.1.0",
|
||||
"sharp": "^0.33.4",
|
||||
"styled-components": "^6.1.11",
|
||||
"styled-components": "^6.1.12",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
"uuid": "^9.0.1",
|
||||
"y-indexeddb": "^9.0.12",
|
||||
"y-prosemirror": "^1.2.8",
|
||||
"y-prosemirror": "^1.2.11",
|
||||
"y-webrtc": "^10.3.0",
|
||||
"yjs": "^13.6.16"
|
||||
"yjs": "^13.6.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.12.2",
|
||||
|
|
@ -73,12 +73,12 @@
|
|||
"@types/react-transition-group": "^4.4.10",
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"eslint-config-next": "^14.2.5",
|
||||
"eslint-plugin-unused-imports": "^3.2.0",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"postcss": "^8.4.40",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "5.4.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1837
apps/web/pnpm-lock.yaml
generated
1837
apps/web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
BIN
apps/web/public/activities_types/assignment-page-activity.png
Normal file
BIN
apps/web/public/activities_types/assignment-page-activity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 941 B |
|
|
@ -74,12 +74,25 @@ export async function createExternalVideoActivity(
|
|||
}
|
||||
|
||||
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,
|
||||
next: any,
|
||||
access_token: string
|
||||
) {
|
||||
const result = await fetch(
|
||||
`${getAPIUrl()}activities/${activity_id}`,
|
||||
`${getAPIUrl()}activities/id/${activity_id}`,
|
||||
RequestBodyWithAuthHeader('GET', null, next, access_token)
|
||||
)
|
||||
const res = await result.json()
|
||||
|
|
|
|||
292
apps/web/services/courses/assignments.ts
Normal file
292
apps/web/services/courses/assignments.ts
Normal 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
|
||||
}
|
||||
|
|
@ -55,6 +55,15 @@ export async function getCourse(course_uuid: string, next: any, access_token:any
|
|||
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) {
|
||||
const formData = new FormData()
|
||||
formData.append('thumbnail', thumbnail)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
orgUUID: string,
|
||||
courseId: string,
|
||||
activityId: string,
|
||||
courseUUID: string,
|
||||
activityUUID: string,
|
||||
fileId: string,
|
||||
activityType: string
|
||||
) {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,20 @@
|
|||
@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,
|
||||
body {
|
||||
padding: 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue