mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: implement usage limits across the app
This commit is contained in:
parent
85ffb44d93
commit
a5fbf49304
15 changed files with 281 additions and 160 deletions
|
|
@ -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},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
141
apps/api/src/security/features_utils/usage.py
Normal file
141
apps/api/src/security/features_utils/usage.py
Normal 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
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
)
|
|
||||||
|
|
@ -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()),
|
||||||
)
|
)
|
||||||
|
|
@ -1410,7 +1427,6 @@ async def grade_assignment_submission(
|
||||||
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)
|
||||||
|
|
||||||
# Check if assignment user submission exists
|
# Check if assignment user submission exists
|
||||||
|
|
@ -1552,7 +1568,6 @@ async def mark_activity_as_done_for_user(
|
||||||
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)
|
||||||
|
|
||||||
if not activity:
|
if not activity:
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
)
|
)
|
||||||
|
|
@ -396,12 +412,11 @@ async def get_courses_orgslug(
|
||||||
).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 = (
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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"}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
|
|
@ -276,8 +289,6 @@ async def add_users_to_usergroup(
|
||||||
detail="UserGroup not found",
|
detail="UserGroup not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# RBAC check
|
# RBAC check
|
||||||
await rbac_check(
|
await rbac_check(
|
||||||
request,
|
request,
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
if (res.success) {
|
||||||
await revalidateTags(['courses'], orgslug)
|
await revalidateTags(['courses'], orgslug)
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
|
toast.success('Course created successfully')
|
||||||
|
|
||||||
if (status.org_id == orgId) {
|
if (res.data.org_id == orgId) {
|
||||||
closeModal()
|
closeModal()
|
||||||
router.refresh()
|
router.refresh()
|
||||||
await revalidateTags(['courses'], orgslug)
|
await revalidateTags(['courses'], orgslug)
|
||||||
} else {
|
}
|
||||||
alert('Error creating course, please see console logs')
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setIsSubmitting(false)
|
||||||
|
toast.error(res.data.detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue