mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
commit
1bc078bd0b
31 changed files with 3053 additions and 818 deletions
|
|
@ -0,0 +1,47 @@
|
||||||
|
"""Org IDs Cascades
|
||||||
|
|
||||||
|
Revision ID: 83b6d9d6f57a
|
||||||
|
Revises: cb2029aadc2d
|
||||||
|
Create Date: 2024-08-29 19:38:10.022100
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa # noqa: F401
|
||||||
|
import sqlmodel # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '83b6d9d6f57a'
|
||||||
|
down_revision: Union[str, None] = 'cb2029aadc2d'
|
||||||
|
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.drop_constraint('chapter_org_id_fkey', 'chapter', type_='foreignkey')
|
||||||
|
op.drop_constraint('chapteractivity_org_id_fkey', 'chapteractivity', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'chapteractivity', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.drop_constraint('collectioncourse_org_id_fkey', 'collectioncourse', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'collectioncourse', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.drop_constraint('coursechapter_org_id_fkey', 'coursechapter', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'coursechapter', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.drop_constraint('courseupdate_org_id_fkey', 'courseupdate', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'courseupdate', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_constraint(None, 'courseupdate', type_='foreignkey')
|
||||||
|
op.create_foreign_key('courseupdate_org_id_fkey', 'courseupdate', 'organization', ['org_id'], ['id'])
|
||||||
|
op.drop_constraint(None, 'coursechapter', type_='foreignkey')
|
||||||
|
op.create_foreign_key('coursechapter_org_id_fkey', 'coursechapter', 'organization', ['org_id'], ['id'])
|
||||||
|
op.drop_constraint(None, 'collectioncourse', type_='foreignkey')
|
||||||
|
op.create_foreign_key('collectioncourse_org_id_fkey', 'collectioncourse', 'organization', ['org_id'], ['id'])
|
||||||
|
op.drop_constraint(None, 'chapteractivity', type_='foreignkey')
|
||||||
|
op.create_foreign_key('chapteractivity_org_id_fkey', 'chapteractivity', 'organization', ['org_id'], ['id'])
|
||||||
|
op.create_foreign_key('chapter_org_id_fkey', 'chapter', 'organization', ['org_id'], ['id'])
|
||||||
|
# ### end Alembic commands ###
|
||||||
153
apps/api/migrations/versions/cb2029aadc2d_cloud_changes.py
Normal file
153
apps/api/migrations/versions/cb2029aadc2d_cloud_changes.py
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
"""Cloud Changes
|
||||||
|
|
||||||
|
Revision ID: cb2029aadc2d
|
||||||
|
Revises: d8bc71595932
|
||||||
|
Create Date: 2024-08-29 19:24:34.859544
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa # noqa: F401
|
||||||
|
import sqlmodel # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = 'cb2029aadc2d'
|
||||||
|
down_revision: Union[str, None] = 'd8bc71595932'
|
||||||
|
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.alter_column('activity', 'course_id',
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.drop_constraint('activity_org_id_fkey', 'activity', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'activity', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.drop_constraint('block_org_id_fkey', 'block', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'block', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.alter_column('collection', 'org_id',
|
||||||
|
existing_type=sa.INTEGER(),
|
||||||
|
type_=sa.BigInteger(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.drop_constraint('collection_org_id_fkey', 'collection', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'collection', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.alter_column('collectioncourse', 'collection_id',
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('collectioncourse', 'course_id',
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.drop_constraint('course_org_id_fkey', 'course', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'course', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.alter_column('coursechapter', 'course_id',
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('coursechapter', 'chapter_id',
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.drop_constraint('resourceauthor_user_id_fkey', 'resourceauthor', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'resourceauthor', 'user', ['user_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.drop_constraint('role_org_id_fkey', 'role', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'role', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.drop_constraint('trailrun_user_id_fkey', 'trailrun', type_='foreignkey')
|
||||||
|
op.drop_constraint('trailrun_course_id_fkey', 'trailrun', type_='foreignkey')
|
||||||
|
op.drop_constraint('trailrun_trail_id_fkey', 'trailrun', type_='foreignkey')
|
||||||
|
op.drop_constraint('trailrun_org_id_fkey', 'trailrun', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'trailrun', 'user', ['user_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.create_foreign_key(None, 'trailrun', 'course', ['course_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.create_foreign_key(None, 'trailrun', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.create_foreign_key(None, 'trailrun', 'trail', ['trail_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.alter_column('trailstep', 'trailrun_id',
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.drop_constraint('trailstep_activity_id_fkey', 'trailstep', type_='foreignkey')
|
||||||
|
op.drop_constraint('trailstep_org_id_fkey', 'trailstep', type_='foreignkey')
|
||||||
|
op.drop_constraint('trailstep_course_id_fkey', 'trailstep', type_='foreignkey')
|
||||||
|
op.drop_constraint('trailstep_user_id_fkey', 'trailstep', type_='foreignkey')
|
||||||
|
op.drop_constraint('trailstep_trail_id_fkey', 'trailstep', type_='foreignkey')
|
||||||
|
op.create_foreign_key(None, 'trailstep', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.create_foreign_key(None, 'trailstep', 'user', ['user_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.create_foreign_key(None, 'trailstep', 'trail', ['trail_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.create_foreign_key(None, 'trailstep', 'course', ['course_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.create_foreign_key(None, 'trailstep', 'activity', ['activity_id'], ['id'], ondelete='CASCADE')
|
||||||
|
op.alter_column('userorganization', 'org_id',
|
||||||
|
existing_type=sa.BIGINT(),
|
||||||
|
type_=sa.Integer(),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.alter_column('userorganization', 'org_id',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.drop_constraint(None, 'trailstep', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'trailstep', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'trailstep', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'trailstep', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'trailstep', type_='foreignkey')
|
||||||
|
op.create_foreign_key('trailstep_trail_id_fkey', 'trailstep', 'trail', ['trail_id'], ['id'])
|
||||||
|
op.create_foreign_key('trailstep_user_id_fkey', 'trailstep', 'user', ['user_id'], ['id'])
|
||||||
|
op.create_foreign_key('trailstep_course_id_fkey', 'trailstep', 'course', ['course_id'], ['id'])
|
||||||
|
op.create_foreign_key('trailstep_org_id_fkey', 'trailstep', 'organization', ['org_id'], ['id'])
|
||||||
|
op.create_foreign_key('trailstep_activity_id_fkey', 'trailstep', 'activity', ['activity_id'], ['id'])
|
||||||
|
op.alter_column('trailstep', 'trailrun_id',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.drop_constraint(None, 'trailrun', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'trailrun', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'trailrun', type_='foreignkey')
|
||||||
|
op.drop_constraint(None, 'trailrun', type_='foreignkey')
|
||||||
|
op.create_foreign_key('trailrun_org_id_fkey', 'trailrun', 'organization', ['org_id'], ['id'])
|
||||||
|
op.create_foreign_key('trailrun_trail_id_fkey', 'trailrun', 'trail', ['trail_id'], ['id'])
|
||||||
|
op.create_foreign_key('trailrun_course_id_fkey', 'trailrun', 'course', ['course_id'], ['id'])
|
||||||
|
op.create_foreign_key('trailrun_user_id_fkey', 'trailrun', 'user', ['user_id'], ['id'])
|
||||||
|
op.drop_constraint(None, 'role', type_='foreignkey')
|
||||||
|
op.create_foreign_key('role_org_id_fkey', 'role', 'organization', ['org_id'], ['id'])
|
||||||
|
op.drop_constraint(None, 'resourceauthor', type_='foreignkey')
|
||||||
|
op.create_foreign_key('resourceauthor_user_id_fkey', 'resourceauthor', 'user', ['user_id'], ['id'])
|
||||||
|
op.alter_column('coursechapter', 'chapter_id',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('coursechapter', 'course_id',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.drop_constraint(None, 'course', type_='foreignkey')
|
||||||
|
op.create_foreign_key('course_org_id_fkey', 'course', 'organization', ['org_id'], ['id'])
|
||||||
|
op.alter_column('collectioncourse', 'course_id',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.alter_column('collectioncourse', 'collection_id',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.drop_constraint(None, 'collection', type_='foreignkey')
|
||||||
|
op.create_foreign_key('collection_org_id_fkey', 'collection', 'organization', ['org_id'], ['id'])
|
||||||
|
op.alter_column('collection', 'org_id',
|
||||||
|
existing_type=sa.BigInteger(),
|
||||||
|
type_=sa.INTEGER(),
|
||||||
|
existing_nullable=True)
|
||||||
|
op.drop_constraint(None, 'block', type_='foreignkey')
|
||||||
|
op.create_foreign_key('block_org_id_fkey', 'block', 'organization', ['org_id'], ['id'])
|
||||||
|
op.drop_constraint(None, 'activity', type_='foreignkey')
|
||||||
|
op.create_foreign_key('activity_org_id_fkey', 'activity', 'organization', ['org_id'], ['id'])
|
||||||
|
op.alter_column('activity', 'course_id',
|
||||||
|
existing_type=sa.Integer(),
|
||||||
|
type_=sa.BIGINT(),
|
||||||
|
existing_nullable=True)
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
@ -11,6 +11,8 @@ class CollectionCourse(SQLModel, table=True):
|
||||||
course_id: int = Field(
|
course_id: int = Field(
|
||||||
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
|
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
|
||||||
)
|
)
|
||||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
org_id: int = Field(
|
||||||
|
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
creation_date: str
|
creation_date: str
|
||||||
update_date: str
|
update_date: str
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from sqlalchemy import BigInteger, Column, ForeignKey
|
from sqlalchemy import BigInteger, Column, ForeignKey, Integer
|
||||||
from sqlmodel import Field, SQLModel
|
from sqlmodel import Field, SQLModel
|
||||||
|
|
||||||
class ChapterActivity(SQLModel, table=True):
|
class ChapterActivity(SQLModel, table=True):
|
||||||
|
|
@ -8,6 +8,8 @@ class ChapterActivity(SQLModel, table=True):
|
||||||
chapter_id: int = Field(sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE")))
|
chapter_id: int = Field(sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE")))
|
||||||
activity_id: int = Field(sa_column=Column(BigInteger, ForeignKey("activity.id", ondelete="CASCADE")))
|
activity_id: int = Field(sa_column=Column(BigInteger, ForeignKey("activity.id", ondelete="CASCADE")))
|
||||||
course_id : int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
|
course_id : int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
|
||||||
org_id : int = Field(default=None, foreign_key="organization.id")
|
org_id: int = Field(
|
||||||
|
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
creation_date: str
|
creation_date: str
|
||||||
update_date: str
|
update_date: str
|
||||||
|
|
@ -9,7 +9,9 @@ class ChapterBase(SQLModel):
|
||||||
name: str
|
name: str
|
||||||
description: Optional[str] = ""
|
description: Optional[str] = ""
|
||||||
thumbnail_image: Optional[str] = ""
|
thumbnail_image: Optional[str] = ""
|
||||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
org_id: int = Field(
|
||||||
|
sa_column=Column("org_id", ForeignKey("organization.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
course_id: int = Field(
|
course_id: int = Field(
|
||||||
sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))
|
sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ class CourseChapter(SQLModel, table=True):
|
||||||
chapter_id: int = Field(
|
chapter_id: int = Field(
|
||||||
sa_column=Column(Integer, ForeignKey("chapter.id", ondelete="CASCADE"))
|
sa_column=Column(Integer, ForeignKey("chapter.id", ondelete="CASCADE"))
|
||||||
)
|
)
|
||||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
org_id: int = Field(
|
||||||
|
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
creation_date: str
|
creation_date: str
|
||||||
update_date: str
|
update_date: str
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,9 @@ class CourseUpdate(SQLModel, table=True):
|
||||||
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
|
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
|
||||||
)
|
)
|
||||||
linked_activity_uuids: Optional[str] = Field(default=None)
|
linked_activity_uuids: Optional[str] = Field(default=None)
|
||||||
org_id: int = Field(default=None, foreign_key="organization.id")
|
org_id: int = Field(
|
||||||
|
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
|
||||||
|
)
|
||||||
creation_date: str
|
creation_date: str
|
||||||
update_date: str
|
update_date: str
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ from src.services.orgs.orgs import (
|
||||||
get_organization,
|
get_organization,
|
||||||
get_organization_by_slug,
|
get_organization_by_slug,
|
||||||
get_orgs_by_user,
|
get_orgs_by_user,
|
||||||
|
get_orgs_by_user_admin,
|
||||||
update_org,
|
update_org,
|
||||||
update_org_logo,
|
update_org_logo,
|
||||||
update_org_signup_mechanism,
|
update_org_signup_mechanism,
|
||||||
|
|
@ -329,6 +330,22 @@ async def api_user_orgs(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/user_admin/page/{page}/limit/{limit}")
|
||||||
|
async def api_user_orgs_admin(
|
||||||
|
request: Request,
|
||||||
|
page: int,
|
||||||
|
limit: int,
|
||||||
|
current_user: PublicUser = Depends(get_current_user),
|
||||||
|
db_session: Session = Depends(get_db_session),
|
||||||
|
) -> List[OrganizationRead]:
|
||||||
|
"""
|
||||||
|
Get orgs by page and limit by current user
|
||||||
|
"""
|
||||||
|
return await get_orgs_by_user_admin(
|
||||||
|
request, db_session, str(current_user.id), page, limit
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{org_id}")
|
@router.put("/{org_id}")
|
||||||
async def api_update_org(
|
async def api_update_org(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,7 @@ def install_create_organization(org_object: OrganizationCreate, db_session: Sess
|
||||||
|
|
||||||
# Org Config
|
# Org Config
|
||||||
org_config = OrganizationConfigBase(
|
org_config = OrganizationConfigBase(
|
||||||
config_version="1.0",
|
config_version="1.1",
|
||||||
general=OrgGeneralConfig(
|
general=OrgGeneralConfig(
|
||||||
enabled=True,
|
enabled=True,
|
||||||
color="normal",
|
color="normal",
|
||||||
|
|
|
||||||
|
|
@ -377,7 +377,7 @@ def send_invite_email(
|
||||||
<body>
|
<body>
|
||||||
<p>Hello {email}</p>
|
<p>Hello {email}</p>
|
||||||
<p>You have been invited to {org.name} by @{user.username}. Your invite code is {invite['invite_code']}.</p>
|
<p>You have been invited to {org.name} by @{user.username}. Your invite code is {invite['invite_code']}.</p>
|
||||||
<p>Click <a href="{org.slug}.learnhouse.io/signup?inviteCode={invite['invite_code']}">here</a> to sign up.</p>
|
<p>Click <a href="{org.slug}.learnhouse.io/signup?orgslug={org.slug}&inviteCode={invite['invite_code']}">here</a> to sign up.</p>
|
||||||
<p>Thank you</p>
|
<p>Thank you</p>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ async def create_org(
|
||||||
db_session.refresh(user_org)
|
db_session.refresh(user_org)
|
||||||
|
|
||||||
org_config = org_config = OrganizationConfigBase(
|
org_config = org_config = OrganizationConfigBase(
|
||||||
config_version="1.0",
|
config_version="1.1å",
|
||||||
general=OrgGeneralConfig(
|
general=OrgGeneralConfig(
|
||||||
enabled=True,
|
enabled=True,
|
||||||
color="normal",
|
color="normal",
|
||||||
|
|
@ -179,10 +179,7 @@ async def create_org(
|
||||||
collaboration=CollaborationOrgConfig(enabled=True, limit=0),
|
collaboration=CollaborationOrgConfig(enabled=True, limit=0),
|
||||||
api=APIOrgConfig(enabled=True, limit=0),
|
api=APIOrgConfig(enabled=True, limit=0),
|
||||||
),
|
),
|
||||||
cloud=OrgCloudConfig(
|
cloud=OrgCloudConfig(plan="free", custom_domain=False),
|
||||||
plan='free',
|
|
||||||
custom_domain=False
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
org_config = json.loads(org_config.json())
|
org_config = json.loads(org_config.json())
|
||||||
|
|
@ -463,7 +460,7 @@ async def delete_org(
|
||||||
return {"detail": "Organization deleted"}
|
return {"detail": "Organization deleted"}
|
||||||
|
|
||||||
|
|
||||||
async def get_orgs_by_user(
|
async def get_orgs_by_user_admin(
|
||||||
request: Request,
|
request: Request,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
user_id: str,
|
user_id: str,
|
||||||
|
|
@ -507,6 +504,47 @@ async def get_orgs_by_user(
|
||||||
return orgsWithConfig
|
return orgsWithConfig
|
||||||
|
|
||||||
|
|
||||||
|
async def get_orgs_by_user(
|
||||||
|
request: Request,
|
||||||
|
db_session: Session,
|
||||||
|
user_id: str,
|
||||||
|
page: int = 1,
|
||||||
|
limit: int = 10,
|
||||||
|
) -> list[OrganizationRead]:
|
||||||
|
|
||||||
|
statement = (
|
||||||
|
select(Organization)
|
||||||
|
.join(UserOrganization)
|
||||||
|
.where(UserOrganization.user_id == user_id)
|
||||||
|
.offset((page - 1) * limit)
|
||||||
|
.limit(limit)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get organizations where the user is an admin
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
orgs = result.all()
|
||||||
|
|
||||||
|
orgsWithConfig = []
|
||||||
|
|
||||||
|
for org in orgs:
|
||||||
|
|
||||||
|
# Get org config
|
||||||
|
statement = select(OrganizationConfig).where(
|
||||||
|
OrganizationConfig.org_id == org.id
|
||||||
|
)
|
||||||
|
result = db_session.exec(statement)
|
||||||
|
|
||||||
|
org_config = result.first()
|
||||||
|
|
||||||
|
config = OrganizationConfig.model_validate(org_config) if org_config else {}
|
||||||
|
|
||||||
|
org = OrganizationRead(**org.model_dump(), config=config)
|
||||||
|
|
||||||
|
orgsWithConfig.append(org)
|
||||||
|
|
||||||
|
return orgsWithConfig
|
||||||
|
|
||||||
|
|
||||||
# Config related
|
# Config related
|
||||||
async def update_org_signup_mechanism(
|
async def update_org_signup_mechanism(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
|
||||||
2
apps/web/.gitignore
vendored
2
apps/web/.gitignore
vendored
|
|
@ -45,3 +45,5 @@ next.config.original.js
|
||||||
.sentryclirc
|
.sentryclirc
|
||||||
|
|
||||||
certificates
|
certificates
|
||||||
|
# Sentry Config File
|
||||||
|
.env.sentry-build-plugin
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ const LoginClient = (props: LoginClientProps) => {
|
||||||
</FormField>
|
</FormField>
|
||||||
<div>
|
<div>
|
||||||
<Link
|
<Link
|
||||||
href={getUriWithOrg(props.org.slug, '/forgot')}
|
href={{ pathname: getUriWithoutOrg('/forgot'), query: props.org.slug ? { orgslug: props.org.slug } : null }}
|
||||||
passHref
|
passHref
|
||||||
className="text-xs text-gray-500 hover:underline"
|
className="text-xs text-gray-500 hover:underline"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
23
apps/web/app/global-error.tsx
Normal file
23
apps/web/app/global-error.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as Sentry from "@sentry/nextjs";
|
||||||
|
import NextError from "next/error";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
|
||||||
|
useEffect(() => {
|
||||||
|
Sentry.captureException(error);
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
{/* `NextError` is the default Next.js error page component. Its type
|
||||||
|
definition requires a `statusCode` prop. However, since the App Router
|
||||||
|
does not expose status codes for errors, we simply pass 0 to render a
|
||||||
|
generic error message. */}
|
||||||
|
<NextError statusCode={0} />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
||||||
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
import { useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
||||||
import { useLHSession } from '@components/Contexts/LHSessionContext';
|
import { useLHSession } from '@components/Contexts/LHSessionContext';
|
||||||
import { useOrg } from '@components/Contexts/OrgContext';
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'
|
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ type QuizSchema = {
|
||||||
text: string;
|
text: string;
|
||||||
fileID: string;
|
fileID: string;
|
||||||
type: 'text' | 'image' | 'audio' | 'video';
|
type: 'text' | 'image' | 'audio' | 'video';
|
||||||
correct: boolean;
|
assigned_right_answer: boolean;
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -25,6 +25,7 @@ type QuizSubmitSchema = {
|
||||||
submissions: {
|
submissions: {
|
||||||
questionUUID: string;
|
questionUUID: string;
|
||||||
optionUUID: string;
|
optionUUID: string;
|
||||||
|
answer: boolean
|
||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -34,6 +35,12 @@ type TaskQuizObjectProps = {
|
||||||
assignmentTaskUUID?: string;
|
assignmentTaskUUID?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Submission = {
|
||||||
|
questionUUID: string;
|
||||||
|
optionUUID: string;
|
||||||
|
answer: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectProps) {
|
function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectProps) {
|
||||||
const session = useLHSession() as any;
|
const session = useLHSession() as any;
|
||||||
const access_token = session?.data?.tokens?.access_token;
|
const access_token = session?.data?.tokens?.access_token;
|
||||||
|
|
@ -44,7 +51,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
|
|
||||||
/* TEACHER VIEW CODE */
|
/* TEACHER VIEW CODE */
|
||||||
const [questions, setQuestions] = useState<QuizSchema[]>([
|
const [questions, setQuestions] = useState<QuizSchema[]>([
|
||||||
{ questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] },
|
{ questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', assigned_right_answer: false, optionUUID: 'option_' + uuidv4() }] },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleQuestionChange = (index: number, value: string) => {
|
const handleQuestionChange = (index: number, value: string) => {
|
||||||
|
|
@ -61,7 +68,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
|
|
||||||
const addOption = (qIndex: number) => {
|
const addOption = (qIndex: number) => {
|
||||||
const updatedQuestions = [...questions];
|
const updatedQuestions = [...questions];
|
||||||
updatedQuestions[qIndex].options.push({ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() });
|
updatedQuestions[qIndex].options.push({ text: '', fileID: '', type: 'text', assigned_right_answer: false, optionUUID: 'option_' + uuidv4() });
|
||||||
setQuestions(updatedQuestions);
|
setQuestions(updatedQuestions);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -72,7 +79,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
};
|
};
|
||||||
|
|
||||||
const addQuestion = () => {
|
const addQuestion = () => {
|
||||||
setQuestions([...questions, { questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] }]);
|
setQuestions([...questions, { questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', assigned_right_answer: false, optionUUID: 'option_' + uuidv4() }] }]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeQuestion = (qIndex: number) => {
|
const removeQuestion = (qIndex: number) => {
|
||||||
|
|
@ -81,12 +88,12 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
setQuestions(updatedQuestions);
|
setQuestions(updatedQuestions);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleCorrectOption = (qIndex: number, oIndex: number) => {
|
const toggleOption = (qIndex: number, oIndex: number) => {
|
||||||
const updatedQuestions = [...questions];
|
const updatedQuestions = [...questions];
|
||||||
// Find the option to toggle
|
// Find the option to toggle
|
||||||
const optionToToggle = updatedQuestions[qIndex].options[oIndex];
|
const optionToToggle = updatedQuestions[qIndex].options[oIndex];
|
||||||
// Toggle the 'correct' property of the option
|
// Toggle the 'correct' property of the option
|
||||||
optionToToggle.correct = !optionToToggle.correct;
|
optionToToggle.assigned_right_answer = !optionToToggle.assigned_right_answer;
|
||||||
setQuestions(updatedQuestions);
|
setQuestions(updatedQuestions);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -123,20 +130,24 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
|
|
||||||
async function chooseOption(qIndex: number, oIndex: number) {
|
async function chooseOption(qIndex: number, oIndex: number) {
|
||||||
const updatedSubmissions = [...userSubmissions.submissions];
|
const updatedSubmissions = [...userSubmissions.submissions];
|
||||||
const questionUUID = questions[qIndex].questionUUID;
|
const question = questions[qIndex];
|
||||||
const optionUUID = questions[qIndex].options[oIndex].optionUUID;
|
const option = question?.options[oIndex];
|
||||||
|
|
||||||
// Check if this question already has a submission with the selected option
|
if (!question || !option) return;
|
||||||
const existingSubmissionIndex = updatedSubmissions.findIndex(
|
|
||||||
|
const questionUUID = question.questionUUID;
|
||||||
|
const optionUUID = option.optionUUID;
|
||||||
|
|
||||||
|
if (!questionUUID || !optionUUID) return;
|
||||||
|
|
||||||
|
const submissionIndex = updatedSubmissions.findIndex(
|
||||||
(submission) => submission.questionUUID === questionUUID && submission.optionUUID === optionUUID
|
(submission) => submission.questionUUID === questionUUID && submission.optionUUID === optionUUID
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingSubmissionIndex === -1 && optionUUID && questionUUID) {
|
if (submissionIndex === -1) {
|
||||||
// If the selected option is not already chosen, add it to the submissions
|
updatedSubmissions.push({ questionUUID, optionUUID, answer: true });
|
||||||
updatedSubmissions.push({ questionUUID, optionUUID });
|
|
||||||
} else {
|
} else {
|
||||||
// If the selected option is already chosen, remove it from the submissions
|
updatedSubmissions[submissionIndex].answer = !updatedSubmissions[submissionIndex].answer;
|
||||||
updatedSubmissions.splice(existingSubmissionIndex, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setUserSubmissions({
|
setUserSubmissions({
|
||||||
|
|
@ -176,12 +187,34 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
|
|
||||||
|
|
||||||
const submitFC = async () => {
|
const submitFC = async () => {
|
||||||
|
// Ensure all questions and options have submissions
|
||||||
|
const updatedSubmissions: Submission[] = questions.flatMap(question => {
|
||||||
|
return question.options.map(option => {
|
||||||
|
const existingSubmission = userSubmissions.submissions.find(
|
||||||
|
submission => submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
|
||||||
|
);
|
||||||
|
|
||||||
|
return existingSubmission || {
|
||||||
|
questionUUID: question.questionUUID || '',
|
||||||
|
optionUUID: option.optionUUID || '',
|
||||||
|
answer: false // Mark unsubmitted options as false
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update userSubmissions with the complete set of submissions
|
||||||
|
const updatedUserSubmissions: QuizSubmitSchema = {
|
||||||
|
...userSubmissions,
|
||||||
|
submissions: updatedSubmissions
|
||||||
|
};
|
||||||
|
|
||||||
// Save the quiz to the server
|
// Save the quiz to the server
|
||||||
const values = {
|
const values = {
|
||||||
task_submission: userSubmissions,
|
task_submission: updatedUserSubmissions,
|
||||||
grade: 0,
|
grade: 0,
|
||||||
task_submission_grade_feedback: '',
|
task_submission_grade_feedback: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (assignmentTaskUUID) {
|
if (assignmentTaskUUID) {
|
||||||
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
|
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
|
||||||
if (res) {
|
if (res) {
|
||||||
|
|
@ -190,6 +223,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
});
|
});
|
||||||
toast.success('Task saved successfully');
|
toast.success('Task saved successfully');
|
||||||
setShowSavingDisclaimer(false);
|
setShowSavingDisclaimer(false);
|
||||||
|
setUserSubmissions(updatedUserSubmissions);
|
||||||
} else {
|
} else {
|
||||||
toast.error('Error saving task, please retry later.');
|
toast.error('Error saving task, please retry later.');
|
||||||
}
|
}
|
||||||
|
|
@ -214,30 +248,22 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
|
|
||||||
async function gradeFC() {
|
async function gradeFC() {
|
||||||
if (assignmentTaskUUID) {
|
if (assignmentTaskUUID) {
|
||||||
// Ensure maxPoints is defined
|
const maxPoints = assignmentTaskOutsideProvider?.max_grade_value || 100;
|
||||||
const maxPoints = assignmentTaskOutsideProvider?.max_grade_value || 100; // Default to 100 if not defined
|
const totalOptions = questions.reduce((total, question) => total + question.options.length, 0);
|
||||||
|
let correctAnswers = 0;
|
||||||
|
|
||||||
// Ensure userSubmissions.questions are set
|
questions.forEach((question) => {
|
||||||
const totalQuestions = questions.length;
|
question.options.forEach((option) => {
|
||||||
let correctQuestions = 0;
|
const submission = userSubmissions.submissions.find(
|
||||||
let incorrectQuestions = 0;
|
(sub) => sub.questionUUID === question.questionUUID && sub.optionUUID === option.optionUUID
|
||||||
|
);
|
||||||
userSubmissions.submissions.forEach((submission) => {
|
if (submission?.answer === option.assigned_right_answer) {
|
||||||
const question = questions.find((q) => q.questionUUID === submission.questionUUID);
|
correctAnswers++;
|
||||||
const option = question?.options.find((o) => o.optionUUID === submission.optionUUID);
|
}
|
||||||
if (option?.correct) {
|
});
|
||||||
correctQuestions++;
|
|
||||||
} else {
|
|
||||||
incorrectQuestions++;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate grade with penalties for incorrect answers
|
const finalGrade = Math.round((correctAnswers / totalOptions) * maxPoints);
|
||||||
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
|
// Save the grade to the server
|
||||||
const values = {
|
const values = {
|
||||||
|
|
@ -337,15 +363,15 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
{view === 'teacher' && (
|
{view === 'teacher' && (
|
||||||
<>
|
<>
|
||||||
<div
|
<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'
|
className={`w-fit flex-none flex text-xs px-2 py-0.5 space-x-1 items-center h-fit rounded-lg ${option.assigned_right_answer ? '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`}
|
} hover:bg-lime-300 text-sm transition-all ease-linear cursor-pointer`}
|
||||||
onClick={() => toggleCorrectOption(qIndex, oIndex)}
|
onClick={() => toggleOption(qIndex, oIndex)}
|
||||||
>
|
>
|
||||||
{option.correct ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
|
{option.assigned_right_answer ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
|
||||||
{option.correct ? (
|
{option.assigned_right_answer ? (
|
||||||
<p className="mx-auto font-bold text-xs">Correct</p>
|
<p className="mx-auto font-bold text-xs">True</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="mx-auto font-bold text-xs">Incorrect</p>
|
<p className="mx-auto font-bold text-xs">False</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|
@ -359,30 +385,38 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
{view === 'grading' && (
|
{view === 'grading' && (
|
||||||
<>
|
<>
|
||||||
<div
|
<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'
|
className={`w-fit flex-none flex text-xs px-2 py-0.5 space-x-1 items-center h-fit rounded-lg ${option.assigned_right_answer ? '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`}
|
} 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.assigned_right_answer ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
|
||||||
{option.correct ? (
|
{option.assigned_right_answer ? (
|
||||||
<p className="mx-auto font-bold text-xs">Marked as Correct</p>
|
<p className="mx-auto font-bold text-xs">Marked as True</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="mx-auto font-bold text-xs">Marked as Incorrect</p>
|
<p className="mx-auto font-bold text-xs">Marked as False</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{view === 'student' && (
|
{view === 'student' && (
|
||||||
<div className={`w-[20px] flex-none flex items-center h-[20px] rounded-lg ${userSubmissions.submissions.find(
|
<div
|
||||||
(submission) =>
|
className={`w-[20px] flex-none flex items-center h-[20px] rounded-lg ${
|
||||||
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
|
userSubmissions.submissions.find(
|
||||||
)
|
(submission) =>
|
||||||
? "bg-green-200/60 text-green-500 hover:bg-green-300" // Selected state colors
|
submission.questionUUID === question.questionUUID &&
|
||||||
: "bg-slate-200/60 text-slate-500 hover:bg-slate-300" // Default state colors
|
submission.optionUUID === option.optionUUID &&
|
||||||
} text-sm transition-all ease-linear cursor-pointer`}>
|
submission.answer
|
||||||
|
)
|
||||||
|
? "bg-green-200/60 text-green-500 hover:bg-green-300"
|
||||||
|
: "bg-slate-200/60 text-slate-500 hover:bg-slate-300"
|
||||||
|
} text-sm transition-all ease-linear cursor-pointer`}
|
||||||
|
onClick={() => chooseOption(qIndex, oIndex)}
|
||||||
|
>
|
||||||
{userSubmissions.submissions.find(
|
{userSubmissions.submissions.find(
|
||||||
(submission) =>
|
(submission) =>
|
||||||
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
|
submission.questionUUID === question.questionUUID &&
|
||||||
|
submission.optionUUID === option.optionUUID &&
|
||||||
|
submission.answer
|
||||||
) ? (
|
) ? (
|
||||||
<Check size={12} className="mx-auto" />
|
<Check size={12} className="mx-auto" />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -391,22 +425,30 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{view === 'grading' && (
|
{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
|
<div className={`w-[20px] flex-none flex items-center h-[20px] rounded-lg ${
|
||||||
)
|
userSubmissions.submissions.find(
|
||||||
? "bg-green-200/60 text-green-500 hover:bg-green-300" // Selected state colors
|
(submission) =>
|
||||||
: "bg-slate-200/60 text-slate-500 hover:bg-slate-300" // Default state colors
|
submission.questionUUID === question.questionUUID &&
|
||||||
} text-sm transition-all ease-linear cursor-pointer`}>
|
submission.optionUUID === option.optionUUID &&
|
||||||
{userSubmissions.submissions.find(
|
submission.answer
|
||||||
(submission) =>
|
)
|
||||||
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
|
? "bg-green-200/60 text-green-500"
|
||||||
) ? (
|
: "bg-slate-200/60 text-slate-500"
|
||||||
<Check size={12} className="mx-auto" />
|
} text-sm`}>
|
||||||
) : (
|
{userSubmissions.submissions.find(
|
||||||
<X size={12} className="mx-auto" />
|
(submission) =>
|
||||||
)}
|
submission.questionUUID === question.questionUUID &&
|
||||||
</div>
|
submission.optionUUID === option.optionUUID &&
|
||||||
|
submission.answer
|
||||||
|
) ? (
|
||||||
|
<Check size={12} className="mx-auto" />
|
||||||
|
) : (
|
||||||
|
<X size={12} className="mx-auto" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import Modal from '@components/StyledElements/Modal/Modal';
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config';
|
||||||
import { getUserAvatarMediaDirectory } from '@services/media/media';
|
import { getUserAvatarMediaDirectory } from '@services/media/media';
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
import { Loader, SendHorizonal, UserCheck, X } from 'lucide-react';
|
import { SendHorizonal, UserCheck, X } from 'lucide-react';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import EvaluateAssignment from './Modals/EvaluateAssignment';
|
import EvaluateAssignment from './Modals/EvaluateAssignment';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
||||||
import { Apple, ArrowRightFromLine, BookOpenCheck, Check, Download, Info, Medal, MoveRight, X } from 'lucide-react';
|
import { BookOpenCheck, Check, Download, Info, MoveRight, X } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import TaskQuizObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskQuizObject';
|
import TaskQuizObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskQuizObject';
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { getAPIUrl, getUriWithOrg } from '@services/config/config';
|
||||||
import { getAssignmentsFromACourse } from '@services/courses/assignments';
|
import { getAssignmentsFromACourse } from '@services/courses/assignments';
|
||||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
|
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests';
|
||||||
import { Book, EllipsisVertical, GalleryVertical, GalleryVerticalEnd, Info, Layers2, PenBox, UserRoundPen } from 'lucide-react';
|
import { EllipsisVertical, GalleryVerticalEnd, Info, Layers2, UserRoundPen } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
||||||
import { useAssignmentsTask } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
|
||||||
import { useCourse } from '@components/Contexts/CourseContext';
|
import { useCourse } from '@components/Contexts/CourseContext';
|
||||||
import { useOrg } from '@components/Contexts/OrgContext';
|
import { useOrg } from '@components/Contexts/OrgContext';
|
||||||
import { getTaskRefFileDir } from '@services/media/media';
|
import { getTaskRefFileDir } from '@services/media/media';
|
||||||
|
|
|
||||||
|
|
@ -32,23 +32,19 @@ function QuizBlockComponent(props: any) {
|
||||||
const isEditable = editorState.isEditable
|
const isEditable = editorState.isEditable
|
||||||
|
|
||||||
const handleAnswerClick = (question_id: string, answer_id: string) => {
|
const handleAnswerClick = (question_id: string, answer_id: string) => {
|
||||||
// if the quiz is submitted, do nothing
|
if (submitted) return;
|
||||||
if (submitted) {
|
|
||||||
return
|
const existingAnswerIndex = userAnswers.findIndex(
|
||||||
|
(answer: any) => answer.question_id === question_id && answer.answer_id === answer_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingAnswerIndex !== -1) {
|
||||||
|
// Remove the answer if it's already selected
|
||||||
|
setUserAnswers(userAnswers.filter((_, index) => index !== existingAnswerIndex));
|
||||||
|
} else {
|
||||||
|
// Add the answer
|
||||||
|
setUserAnswers([...userAnswers, { question_id, answer_id }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const userAnswer = {
|
|
||||||
question_id: question_id,
|
|
||||||
answer_id: answer_id,
|
|
||||||
}
|
|
||||||
const newAnswers = [...userAnswers, userAnswer]
|
|
||||||
|
|
||||||
// only accept one answer per question
|
|
||||||
const filteredAnswers = newAnswers.filter(
|
|
||||||
(answer: any) => answer.question_id !== question_id
|
|
||||||
)
|
|
||||||
|
|
||||||
setUserAnswers([...filteredAnswers, userAnswer])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshUserSubmission = () => {
|
const refreshUserSubmission = () => {
|
||||||
|
|
@ -57,38 +53,31 @@ function QuizBlockComponent(props: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUserSubmission = () => {
|
const handleUserSubmission = () => {
|
||||||
if (userAnswers.length === 0) {
|
setSubmitted(true);
|
||||||
setSubmissionMessage('Please answer at least one question!')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setSubmitted(true)
|
const correctAnswers = questions.every((question: Question) => {
|
||||||
|
const correctAnswers = question.answers.filter((answer: Answer) => answer.correct);
|
||||||
// check if all submitted answers are correct
|
const userAnswersForQuestion = userAnswers.filter(
|
||||||
const correctAnswers = questions.map((question: Question) => {
|
|
||||||
const correctAnswer: any = question.answers.find(
|
|
||||||
(answer: Answer) => answer.correct
|
|
||||||
)
|
|
||||||
const userAnswer = userAnswers.find(
|
|
||||||
(userAnswer: any) => userAnswer.question_id === question.question_id
|
(userAnswer: any) => userAnswer.question_id === question.question_id
|
||||||
)
|
);
|
||||||
if (correctAnswer.answer_id === userAnswer.answer_id) {
|
|
||||||
return true
|
// If no correct answers are set and user didn't select any, it's correct
|
||||||
} else {
|
if (correctAnswers.length === 0 && userAnswersForQuestion.length === 0) {
|
||||||
return false
|
return true;
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
// check if all answers are correct
|
// Check if user selected all correct answers and no incorrect ones
|
||||||
const allCorrect = correctAnswers.every(
|
return (
|
||||||
(answer: boolean) => answer === true
|
correctAnswers.length === userAnswersForQuestion.length &&
|
||||||
)
|
correctAnswers.every((correctAnswer: Answer) =>
|
||||||
|
userAnswersForQuestion.some(
|
||||||
|
(userAnswer: any) => userAnswer.answer_id === correctAnswer.answer_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
if (allCorrect) {
|
setSubmissionMessage(correctAnswers ? 'All answers are correct!' : 'Some answers are incorrect!');
|
||||||
setSubmissionMessage('All answers are correct!')
|
|
||||||
} else {
|
|
||||||
setSubmissionMessage('Some answers are incorrect!')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAnswerID = (answerIndex: number, questionId: string) => {
|
const getAnswerID = (answerIndex: number, questionId: string) => {
|
||||||
|
|
@ -204,19 +193,14 @@ function QuizBlockComponent(props: any) {
|
||||||
const markAnswerCorrect = (question_id: string, answer_id: string) => {
|
const markAnswerCorrect = (question_id: string, answer_id: string) => {
|
||||||
const newQuestions = questions.map((question: Question) => {
|
const newQuestions = questions.map((question: Question) => {
|
||||||
if (question.question_id === question_id) {
|
if (question.question_id === question_id) {
|
||||||
question.answers.map((answer: Answer) => {
|
question.answers = question.answers.map((answer: Answer) => ({
|
||||||
if (answer.answer_id === answer_id) {
|
...answer,
|
||||||
answer.correct = true
|
correct: answer.answer_id === answer_id ? !answer.correct : answer.correct,
|
||||||
} else {
|
}));
|
||||||
answer.correct = false
|
|
||||||
}
|
|
||||||
|
|
||||||
return answer
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return question
|
return question;
|
||||||
})
|
});
|
||||||
saveQuestions(newQuestions)
|
saveQuestions(newQuestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -308,29 +292,21 @@ function QuizBlockComponent(props: any) {
|
||||||
key={answer.answer_id}
|
key={answer.answer_id}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'outline outline-3 pr-2 shadow w-full flex items-center space-x-2 h-[30px] bg-opacity-50 hover:bg-opacity-100 hover:shadow-md rounded-s rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear',
|
'outline outline-3 pr-2 shadow w-full flex items-center space-x-2 h-[30px] bg-opacity-50 hover:bg-opacity-100 hover:shadow-md rounded-s rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear',
|
||||||
answer.correct && isEditable
|
answer.correct && isEditable ? 'outline-lime-300' : 'outline-white',
|
||||||
? 'outline-lime-300'
|
userAnswers.some(
|
||||||
: 'outline-white',
|
|
||||||
userAnswers.find(
|
|
||||||
(userAnswer: any) =>
|
(userAnswer: any) =>
|
||||||
userAnswer.question_id === question.question_id &&
|
userAnswer.question_id === question.question_id &&
|
||||||
userAnswer.answer_id === answer.answer_id &&
|
userAnswer.answer_id === answer.answer_id &&
|
||||||
!isEditable
|
!isEditable
|
||||||
)
|
) ? 'outline-slate-300' : '',
|
||||||
? 'outline-slate-300'
|
submitted && answer.correct ? 'outline-lime-300 text-lime' : '',
|
||||||
: '',
|
|
||||||
submitted && answer.correct
|
|
||||||
? 'outline-lime-300 text-lime'
|
|
||||||
: '',
|
|
||||||
submitted &&
|
submitted &&
|
||||||
!answer.correct &&
|
!answer.correct &&
|
||||||
userAnswers.find(
|
userAnswers.some(
|
||||||
(userAnswer: any) =>
|
(userAnswer: any) =>
|
||||||
userAnswer.question_id === question.question_id &&
|
userAnswer.question_id === question.question_id &&
|
||||||
userAnswer.answer_id === answer.answer_id
|
userAnswer.answer_id === answer.answer_id
|
||||||
)
|
) ? 'outline-red-400' : ''
|
||||||
? 'outline-red-400'
|
|
||||||
: ''
|
|
||||||
)}
|
)}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleAnswerClick(question.question_id, answer.answer_id)
|
handleAnswerClick(question.question_id, answer.answer_id)
|
||||||
|
|
@ -347,7 +323,7 @@ function QuizBlockComponent(props: any) {
|
||||||
: '',
|
: '',
|
||||||
submitted &&
|
submitted &&
|
||||||
!answer.correct &&
|
!answer.correct &&
|
||||||
userAnswers.find(
|
userAnswers.some(
|
||||||
(userAnswer: any) =>
|
(userAnswer: any) =>
|
||||||
userAnswer.question_id === question.question_id &&
|
userAnswer.question_id === question.question_id &&
|
||||||
userAnswer.answer_id === answer.answer_id
|
userAnswer.answer_id === answer.answer_id
|
||||||
|
|
|
||||||
9
apps/web/instrumentation.ts
Normal file
9
apps/web/instrumentation.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
export async function register() {
|
||||||
|
if (process.env.NEXT_RUNTIME === 'nodejs') {
|
||||||
|
await import('./sentry.server.config');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NEXT_RUNTIME === 'edge') {
|
||||||
|
await import('./sentry.edge.config');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,7 +37,7 @@ export default async function middleware(req: NextRequest) {
|
||||||
|
|
||||||
// Out of orgslug paths & rewrite
|
// Out of orgslug paths & rewrite
|
||||||
const standard_paths = ['/home']
|
const standard_paths = ['/home']
|
||||||
const auth_paths = ['/login', '/signup', '/reset']
|
const auth_paths = ['/login', '/signup', '/reset', '/forgot']
|
||||||
if (standard_paths.includes(pathname)) {
|
if (standard_paths.includes(pathname)) {
|
||||||
// Redirect to the same pathname with the original search params
|
// Redirect to the same pathname with the original search params
|
||||||
return NextResponse.rewrite(new URL(`${pathname}${search}`, req.url))
|
return NextResponse.rewrite(new URL(`${pathname}${search}`, req.url))
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,46 @@ const nextConfig = {
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig
|
||||||
|
|
||||||
|
|
||||||
|
// Injected content via Sentry wizard below
|
||||||
|
|
||||||
|
const { withSentryConfig } = require("@sentry/nextjs");
|
||||||
|
|
||||||
|
module.exports = withSentryConfig(
|
||||||
|
module.exports,
|
||||||
|
{
|
||||||
|
// For all available options, see:
|
||||||
|
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||||
|
|
||||||
|
org: "learnhouse",
|
||||||
|
project: "learnhouse-web",
|
||||||
|
|
||||||
|
// Only print logs for uploading source maps in CI
|
||||||
|
silent: !process.env.CI,
|
||||||
|
|
||||||
|
// For all available options, see:
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||||
|
|
||||||
|
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||||
|
widenClientFileUpload: true,
|
||||||
|
|
||||||
|
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
|
||||||
|
// This can increase your server load as well as your hosting bill.
|
||||||
|
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
|
||||||
|
// side errors will fail.
|
||||||
|
tunnelRoute: "/monitoring",
|
||||||
|
|
||||||
|
// Hides source maps from generated client bundles
|
||||||
|
hideSourceMaps: true,
|
||||||
|
|
||||||
|
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||||
|
disableLogger: true,
|
||||||
|
|
||||||
|
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
|
||||||
|
// See the following for more information:
|
||||||
|
// https://docs.sentry.io/product/crons/
|
||||||
|
// https://vercel.com/docs/cron-jobs
|
||||||
|
automaticVercelMonitors: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -19,19 +19,20 @@
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-switch": "^1.1.0",
|
"@radix-ui/react-switch": "^1.1.0",
|
||||||
"@radix-ui/react-tooltip": "^1.1.2",
|
"@radix-ui/react-tooltip": "^1.1.2",
|
||||||
|
"@sentry/nextjs": "^8.27.0",
|
||||||
"@stitches/react": "^1.2.8",
|
"@stitches/react": "^1.2.8",
|
||||||
"@tiptap/core": "^2.5.8",
|
"@tiptap/core": "^2.6.6",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.5.8",
|
"@tiptap/extension-code-block-lowlight": "^2.6.6",
|
||||||
"@tiptap/extension-collaboration": "^2.5.8",
|
"@tiptap/extension-collaboration": "^2.6.6",
|
||||||
"@tiptap/extension-collaboration-cursor": "^2.5.8",
|
"@tiptap/extension-collaboration-cursor": "^2.6.6",
|
||||||
"@tiptap/extension-youtube": "^2.5.8",
|
"@tiptap/extension-youtube": "^2.6.6",
|
||||||
"@tiptap/html": "^2.5.8",
|
"@tiptap/html": "^2.6.6",
|
||||||
"@tiptap/pm": "^2.5.8",
|
"@tiptap/pm": "^2.6.6",
|
||||||
"@tiptap/react": "^2.5.8",
|
"@tiptap/react": "^2.6.6",
|
||||||
"@tiptap/starter-kit": "^2.5.8",
|
"@tiptap/starter-kit": "^2.6.6",
|
||||||
"@types/randomcolor": "^0.5.9",
|
"@types/randomcolor": "^0.5.9",
|
||||||
"avvvatars-react": "^0.4.2",
|
"avvvatars-react": "^0.4.2",
|
||||||
"dayjs": "^1.11.12",
|
"dayjs": "^1.11.13",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"framer-motion": "^10.18.0",
|
"framer-motion": "^10.18.0",
|
||||||
"get-youtube-id": "^1.0.1",
|
"get-youtube-id": "^1.0.1",
|
||||||
|
|
@ -39,7 +40,7 @@
|
||||||
"katex": "^0.16.11",
|
"katex": "^0.16.11",
|
||||||
"lowlight": "^3.1.0",
|
"lowlight": "^3.1.0",
|
||||||
"lucide-react": "^0.424.0",
|
"lucide-react": "^0.424.0",
|
||||||
"next": "14.2.5",
|
"next": "14.2.7",
|
||||||
"next-auth": "^4.24.7",
|
"next-auth": "^4.24.7",
|
||||||
"nextjs-toploader": "^1.6.12",
|
"nextjs-toploader": "^1.6.12",
|
||||||
"prosemirror-state": "^1.4.3",
|
"prosemirror-state": "^1.4.3",
|
||||||
|
|
@ -53,14 +54,14 @@
|
||||||
"react-katex": "^3.0.1",
|
"react-katex": "^3.0.1",
|
||||||
"react-spinners": "^0.13.8",
|
"react-spinners": "^0.13.8",
|
||||||
"react-youtube": "^10.1.0",
|
"react-youtube": "^10.1.0",
|
||||||
"sharp": "^0.33.4",
|
"sharp": "^0.33.5",
|
||||||
"styled-components": "^6.1.12",
|
"styled-components": "^6.1.12",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.5.2",
|
||||||
"tailwind-scrollbar": "^3.1.0",
|
"tailwind-scrollbar": "^3.1.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"y-indexeddb": "^9.0.12",
|
"y-indexeddb": "^9.0.12",
|
||||||
"y-prosemirror": "^1.2.11",
|
"y-prosemirror": "^1.2.12",
|
||||||
"y-webrtc": "^10.3.0",
|
"y-webrtc": "^10.3.0",
|
||||||
"yjs": "^13.6.18"
|
"yjs": "^13.6.18"
|
||||||
},
|
},
|
||||||
|
|
@ -70,15 +71,15 @@
|
||||||
"@types/react-beautiful-dnd": "^13.1.8",
|
"@types/react-beautiful-dnd": "^13.1.8",
|
||||||
"@types/react-dom": "18.2.23",
|
"@types/react-dom": "18.2.23",
|
||||||
"@types/react-katex": "^3.0.4",
|
"@types/react-katex": "^3.0.4",
|
||||||
"@types/react-transition-group": "^4.4.10",
|
"@types/react-transition-group": "^4.4.11",
|
||||||
"@types/styled-components": "^5.1.34",
|
"@types/styled-components": "^5.1.34",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-next": "^14.2.5",
|
"eslint-config-next": "^14.2.7",
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
"eslint-plugin-unused-imports": "^3.2.0",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.41",
|
||||||
"tailwindcss": "^3.4.7",
|
"tailwindcss": "^3.4.10",
|
||||||
"typescript": "5.4.4"
|
"typescript": "5.4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2994
apps/web/pnpm-lock.yaml
generated
2994
apps/web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
31
apps/web/sentry.client.config.ts
Normal file
31
apps/web/sentry.client.config.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
// This file configures the initialization of Sentry on the client.
|
||||||
|
// The config you add here will be used whenever a users loads a page in their browser.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from '@sentry/nextjs'
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.SENTRY_DSN,
|
||||||
|
|
||||||
|
// Adjust this value in production, or use tracesSampler for greater control
|
||||||
|
tracesSampleRate: 0.5,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
replaysOnErrorSampleRate: 1.0,
|
||||||
|
|
||||||
|
// This sets the sample rate to be 10%. You may want this to be 100% while
|
||||||
|
// in development and sample at a lower rate in production
|
||||||
|
replaysSessionSampleRate: 0.1,
|
||||||
|
|
||||||
|
enabled: process.env.NODE_ENV === 'development',
|
||||||
|
|
||||||
|
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
|
||||||
|
integrations: [
|
||||||
|
Sentry.replayIntegration({
|
||||||
|
// Additional Replay configuration goes in here, for example:
|
||||||
|
maskAllText: true,
|
||||||
|
blockAllMedia: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
18
apps/web/sentry.edge.config.ts
Normal file
18
apps/web/sentry.edge.config.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
|
||||||
|
// The config you add here will be used whenever one of the edge features is loaded.
|
||||||
|
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from '@sentry/nextjs'
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.SENTRY_DSN,
|
||||||
|
|
||||||
|
// Adjust this value in production, or use tracesSampler for greater control
|
||||||
|
tracesSampleRate: 0.5,
|
||||||
|
|
||||||
|
enabled: process.env.NODE_ENV === 'development',
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
})
|
||||||
20
apps/web/sentry.server.config.ts
Normal file
20
apps/web/sentry.server.config.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
// This file configures the initialization of Sentry on the server.
|
||||||
|
// The config you add here will be used whenever the server handles a request.
|
||||||
|
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
|
||||||
|
|
||||||
|
import * as Sentry from '@sentry/nextjs'
|
||||||
|
|
||||||
|
Sentry.init({
|
||||||
|
dsn: process.env.SENTRY_DSN,
|
||||||
|
|
||||||
|
// Adjust this value in production, or use tracesSampler for greater control
|
||||||
|
tracesSampleRate: 0.5,
|
||||||
|
|
||||||
|
// Setting this option to true will print useful information to the console while you're setting up Sentry.
|
||||||
|
debug: false,
|
||||||
|
|
||||||
|
enabled: process.env.NODE_ENV === 'development',
|
||||||
|
|
||||||
|
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
|
||||||
|
// spotlight: process.env.NODE_ENV === 'development',
|
||||||
|
})
|
||||||
|
|
@ -9,8 +9,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"prettier": "^3.3.0",
|
"prettier": "^3.3.3",
|
||||||
"turbo": "^1.13.3"
|
"turbo": "^1.13.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@9.0.6"
|
"packageManager": "pnpm@9.0.6"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
68
pnpm-lock.yaml
generated
68
pnpm-lock.yaml
generated
|
|
@ -12,11 +12,11 @@ importers:
|
||||||
specifier: ^8.57.0
|
specifier: ^8.57.0
|
||||||
version: 8.57.0
|
version: 8.57.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.3.0
|
specifier: ^3.3.3
|
||||||
version: 3.3.0
|
version: 3.3.3
|
||||||
turbo:
|
turbo:
|
||||||
specifier: ^1.13.3
|
specifier: ^1.13.4
|
||||||
version: 1.13.3
|
version: 1.13.4
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
|
@ -323,8 +323,8 @@ packages:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
prettier@3.3.0:
|
prettier@3.3.3:
|
||||||
resolution: {integrity: sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g==}
|
resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
|
@ -374,38 +374,38 @@ packages:
|
||||||
text-table@0.2.0:
|
text-table@0.2.0:
|
||||||
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
|
||||||
|
|
||||||
turbo-darwin-64@1.13.3:
|
turbo-darwin-64@1.13.4:
|
||||||
resolution: {integrity: sha512-glup8Qx1qEFB5jerAnXbS8WrL92OKyMmg5Hnd4PleLljAeYmx+cmmnsmLT7tpaVZIN58EAAwu8wHC6kIIqhbWA==}
|
resolution: {integrity: sha512-A0eKd73R7CGnRinTiS7txkMElg+R5rKFp9HV7baDiEL4xTG1FIg/56Vm7A5RVgg8UNgG2qNnrfatJtb+dRmNdw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
turbo-darwin-arm64@1.13.3:
|
turbo-darwin-arm64@1.13.4:
|
||||||
resolution: {integrity: sha512-/np2xD+f/+9qY8BVtuOQXRq5f9LehCFxamiQnwdqWm5iZmdjygC5T3uVSYuagVFsZKMvX3ycySwh8dylGTl6lg==}
|
resolution: {integrity: sha512-eG769Q0NF6/Vyjsr3mKCnkG/eW6dKMBZk6dxWOdrHfrg6QgfkBUk0WUUujzdtVPiUIvsh4l46vQrNVd9EOtbyA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
turbo-linux-64@1.13.3:
|
turbo-linux-64@1.13.4:
|
||||||
resolution: {integrity: sha512-G+HGrau54iAnbXLfl+N/PynqpDwi/uDzb6iM9hXEDG+yJnSJxaHMShhOkXYJPk9offm9prH33Khx2scXrYVW1g==}
|
resolution: {integrity: sha512-Bq0JphDeNw3XEi+Xb/e4xoKhs1DHN7OoLVUbTIQz+gazYjigVZvtwCvgrZI7eW9Xo1eOXM2zw2u1DGLLUfmGkQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
turbo-linux-arm64@1.13.3:
|
turbo-linux-arm64@1.13.4:
|
||||||
resolution: {integrity: sha512-qWwEl5VR02NqRyl68/3pwp3c/olZuSp+vwlwrunuoNTm6JXGLG5pTeme4zoHNnk0qn4cCX7DFrOboArlYxv0wQ==}
|
resolution: {integrity: sha512-BJcXw1DDiHO/okYbaNdcWN6szjXyHWx9d460v6fCHY65G8CyqGU3y2uUTPK89o8lq/b2C8NK0yZD+Vp0f9VoIg==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
turbo-windows-64@1.13.3:
|
turbo-windows-64@1.13.4:
|
||||||
resolution: {integrity: sha512-Nudr4bRChfJzBPzEmpVV85VwUYRCGKecwkBFpbp2a4NtrJ3+UP1VZES653ckqCu2FRyRuS0n03v9euMbAvzH+Q==}
|
resolution: {integrity: sha512-OFFhXHOFLN7A78vD/dlVuuSSVEB3s9ZBj18Tm1hk3aW1HTWTuAw0ReN6ZNlVObZUHvGy8d57OAGGxf2bT3etQw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
turbo-windows-arm64@1.13.3:
|
turbo-windows-arm64@1.13.4:
|
||||||
resolution: {integrity: sha512-ouJCgsVLd3icjRLmRvHQDDZnmGzT64GBupM1Y+TjtYn2LVaEBoV6hicFy8x5DUpnqdLy+YpCzRMkWlwhmkX7sQ==}
|
resolution: {integrity: sha512-u5A+VOKHswJJmJ8o8rcilBfU5U3Y1TTAfP9wX8bFh8teYF1ghP0EhtMRLjhtp6RPa+XCxHHVA2CiC3gbh5eg5g==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
turbo@1.13.3:
|
turbo@1.13.4:
|
||||||
resolution: {integrity: sha512-n17HJv4F4CpsYTvKzUJhLbyewbXjq1oLCi90i5tW1TiWDz16ML1eDG7wi5dHaKxzh5efIM56SITnuVbMq5dk4g==}
|
resolution: {integrity: sha512-1q7+9UJABuBAHrcC4Sxp5lOqYS5mvxRrwa33wpIyM18hlOCpRD/fTJNxZ0vhbMcJmz15o9kkVm743mPn7p6jpQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
|
|
@ -757,7 +757,7 @@ snapshots:
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
prettier@3.3.0: {}
|
prettier@3.3.3: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
|
|
@ -793,32 +793,32 @@ snapshots:
|
||||||
|
|
||||||
text-table@0.2.0: {}
|
text-table@0.2.0: {}
|
||||||
|
|
||||||
turbo-darwin-64@1.13.3:
|
turbo-darwin-64@1.13.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo-darwin-arm64@1.13.3:
|
turbo-darwin-arm64@1.13.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo-linux-64@1.13.3:
|
turbo-linux-64@1.13.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo-linux-arm64@1.13.3:
|
turbo-linux-arm64@1.13.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo-windows-64@1.13.3:
|
turbo-windows-64@1.13.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo-windows-arm64@1.13.3:
|
turbo-windows-arm64@1.13.4:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
turbo@1.13.3:
|
turbo@1.13.4:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
turbo-darwin-64: 1.13.3
|
turbo-darwin-64: 1.13.4
|
||||||
turbo-darwin-arm64: 1.13.3
|
turbo-darwin-arm64: 1.13.4
|
||||||
turbo-linux-64: 1.13.3
|
turbo-linux-64: 1.13.4
|
||||||
turbo-linux-arm64: 1.13.3
|
turbo-linux-arm64: 1.13.4
|
||||||
turbo-windows-64: 1.13.3
|
turbo-windows-64: 1.13.4
|
||||||
turbo-windows-arm64: 1.13.3
|
turbo-windows-arm64: 1.13.4
|
||||||
|
|
||||||
type-check@0.4.0:
|
type-check@0.4.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue