Compare commits

...

2 Commits

  1. 246
      backend/blueprints/chat/__init__.py
  2. 6
      backend/blueprints/course/__init__.py
  3. 35
      backend/blueprints/profile/__init__.py
  4. 3
      backend/config.py
  5. 2
      backend/db/model.py

@ -1,122 +1,49 @@
import uuid
from flask import Blueprint, request, jsonify,g
from uuid import UUID
from db.model import db, User, Course, Enrollment,Chat
from ...db.model import db, User, Course, Enrollment,Chat
from utils.auth import auth_required
import requests
from sqlalchemy import desc
from config import SPAM_SCORE_THRESHOLD, AI_SPAM_SERVICES_MICROSERVICE
from sqlalchemy import desc, select, and_
chat = Blueprint('chat', __name__)
SPAM_DETECTION_URL = "http://localhost:5000/test-spam"
@chat.route('/course/<uuid:course_id>/users', methods=['POST'])
def get_users_assigned_to_course(course_id: UUID):
"""
Fetch all users assigned to a specific course.
:param course_id: ID of the course to fetch users for (UUID).
:return: JSON response with users assigned to the course.
"""
try:
# Query the course to ensure it exists
course = Course.query.get(course_id)
if not course:
return jsonify({"error": "Course not found."}), 404
# Get the list of users assigned to the course
users = User.query.filter(User.enrollments.any(course_id=course_id)).all()
# Prepare the response data
user_data = [
{
"id": user.id,
"email": user.email,
"username": user.username,
"firstName": user.firstName,
"lastName": user.lastName,
}
for user in users
]
return jsonify({"course": course.name, "users": user_data}), 200
except Exception as e:
return jsonify({"error": f"An error occurred: {str(e)}"}), 500
@chat.route('/course/<uuid:course_id>/chat', methods=['POST'])
def join_chat(course_id: UUID):
"""
Allow users to join the chat only if they are enrolled in the course.
:param course_id: ID of the course (UUID).
:return: JSON response indicating whether the user can join or not.
"""
try:
# Get the user from the request (assume user_id is passed in the request body)
user_id = request.json.get('user_id')
if not user_id:
return jsonify({"error": "User ID is required."}), 400
# Query the user and ensure they exist
user = User.query.get(user_id)
if not user:
return jsonify({"error": "User not found."}), 404
# Check if the user is enrolled in the course
enrollment = Enrollment.query.filter_by(userID=user_id, courseID=course_id).first()
if not enrollment:
return jsonify({"error": "User is not enrolled in this course."}), 403
# If enrolled, allow the user to join the chat
return jsonify({"message": f"User {user.username} is enrolled in the course and can join the chat."}), 200
except Exception as e:
return jsonify({"error": f"An error occurred: {str(e)}"}), 500
@chat.route("/send-message", methods=["POST"])
@chat.route("/send", methods=["POST"])
@auth_required()
def create_chat():
"""
Create a chat message for a specific course.
"""
current_user = g.current_user # Fetch the logged-in user
data = request.get_json()
# Validate the payload
course_id = data.get("course_id")
message = data.get("message")
if not course_id or not message:
return jsonify({"error": "Course ID and message are required."}), 400
current_user: User = g.current_user # Fetch the logged-in user
data = request.args
if not data.get("course_id"):
return jsonify({"error": "Unknown Course"}), 400
if not data.get("message", "").strip():
return jsonify({'message': 'Silently rejected blank message'}), 200
try:
# Call the spam detection service
spam_response = requests.post(SPAM_DETECTION_URL, json={"test_message": message})
# Check if the user is enrolled in the course
enrollment_record: Enrollment = db.session.execute(
select(Enrollment).where(and_(
Enrollment.courseID == data.get("course_id"),
Enrollment.userID == current_user.id)
)
)
if not enrollment_record:
return jsonify({"error": "You are not enrolled in this course."}), 403
if enrollment_record.currentPage < enrollment_record.course.pageForCommunity:
return jsonify({
'message': 'You have not met the requirements to enter the chat.'
}), 403
# Create and save the chat message
spam_response = requests.post(AI_SPAM_SERVICES_MICROSERVICE, json={"test_message": data.get("message").strip()})
if spam_response.status_code != 200:
return jsonify({"error": "Failed to check message for spam."}), 500
spam_score = int(spam_response.json().get("spam_score", 0))
if spam_score > 6:
if spam_score > SPAM_SCORE_THRESHOLD:
return jsonify({"error": "This message contains suspicious links or is vulnerable."}), 400
# Verify the course exists
course = db.session.query(Course).filter_by(id=course_id).first()
if not course:
return jsonify({"error": "Invalid course ID."}), 404
# Check if the user is enrolled in the course
is_enrolled = db.session.query(Course).join(Enrollment).filter(
Enrollment.userID == current_user.id,
Enrollment.courseID == course_id
).first()
if not is_enrolled:
return jsonify({"error": "You are not enrolled in this course."}), 403
# Create and save the chat message
new_chat = Chat(
textContent=message,
textContent=data.get("message").strip(),
userID=current_user.id,
courseID=course_id,
courseID=data.get("course_id"),
)
db.session.add(new_chat)
db.session.commit()
@ -128,56 +55,71 @@ def create_chat():
@chat.route("/get", methods=["GET"])
@auth_required()
def get_chat_history():
"""
Fetch chat history for a course.
"""
current_user = g.current_user # Logged-in user
course_id = request.args.get("course_id")
limit = int(request.args.get("limit", 20)) # Default to 20 messages
offset = int(request.args.get("offset", 0)) # Default to no offset (latest chats)
if not course_id:
return jsonify({"error": "Course ID is required."}), 400
def get_messages():
try:
# Verify the course exists
course = db.session.query(Course).filter_by(id=course_id).first()
if not course:
return jsonify({"error": "Invalid course ID."}), 404
# Check if the user is enrolled in the course
is_enrolled = db.session.query(Course).join(Enrollment).filter(
Enrollment.userID == current_user.id,
Enrollment.courseID == course_id
).first()
if not is_enrolled:
course_id: uuid.UUID = uuid.UUID(request.args.get('course_id'))
current_user: User = g.current_user
limit = int(request.args.get('limit', 10))
before_id = request.args.get('before')
after_id = request.args.get('after')
# Verify user's enrollment
enrollment = db.session.execute(
select(Enrollment).where(
and_(Enrollment.courseID == course_id, Enrollment.userID == current_user.id)
)
).scalar()
if not enrollment:
return jsonify({"error": "You are not enrolled in this course."}), 403
# Fetch the latest chat messages with limit and offset
chats = (
db.session.query(Chat)
.filter_by(courseID=course_id)
.order_by(desc(Chat.chatDate))
.offset(offset)
.limit(limit)
.all()
)
# Format the chat messages
chat_history = [
{
"id": str(chat.id),
"textContent": chat.textContent,
"userID": str(chat.userID),
"chatDate": chat.chatDate.isoformat(),
"is_mine": chat.userID == current_user.id,
}
for chat in chats
]
return jsonify({"chats": chat_history}), 200
query = select(Chat).where(Chat.courseID == course_id)
if before_id:
try:
reference_message: Chat = db.session.execute(
select(Chat).where(Chat.id == uuid.UUID(before_id))
).scalar()
if not reference_message:
return jsonify({'message': 'Reference message not found'}), 404
query = query.order_by(Chat.chatDate.desc()).where(Chat.chatDate < reference_message.chatDate)
except ValueError:
return jsonify({'message': 'Invalid message ID format'}), 400
elif after_id:
try:
reference_message = db.session.execute(
select(Chat).where(Chat.id == uuid.UUID(after_id))
).scalar()
if not reference_message:
return jsonify({'message': 'Reference message not found'}), 404
query = query.order_by(Chat.chatDate.asc()).where(Chat.chatDate > reference_message.chatDate)
# For after queries, reverse the order later
query = query.order_by(Chat.chatDate)
except ValueError:
return jsonify({'message': 'Invalid message ID format'}), 400
else:
# Default ordering
query = query.order_by(desc(Chat.chatDate))
# Apply limit and execute query
query = query.limit(limit)
messages = list(db.session.execute(query).scalars())
# Reverse the order for 'after' queries to maintain consistency
if after_id:
messages.reverse()
# Format messages
chat_messages = [{
'id': str(msg.id),
'text': msg.textContent,
'userId': str(msg.userID),
'username': msg.user.username,
'timestamp': msg.chatDate.isoformat()
} for msg in messages]
return jsonify({
'messages': chat_messages,
'count': len(chat_messages),
'hasMore': len(chat_messages) == limit
}), 200
except Exception as e:
return jsonify({"error": f"An error occurred: {str(e)}"}), 500
return jsonify({'message': f'An error occurred: {str(e)}'}), 500

@ -104,9 +104,6 @@ def enroll_user():
return jsonify({'message': 'Already enrolled to this course'})
return jsonify({'message': 'Enrollment successful'}), 200
@course.route('/create', methods=['POST'])
@auth_required()
def create_course():
@ -128,7 +125,7 @@ def create_course():
return jsonify({'message': 'Course name cannot be empty'}), 401
course_description: str = form_data.get('course_description', '')
category_id: uuid.UUID = uuid.UUID(form_data['category_uuid'])
pages_required_for_community: int = int(form_data['community_unlock_at_pages']) # TODO: Add this field to model
page_for_community: int = int(form_data.get('page_for_community', 1)) # TODO: Add this field to model
new_course: Course = Course(
name=course_name,
@ -136,6 +133,7 @@ def create_course():
authorID=g.current_user.id,
description=course_description,
isActive=True,
pageForCommunity=page_for_community,
publishedStatus=int(published_status),
coverImage=cover_file_name,
serverFilename =pdf_file_name,

@ -15,7 +15,6 @@ from sqlalchemy.exc import IntegrityError
# from flask import url_for
profile = Blueprint('profile', __name__)
# Function to check allowed file extensions
def allowed_file(filename):
"""Check if the uploaded file has an allowed extension."""
return '.' in filename and filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']
@ -110,13 +109,8 @@ def manage_profile():
Handle GET and PUT requests for the logged-in user's profile.
"""
current_user: User = g.current_user
if request.method == 'GET':
try:
profile_picture = url_for('send_file', filename=current_user.pfpFilename, _external=True)
except:
profile_picture = ""
profile_picture = url_for('send_file', filename=current_user.pfpFilename, _external=True)
try:
# Construct the user profile data
profile_data = {
@ -133,24 +127,17 @@ def manage_profile():
"pfp_filename": current_user.pfpFilename,
"profile_picture": profile_picture,
}
return jsonify({"profile": profile_data}), 200
except Exception as e:
return jsonify({"error": f"Failed to fetch profile. Error: {str(e)}"}), 500
elif request.method == 'PUT':
# Update the user's profile using form data
try:
# email = request.form.get('email')
first_name = request.form.get('firstName')
last_name = request.form.get('lastName')
username = request.form.get('username')
dob = request.form.get('dob')
bio = request.form.get('bio')
# Update fields if provided
# if email:
# current_user.email = email
if first_name:
current_user.firstName = first_name
if last_name:
@ -161,10 +148,7 @@ def manage_profile():
current_user.dob = dob # Ensure the date format is validated
if bio:
current_user.bio = bio
# Commit changes to the database
db.session.commit()
return jsonify({"message": "Profile updated successfully."}), 200
except IntegrityError:
db.session.rollback()
@ -174,7 +158,6 @@ def manage_profile():
return jsonify({"error": f"Failed to update profile. Error: {str(e)}"}), 500
@profile.route('/update-profile-picture', methods=['PATCH'])
@auth_required()
def update_profile_picture():
@ -194,22 +177,18 @@ def update_profile_picture():
return jsonify({"error": "No selected file"}), 400
if not allowed_file(file.filename):
return jsonify({"error": "Invalid file type"}), 400
# Secure the filename and save the new file
filename = secure_filename(f"user_{user.id}_{file.filename}")
new_filepath = os.path.join(USER_UPLOADS_DIR, filename)
file.save(new_filepath)
# Delete the old profile picture (if it's not the default)
if user.pfpFilename != DEFAULT_PROFILE_FILE:
old_filepath = os.path.join(USER_UPLOADS_DIR, user.pfpFilename)
if os.path.exists(old_filepath):
os.remove(old_filepath)
# Update the user's profile picture
user.pfpFilename = filename
db.session.commit()
# Generate the new profile URL
profile_url = url_for('send_file',filename=user.pfpFilename,_external=True)
@ -232,37 +211,25 @@ def change_password():
"""
user = g.current_user
data = request.form
# Validate input data
current_password = data.get('current_password')
new_password = data.get('new_password')
confirm_password = data.get('confirm_password')
if not current_password or not new_password or not confirm_password:
return jsonify({"error": "All fields (current_password, new_password, confirm_password) are required"}), 400
# Check if current password matches the user's existing password
if not check_password_hash(user.hash_password, current_password):
return jsonify({"error": "Current password is incorrect"}), 400
# Check if new password and confirmation match
if new_password != confirm_password:
return jsonify({"error": "New password and confirm password do not match"}), 400
# Check for password complexity (optional)
# Validate password
try:
password_check_sanity(new_password)
except InsecurePasswordException as e:
return jsonify({"error": str(e)}), 400
# Update the user's password
user.hash_password = generate_password_hash(new_password)
db.session.commit()
return jsonify({"message": "Password updated successfully"}), 200
# @profile.route('/hello')
# @auth_required()
# @requires_role([UserRole.ADMIN])

@ -20,6 +20,9 @@ DISABLE_PASSWORD_SANITY_CHECKS: bool = False
PROJECT_ROOT: os.path = os.path.dirname(os.path.abspath(__file__))
USER_UPLOADS_DIR: str = os.path.abspath(os.path.join(PROJECT_ROOT, "uploads"))
SPAM_SCORE_THRESHOLD: int = 6
AI_SPAM_SERVICES_MICROSERVICE: str ='http://localhost:5000/test-spam'
DB_URI: str = f"{DB_ENGINE}://{DB_USER}:{DB_PASSWORD}@{DB_HOST}:{DB_PORT}/{DB_NAME}"
ACTIVATE_ACCOUNTS_ON_SIGNUP: bool = True

@ -73,6 +73,8 @@ class Course(db.Model):
authorID: Mapped[uuid.UUID] = mapped_column(ForeignKey("user.id"))
author: Mapped["User"] = relationship(back_populates="publications")
description: Mapped[str] = mapped_column(String(1024), nullable=False, default='')
pageForCommunity: Mapped[[int]] = mapped_column(Integer, nullable=False, default=1)
totalPages: Mapped[[int]] = mapped_column(Integer, nullable=False, default=1)
totalEnrolled: Mapped[int] = mapped_column(Integer, nullable=False, default='')
isActive: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
publishedStatus: Mapped[int] = mapped_column(SmallInteger, nullable=False, default=PublishedStatus.DRAFT)

Loading…
Cancel
Save