feat: implement usage limits across the app

This commit is contained in:
swve 2024-08-11 21:08:18 +02:00
parent 85ffb44d93
commit a5fbf49304
15 changed files with 281 additions and 160 deletions

View file

@ -30,7 +30,7 @@ def migrate_v0_to_v1(v0_config):
), ),
}, },
"usergroups": { "usergroups": {
"enabled": True, "enabled": False,
"limit": ( "limit": (
v0_config["GeneralConfig"]["limits"]["max_staff"] v0_config["GeneralConfig"]["limits"]["max_staff"]
if v0_config["GeneralConfig"]["limits"]["limits_enabled"] if v0_config["GeneralConfig"]["limits"]["limits_enabled"]
@ -46,7 +46,7 @@ def migrate_v0_to_v1(v0_config):
), ),
}, },
"ai": { "ai": {
"enabled": v0_config["AIConfig"]["enabled"], "enabled": False,
"limit": ( "limit": (
v0_config["AIConfig"]["limits"]["max_asks"] v0_config["AIConfig"]["limits"]["max_asks"]
if v0_config["AIConfig"]["limits"]["limits_enabled"] if v0_config["AIConfig"]["limits"]["limits_enabled"]
@ -54,12 +54,12 @@ def migrate_v0_to_v1(v0_config):
), ),
"model": 'gpt-4o-mini', "model": 'gpt-4o-mini',
}, },
"assignments": {"enabled": True, "limit": 10}, "assignments": {"enabled": True, "limit": 5},
"payments": {"enabled": False, "stripe_key": ""}, "payments": {"enabled": False, "stripe_key": ""},
"discussions": {"enabled": False, "limit": 10}, "discussions": {"enabled": False, "limit": 10},
"analytics": {"enabled": False, "limit": 10}, "analytics": {"enabled": False, "limit": 10},
"collaboration": { "collaboration": {
"enabled": v0_config["GeneralConfig"]["collaboration"], "enabled": False,
"limit": 10, "limit": 10,
}, },
"api": {"enabled": False, "limit": 10}, "api": {"enabled": False, "limit": 10},

View file

@ -92,17 +92,17 @@ async def api_update_activity(
) )
@router.delete("/{activity_id}") @router.delete("/{activity_uuid}")
async def api_delete_activity( async def api_delete_activity(
request: Request, request: Request,
activity_id: str, activity_uuid: str,
current_user: PublicUser = Depends(get_current_user), current_user: PublicUser = Depends(get_current_user),
db_session=Depends(get_db_session), db_session=Depends(get_db_session),
): ):
""" """
Delete activity by activity_id Delete activity by activity_id
""" """
return await delete_activity(request, activity_id, current_user, db_session) return await delete_activity(request, activity_uuid, current_user, db_session)
# Video activity # Video activity

View file

@ -0,0 +1,141 @@
import redis
from src.db.organization_config import OrganizationConfig
from config.config import get_learnhouse_config
from typing import Literal, TypeAlias
from fastapi import HTTPException
from sqlmodel import Session, select
FeatureSet: TypeAlias = Literal[
"ai",
"analytics",
"api",
"assignments",
"collaboration",
"courses",
"discussions",
"members",
"payments",
"storage",
"usergroups",
]
def check_limits_with_usage(
feature: FeatureSet,
org_id: int,
db_session: Session,
):
# Get the Organization Config
statement = select(OrganizationConfig).where(OrganizationConfig.org_id == org_id)
result = db_session.exec(statement)
org_config = result.first()
if org_config is None:
raise HTTPException(
status_code=404,
detail="Organization has no config",
)
# Check if the Organizations has AI enabled
if org_config.config["features"][feature]["enabled"] == False:
raise HTTPException(
status_code=403,
detail=f"{feature.capitalize()} is not enabled for this organization",
)
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
# Check limits
feature_limit = org_config.config["features"][feature]["limit"]
if feature_limit > 0:
# Get the number of feature usage
feature_usage = r.get(f"{feature}_usage:{org_id}")
# Get a number of feature asks
if feature_usage is None:
feature_usage_count = 0
else:
feature_usage_count = int(feature_usage) # type: ignore
# Check if the Number of usage is less than the max_asks limit
if feature_limit <= feature_usage_count:
raise HTTPException(
status_code=403,
detail=f"Usage Limit has been reached for {feature.capitalize()}",
)
return True
def increase_feature_usage(
feature: FeatureSet,
org_id: int,
db_session: Session,
):
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
# Get the number of feature usage
feature_usage = r.get(f"{feature}_usage:{org_id}")
# Get a number of feature asks
if feature_usage is None:
feature_usage_count = 0
else:
feature_usage_count = int(feature_usage) # type: ignore
# Increment the feature usage
r.set(f"{feature}_usage:{org_id}", feature_usage_count + 1)
return True
def decrease_feature_usage(
feature: FeatureSet,
org_id: int,
db_session: Session,
):
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
# Get the number of feature usage
feature_usage = r.get(f"{feature}_usage:{org_id}")
# Get a number of feature asks
if feature_usage is None:
feature_usage_count = 0
else:
feature_usage_count = int(feature_usage) # type: ignore
# Increment the feature usage
r.set(f"{feature}_usage:{org_id}", feature_usage_count - 1)
return True

View file

@ -2,7 +2,10 @@ from fastapi import Depends, HTTPException, Request
from sqlmodel import Session, select from sqlmodel import Session, select
from src.db.organization_config import OrganizationConfig from src.db.organization_config import OrganizationConfig
from src.db.organizations import Organization from src.db.organizations import Organization
from src.services.ai.utils import check_limits_and_config, count_ai_ask from src.security.features_utils.usage import (
check_limits_with_usage,
increase_feature_usage,
)
from src.db.courses.courses import Course, CourseRead from src.db.courses.courses import Course, CourseRead
from src.core.events.database import get_db_session from src.core.events.database import get_db_session
from src.db.users import PublicUser from src.db.users import PublicUser
@ -52,9 +55,15 @@ def ai_start_activity_chat_session(
statement = select(Organization).where(Organization.id == course.org_id) statement = select(Organization).where(Organization.id == course.org_id)
org = db_session.exec(statement).first() org = db_session.exec(statement).first()
if not org or org.id is None:
raise HTTPException(
status_code=404,
detail="Organization not found",
)
# Check limits and usage # Check limits and usage
check_limits_and_config(db_session, org) # type: ignore check_limits_with_usage("ai", org.id, db_session)
count_ai_ask(org, "increment") # type: ignore increase_feature_usage("ai", org.id, db_session)
if not activity: if not activity:
raise HTTPException( raise HTTPException(
@ -147,8 +156,8 @@ def ai_send_activity_chat_message(
org = db_session.exec(statement).first() org = db_session.exec(statement).first()
# Check limits and usage # Check limits and usage
check_limits_and_config(db_session, org) # type: ignore check_limits_with_usage("ai", course.org_id, db_session)
count_ai_ask(org, "increment") # type: ignore increase_feature_usage("ai", course.org_id, db_session)
if not activity: if not activity:
raise HTTPException( raise HTTPException(

View file

@ -1,114 +0,0 @@
from typing import Literal
import redis
from fastapi import HTTPException
from sqlmodel import Session, select
from config.config import get_learnhouse_config
from src.db.organization_config import OrganizationConfig
from src.db.organizations import Organization
def count_ai_ask(
organization: Organization,
operation: Literal["increment", "decrement"],
):
"""
Count the number of AI asks
"""
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
# Get the number of AI asks
ai_asks = r.get(f"ai_asks:{organization.org_uuid}")
if ai_asks is None:
ai_asks = 0
# Increment or decrement the number of AI asks
if operation == "increment":
ai_asks = int(ai_asks) + 1
elif operation == "decrement":
ai_asks = int(ai_asks) - 1
# Update the number of AI asks
r.set(f"ai_asks:{organization.org_uuid}", ai_asks)
# Set the expiration time to 30 days
r.expire(f"ai_asks:{organization.org_uuid}", 2592000)
def check_limits_and_config(db_session: Session, organization: Organization):
"""
Check the limits and config of an Organization
"""
# Get the Organization Config
statement = select(OrganizationConfig).where(
OrganizationConfig.org_id == organization.id
)
result = db_session.exec(statement)
org_config = result.first()
if org_config is None:
raise HTTPException(
status_code=404,
detail="Organization has no config",
)
# Check if the Organizations has AI enabled
if org_config.config["features"]["ai"]["enabled"] == False:
raise HTTPException(
status_code=403,
detail="Organization has AI disabled",
)
# Check if the Organization has Limits enabled and if the max_asks limit has been reached
if org_config.config["features"]["ai"]["limit"] > 0:
LH_CONFIG = get_learnhouse_config()
redis_conn_string = LH_CONFIG.redis_config.redis_connection_string
if not redis_conn_string:
raise HTTPException(
status_code=500,
detail="Redis connection string not found",
)
# Connect to Redis
r = redis.Redis.from_url(redis_conn_string)
if not r:
raise HTTPException(
status_code=500,
detail="Could not connect to Redis",
)
# Get the number of AI asks
ai_asks = r.get(f"ai_asks:{organization.org_uuid}")
# Get a number of AI asks
if ai_asks is None:
ai_asks = 0
else:
ai_asks = int(ai_asks)
# Check if the Number of asks is less than the max_asks limit
if org_config.config["features"]["ai"]["limit"] <= ai_asks:
raise HTTPException(
status_code=403,
detail="Organization has reached the max number of AI asks",
)

View file

@ -28,6 +28,11 @@ from src.db.organizations import Organization
from src.db.trail_runs import TrailRun from src.db.trail_runs import TrailRun
from src.db.trail_steps import TrailStep from src.db.trail_steps import TrailStep
from src.db.users import AnonymousUser, PublicUser, User from src.db.users import AnonymousUser, PublicUser, User
from src.security.features_utils.usage import (
check_limits_with_usage,
decrease_feature_usage,
increase_feature_usage,
)
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship_and_usergroups, authorization_verify_based_on_roles_and_authorship_and_usergroups,
authorization_verify_if_element_is_public, authorization_verify_if_element_is_public,
@ -61,6 +66,9 @@ async def create_assignment(
# RBAC check # RBAC check
await rbac_check(request, course.course_uuid, current_user, "create", db_session) await rbac_check(request, course.course_uuid, current_user, "create", db_session)
# Usage check
check_limits_with_usage("assignments", course.org_id, db_session)
# Create Assignment # Create Assignment
assignment = Assignment(**assignment_object.model_dump()) assignment = Assignment(**assignment_object.model_dump())
@ -74,6 +82,9 @@ async def create_assignment(
db_session.commit() db_session.commit()
db_session.refresh(assignment) db_session.refresh(assignment)
# Feature usage
increase_feature_usage("assignments", course.org_id, db_session)
# return assignment read # return assignment read
return AssignmentRead.model_validate(assignment) return AssignmentRead.model_validate(assignment)
@ -228,6 +239,9 @@ async def delete_assignment(
# RBAC check # RBAC check
await rbac_check(request, course.course_uuid, current_user, "delete", db_session) await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
# Feature usage
decrease_feature_usage("assignments", course.org_id, db_session)
# Delete Assignment # Delete Assignment
db_session.delete(assignment) db_session.delete(assignment)
db_session.commit() db_session.commit()
@ -275,6 +289,9 @@ async def delete_assignment_from_activity_uuid(
# RBAC check # RBAC check
await rbac_check(request, course.course_uuid, current_user, "delete", db_session) await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
# Feature usage
decrease_feature_usage("assignments", course.org_id, db_session)
# Delete Assignment # Delete Assignment
db_session.delete(assignment) db_session.delete(assignment)
@ -1119,9 +1136,9 @@ async def create_assignment_submission(
# Add TrailStep # Add TrailStep
trail = await check_trail_presence( trail = await check_trail_presence(
org_id=course.org_id, org_id=course.org_id,
user_id=user.id, user_id=user.id, # type: ignore
request=request, request=request,
user=user, user=user, # type: ignore
db_session=db_session, db_session=db_session,
) )
@ -1137,7 +1154,7 @@ async def create_assignment_submission(
trail_id=trail.id if trail.id is not None else 0, trail_id=trail.id if trail.id is not None else 0,
course_id=course.id if course.id is not None else 0, course_id=course.id if course.id is not None else 0,
org_id=course.org_id, org_id=course.org_id,
user_id=user.id, user_id=user.id, # type: ignore
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),
) )
@ -1162,7 +1179,7 @@ async def create_assignment_submission(
complete=True, complete=True,
teacher_verified=False, teacher_verified=False,
grade="", grade="",
user_id=user.id, user_id=user.id, # type: ignore
creation_date=str(datetime.now()), creation_date=str(datetime.now()),
update_date=str(datetime.now()), update_date=str(datetime.now()),
) )
@ -1400,7 +1417,7 @@ async def grade_assignment_submission(
status_code=404, status_code=404,
detail="Assignment not found", detail="Assignment not found",
) )
statement = select(Course).where(Course.id == assignment.course_id) statement = select(Course).where(Course.id == assignment.course_id)
course = db_session.exec(statement).first() course = db_session.exec(statement).first()
@ -1409,7 +1426,6 @@ async def grade_assignment_submission(
status_code=404, status_code=404,
detail="Course not found", detail="Course not found",
) )
await rbac_check(request, course.course_uuid, current_user, "update", db_session) await rbac_check(request, course.course_uuid, current_user, "update", db_session)
@ -1551,7 +1567,6 @@ async def mark_activity_as_done_for_user(
status_code=404, status_code=404,
detail="Course not found", detail="Course not found",
) )
await rbac_check(request, course.course_uuid, current_user, "update", db_session) await rbac_check(request, course.course_uuid, current_user, "update", db_session)
@ -1596,6 +1611,7 @@ async def mark_activity_as_done_for_user(
# return OK # return OK
return {"message": "Activity marked as done for user"} return {"message": "Activity marked as done for user"}
async def get_assignments_from_course( async def get_assignments_from_course(
request: Request, request: Request,
course_uuid: str, course_uuid: str,

View file

@ -5,6 +5,11 @@ from sqlmodel import Session, select
from src.db.usergroup_resources import UserGroupResource from src.db.usergroup_resources import UserGroupResource
from src.db.usergroup_user import UserGroupUser from src.db.usergroup_user import UserGroupUser
from src.db.organizations import Organization from src.db.organizations import Organization
from src.security.features_utils.usage import (
check_limits_with_usage,
decrease_feature_usage,
increase_feature_usage,
)
from src.services.trail.trail import get_user_trail_with_orgid from src.services.trail.trail import get_user_trail_with_orgid
from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum
from src.db.users import PublicUser, AnonymousUser, User, UserRead from src.db.users import PublicUser, AnonymousUser, User, UserRead
@ -58,6 +63,7 @@ async def get_course(
return course return course
async def get_course_by_id( async def get_course_by_id(
request: Request, request: Request,
course_id: str, course_id: str,
@ -91,6 +97,7 @@ async def get_course_by_id(
return course return course
async def get_course_meta( async def get_course_meta(
request: Request, request: Request,
course_uuid: str, course_uuid: str,
@ -158,6 +165,9 @@ async def create_course(
# RBAC check # RBAC check
await rbac_check(request, "course_x", current_user, "create", db_session) await rbac_check(request, "course_x", current_user, "create", db_session)
# Usage check
check_limits_with_usage("courses", org_id, db_session)
# Complete course object # Complete course object
course.org_id = course.org_id course.org_id = course.org_id
@ -207,6 +217,9 @@ async def create_course(
) )
authors = db_session.exec(authors_statement).all() authors = db_session.exec(authors_statement).all()
# Feature usage
increase_feature_usage("courses", course.org_id, db_session)
# convert from User to UserRead # convert from User to UserRead
authors = [UserRead.model_validate(author) for author in authors] authors = [UserRead.model_validate(author) for author in authors]
@ -344,6 +357,9 @@ async def delete_course(
# RBAC check # RBAC check
await rbac_check(request, course.course_uuid, current_user, "delete", db_session) await rbac_check(request, course.course_uuid, current_user, "delete", db_session)
# Feature usage
decrease_feature_usage("courses", course.org_id, db_session)
db_session.delete(course) db_session.delete(course)
db_session.commit() db_session.commit()
@ -358,7 +374,7 @@ async def get_courses_orgslug(
page: int = 1, page: int = 1,
limit: int = 10, limit: int = 10,
): ):
# TODO : This entire function is a mess. It needs to be rewritten. # TODO : This entire function is a mess. It needs to be rewritten.
# Query for public courses # Query for public courses
@ -372,7 +388,7 @@ async def get_courses_orgslug(
statement_author = ( statement_author = (
select(Course) select(Course)
.join(Organization) .join(Organization)
.join(ResourceAuthor, ResourceAuthor.user_id == current_user.id) .join(ResourceAuthor, ResourceAuthor.user_id == current_user.id) # type: ignore
.where( .where(
Organization.slug == org_slug, Organization.slug == org_slug,
ResourceAuthor.resource_uuid == Course.course_uuid, ResourceAuthor.resource_uuid == Course.course_uuid,
@ -383,9 +399,9 @@ async def get_courses_orgslug(
statement_usergroup = ( statement_usergroup = (
select(Course) select(Course)
.join(Organization) .join(Organization)
.join(UserGroupResource, UserGroupResource.resource_uuid == Course.course_uuid) .join(UserGroupResource, UserGroupResource.resource_uuid == Course.course_uuid) # type: ignore
.join( .join(
UserGroupUser, UserGroupUser.usergroup_id == UserGroupResource.usergroup_id UserGroupUser, UserGroupUser.usergroup_id == UserGroupResource.usergroup_id # type: ignore
) )
.where(Organization.slug == org_slug, UserGroupUser.user_id == current_user.id) .where(Organization.slug == org_slug, UserGroupUser.user_id == current_user.id)
) )
@ -395,13 +411,12 @@ async def get_courses_orgslug(
statement_public, statement_author, statement_usergroup statement_public, statement_author, statement_usergroup
).subquery() ).subquery()
# TODO: migrate this to exec # TODO: migrate this to exec
courses = db_session.execute(select(statement_complete)).all() courses = db_session.execute(select(statement_complete)).all() # type: ignore
# TODO: I have no idea why this is necessary, but it is # TODO: I have no idea why this is necessary, but it is
courses = [CourseRead(**course._asdict(), authors=[]) for course in courses] courses = [CourseRead(**course._asdict(), authors=[]) for course in courses]
# for every course, get the authors # for every course, get the authors
for course in courses: for course in courses:
authors_statement = ( authors_statement = (

View file

@ -6,6 +6,10 @@ from sqlmodel import Session, select
from src.db.organizations import Organization from src.db.organizations import Organization
from src.db.user_organizations import UserOrganization from src.db.user_organizations import UserOrganization
from src.db.users import AnonymousUser, PublicUser, User from src.db.users import AnonymousUser, PublicUser, User
from src.security.features_utils.usage import (
check_limits_with_usage,
increase_feature_usage,
)
from src.services.orgs.invites import get_invite_code from src.services.orgs.invites import get_invite_code
from src.services.orgs.orgs import get_org_join_mechanism from src.services.orgs.orgs import get_org_join_mechanism
@ -27,12 +31,14 @@ async def join_org(
org = result.first() org = result.first()
if not org: if not org or org.id is None:
raise HTTPException( raise HTTPException(
status_code=404, status_code=404,
detail="Organization not found", detail="Organization not found",
) )
check_limits_with_usage("members", org.id, db_session)
join_method = await get_org_join_mechanism( join_method = await get_org_join_mechanism(
request, args.org_id, current_user, db_session request, args.org_id, current_user, db_session
) )
@ -104,6 +110,8 @@ async def join_org(
db_session.add(user_organization) db_session.add(user_organization)
db_session.commit() db_session.commit()
increase_feature_usage("members", org.id, db_session)
return "Great, You're part of the Organization" return "Great, You're part of the Organization"
else: else:

View file

@ -5,6 +5,7 @@ import logging
import redis import redis
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from sqlmodel import Session, select from sqlmodel import Session, select
from src.security.features_utils.usage import decrease_feature_usage
from src.services.orgs.invites import send_invite_email from src.services.orgs.invites import send_invite_email
from config.config import get_learnhouse_config from config.config import get_learnhouse_config
from src.services.orgs.orgs import rbac_check from src.services.orgs.orgs import rbac_check
@ -147,6 +148,8 @@ async def remove_user_from_org(
db_session.delete(user_org) db_session.delete(user_org)
db_session.commit() db_session.commit()
decrease_feature_usage("members", org_id, db_session)
return {"detail": "User removed from org"} return {"detail": "User removed from org"}

View file

@ -4,6 +4,10 @@ from typing import Literal
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, Request from fastapi import HTTPException, Request
from sqlmodel import Session, select from sqlmodel import Session, select
from src.security.features_utils.usage import (
check_limits_with_usage,
increase_feature_usage,
)
from src.security.rbac.rbac import ( from src.security.rbac.rbac import (
authorization_verify_based_on_roles_and_authorship_and_usergroups, authorization_verify_based_on_roles_and_authorship_and_usergroups,
authorization_verify_if_user_is_anon, authorization_verify_if_user_is_anon,
@ -35,14 +39,17 @@ async def create_usergroup(
# Check if Organization exists # Check if Organization exists
statement = select(Organization).where(Organization.id == usergroup_create.org_id) statement = select(Organization).where(Organization.id == usergroup_create.org_id)
result = db_session.exec(statement) org = db_session.exec(statement).first()
if not result.first(): if not org or org.id is None:
raise HTTPException( raise HTTPException(
status_code=400, status_code=400,
detail="Organization does not exist", detail="Organization does not exist",
) )
# Usage check
check_limits_with_usage("courses", org.id, db_session)
# Complete the object # Complete the object
usergroup.usergroup_uuid = f"usergroup_{uuid4()}" usergroup.usergroup_uuid = f"usergroup_{uuid4()}"
usergroup.creation_date = str(datetime.now()) usergroup.creation_date = str(datetime.now())
@ -53,6 +60,9 @@ async def create_usergroup(
db_session.commit() db_session.commit()
db_session.refresh(usergroup) db_session.refresh(usergroup)
# Feature usage
increase_feature_usage("usergroups", org.id, db_session)
usergroup = UserGroupRead.model_validate(usergroup) usergroup = UserGroupRead.model_validate(usergroup)
return usergroup return usergroup
@ -253,6 +263,9 @@ async def delete_usergroup_by_id(
db_session=db_session, db_session=db_session,
) )
# Feature usage
increase_feature_usage("usergroups", usergroup.org_id, db_session)
db_session.delete(usergroup) db_session.delete(usergroup)
db_session.commit() db_session.commit()
@ -275,8 +288,6 @@ async def add_users_to_usergroup(
status_code=404, status_code=404,
detail="UserGroup not found", detail="UserGroup not found",
) )
# RBAC check # RBAC check
await rbac_check( await rbac_check(
@ -353,7 +364,9 @@ async def remove_users_from_usergroup(
user_ids_array = user_ids.split(",") user_ids_array = user_ids.split(",")
for user_id in user_ids_array: for user_id in user_ids_array:
statement = select(UserGroupUser).where(UserGroupUser.user_id == user_id, UserGroupUser.usergroup_id == usergroup_id) statement = select(UserGroupUser).where(
UserGroupUser.user_id == user_id, UserGroupUser.usergroup_id == usergroup_id
)
usergroup_user = db_session.exec(statement).first() usergroup_user = db_session.exec(statement).first()
if usergroup_user: if usergroup_user:

View file

@ -3,6 +3,10 @@ from typing import Literal
from uuid import uuid4 from uuid import uuid4
from fastapi import HTTPException, Request, UploadFile, status from fastapi import HTTPException, Request, UploadFile, status
from sqlmodel import Session, select from sqlmodel import Session, select
from src.security.features_utils.usage import (
check_limits_with_usage,
increase_feature_usage,
)
from src.services.users.usergroups import add_users_to_usergroup from src.services.users.usergroups import add_users_to_usergroup
from src.services.users.emails import ( from src.services.users.emails import (
send_account_creation_email, send_account_creation_email,
@ -61,6 +65,9 @@ async def create_user(
detail="Organization does not exist", detail="Organization does not exist",
) )
# Usage check
check_limits_with_usage("members", org_id, db_session)
# Username # Username
statement = select(User).where(User.username == user.username) statement = select(User).where(User.username == user.username)
result = db_session.exec(statement) result = db_session.exec(statement)
@ -106,6 +113,8 @@ async def create_user(
user = UserRead.model_validate(user) user = UserRead.model_validate(user)
increase_feature_usage("members", org_id, db_session)
# Send Account creation email # Send Account creation email
send_account_creation_email( send_account_creation_email(
user=user, user=user,
@ -135,6 +144,9 @@ async def create_user_with_invite(
detail="Invite code is incorrect", detail="Invite code is incorrect",
) )
# Usage check
check_limits_with_usage("members", org_id, db_session)
# Check if invite code contains UserGroup # Check if invite code contains UserGroup
if inviteCode.usergroup_id: if inviteCode.usergroup_id:
# Add user to UserGroup # Add user to UserGroup
@ -148,6 +160,8 @@ async def create_user_with_invite(
user = await create_user(request, db_session, current_user, user_object, org_id) user = await create_user(request, db_session, current_user, user_object, org_id)
increase_feature_usage("members", org_id, db_session)
return user return user

View file

@ -15,7 +15,8 @@ import { getAPIUrl } from '@services/config/config'
import { mutate } from 'swr' import { mutate } from 'swr'
import { createAssignment } from '@services/courses/assignments' import { createAssignment } from '@services/courses/assignments'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import { createActivity } from '@services/courses/activities' import { createActivity, deleteActivity } from '@services/courses/activities'
import toast from 'react-hot-toast'
function NewAssignment({ submitActivity, chapterId, course, closeModal }: any) { function NewAssignment({ submitActivity, chapterId, course, closeModal }: any) {
const org = useOrg() as any; const org = useOrg() as any;
@ -55,7 +56,7 @@ function NewAssignment({ submitActivity, chapterId, course, closeModal }: any) {
} }
const activity_res = await createActivity(activity, chapterId, org?.id, session.data?.tokens?.access_token) const activity_res = await createActivity(activity, chapterId, org?.id, session.data?.tokens?.access_token)
await createAssignment({ const res = await createAssignment({
title: activityName, title: activityName,
description: activityDescription, description: activityDescription,
due_date: dueDate, due_date: dueDate,
@ -66,6 +67,14 @@ function NewAssignment({ submitActivity, chapterId, course, closeModal }: any) {
activity_id: activity_res?.id, activity_id: activity_res?.id,
}, session.data?.tokens?.access_token) }, session.data?.tokens?.access_token)
if (res.success) {
toast.success('Assignment created successfully')
} else {
toast.error(res.data.detail)
await deleteActivity(activity_res.activity_uuid, session.data?.tokens?.access_token)
}
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`) mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
setIsSubmitting(false) setIsSubmitting(false)
closeModal() closeModal()

View file

@ -16,6 +16,7 @@ import { BarLoader } from 'react-spinners'
import { revalidateTags } from '@services/utils/ts/requests' import { revalidateTags } from '@services/utils/ts/requests'
import { useRouter } from 'next/navigation' import { useRouter } from 'next/navigation'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
import toast from 'react-hot-toast'
function CreateCourseModal({ closeModal, orgslug }: any) { function CreateCourseModal({ closeModal, orgslug }: any) {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
@ -69,21 +70,27 @@ function CreateCourseModal({ closeModal, orgslug }: any) {
e.preventDefault() e.preventDefault()
setIsSubmitting(true) setIsSubmitting(true)
let status = await createNewCourse( let res = await createNewCourse(
orgId, orgId,
{ name, description, tags, visibility }, { name, description, tags, visibility },
thumbnail, thumbnail,
session.data?.tokens?.access_token session.data?.tokens?.access_token
) )
await revalidateTags(['courses'], orgslug) if (res.success) {
setIsSubmitting(false)
if (status.org_id == orgId) {
closeModal()
router.refresh()
await revalidateTags(['courses'], orgslug) await revalidateTags(['courses'], orgslug)
} else { setIsSubmitting(false)
alert('Error creating course, please see console logs') toast.success('Course created successfully')
if (res.data.org_id == orgId) {
closeModal()
router.refresh()
await revalidateTags(['courses'], orgslug)
}
}
else {
setIsSubmitting(false)
toast.error(res.data.detail)
} }
} }

View file

@ -99,9 +99,9 @@ export async function getActivityByID(
return res return res
} }
export async function deleteActivity(activity_id: any, access_token: string) { export async function deleteActivity(activity_uuid: any, access_token: string) {
const result = await fetch( const result = await fetch(
`${getAPIUrl()}activities/${activity_id}`, `${getAPIUrl()}activities/${activity_uuid}`,
RequestBodyWithAuthHeader('DELETE', null, null, access_token) RequestBodyWithAuthHeader('DELETE', null, null, access_token)
) )
const res = await result.json() const res = await result.json()

View file

@ -98,7 +98,7 @@ export async function createNewCourse(
`${getAPIUrl()}courses/?org_id=${org_id}`, `${getAPIUrl()}courses/?org_id=${org_id}`,
RequestBodyFormWithAuthHeader('POST', formData, null, access_token) RequestBodyFormWithAuthHeader('POST', formData, null, access_token)
) )
const res = await errorHandling(result) const res = await getResponseMetadata(result)
return res return res
} }