Implement CBT system, student/teacher/course management, and update documentation
This commit is contained in:
parent
aab6d7f29a
commit
3e031c0e4d
179
README.md
179
README.md
@ -1,3 +1,178 @@
|
||||
# FastAPI Application
|
||||
# Role-Based School Management System with CBT
|
||||
|
||||
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
|
||||
This is a comprehensive school management system with role-based access control and a Computer-Based Testing (CBT) module built with FastAPI and SQLite.
|
||||
|
||||
## Features
|
||||
|
||||
- **User Management** - Register and manage users with different roles (admin, teacher, student)
|
||||
- **Role-Based Access Control** - Different permissions based on user roles
|
||||
- **Student Management** - Register, update, and track student information
|
||||
- **Teacher Management** - Manage teacher profiles and course assignments
|
||||
- **Course Management** - Create and manage courses, assign teachers
|
||||
- **Class Enrollment** - Enroll students in courses, track academic progress
|
||||
- **Computer-Based Testing (CBT)**:
|
||||
- **Exam Creation** - Create exams with different question types
|
||||
- **Question Management** - Add, update, and delete questions and answer options
|
||||
- **Exam Taking** - Students can take exams and submit answers
|
||||
- **Result Analysis** - Automatic grading and score calculation
|
||||
|
||||
## API Documentation
|
||||
|
||||
The API documentation is available at the `/docs` or `/redoc` endpoints when the application is running.
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### Authentication
|
||||
- `POST /api/v1/login/access-token` - Get JWT token
|
||||
- `POST /api/v1/login/test-token` - Test token validation
|
||||
|
||||
#### Users
|
||||
- `GET /api/v1/users/` - List users
|
||||
- `POST /api/v1/users/` - Create a new user
|
||||
- `GET /api/v1/users/{user_id}` - Get user details
|
||||
- `PUT /api/v1/users/{user_id}` - Update a user
|
||||
- `DELETE /api/v1/users/{user_id}` - Delete a user
|
||||
|
||||
#### Roles
|
||||
- `GET /api/v1/roles/` - List roles
|
||||
- `POST /api/v1/roles/` - Create a new role
|
||||
- `GET /api/v1/roles/{role_id}` - Get role details
|
||||
- `PUT /api/v1/roles/{role_id}` - Update a role
|
||||
- `DELETE /api/v1/roles/{role_id}` - Delete a role
|
||||
|
||||
#### Students
|
||||
- `GET /api/v1/students/` - List students
|
||||
- `POST /api/v1/students/` - Create a new student profile
|
||||
- `GET /api/v1/students/{student_id}` - Get student details
|
||||
- `PUT /api/v1/students/{student_id}` - Update a student
|
||||
- `DELETE /api/v1/students/{student_id}` - Delete a student
|
||||
- `GET /api/v1/students/{student_id}/exams` - Get student's exam results
|
||||
|
||||
#### Teachers
|
||||
- `GET /api/v1/teachers/` - List teachers
|
||||
- `POST /api/v1/teachers/` - Create a new teacher profile
|
||||
- `GET /api/v1/teachers/{teacher_id}` - Get teacher details
|
||||
- `PUT /api/v1/teachers/{teacher_id}` - Update a teacher
|
||||
- `DELETE /api/v1/teachers/{teacher_id}` - Delete a teacher
|
||||
- `GET /api/v1/teachers/{teacher_id}/courses` - Get courses taught by teacher
|
||||
|
||||
#### Courses
|
||||
- `GET /api/v1/courses/` - List courses
|
||||
- `POST /api/v1/courses/` - Create a new course
|
||||
- `GET /api/v1/courses/{course_id}` - Get course details
|
||||
- `PUT /api/v1/courses/{course_id}` - Update a course
|
||||
- `DELETE /api/v1/courses/{course_id}` - Delete a course
|
||||
- `GET /api/v1/courses/{course_id}/exams` - Get exams for a course
|
||||
|
||||
#### Enrollments
|
||||
- `GET /api/v1/enrollments/` - List enrollments
|
||||
- `POST /api/v1/enrollments/` - Create a new enrollment
|
||||
- `GET /api/v1/enrollments/{enrollment_id}` - Get enrollment details
|
||||
- `PUT /api/v1/enrollments/{enrollment_id}` - Update an enrollment
|
||||
- `DELETE /api/v1/enrollments/{enrollment_id}` - Delete an enrollment
|
||||
|
||||
#### Exams (CBT)
|
||||
- `GET /api/v1/exams/` - List exams
|
||||
- `GET /api/v1/exams/active` - Get active exams
|
||||
- `GET /api/v1/exams/available` - Get available exams
|
||||
- `POST /api/v1/exams/` - Create a new exam
|
||||
- `GET /api/v1/exams/{exam_id}` - Get exam details with questions
|
||||
- `GET /api/v1/exams/{exam_id}/creator` - Get exam with creator info
|
||||
- `PUT /api/v1/exams/{exam_id}` - Update an exam
|
||||
- `DELETE /api/v1/exams/{exam_id}` - Delete an exam
|
||||
|
||||
#### Questions (CBT)
|
||||
- `GET /api/v1/questions/` - List questions
|
||||
- `POST /api/v1/questions/` - Create a new question
|
||||
- `GET /api/v1/questions/{question_id}` - Get question with options
|
||||
- `PUT /api/v1/questions/{question_id}` - Update a question
|
||||
- `DELETE /api/v1/questions/{question_id}` - Delete a question
|
||||
|
||||
#### Question Options (CBT)
|
||||
- `GET /api/v1/options/` - List question options
|
||||
- `POST /api/v1/options/` - Create a new option
|
||||
- `GET /api/v1/options/{option_id}` - Get option details
|
||||
- `PUT /api/v1/options/{option_id}` - Update an option
|
||||
- `DELETE /api/v1/options/{option_id}` - Delete an option
|
||||
|
||||
#### Exam Results (CBT)
|
||||
- `GET /api/v1/exam-results/` - List exam results
|
||||
- `POST /api/v1/exam-results/` - Start a new exam attempt
|
||||
- `GET /api/v1/exam-results/{result_id}` - Get exam result with answers
|
||||
- `PUT /api/v1/exam-results/{result_id}` - Update an exam result
|
||||
- `POST /api/v1/exam-results/{result_id}/complete` - Complete an exam and calculate score
|
||||
- `DELETE /api/v1/exam-results/{result_id}` - Delete an exam result
|
||||
|
||||
### Health Check
|
||||
- `GET /health` - Check application health status
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The application uses the following environment variables:
|
||||
|
||||
- `SECRET_KEY` - Secret key for the application (default: auto-generated)
|
||||
- `JWT_SECRET_KEY` - Secret key for JWT token generation (default: same as SECRET_KEY)
|
||||
- `DATABASE_URL` - SQLite database URL (default: `sqlite:////app/storage/db/db.sqlite`)
|
||||
- `ENVIRONMENT` - Application environment (default: `development`)
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.8+
|
||||
- pip
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository
|
||||
2. Install the dependencies:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Run the migrations:
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
4. Start the application:
|
||||
```bash
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
||||
```
|
||||
|
||||
5. Open the API documentation at http://localhost:8000/docs
|
||||
|
||||
## Database Schema
|
||||
|
||||
The application uses SQLite as the database. The main entities are:
|
||||
|
||||
- **User** - Base user model with authentication details
|
||||
- **Role** - User roles (admin, teacher, student)
|
||||
- **Student** - Student profile linked to a user
|
||||
- **Teacher** - Teacher profile linked to a user
|
||||
- **Course** - Course details and teacher assignment
|
||||
- **ClassEnrollment** - Student enrollment in courses
|
||||
- **Exam** - CBT exam definitions
|
||||
- **Question** - Exam questions with different types
|
||||
- **QuestionOption** - Answer options for questions
|
||||
- **ExamResult** - Student exam attempts and scores
|
||||
- **StudentAnswer** - Student answers to questions
|
||||
|
||||
## CBT System
|
||||
|
||||
The Computer-Based Testing (CBT) system allows teachers to create exams with different question types, and students to take these exams online. The system supports:
|
||||
|
||||
- Multiple choice questions
|
||||
- True/False questions
|
||||
- Short answer questions
|
||||
- Essay questions
|
||||
|
||||
Exams can be time-limited, have specific availability periods, and include different scoring mechanisms. After an exam is completed, the system automatically calculates the score based on the correct answers.
|
||||
|
||||
## Security
|
||||
|
||||
The application uses JWT tokens for authentication and role-based authorization for controlling access to resources. Password hashing is implemented using bcrypt.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
@ -1,6 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.endpoints import login, users, roles, students, teachers, courses, enrollments
|
||||
from app.api.v1.endpoints import (
|
||||
login, users, roles, students, teachers, courses, enrollments,
|
||||
exams, questions, question_options, exam_results
|
||||
)
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(login.router, tags=["login"])
|
||||
@ -9,4 +12,8 @@ api_router.include_router(roles.router, prefix="/roles", tags=["roles"])
|
||||
api_router.include_router(students.router, prefix="/students", tags=["students"])
|
||||
api_router.include_router(teachers.router, prefix="/teachers", tags=["teachers"])
|
||||
api_router.include_router(courses.router, prefix="/courses", tags=["courses"])
|
||||
api_router.include_router(enrollments.router, prefix="/enrollments", tags=["enrollments"])
|
||||
api_router.include_router(enrollments.router, prefix="/enrollments", tags=["enrollments"])
|
||||
api_router.include_router(exams.router, prefix="/exams", tags=["exams"])
|
||||
api_router.include_router(questions.router, prefix="/questions", tags=["questions"])
|
||||
api_router.include_router(question_options.router, prefix="/options", tags=["question_options"])
|
||||
api_router.include_router(exam_results.router, prefix="/exam-results", tags=["exam_results"])
|
@ -0,0 +1,4 @@
|
||||
# Re-export all endpoint modules
|
||||
__all__ = ["login", "users", "roles", "exams", "questions", "question_options", "exam_results"]
|
||||
|
||||
from app.api.v1.endpoints import login, users, roles, exams, questions, question_options, exam_results
|
211
app/api/v1/endpoints/courses.py
Normal file
211
app/api/v1/endpoints/courses.py
Normal file
@ -0,0 +1,211 @@
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import models, schemas
|
||||
from app.api.v1.deps import get_db, get_current_active_user, get_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[schemas.Course])
|
||||
def read_courses(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
teacher_id: Optional[int] = None,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve courses.
|
||||
"""
|
||||
if teacher_id:
|
||||
# Filter courses by teacher_id
|
||||
courses = db.query(models.Course).filter(models.Course.teacher_id == teacher_id).offset(skip).limit(limit).all()
|
||||
else:
|
||||
# Get all courses
|
||||
courses = db.query(models.Course).offset(skip).limit(limit).all()
|
||||
|
||||
return courses
|
||||
|
||||
|
||||
@router.get("/{course_id}", response_model=schemas.CourseWithTeacher)
|
||||
def read_course(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
course_id: int = Path(..., title="The ID of the course to get"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get course by ID with teacher details.
|
||||
"""
|
||||
course = db.query(models.Course).filter(models.Course.id == course_id).first()
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Get teacher details if there is a teacher assigned
|
||||
teacher = None
|
||||
if course.teacher_id:
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.id == course.teacher_id).first()
|
||||
|
||||
# Create a combined response
|
||||
result = schemas.CourseWithTeacher.from_orm(course)
|
||||
if teacher:
|
||||
result.teacher = schemas.Teacher.from_orm(teacher)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/", response_model=schemas.Course)
|
||||
def create_course(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
course_in: schemas.CourseCreate,
|
||||
current_user: models.User = Depends(get_admin_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Create new course.
|
||||
"""
|
||||
# Check if course code is unique
|
||||
existing_course = db.query(models.Course).filter(models.Course.course_code == course_in.course_code).first()
|
||||
if existing_course:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Course with code {course_in.course_code} already exists",
|
||||
)
|
||||
|
||||
# Check if teacher exists if provided
|
||||
if course_in.teacher_id:
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.id == course_in.teacher_id).first()
|
||||
if not teacher:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Teacher with ID {course_in.teacher_id} not found",
|
||||
)
|
||||
|
||||
# Create the course
|
||||
db_course = models.Course(
|
||||
course_code=course_in.course_code,
|
||||
title=course_in.title,
|
||||
description=course_in.description,
|
||||
credits=course_in.credits,
|
||||
is_active=course_in.is_active,
|
||||
teacher_id=course_in.teacher_id,
|
||||
)
|
||||
db.add(db_course)
|
||||
db.commit()
|
||||
db.refresh(db_course)
|
||||
|
||||
return db_course
|
||||
|
||||
|
||||
@router.put("/{course_id}", response_model=schemas.Course)
|
||||
def update_course(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
course_id: int = Path(..., title="The ID of the course to update"),
|
||||
course_in: schemas.CourseUpdate,
|
||||
current_user: models.User = Depends(get_admin_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update a course.
|
||||
"""
|
||||
course = db.query(models.Course).filter(models.Course.id == course_id).first()
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Check if course code is unique if it's being updated
|
||||
if course_in.course_code and course_in.course_code != course.course_code:
|
||||
existing_course = db.query(models.Course).filter(models.Course.course_code == course_in.course_code).first()
|
||||
if existing_course:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Course with code {course_in.course_code} already exists",
|
||||
)
|
||||
|
||||
# Check if teacher exists if provided
|
||||
if course_in.teacher_id and course_in.teacher_id != course.teacher_id:
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.id == course_in.teacher_id).first()
|
||||
if not teacher:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Teacher with ID {course_in.teacher_id} not found",
|
||||
)
|
||||
|
||||
# Update course fields
|
||||
update_data = course_in.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(course, field, value)
|
||||
|
||||
db.add(course)
|
||||
db.commit()
|
||||
db.refresh(course)
|
||||
|
||||
return course
|
||||
|
||||
|
||||
@router.delete("/{course_id}", response_model=None, status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_course(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
course_id: int = Path(..., title="The ID of the course to delete"),
|
||||
current_user: models.User = Depends(get_admin_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Delete a course.
|
||||
"""
|
||||
course = db.query(models.Course).filter(models.Course.id == course_id).first()
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Check if there are any exams associated with this course
|
||||
exams = db.query(models.Exam).filter(models.Exam.course_id == course_id).first()
|
||||
if exams:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete course with associated exams",
|
||||
)
|
||||
|
||||
# Check if there are any enrollments for this course
|
||||
enrollments = db.query(models.ClassEnrollment).filter(models.ClassEnrollment.course_id == course_id).first()
|
||||
if enrollments:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete course with student enrollments",
|
||||
)
|
||||
|
||||
db.delete(course)
|
||||
db.commit()
|
||||
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.get("/{course_id}/exams", response_model=List[schemas.Exam])
|
||||
def read_course_exams(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
course_id: int = Path(..., title="The ID of the course to get exams for"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get all exams for a course.
|
||||
"""
|
||||
course = db.query(models.Course).filter(models.Course.id == course_id).first()
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
exams = db.query(models.Exam).filter(models.Exam.course_id == course_id).all()
|
||||
return exams
|
247
app/api/v1/endpoints/enrollments.py
Normal file
247
app/api/v1/endpoints/enrollments.py
Normal file
@ -0,0 +1,247 @@
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
|
||||
from app import crud, models, schemas
|
||||
from app.api.v1.deps import get_db, get_current_active_user, get_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[schemas.ClassEnrollment])
|
||||
def read_enrollments(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
student_id: Optional[int] = None,
|
||||
course_id: Optional[int] = None,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve class enrollments.
|
||||
"""
|
||||
# Regular users can only see their own enrollments if they are a student
|
||||
if not crud.user.is_admin(current_user):
|
||||
user_student = db.query(models.Student).filter(models.Student.user_id == current_user.id).first()
|
||||
if user_student:
|
||||
student_id = user_student.id
|
||||
else:
|
||||
# Check if they are a teacher, in which case they can see enrollments for their courses
|
||||
user_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not user_teacher:
|
||||
return []
|
||||
|
||||
# Get courses taught by this teacher
|
||||
teacher_courses = db.query(models.Course).filter(models.Course.teacher_id == user_teacher.id).all()
|
||||
if not teacher_courses:
|
||||
return []
|
||||
|
||||
course_ids = [course.id for course in teacher_courses]
|
||||
enrollments = db.query(models.ClassEnrollment).filter(
|
||||
models.ClassEnrollment.course_id.in_(course_ids)
|
||||
).offset(skip).limit(limit).all()
|
||||
return enrollments
|
||||
|
||||
# Apply filters
|
||||
query = db.query(models.ClassEnrollment)
|
||||
if student_id:
|
||||
query = query.filter(models.ClassEnrollment.student_id == student_id)
|
||||
if course_id:
|
||||
query = query.filter(models.ClassEnrollment.course_id == course_id)
|
||||
|
||||
enrollments = query.offset(skip).limit(limit).all()
|
||||
return enrollments
|
||||
|
||||
|
||||
@router.get("/{enrollment_id}", response_model=schemas.ClassEnrollmentWithDetails)
|
||||
def read_enrollment(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
enrollment_id: int = Path(..., title="The ID of the enrollment to get"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get class enrollment by ID with student and course details.
|
||||
"""
|
||||
enrollment = db.query(models.ClassEnrollment).filter(models.ClassEnrollment.id == enrollment_id).first()
|
||||
if not enrollment:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Class enrollment not found",
|
||||
)
|
||||
|
||||
# Check permissions
|
||||
if not crud.user.is_admin(current_user):
|
||||
# Students can only see their own enrollments
|
||||
user_student = db.query(models.Student).filter(models.Student.user_id == current_user.id).first()
|
||||
if user_student and user_student.id == enrollment.student_id:
|
||||
pass # OK, student is viewing their own enrollment
|
||||
else:
|
||||
# Check if they are a teacher for this course
|
||||
user_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not user_teacher:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not enough permissions to view this enrollment",
|
||||
)
|
||||
|
||||
course = db.query(models.Course).filter(
|
||||
models.Course.id == enrollment.course_id,
|
||||
models.Course.teacher_id == user_teacher.id
|
||||
).first()
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not enough permissions to view this enrollment",
|
||||
)
|
||||
|
||||
# Get student and course details
|
||||
student = db.query(models.Student).filter(models.Student.id == enrollment.student_id).first()
|
||||
course = db.query(models.Course).filter(models.Course.id == enrollment.course_id).first()
|
||||
|
||||
# Create a combined response
|
||||
result = schemas.ClassEnrollmentWithDetails.from_orm(enrollment)
|
||||
result.student = schemas.Student.from_orm(student) if student else None
|
||||
result.course = schemas.Course.from_orm(course) if course else None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/", response_model=schemas.ClassEnrollment)
|
||||
def create_enrollment(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
enrollment_in: schemas.ClassEnrollmentCreate,
|
||||
current_user: models.User = Depends(get_admin_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Create new class enrollment.
|
||||
"""
|
||||
# Check if student exists
|
||||
student = db.query(models.Student).filter(models.Student.id == enrollment_in.student_id).first()
|
||||
if not student:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Student with ID {enrollment_in.student_id} not found",
|
||||
)
|
||||
|
||||
# Check if course exists
|
||||
course = db.query(models.Course).filter(models.Course.id == enrollment_in.course_id).first()
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Course with ID {enrollment_in.course_id} not found",
|
||||
)
|
||||
|
||||
# Check if student is already enrolled in this course for this semester/academic year
|
||||
existing_enrollment = db.query(models.ClassEnrollment).filter(
|
||||
models.ClassEnrollment.student_id == enrollment_in.student_id,
|
||||
models.ClassEnrollment.course_id == enrollment_in.course_id,
|
||||
models.ClassEnrollment.semester == enrollment_in.semester,
|
||||
models.ClassEnrollment.academic_year == enrollment_in.academic_year,
|
||||
).first()
|
||||
if existing_enrollment:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Student is already enrolled in this course for {enrollment_in.semester} {enrollment_in.academic_year}",
|
||||
)
|
||||
|
||||
# Create enrollment with current date if not provided
|
||||
enrollment_data = enrollment_in.dict()
|
||||
if not enrollment_data.get("enrollment_date"):
|
||||
enrollment_data["enrollment_date"] = datetime.utcnow()
|
||||
|
||||
db_enrollment = models.ClassEnrollment(**enrollment_data)
|
||||
db.add(db_enrollment)
|
||||
db.commit()
|
||||
db.refresh(db_enrollment)
|
||||
|
||||
return db_enrollment
|
||||
|
||||
|
||||
@router.put("/{enrollment_id}", response_model=schemas.ClassEnrollment)
|
||||
def update_enrollment(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
enrollment_id: int = Path(..., title="The ID of the enrollment to update"),
|
||||
enrollment_in: schemas.ClassEnrollmentUpdate,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update a class enrollment.
|
||||
"""
|
||||
enrollment = db.query(models.ClassEnrollment).filter(models.ClassEnrollment.id == enrollment_id).first()
|
||||
if not enrollment:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Class enrollment not found",
|
||||
)
|
||||
|
||||
# Check permissions - only teachers of the course or admins can update enrollments
|
||||
if not crud.user.is_admin(current_user):
|
||||
user_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not user_teacher:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not enough permissions to update this enrollment",
|
||||
)
|
||||
|
||||
course = db.query(models.Course).filter(
|
||||
models.Course.id == enrollment.course_id,
|
||||
models.Course.teacher_id == user_teacher.id
|
||||
).first()
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not enough permissions to update this enrollment",
|
||||
)
|
||||
|
||||
# Update enrollment fields
|
||||
update_data = enrollment_in.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(enrollment, field, value)
|
||||
|
||||
db.add(enrollment)
|
||||
db.commit()
|
||||
db.refresh(enrollment)
|
||||
|
||||
return enrollment
|
||||
|
||||
|
||||
@router.delete("/{enrollment_id}", response_model=None, status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_enrollment(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
enrollment_id: int = Path(..., title="The ID of the enrollment to delete"),
|
||||
current_user: models.User = Depends(get_admin_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Delete a class enrollment.
|
||||
"""
|
||||
enrollment = db.query(models.ClassEnrollment).filter(models.ClassEnrollment.id == enrollment_id).first()
|
||||
if not enrollment:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Class enrollment not found",
|
||||
)
|
||||
|
||||
# Check if there are any exam results for this student in this course
|
||||
exam_results = db.query(models.ExamResult).join(
|
||||
models.Exam, models.Exam.id == models.ExamResult.exam_id
|
||||
).filter(
|
||||
models.ExamResult.student_id == enrollment.student_id,
|
||||
models.Exam.course_id == enrollment.course_id
|
||||
).first()
|
||||
|
||||
if exam_results:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete enrollment with associated exam results",
|
||||
)
|
||||
|
||||
db.delete(enrollment)
|
||||
db.commit()
|
||||
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
289
app/api/v1/endpoints/exam_results.py
Normal file
289
app/api/v1/endpoints/exam_results.py
Normal file
@ -0,0 +1,289 @@
|
||||
from typing import Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import crud, models, schemas
|
||||
from app.api.v1.deps import get_db, get_current_active_user, get_teacher_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[schemas.ExamResult])
|
||||
def read_exam_results(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
exam_id: Optional[int] = None,
|
||||
student_id: Optional[int] = None,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve exam results.
|
||||
"""
|
||||
# Check permissions
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
is_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first() is not None
|
||||
|
||||
# Students can only see their own results
|
||||
if not (is_admin or is_teacher):
|
||||
student = db.query(models.Student).filter(models.Student.user_id == current_user.id).first()
|
||||
if not student:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to view these exam results",
|
||||
)
|
||||
student_id = student.id
|
||||
|
||||
# Apply filters
|
||||
if exam_id and student_id:
|
||||
exam_result = crud.exam_result.get_by_student_and_exam(
|
||||
db, student_id=student_id, exam_id=exam_id
|
||||
)
|
||||
return [exam_result] if exam_result else []
|
||||
elif exam_id:
|
||||
return crud.exam_result.get_by_exam(db, exam_id=exam_id)
|
||||
elif student_id:
|
||||
return crud.exam_result.get_by_student(db, student_id=student_id)
|
||||
else:
|
||||
return crud.exam_result.get_multi(db, skip=skip, limit=limit)
|
||||
|
||||
|
||||
@router.post("/", response_model=schemas.ExamResult)
|
||||
def create_exam_result(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
exam_result_in: schemas.ExamResultCreate,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Create new exam result (start an exam).
|
||||
"""
|
||||
# Check if the exam exists
|
||||
exam = crud.exam.get(db=db, id=exam_result_in.exam_id)
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Exam with ID {exam_result_in.exam_id} not found",
|
||||
)
|
||||
|
||||
# Check if the student exists
|
||||
student = crud.student.get(db=db, id=exam_result_in.student_id)
|
||||
if not student:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Student with ID {exam_result_in.student_id} not found",
|
||||
)
|
||||
|
||||
# Check if the current user is the student or has permission
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
is_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first() is not None
|
||||
|
||||
if not (is_admin or is_teacher):
|
||||
if student.user_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to start an exam for this student",
|
||||
)
|
||||
|
||||
# Check if the exam is available
|
||||
now = datetime.utcnow()
|
||||
if not exam.is_active:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="This exam is not active",
|
||||
)
|
||||
|
||||
if exam.start_time and exam.start_time > now:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="This exam is not yet available",
|
||||
)
|
||||
|
||||
if exam.end_time and exam.end_time < now:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="This exam has expired",
|
||||
)
|
||||
|
||||
# Check if there's already an active attempt
|
||||
existing_attempt = crud.exam_result.get_active_exam_attempt(
|
||||
db, student_id=student.id, exam_id=exam.id
|
||||
)
|
||||
if existing_attempt:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="There is already an active attempt for this exam",
|
||||
)
|
||||
|
||||
# Create the exam result
|
||||
exam_result = crud.exam_result.create(db=db, obj_in=exam_result_in)
|
||||
return exam_result
|
||||
|
||||
|
||||
@router.get("/{result_id}", response_model=schemas.ExamResultWithAnswers)
|
||||
def read_exam_result(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
result_id: int = Path(..., title="The ID of the exam result to get"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get exam result by ID with answers.
|
||||
"""
|
||||
exam_result = crud.exam_result.get(db=db, id=result_id)
|
||||
if not exam_result:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam result not found",
|
||||
)
|
||||
|
||||
# Check permissions
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
is_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first() is not None
|
||||
|
||||
# Students can only see their own results
|
||||
if not (is_admin or is_teacher):
|
||||
student = db.query(models.Student).filter(models.Student.user_id == current_user.id).first()
|
||||
if not student or student.id != exam_result.student_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to view this exam result",
|
||||
)
|
||||
|
||||
# Get answers for this exam result
|
||||
answers = crud.student_answer.get_by_exam_result(db=db, exam_result_id=result_id)
|
||||
|
||||
# Create a combined response
|
||||
result = schemas.ExamResultWithAnswers.from_orm(exam_result)
|
||||
result.answers = answers
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/{result_id}", response_model=schemas.ExamResult)
|
||||
def update_exam_result(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
result_id: int = Path(..., title="The ID of the exam result to update"),
|
||||
result_in: schemas.ExamResultUpdate,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update an exam result.
|
||||
"""
|
||||
exam_result = crud.exam_result.get(db=db, id=result_id)
|
||||
if not exam_result:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam result not found",
|
||||
)
|
||||
|
||||
# Check permissions
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
is_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first() is not None
|
||||
|
||||
# Only teacher and admin can update results directly
|
||||
if not (is_admin or is_teacher):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to update this exam result",
|
||||
)
|
||||
|
||||
exam_result = crud.exam_result.update(db=db, db_obj=exam_result, obj_in=result_in)
|
||||
return exam_result
|
||||
|
||||
|
||||
@router.post("/{result_id}/complete", response_model=schemas.ExamResult)
|
||||
def complete_exam(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
result_id: int = Path(..., title="The ID of the exam result to complete"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Complete an exam and calculate the score.
|
||||
"""
|
||||
exam_result = crud.exam_result.get(db=db, id=result_id)
|
||||
if not exam_result:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam result not found",
|
||||
)
|
||||
|
||||
# Check if already completed
|
||||
if exam_result.is_completed:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="This exam is already completed",
|
||||
)
|
||||
|
||||
# Check permissions
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
is_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first() is not None
|
||||
|
||||
# Students can only complete their own exams
|
||||
if not (is_admin or is_teacher):
|
||||
student = db.query(models.Student).filter(models.Student.user_id == current_user.id).first()
|
||||
if not student or student.id != exam_result.student_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to complete this exam",
|
||||
)
|
||||
|
||||
# Get the exam details
|
||||
exam = crud.exam.get(db=db, id=exam_result.exam_id)
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam not found",
|
||||
)
|
||||
|
||||
# Get all questions for this exam
|
||||
questions = crud.question.get_by_exam_id(db=db, exam_id=exam.id)
|
||||
|
||||
# Calculate the maximum possible score
|
||||
max_score = sum(q.points for q in questions)
|
||||
|
||||
# Get all student answers for this exam
|
||||
answers = crud.student_answer.get_by_exam_result(db=db, exam_result_id=exam_result.id)
|
||||
|
||||
# Calculate the score
|
||||
score = sum(a.points_earned for a in answers)
|
||||
|
||||
# Complete the exam
|
||||
exam_result = crud.exam_result.complete_exam(
|
||||
db=db, db_obj=exam_result, score=score, max_score=max_score
|
||||
)
|
||||
|
||||
return exam_result
|
||||
|
||||
|
||||
@router.delete("/{result_id}", response_model=None, status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_exam_result(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
result_id: int = Path(..., title="The ID of the exam result to delete"),
|
||||
current_user: models.User = Depends(get_teacher_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Delete an exam result.
|
||||
"""
|
||||
exam_result = crud.exam_result.get(db=db, id=result_id)
|
||||
if not exam_result:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam result not found",
|
||||
)
|
||||
|
||||
# Only admin can delete results
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
if not is_admin:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Only administrators can delete exam results",
|
||||
)
|
||||
|
||||
crud.exam_result.remove(db=db, id=result_id)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
209
app/api/v1/endpoints/exams.py
Normal file
209
app/api/v1/endpoints/exams.py
Normal file
@ -0,0 +1,209 @@
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import crud, models, schemas
|
||||
from app.api.v1.deps import get_db, get_current_active_user, get_teacher_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[schemas.Exam])
|
||||
def read_exams(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
course_id: Optional[int] = None,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve exams.
|
||||
"""
|
||||
if course_id:
|
||||
exams = crud.exam.get_by_course_id(db, course_id=course_id)
|
||||
else:
|
||||
exams = crud.exam.get_multi(db, skip=skip, limit=limit)
|
||||
return exams
|
||||
|
||||
|
||||
@router.get("/active", response_model=List[schemas.Exam])
|
||||
def read_active_exams(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve active exams.
|
||||
"""
|
||||
exams = crud.exam.get_active_exams(db, skip=skip, limit=limit)
|
||||
return exams
|
||||
|
||||
|
||||
@router.get("/available", response_model=List[schemas.Exam])
|
||||
def read_available_exams(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve available exams (active and within time window).
|
||||
"""
|
||||
exams = crud.exam.get_available_exams(db, skip=skip, limit=limit)
|
||||
return exams
|
||||
|
||||
|
||||
@router.post("/", response_model=schemas.Exam)
|
||||
def create_exam(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
exam_in: schemas.ExamCreate,
|
||||
current_user: models.User = Depends(get_teacher_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Create new exam.
|
||||
"""
|
||||
# Check if the course exists
|
||||
course = db.query(models.Course).filter(models.Course.id == exam_in.course_id).first()
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Course with ID {exam_in.course_id} not found",
|
||||
)
|
||||
|
||||
# Check if the user is a teacher of this course or an admin
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
|
||||
if not is_admin:
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not teacher or teacher.id != course.teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to create an exam for this course",
|
||||
)
|
||||
|
||||
# Set the created_by field to the current user's ID
|
||||
exam_data = exam_in.dict()
|
||||
exam_data["created_by"] = current_user.id
|
||||
exam_obj = schemas.ExamCreate(**exam_data)
|
||||
|
||||
exam = crud.exam.create(db=db, obj_in=exam_obj)
|
||||
return exam
|
||||
|
||||
|
||||
@router.get("/{exam_id}", response_model=schemas.ExamWithQuestions)
|
||||
def read_exam(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
exam_id: int,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get exam by ID with questions.
|
||||
"""
|
||||
exam = crud.exam.get(db=db, id=exam_id)
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam not found",
|
||||
)
|
||||
|
||||
# Get questions for this exam
|
||||
questions = crud.question.get_by_exam_id(db=db, exam_id=exam_id)
|
||||
|
||||
# Create a combined response
|
||||
result = schemas.ExamWithQuestions.from_orm(exam)
|
||||
result.questions = questions
|
||||
|
||||
return result
|
||||
|
||||
@router.get("/{exam_id}/creator", response_model=schemas.ExamWithCreator)
|
||||
def read_exam_with_creator(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
exam_id: int,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get exam by ID with creator information.
|
||||
"""
|
||||
exam = crud.exam.get(db=db, id=exam_id)
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam not found",
|
||||
)
|
||||
|
||||
# Create a combined response
|
||||
result = schemas.ExamWithCreator.from_orm(exam)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/{exam_id}", response_model=schemas.Exam)
|
||||
def update_exam(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
exam_id: int,
|
||||
exam_in: schemas.ExamUpdate,
|
||||
current_user: models.User = Depends(get_teacher_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update an exam.
|
||||
"""
|
||||
exam = crud.exam.get(db=db, id=exam_id)
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam not found",
|
||||
)
|
||||
|
||||
# Check if the user is a teacher of this course or an admin
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
|
||||
if not is_admin:
|
||||
course = db.query(models.Course).filter(models.Course.id == exam.course_id).first()
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not teacher or not course or teacher.id != course.teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to update this exam",
|
||||
)
|
||||
|
||||
exam = crud.exam.update(db=db, db_obj=exam, obj_in=exam_in)
|
||||
return exam
|
||||
|
||||
|
||||
@router.delete("/{exam_id}", response_model=schemas.Exam)
|
||||
def delete_exam(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
exam_id: int,
|
||||
current_user: models.User = Depends(get_teacher_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Delete an exam.
|
||||
"""
|
||||
exam = crud.exam.get(db=db, id=exam_id)
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam not found",
|
||||
)
|
||||
|
||||
# Check if the user is a teacher of this course or an admin
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
|
||||
if not is_admin:
|
||||
course = db.query(models.Course).filter(models.Course.id == exam.course_id).first()
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not teacher or not course or teacher.id != course.teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to delete this exam",
|
||||
)
|
||||
|
||||
exam = crud.exam.remove(db=db, id=exam_id)
|
||||
return exam
|
202
app/api/v1/endpoints/question_options.py
Normal file
202
app/api/v1/endpoints/question_options.py
Normal file
@ -0,0 +1,202 @@
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import crud, models, schemas
|
||||
from app.api.v1.deps import get_db, get_current_active_user, get_teacher_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[schemas.QuestionOption])
|
||||
def read_options(
|
||||
db: Session = Depends(get_db),
|
||||
question_id: Optional[int] = None,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve question options.
|
||||
"""
|
||||
if question_id:
|
||||
options = crud.question_option.get_by_question_id(db, question_id=question_id)
|
||||
else:
|
||||
options = crud.question_option.get_multi(db)
|
||||
return options
|
||||
|
||||
|
||||
@router.post("/", response_model=schemas.QuestionOption)
|
||||
def create_option(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
option_in: schemas.QuestionOptionCreate,
|
||||
current_user: models.User = Depends(get_teacher_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Create new question option.
|
||||
"""
|
||||
# Check if the question exists
|
||||
question = crud.question.get(db=db, id=option_in.question_id)
|
||||
if not question:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Question with ID {option_in.question_id} not found",
|
||||
)
|
||||
|
||||
# Check if the user is a teacher of this course or an admin
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
|
||||
if not is_admin:
|
||||
exam = db.query(models.Exam).filter(models.Exam.id == question.exam_id).first()
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam not found",
|
||||
)
|
||||
|
||||
course = db.query(models.Course).filter(models.Course.id == exam.course_id).first()
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not teacher or not course or teacher.id != course.teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to create an option for this question",
|
||||
)
|
||||
|
||||
# If this is marked as correct, check if there are other correct options
|
||||
if option_in.is_correct and question.question_type != schemas.QuestionType.MULTIPLE_CHOICE:
|
||||
existing_correct = crud.question_option.get_correct_option(db=db, question_id=question.id)
|
||||
if existing_correct:
|
||||
# For non-multiple-choice questions, only one option can be correct
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"This question type ({question.question_type}) can only have one correct option",
|
||||
)
|
||||
|
||||
option = crud.question_option.create(db=db, obj_in=option_in)
|
||||
return option
|
||||
|
||||
|
||||
@router.get("/{option_id}", response_model=schemas.QuestionOption)
|
||||
def read_option(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
option_id: int = Path(..., title="The ID of the option to get"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get question option by ID.
|
||||
"""
|
||||
option = crud.question_option.get(db=db, id=option_id)
|
||||
if not option:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Question option not found",
|
||||
)
|
||||
return option
|
||||
|
||||
|
||||
@router.put("/{option_id}", response_model=schemas.QuestionOption)
|
||||
def update_option(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
option_id: int = Path(..., title="The ID of the option to update"),
|
||||
option_in: schemas.QuestionOptionUpdate,
|
||||
current_user: models.User = Depends(get_teacher_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update a question option.
|
||||
"""
|
||||
option = crud.question_option.get(db=db, id=option_id)
|
||||
if not option:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Question option not found",
|
||||
)
|
||||
|
||||
# Check if the user is a teacher of this course or an admin
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
|
||||
if not is_admin:
|
||||
question = db.query(models.Question).filter(models.Question.id == option.question_id).first()
|
||||
if not question:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Question not found",
|
||||
)
|
||||
|
||||
exam = db.query(models.Exam).filter(models.Exam.id == question.exam_id).first()
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam not found",
|
||||
)
|
||||
|
||||
course = db.query(models.Course).filter(models.Course.id == exam.course_id).first()
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not teacher or not course or teacher.id != course.teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to update this question option",
|
||||
)
|
||||
|
||||
# If changing to correct, check if there are other correct options
|
||||
if option_in.is_correct is not None and option_in.is_correct:
|
||||
question = db.query(models.Question).filter(models.Question.id == option.question_id).first()
|
||||
if question and question.question_type != schemas.QuestionType.MULTIPLE_CHOICE:
|
||||
existing_correct = crud.question_option.get_correct_option(db=db, question_id=question.id)
|
||||
if existing_correct and existing_correct.id != option_id:
|
||||
# For non-multiple-choice questions, only one option can be correct
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"This question type ({question.question_type}) can only have one correct option",
|
||||
)
|
||||
|
||||
option = crud.question_option.update(db=db, db_obj=option, obj_in=option_in)
|
||||
return option
|
||||
|
||||
|
||||
@router.delete("/{option_id}", response_model=schemas.QuestionOption)
|
||||
def delete_option(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
option_id: int = Path(..., title="The ID of the option to delete"),
|
||||
current_user: models.User = Depends(get_teacher_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Delete a question option.
|
||||
"""
|
||||
option = crud.question_option.get(db=db, id=option_id)
|
||||
if not option:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Question option not found",
|
||||
)
|
||||
|
||||
# Check if the user is a teacher of this course or an admin
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
|
||||
if not is_admin:
|
||||
question = db.query(models.Question).filter(models.Question.id == option.question_id).first()
|
||||
if not question:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Question not found",
|
||||
)
|
||||
|
||||
exam = db.query(models.Exam).filter(models.Exam.id == question.exam_id).first()
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam not found",
|
||||
)
|
||||
|
||||
course = db.query(models.Course).filter(models.Course.id == exam.course_id).first()
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not teacher or not course or teacher.id != course.teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to delete this question option",
|
||||
)
|
||||
|
||||
option = crud.question_option.remove(db=db, id=option_id)
|
||||
return option
|
169
app/api/v1/endpoints/questions.py
Normal file
169
app/api/v1/endpoints/questions.py
Normal file
@ -0,0 +1,169 @@
|
||||
from typing import Any, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import crud, models, schemas
|
||||
from app.api.v1.deps import get_db, get_current_active_user, get_teacher_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[schemas.Question])
|
||||
def read_questions(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
exam_id: Optional[int] = None,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve questions.
|
||||
"""
|
||||
if exam_id:
|
||||
questions = crud.question.get_by_exam_id(db, exam_id=exam_id)
|
||||
else:
|
||||
questions = crud.question.get_multi(db, skip=skip, limit=limit)
|
||||
return questions
|
||||
|
||||
|
||||
@router.post("/", response_model=schemas.Question)
|
||||
def create_question(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
question_in: schemas.QuestionCreate,
|
||||
current_user: models.User = Depends(get_teacher_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Create new question.
|
||||
"""
|
||||
# Check if the exam exists
|
||||
exam = crud.exam.get(db=db, id=question_in.exam_id)
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Exam with ID {question_in.exam_id} not found",
|
||||
)
|
||||
|
||||
# Check if the user is a teacher of this course or an admin
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
|
||||
if not is_admin:
|
||||
course = db.query(models.Course).filter(models.Course.id == exam.course_id).first()
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not teacher or not course or teacher.id != course.teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to create a question for this exam",
|
||||
)
|
||||
|
||||
question = crud.question.create(db=db, obj_in=question_in)
|
||||
return question
|
||||
|
||||
|
||||
@router.get("/{question_id}", response_model=schemas.QuestionWithOptions)
|
||||
def read_question(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
question_id: int = Path(..., title="The ID of the question to get"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get question by ID with options.
|
||||
"""
|
||||
question = crud.question.get(db=db, id=question_id)
|
||||
if not question:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Question not found",
|
||||
)
|
||||
|
||||
# Get options for this question
|
||||
options = crud.question_option.get_by_question_id(db=db, question_id=question_id)
|
||||
|
||||
# Create a combined response
|
||||
result = schemas.QuestionWithOptions.from_orm(question)
|
||||
result.options = options
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.put("/{question_id}", response_model=schemas.Question)
|
||||
def update_question(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
question_id: int = Path(..., title="The ID of the question to update"),
|
||||
question_in: schemas.QuestionUpdate,
|
||||
current_user: models.User = Depends(get_teacher_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update a question.
|
||||
"""
|
||||
question = crud.question.get(db=db, id=question_id)
|
||||
if not question:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Question not found",
|
||||
)
|
||||
|
||||
# Check if the user is a teacher of this course or an admin
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
|
||||
if not is_admin:
|
||||
exam = db.query(models.Exam).filter(models.Exam.id == question.exam_id).first()
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam not found",
|
||||
)
|
||||
|
||||
course = db.query(models.Course).filter(models.Course.id == exam.course_id).first()
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not teacher or not course or teacher.id != course.teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to update this question",
|
||||
)
|
||||
|
||||
question = crud.question.update(db=db, db_obj=question, obj_in=question_in)
|
||||
return question
|
||||
|
||||
|
||||
@router.delete("/{question_id}", response_model=schemas.Question)
|
||||
def delete_question(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
question_id: int = Path(..., title="The ID of the question to delete"),
|
||||
current_user: models.User = Depends(get_teacher_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Delete a question.
|
||||
"""
|
||||
question = crud.question.get(db=db, id=question_id)
|
||||
if not question:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Question not found",
|
||||
)
|
||||
|
||||
# Check if the user is a teacher of this course or an admin
|
||||
is_admin = any(role.name == "admin" for role in db.query(models.Role).all() if role.id == current_user.role_id)
|
||||
|
||||
if not is_admin:
|
||||
exam = db.query(models.Exam).filter(models.Exam.id == question.exam_id).first()
|
||||
if not exam:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Exam not found",
|
||||
)
|
||||
|
||||
course = db.query(models.Course).filter(models.Course.id == exam.course_id).first()
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
if not teacher or not course or teacher.id != course.teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You don't have permission to delete this question",
|
||||
)
|
||||
|
||||
question = crud.question.remove(db=db, id=question_id)
|
||||
return question
|
188
app/api/v1/endpoints/students.py
Normal file
188
app/api/v1/endpoints/students.py
Normal file
@ -0,0 +1,188 @@
|
||||
from typing import Any, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import crud, models, schemas
|
||||
from app.api.v1.deps import get_db, get_current_active_user, get_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[schemas.Student])
|
||||
def read_students(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve students.
|
||||
"""
|
||||
# Regular users can only see their own student profile if they are a student
|
||||
if crud.user.is_admin(current_user):
|
||||
students = crud.student.get_multi(db, skip=skip, limit=limit)
|
||||
else:
|
||||
student = db.query(models.Student).filter(models.Student.user_id == current_user.id).first()
|
||||
students = [student] if student else []
|
||||
|
||||
return students
|
||||
|
||||
|
||||
@router.get("/{student_id}", response_model=schemas.StudentWithUser)
|
||||
def read_student(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
student_id: int = Path(..., title="The ID of the student to get"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get student by ID with user details.
|
||||
"""
|
||||
student = crud.student.get(db=db, id=student_id)
|
||||
if not student:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Student not found",
|
||||
)
|
||||
|
||||
# Regular users can only see their own student profile
|
||||
if not crud.user.is_admin(current_user) and student.user_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not enough permissions to view this student profile"
|
||||
)
|
||||
|
||||
# Get user details
|
||||
user = crud.user.get(db=db, id=student.user_id)
|
||||
|
||||
# Create a combined response
|
||||
result = schemas.StudentWithUser.from_orm(student)
|
||||
result.user = user
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/", response_model=schemas.Student)
|
||||
def create_student(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
student_in: schemas.StudentCreate,
|
||||
current_user: models.User = Depends(get_admin_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Create new student.
|
||||
"""
|
||||
# Check if the user exists
|
||||
user = crud.user.get(db=db, id=student_in.user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"User with ID {student_in.user_id} not found",
|
||||
)
|
||||
|
||||
# Check if student profile already exists for this user
|
||||
existing_student = db.query(models.Student).filter(models.Student.user_id == user.id).first()
|
||||
if existing_student:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Student profile already exists for user ID {user.id}",
|
||||
)
|
||||
|
||||
# Check if student_id is unique
|
||||
existing_student_id = db.query(models.Student).filter(models.Student.student_id == student_in.student_id).first()
|
||||
if existing_student_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Student ID {student_in.student_id} is already taken",
|
||||
)
|
||||
|
||||
student = crud.student.create(db=db, obj_in=student_in)
|
||||
return student
|
||||
|
||||
|
||||
@router.put("/{student_id}", response_model=schemas.Student)
|
||||
def update_student(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
student_id: int = Path(..., title="The ID of the student to update"),
|
||||
student_in: schemas.StudentUpdate,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update a student.
|
||||
"""
|
||||
student = crud.student.get(db=db, id=student_id)
|
||||
if not student:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Student not found",
|
||||
)
|
||||
|
||||
# Regular users can only update their own student profile
|
||||
if not crud.user.is_admin(current_user) and student.user_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not enough permissions to update this student profile"
|
||||
)
|
||||
|
||||
# Check if student_id is unique if it's being updated
|
||||
if student_in.student_id and student_in.student_id != student.student_id:
|
||||
existing_student_id = db.query(models.Student).filter(models.Student.student_id == student_in.student_id).first()
|
||||
if existing_student_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Student ID {student_in.student_id} is already taken",
|
||||
)
|
||||
|
||||
student = crud.student.update(db=db, db_obj=student, obj_in=student_in)
|
||||
return student
|
||||
|
||||
|
||||
@router.delete("/{student_id}", response_model=None, status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_student(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
student_id: int = Path(..., title="The ID of the student to delete"),
|
||||
current_user: models.User = Depends(get_admin_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Delete a student.
|
||||
"""
|
||||
student = crud.student.get(db=db, id=student_id)
|
||||
if not student:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Student not found",
|
||||
)
|
||||
|
||||
crud.student.remove(db=db, id=student_id)
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.get("/{student_id}/exams", response_model=List[schemas.ExamResult])
|
||||
def read_student_exam_results(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
student_id: int = Path(..., title="The ID of the student to get exam results for"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get all exam results for a student.
|
||||
"""
|
||||
student = crud.student.get(db=db, id=student_id)
|
||||
if not student:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Student not found",
|
||||
)
|
||||
|
||||
# Regular users can only see their own exam results
|
||||
if not crud.user.is_admin(current_user) and student.user_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not enough permissions to view this student's exam results"
|
||||
)
|
||||
|
||||
exam_results = crud.exam_result.get_by_student(db=db, student_id=student_id)
|
||||
return exam_results
|
204
app/api/v1/endpoints/teachers.py
Normal file
204
app/api/v1/endpoints/teachers.py
Normal file
@ -0,0 +1,204 @@
|
||||
from typing import Any, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Path, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app import crud, models, schemas
|
||||
from app.api.v1.deps import get_db, get_current_active_user, get_admin_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[schemas.Teacher])
|
||||
def read_teachers(
|
||||
db: Session = Depends(get_db),
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Retrieve teachers.
|
||||
"""
|
||||
# Regular users can only see their own teacher profile if they are a teacher
|
||||
if crud.user.is_admin(current_user):
|
||||
teachers = db.query(models.Teacher).offset(skip).limit(limit).all()
|
||||
else:
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.user_id == current_user.id).first()
|
||||
teachers = [teacher] if teacher else []
|
||||
|
||||
return teachers
|
||||
|
||||
|
||||
@router.get("/{teacher_id}", response_model=schemas.TeacherWithUser)
|
||||
def read_teacher(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
teacher_id: int = Path(..., title="The ID of the teacher to get"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get teacher by ID.
|
||||
"""
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.id == teacher_id).first()
|
||||
if not teacher:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Teacher not found",
|
||||
)
|
||||
|
||||
# Regular users can only see their own teacher profile
|
||||
if not crud.user.is_admin(current_user) and teacher.user_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not enough permissions to view this teacher profile"
|
||||
)
|
||||
|
||||
# Get user details
|
||||
user = crud.user.get(db=db, id=teacher.user_id)
|
||||
|
||||
# Create a combined response
|
||||
result = schemas.TeacherWithUser.from_orm(teacher)
|
||||
result.user = user
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@router.post("/", response_model=schemas.Teacher)
|
||||
def create_teacher(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
teacher_in: schemas.teacher.TeacherCreate,
|
||||
current_user: models.User = Depends(get_admin_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Create new teacher.
|
||||
"""
|
||||
# Check if the user exists
|
||||
user = crud.user.get(db=db, id=teacher_in.user_id)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"User with ID {teacher_in.user_id} not found",
|
||||
)
|
||||
|
||||
# Check if teacher profile already exists for this user
|
||||
existing_teacher = db.query(models.Teacher).filter(models.Teacher.user_id == user.id).first()
|
||||
if existing_teacher:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Teacher profile already exists for user ID {user.id}",
|
||||
)
|
||||
|
||||
# Check if teacher_id is unique
|
||||
existing_teacher_id = db.query(models.Teacher).filter(models.Teacher.teacher_id == teacher_in.teacher_id).first()
|
||||
if existing_teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Teacher ID {teacher_in.teacher_id} is already taken",
|
||||
)
|
||||
|
||||
# Create teacher profile
|
||||
db_teacher = models.Teacher(
|
||||
teacher_id=teacher_in.teacher_id,
|
||||
date_of_birth=teacher_in.date_of_birth,
|
||||
address=teacher_in.address,
|
||||
phone_number=teacher_in.phone_number,
|
||||
department=teacher_in.department,
|
||||
hire_date=teacher_in.hire_date,
|
||||
user_id=teacher_in.user_id,
|
||||
)
|
||||
db.add(db_teacher)
|
||||
db.commit()
|
||||
db.refresh(db_teacher)
|
||||
|
||||
return db_teacher
|
||||
|
||||
|
||||
@router.put("/{teacher_id}", response_model=schemas.Teacher)
|
||||
def update_teacher(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
teacher_id: int = Path(..., title="The ID of the teacher to update"),
|
||||
teacher_in: schemas.teacher.TeacherUpdate,
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Update a teacher.
|
||||
"""
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.id == teacher_id).first()
|
||||
if not teacher:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Teacher not found",
|
||||
)
|
||||
|
||||
# Regular users can only update their own teacher profile
|
||||
if not crud.user.is_admin(current_user) and teacher.user_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="Not enough permissions to update this teacher profile"
|
||||
)
|
||||
|
||||
# Check if teacher_id is unique if it's being updated
|
||||
if teacher_in.teacher_id and teacher_in.teacher_id != teacher.teacher_id:
|
||||
existing_teacher_id = db.query(models.Teacher).filter(models.Teacher.teacher_id == teacher_in.teacher_id).first()
|
||||
if existing_teacher_id:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Teacher ID {teacher_in.teacher_id} is already taken",
|
||||
)
|
||||
|
||||
# Update teacher fields
|
||||
update_data = teacher_in.dict(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(teacher, field, value)
|
||||
|
||||
db.add(teacher)
|
||||
db.commit()
|
||||
db.refresh(teacher)
|
||||
|
||||
return teacher
|
||||
|
||||
|
||||
@router.delete("/{teacher_id}", response_model=None, status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_teacher(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
teacher_id: int = Path(..., title="The ID of the teacher to delete"),
|
||||
current_user: models.User = Depends(get_admin_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Delete a teacher.
|
||||
"""
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.id == teacher_id).first()
|
||||
if not teacher:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Teacher not found",
|
||||
)
|
||||
|
||||
db.delete(teacher)
|
||||
db.commit()
|
||||
|
||||
return Response(status_code=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@router.get("/{teacher_id}/courses", response_model=List[schemas.Course])
|
||||
def read_teacher_courses(
|
||||
*,
|
||||
db: Session = Depends(get_db),
|
||||
teacher_id: int = Path(..., title="The ID of the teacher to get courses for"),
|
||||
current_user: models.User = Depends(get_current_active_user),
|
||||
) -> Any:
|
||||
"""
|
||||
Get all courses taught by a teacher.
|
||||
"""
|
||||
teacher = db.query(models.Teacher).filter(models.Teacher.id == teacher_id).first()
|
||||
if not teacher:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Teacher not found",
|
||||
)
|
||||
|
||||
courses = db.query(models.Course).filter(models.Course.teacher_id == teacher_id).all()
|
||||
return courses
|
@ -25,6 +25,9 @@ class Settings(BaseSettings):
|
||||
|
||||
PROJECT_NAME: str = "Role-Based School Management System"
|
||||
|
||||
# Environment
|
||||
ENVIRONMENT: str = os.environ.get("ENVIRONMENT", "development")
|
||||
|
||||
# Database configuration
|
||||
SQLALCHEMY_DATABASE_URL: str = os.environ.get(
|
||||
"DATABASE_URL", "sqlite:////app/storage/db/db.sqlite"
|
||||
|
@ -1,5 +1,19 @@
|
||||
from app.crud.crud_user import user
|
||||
from app.crud.crud_role import role
|
||||
from app.crud.crud_student import student
|
||||
from app.crud.crud_exam import exam
|
||||
from app.crud.crud_question import question
|
||||
from app.crud.crud_question_option import question_option
|
||||
from app.crud.crud_exam_result import exam_result
|
||||
from app.crud.crud_student_answer import student_answer
|
||||
|
||||
__all__ = ["user", "role", "student"]
|
||||
__all__ = [
|
||||
"user",
|
||||
"role",
|
||||
"student",
|
||||
"exam",
|
||||
"question",
|
||||
"question_option",
|
||||
"exam_result",
|
||||
"student_answer"
|
||||
]
|
45
app/crud/crud_exam.py
Normal file
45
app/crud/crud_exam.py
Normal file
@ -0,0 +1,45 @@
|
||||
from typing import List
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
|
||||
from app.crud.base import CRUDBase
|
||||
from app.models.exam import Exam
|
||||
from app.schemas.exam import ExamCreate, ExamUpdate
|
||||
|
||||
|
||||
class CRUDExam(CRUDBase[Exam, ExamCreate, ExamUpdate]):
|
||||
def get_by_course_id(self, db: Session, *, course_id: int) -> List[Exam]:
|
||||
"""Get all exams for a specific course"""
|
||||
return db.query(self.model).filter(Exam.course_id == course_id).all()
|
||||
|
||||
def get_by_creator(self, db: Session, *, creator_id: int) -> List[Exam]:
|
||||
"""Get all exams created by a specific user"""
|
||||
return db.query(self.model).filter(Exam.created_by == creator_id).all()
|
||||
|
||||
def get_active_exams(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[Exam]:
|
||||
"""Get all active exams"""
|
||||
return (
|
||||
db.query(self.model)
|
||||
.filter(Exam.is_active)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_available_exams(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[Exam]:
|
||||
"""Get all exams that are currently available (active and within time window)"""
|
||||
now = datetime.utcnow()
|
||||
return (
|
||||
db.query(self.model)
|
||||
.filter(
|
||||
Exam.is_active,
|
||||
(Exam.start_time.is_(None) | (Exam.start_time <= now)),
|
||||
(Exam.end_time.is_(None) | (Exam.end_time >= now)),
|
||||
)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
exam = CRUDExam(Exam)
|
60
app/crud/crud_exam_result.py
Normal file
60
app/crud/crud_exam_result.py
Normal file
@ -0,0 +1,60 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
|
||||
from app.crud.base import CRUDBase
|
||||
from app.models.exam_result import ExamResult
|
||||
from app.schemas.exam_result import ExamResultCreate, ExamResultUpdate
|
||||
|
||||
|
||||
class CRUDExamResult(CRUDBase[ExamResult, ExamResultCreate, ExamResultUpdate]):
|
||||
def get_by_student(self, db: Session, *, student_id: int) -> List[ExamResult]:
|
||||
"""Get all exam results for a specific student"""
|
||||
return db.query(self.model).filter(ExamResult.student_id == student_id).all()
|
||||
|
||||
def get_by_exam(self, db: Session, *, exam_id: int) -> List[ExamResult]:
|
||||
"""Get all exam results for a specific exam"""
|
||||
return db.query(self.model).filter(ExamResult.exam_id == exam_id).all()
|
||||
|
||||
def get_by_student_and_exam(
|
||||
self, db: Session, *, student_id: int, exam_id: int
|
||||
) -> Optional[ExamResult]:
|
||||
"""Get a student's result for a specific exam"""
|
||||
return (
|
||||
db.query(self.model)
|
||||
.filter(
|
||||
ExamResult.student_id == student_id,
|
||||
ExamResult.exam_id == exam_id
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
def get_active_exam_attempt(
|
||||
self, db: Session, *, student_id: int, exam_id: int
|
||||
) -> Optional[ExamResult]:
|
||||
"""Get a student's active (incomplete) attempt for an exam"""
|
||||
return (
|
||||
db.query(self.model)
|
||||
.filter(
|
||||
ExamResult.student_id == student_id,
|
||||
ExamResult.exam_id == exam_id,
|
||||
~ExamResult.is_completed
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
def complete_exam(
|
||||
self, db: Session, *, db_obj: ExamResult, score: float, max_score: float
|
||||
) -> ExamResult:
|
||||
"""Mark an exam as completed with the final score"""
|
||||
db_obj.score = score
|
||||
db_obj.max_score = max_score
|
||||
db_obj.completed_at = datetime.utcnow()
|
||||
db_obj.is_completed = True
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
|
||||
exam_result = CRUDExamResult(ExamResult)
|
36
app/crud/crud_question.py
Normal file
36
app/crud/crud_question.py
Normal file
@ -0,0 +1,36 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.crud.base import CRUDBase
|
||||
from app.models.question import Question, QuestionType
|
||||
from app.schemas.question import QuestionCreate, QuestionUpdate
|
||||
|
||||
|
||||
class CRUDQuestion(CRUDBase[Question, QuestionCreate, QuestionUpdate]):
|
||||
def get_by_exam_id(self, db: Session, *, exam_id: int) -> List[Question]:
|
||||
"""Get all questions for a specific exam"""
|
||||
return db.query(self.model).filter(Question.exam_id == exam_id).all()
|
||||
|
||||
def get_by_exam_id_and_type(
|
||||
self, db: Session, *, exam_id: int, question_type: QuestionType
|
||||
) -> List[Question]:
|
||||
"""Get all questions of a specific type for an exam"""
|
||||
return (
|
||||
db.query(self.model)
|
||||
.filter(
|
||||
Question.exam_id == exam_id,
|
||||
Question.question_type == question_type
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
def get_with_options(self, db: Session, *, id: int) -> Optional[Question]:
|
||||
"""Get a question with its options"""
|
||||
return (
|
||||
db.query(self.model)
|
||||
.filter(Question.id == id)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
question = CRUDQuestion(Question)
|
26
app/crud/crud_question_option.py
Normal file
26
app/crud/crud_question_option.py
Normal file
@ -0,0 +1,26 @@
|
||||
from typing import List, Optional
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.crud.base import CRUDBase
|
||||
from app.models.question_option import QuestionOption
|
||||
from app.schemas.question_option import QuestionOptionCreate, QuestionOptionUpdate
|
||||
|
||||
|
||||
class CRUDQuestionOption(CRUDBase[QuestionOption, QuestionOptionCreate, QuestionOptionUpdate]):
|
||||
def get_by_question_id(self, db: Session, *, question_id: int) -> List[QuestionOption]:
|
||||
"""Get all options for a specific question"""
|
||||
return db.query(self.model).filter(QuestionOption.question_id == question_id).all()
|
||||
|
||||
def get_correct_option(self, db: Session, *, question_id: int) -> Optional[QuestionOption]:
|
||||
"""Get the correct option for a question"""
|
||||
return (
|
||||
db.query(self.model)
|
||||
.filter(
|
||||
QuestionOption.question_id == question_id,
|
||||
QuestionOption.is_correct
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
|
||||
question_option = CRUDQuestionOption(QuestionOption)
|
49
app/crud/crud_student_answer.py
Normal file
49
app/crud/crud_student_answer.py
Normal file
@ -0,0 +1,49 @@
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.crud.base import CRUDBase
|
||||
from app.models.student_answer import StudentAnswer
|
||||
from app.schemas.student_answer import StudentAnswerCreate, StudentAnswerUpdate
|
||||
|
||||
|
||||
class CRUDStudentAnswer(CRUDBase[StudentAnswer, StudentAnswerCreate, StudentAnswerUpdate]):
|
||||
def get_by_exam_result(self, db: Session, *, exam_result_id: int) -> List[StudentAnswer]:
|
||||
"""Get all answers for a specific exam result"""
|
||||
return db.query(self.model).filter(StudentAnswer.exam_result_id == exam_result_id).all()
|
||||
|
||||
def get_by_question(
|
||||
self, db: Session, *, exam_result_id: int, question_id: int
|
||||
) -> Optional[StudentAnswer]:
|
||||
"""Get a student's answer for a specific question in an exam"""
|
||||
return (
|
||||
db.query(self.model)
|
||||
.filter(
|
||||
StudentAnswer.exam_result_id == exam_result_id,
|
||||
StudentAnswer.question_id == question_id
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
def update_answer(
|
||||
self,
|
||||
db: Session,
|
||||
*,
|
||||
db_obj: StudentAnswer,
|
||||
obj_in: Union[StudentAnswerUpdate, Dict[str, Any]]
|
||||
) -> StudentAnswer:
|
||||
"""Update a student's answer"""
|
||||
return super().update(db, db_obj=db_obj, obj_in=obj_in)
|
||||
|
||||
def grade_answer(
|
||||
self, db: Session, *, db_obj: StudentAnswer, is_correct: bool, points_earned: float
|
||||
) -> StudentAnswer:
|
||||
"""Grade a student's answer"""
|
||||
db_obj.is_correct = is_correct
|
||||
db_obj.points_earned = points_earned
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
|
||||
student_answer = CRUDStudentAnswer(StudentAnswer)
|
@ -53,6 +53,13 @@ class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
|
||||
|
||||
def is_active(self, user: User) -> bool:
|
||||
return user.is_active
|
||||
|
||||
def is_admin(self, user: User) -> bool:
|
||||
"""Check if a user has admin role"""
|
||||
if not user:
|
||||
return False
|
||||
# Assuming that the role with id 1 or name 'admin' is the admin role
|
||||
return user.role_id == 1 or user.role.name.lower() == 'admin'
|
||||
|
||||
|
||||
user = CRUDUser(User)
|
@ -0,0 +1,17 @@
|
||||
# Re-export all model modules
|
||||
__all__ = [
|
||||
"User", "Role", "Student", "Teacher", "Course", "ClassEnrollment",
|
||||
"Exam", "Question", "QuestionOption", "ExamResult", "StudentAnswer"
|
||||
]
|
||||
|
||||
from app.models.user import User
|
||||
from app.models.role import Role
|
||||
from app.models.student import Student
|
||||
from app.models.teacher import Teacher
|
||||
from app.models.course import Course
|
||||
from app.models.class_enrollment import ClassEnrollment
|
||||
from app.models.exam import Exam
|
||||
from app.models.question import Question
|
||||
from app.models.question_option import QuestionOption
|
||||
from app.models.exam_result import ExamResult
|
||||
from app.models.student_answer import StudentAnswer
|
30
app/models/exam.py
Normal file
30
app/models/exam.py
Normal file
@ -0,0 +1,30 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, ForeignKey, Float
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class Exam(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String(200), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
course_id = Column(Integer, ForeignKey("course.id"), nullable=False)
|
||||
duration_minutes = Column(Integer, default=60)
|
||||
passing_score = Column(Float, default=50.0) # Percentage required to pass
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_randomized = Column(Boolean, default=False) # Whether questions should be randomized
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
start_time = Column(DateTime, nullable=True) # When the exam becomes available
|
||||
end_time = Column(DateTime, nullable=True) # When the exam becomes unavailable
|
||||
created_by = Column(Integer, ForeignKey("user.id"), nullable=True)
|
||||
|
||||
# Relationships
|
||||
course = relationship("Course", backref="exams")
|
||||
questions = relationship("Question", back_populates="exam", cascade="all, delete-orphan")
|
||||
exam_results = relationship("ExamResult", back_populates="exam", cascade="all, delete-orphan")
|
||||
creator = relationship("User", back_populates="created_exams")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Exam {self.title}>"
|
25
app/models/exam_result.py
Normal file
25
app/models/exam_result.py
Normal file
@ -0,0 +1,25 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, Float, DateTime, ForeignKey, Text, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class ExamResult(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
exam_id = Column(Integer, ForeignKey("exam.id"), nullable=False)
|
||||
student_id = Column(Integer, ForeignKey("student.id"), nullable=False)
|
||||
score = Column(Float, nullable=True) # Final score
|
||||
max_score = Column(Float, nullable=True) # Maximum possible score
|
||||
started_at = Column(DateTime, default=datetime.utcnow)
|
||||
completed_at = Column(DateTime, nullable=True)
|
||||
is_completed = Column(Boolean, default=False)
|
||||
remarks = Column(Text, nullable=True)
|
||||
|
||||
# Relationships
|
||||
exam = relationship("Exam", back_populates="exam_results")
|
||||
student = relationship("Student", backref="exam_results")
|
||||
answers = relationship("StudentAnswer", back_populates="exam_result", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ExamResult {self.id}: Student {self.student_id}, Exam {self.exam_id}>"
|
29
app/models/question.py
Normal file
29
app/models/question.py
Normal file
@ -0,0 +1,29 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, Float, ForeignKey, Enum
|
||||
from sqlalchemy.orm import relationship
|
||||
import enum
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class QuestionType(str, enum.Enum):
|
||||
MULTIPLE_CHOICE = "multiple_choice"
|
||||
TRUE_FALSE = "true_false"
|
||||
SHORT_ANSWER = "short_answer"
|
||||
ESSAY = "essay"
|
||||
|
||||
|
||||
class Question(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
exam_id = Column(Integer, ForeignKey("exam.id"), nullable=False)
|
||||
text = Column(Text, nullable=False)
|
||||
question_type = Column(Enum(QuestionType), nullable=False, default=QuestionType.MULTIPLE_CHOICE)
|
||||
points = Column(Float, default=1.0)
|
||||
image_url = Column(String(255), nullable=True) # Optional image for the question
|
||||
solution = Column(Text, nullable=True) # Explanation of the answer
|
||||
|
||||
# Relationships
|
||||
exam = relationship("Exam", back_populates="questions")
|
||||
options = relationship("QuestionOption", back_populates="question", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Question {self.id}: {self.text[:30]}...>"
|
17
app/models/question_option.py
Normal file
17
app/models/question_option.py
Normal file
@ -0,0 +1,17 @@
|
||||
from sqlalchemy import Column, Integer, Text, Boolean, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class QuestionOption(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
question_id = Column(Integer, ForeignKey("question.id"), nullable=False)
|
||||
text = Column(Text, nullable=False)
|
||||
is_correct = Column(Boolean, default=False)
|
||||
|
||||
# Relationships
|
||||
question = relationship("Question", back_populates="options")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<QuestionOption {self.id}: {self.text[:30]}...>"
|
22
app/models/student_answer.py
Normal file
22
app/models/student_answer.py
Normal file
@ -0,0 +1,22 @@
|
||||
from sqlalchemy import Column, Integer, Text, Boolean, ForeignKey, Float
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.db.base_class import Base
|
||||
|
||||
|
||||
class StudentAnswer(Base):
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
exam_result_id = Column(Integer, ForeignKey("exam_result.id"), nullable=False)
|
||||
question_id = Column(Integer, ForeignKey("question.id"), nullable=False)
|
||||
selected_option_id = Column(Integer, ForeignKey("question_option.id"), nullable=True) # For multiple choice/true-false
|
||||
text_answer = Column(Text, nullable=True) # For short answer/essay
|
||||
is_correct = Column(Boolean, nullable=True) # Whether the answer is correct
|
||||
points_earned = Column(Float, default=0.0) # Points earned for this answer
|
||||
|
||||
# Relationships
|
||||
exam_result = relationship("ExamResult", back_populates="answers")
|
||||
question = relationship("Question")
|
||||
selected_option = relationship("QuestionOption")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<StudentAnswer {self.id}: Result {self.exam_result_id}, Question {self.question_id}>"
|
@ -23,6 +23,7 @@ class User(Base):
|
||||
role = relationship("Role", back_populates="users")
|
||||
student = relationship("Student", back_populates="user", uselist=False)
|
||||
teacher = relationship("Teacher", back_populates="user", uselist=False)
|
||||
created_exams = relationship("Exam", back_populates="creator")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<User {self.username}>"
|
@ -0,0 +1,40 @@
|
||||
# Re-export all schema modules
|
||||
__all__ = [
|
||||
# Token schemas
|
||||
"Token", "TokenPayload",
|
||||
# User schemas
|
||||
"User", "UserCreate", "UserUpdate", "UserInDB",
|
||||
# Role schemas
|
||||
"Role", "RoleCreate", "RoleUpdate",
|
||||
# Student schemas
|
||||
"Student", "StudentCreate", "StudentUpdate", "StudentWithUser",
|
||||
# Teacher schemas
|
||||
"Teacher", "TeacherCreate", "TeacherUpdate", "TeacherWithUser",
|
||||
# Course schemas
|
||||
"Course", "CourseCreate", "CourseUpdate", "CourseWithTeacher",
|
||||
# Class enrollment schemas
|
||||
"ClassEnrollment", "ClassEnrollmentCreate", "ClassEnrollmentUpdate", "ClassEnrollmentWithDetails",
|
||||
# Exam schemas
|
||||
"Exam", "ExamCreate", "ExamUpdate", "ExamWithQuestions", "ExamWithCreator",
|
||||
# Question schemas
|
||||
"Question", "QuestionCreate", "QuestionUpdate", "QuestionWithOptions", "QuestionType",
|
||||
# Question option schemas
|
||||
"QuestionOption", "QuestionOptionCreate", "QuestionOptionUpdate", "QuestionOptionForExam",
|
||||
# Exam result schemas
|
||||
"ExamResult", "ExamResultCreate", "ExamResultUpdate", "ExamResultWithAnswers",
|
||||
# Student answer schemas
|
||||
"StudentAnswer", "StudentAnswerCreate", "StudentAnswerUpdate"
|
||||
]
|
||||
|
||||
from app.schemas.token import Token, TokenPayload
|
||||
from app.schemas.user import User, UserCreate, UserUpdate, UserInDB
|
||||
from app.schemas.role import Role, RoleCreate, RoleUpdate
|
||||
from app.schemas.student import Student, StudentCreate, StudentUpdate, StudentWithUser
|
||||
from app.schemas.teacher import Teacher, TeacherCreate, TeacherUpdate, TeacherWithUser
|
||||
from app.schemas.course import Course, CourseCreate, CourseUpdate, CourseWithTeacher
|
||||
from app.schemas.class_enrollment import ClassEnrollment, ClassEnrollmentCreate, ClassEnrollmentUpdate, ClassEnrollmentWithDetails
|
||||
from app.schemas.exam import Exam, ExamCreate, ExamUpdate, ExamWithQuestions, ExamWithCreator
|
||||
from app.schemas.question import Question, QuestionCreate, QuestionUpdate, QuestionWithOptions, QuestionType
|
||||
from app.schemas.question_option import QuestionOption, QuestionOptionCreate, QuestionOptionUpdate, QuestionOptionForExam
|
||||
from app.schemas.exam_result import ExamResult, ExamResultCreate, ExamResultUpdate, ExamResultWithAnswers
|
||||
from app.schemas.student_answer import StudentAnswer, StudentAnswerCreate, StudentAnswerUpdate
|
49
app/schemas/class_enrollment.py
Normal file
49
app/schemas/class_enrollment.py
Normal file
@ -0,0 +1,49 @@
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.schemas.student import Student
|
||||
from app.schemas.course import Course
|
||||
|
||||
|
||||
# Shared properties
|
||||
class ClassEnrollmentBase(BaseModel):
|
||||
student_id: int
|
||||
course_id: int
|
||||
enrollment_date: Optional[datetime] = None
|
||||
grade: Optional[float] = None
|
||||
semester: str
|
||||
academic_year: str
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class ClassEnrollmentCreate(ClassEnrollmentBase):
|
||||
pass
|
||||
|
||||
|
||||
# Properties to receive via API on update
|
||||
class ClassEnrollmentUpdate(BaseModel):
|
||||
grade: Optional[float] = None
|
||||
semester: Optional[str] = None
|
||||
academic_year: Optional[str] = None
|
||||
|
||||
|
||||
class ClassEnrollmentInDBBase(ClassEnrollmentBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Additional properties to return via API
|
||||
class ClassEnrollment(ClassEnrollmentInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
# Class enrollment with Student and Course details
|
||||
class ClassEnrollmentWithDetails(ClassEnrollment):
|
||||
student: Optional[Student] = None
|
||||
course: Optional[Course] = None
|
||||
|
||||
# Update forward refs
|
||||
ClassEnrollmentWithDetails.model_rebuild()
|
48
app/schemas/course.py
Normal file
48
app/schemas/course.py
Normal file
@ -0,0 +1,48 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Import at the top to avoid circular imports
|
||||
from app.schemas.teacher import Teacher
|
||||
|
||||
|
||||
# Shared properties
|
||||
class CourseBase(BaseModel):
|
||||
course_code: str
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
credits: Optional[int] = None
|
||||
is_active: bool = True
|
||||
teacher_id: Optional[int] = None
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class CourseCreate(CourseBase):
|
||||
course_code: str
|
||||
title: str
|
||||
|
||||
|
||||
# Properties to receive via API on update
|
||||
class CourseUpdate(CourseBase):
|
||||
course_code: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
teacher_id: Optional[int] = None
|
||||
|
||||
|
||||
class CourseInDBBase(CourseBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Additional properties to return via API
|
||||
class Course(CourseInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
# Course with Teacher details
|
||||
class CourseWithTeacher(Course):
|
||||
teacher: Optional[Teacher] = None
|
||||
|
||||
# Update forward refs
|
||||
CourseWithTeacher.model_rebuild()
|
65
app/schemas/exam.py
Normal file
65
app/schemas/exam.py
Normal file
@ -0,0 +1,65 @@
|
||||
from typing import Optional, List, TYPE_CHECKING
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.schemas.question import Question
|
||||
from app.schemas.user import User
|
||||
|
||||
# Shared properties
|
||||
class ExamBase(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
course_id: int
|
||||
duration_minutes: int = 60
|
||||
passing_score: float = 50.0
|
||||
is_active: bool = True
|
||||
is_randomized: bool = False
|
||||
start_time: Optional[datetime] = None
|
||||
end_time: Optional[datetime] = None
|
||||
created_by: Optional[int] = None
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class ExamCreate(ExamBase):
|
||||
pass
|
||||
|
||||
|
||||
# Properties to receive via API on update
|
||||
class ExamUpdate(ExamBase):
|
||||
title: Optional[str] = None
|
||||
course_id: Optional[int] = None
|
||||
duration_minutes: Optional[int] = None
|
||||
passing_score: Optional[float] = None
|
||||
is_active: Optional[bool] = None
|
||||
is_randomized: Optional[bool] = None
|
||||
|
||||
|
||||
class ExamInDBBase(ExamBase):
|
||||
id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Additional properties to return via API
|
||||
class Exam(ExamInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
# Exam with questions
|
||||
class ExamWithQuestions(Exam):
|
||||
questions: List['Question'] = []
|
||||
|
||||
# Exam with creator info
|
||||
class ExamWithCreator(Exam):
|
||||
creator: Optional['User'] = None
|
||||
|
||||
|
||||
# Update forward references
|
||||
from app.schemas.question import Question # noqa: E402
|
||||
from app.schemas.user import User # noqa: E402
|
||||
ExamWithQuestions.model_rebuild()
|
||||
ExamWithCreator.model_rebuild()
|
54
app/schemas/exam_result.py
Normal file
54
app/schemas/exam_result.py
Normal file
@ -0,0 +1,54 @@
|
||||
from typing import Optional, List, TYPE_CHECKING
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.schemas.student_answer import StudentAnswer
|
||||
|
||||
|
||||
# Shared properties
|
||||
class ExamResultBase(BaseModel):
|
||||
exam_id: int
|
||||
student_id: int
|
||||
is_completed: bool = False
|
||||
remarks: Optional[str] = None
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class ExamResultCreate(ExamResultBase):
|
||||
pass
|
||||
|
||||
|
||||
# Properties to receive via API on update
|
||||
class ExamResultUpdate(BaseModel):
|
||||
score: Optional[float] = None
|
||||
max_score: Optional[float] = None
|
||||
completed_at: Optional[datetime] = None
|
||||
is_completed: Optional[bool] = None
|
||||
remarks: Optional[str] = None
|
||||
|
||||
|
||||
class ExamResultInDBBase(ExamResultBase):
|
||||
id: int
|
||||
score: Optional[float] = None
|
||||
max_score: Optional[float] = None
|
||||
started_at: datetime
|
||||
completed_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Additional properties to return via API
|
||||
class ExamResult(ExamResultInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
# Exam result with student answers
|
||||
class ExamResultWithAnswers(ExamResult):
|
||||
answers: List['StudentAnswer'] = []
|
||||
|
||||
|
||||
# Update forward references
|
||||
from app.schemas.student_answer import StudentAnswer # noqa: E402
|
||||
ExamResultWithAnswers.model_rebuild()
|
60
app/schemas/question.py
Normal file
60
app/schemas/question.py
Normal file
@ -0,0 +1,60 @@
|
||||
from typing import Optional, List, TYPE_CHECKING
|
||||
from enum import Enum
|
||||
from pydantic import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.schemas.question_option import QuestionOption
|
||||
|
||||
|
||||
# Question type enum
|
||||
class QuestionType(str, Enum):
|
||||
MULTIPLE_CHOICE = "multiple_choice"
|
||||
TRUE_FALSE = "true_false"
|
||||
SHORT_ANSWER = "short_answer"
|
||||
ESSAY = "essay"
|
||||
|
||||
|
||||
# Shared properties
|
||||
class QuestionBase(BaseModel):
|
||||
exam_id: int
|
||||
text: str
|
||||
question_type: QuestionType = QuestionType.MULTIPLE_CHOICE
|
||||
points: float = 1.0
|
||||
image_url: Optional[str] = None
|
||||
solution: Optional[str] = None
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class QuestionCreate(QuestionBase):
|
||||
pass
|
||||
|
||||
|
||||
# Properties to receive via API on update
|
||||
class QuestionUpdate(BaseModel):
|
||||
text: Optional[str] = None
|
||||
question_type: Optional[QuestionType] = None
|
||||
points: Optional[float] = None
|
||||
image_url: Optional[str] = None
|
||||
solution: Optional[str] = None
|
||||
|
||||
|
||||
class QuestionInDBBase(QuestionBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Additional properties to return via API
|
||||
class Question(QuestionInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
# Question with options
|
||||
class QuestionWithOptions(Question):
|
||||
options: List['QuestionOption'] = []
|
||||
|
||||
|
||||
# Update forward references
|
||||
from app.schemas.question_option import QuestionOption # noqa: E402
|
||||
QuestionWithOptions.model_rebuild()
|
41
app/schemas/question_option.py
Normal file
41
app/schemas/question_option.py
Normal file
@ -0,0 +1,41 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Shared properties
|
||||
class QuestionOptionBase(BaseModel):
|
||||
question_id: int
|
||||
text: str
|
||||
is_correct: bool = False
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class QuestionOptionCreate(QuestionOptionBase):
|
||||
pass
|
||||
|
||||
|
||||
# Properties to receive via API on update
|
||||
class QuestionOptionUpdate(BaseModel):
|
||||
text: Optional[str] = None
|
||||
is_correct: Optional[bool] = None
|
||||
|
||||
|
||||
class QuestionOptionInDBBase(QuestionOptionBase):
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Additional properties to return via API
|
||||
class QuestionOption(QuestionOptionInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
# For students taking an exam - hide the is_correct field
|
||||
class QuestionOptionForExam(BaseModel):
|
||||
id: int
|
||||
text: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
37
app/schemas/student_answer.py
Normal file
37
app/schemas/student_answer.py
Normal file
@ -0,0 +1,37 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# Shared properties
|
||||
class StudentAnswerBase(BaseModel):
|
||||
exam_result_id: int
|
||||
question_id: int
|
||||
selected_option_id: Optional[int] = None
|
||||
text_answer: Optional[str] = None
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class StudentAnswerCreate(StudentAnswerBase):
|
||||
pass
|
||||
|
||||
|
||||
# Properties to receive via API on update
|
||||
class StudentAnswerUpdate(BaseModel):
|
||||
selected_option_id: Optional[int] = None
|
||||
text_answer: Optional[str] = None
|
||||
is_correct: Optional[bool] = None
|
||||
points_earned: Optional[float] = None
|
||||
|
||||
|
||||
class StudentAnswerInDBBase(StudentAnswerBase):
|
||||
id: int
|
||||
is_correct: Optional[bool] = None
|
||||
points_earned: float = 0.0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Additional properties to return via API
|
||||
class StudentAnswer(StudentAnswerInDBBase):
|
||||
pass
|
48
app/schemas/teacher.py
Normal file
48
app/schemas/teacher.py
Normal file
@ -0,0 +1,48 @@
|
||||
from typing import Optional
|
||||
from datetime import date
|
||||
from pydantic import BaseModel
|
||||
|
||||
# Import at the top to avoid circular imports
|
||||
from app.schemas.user import User as UserOut
|
||||
|
||||
|
||||
# Shared properties
|
||||
class TeacherBase(BaseModel):
|
||||
teacher_id: Optional[str] = None
|
||||
date_of_birth: Optional[date] = None
|
||||
address: Optional[str] = None
|
||||
phone_number: Optional[str] = None
|
||||
department: Optional[str] = None
|
||||
hire_date: Optional[date] = None
|
||||
user_id: Optional[int] = None
|
||||
|
||||
|
||||
# Properties to receive via API on creation
|
||||
class TeacherCreate(TeacherBase):
|
||||
teacher_id: str
|
||||
user_id: int
|
||||
|
||||
|
||||
# Properties to receive via API on update
|
||||
class TeacherUpdate(TeacherBase):
|
||||
pass
|
||||
|
||||
|
||||
class TeacherInDBBase(TeacherBase):
|
||||
id: Optional[int] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
# Additional properties to return via API
|
||||
class Teacher(TeacherInDBBase):
|
||||
pass
|
||||
|
||||
|
||||
# Teacher with User details
|
||||
class TeacherWithUser(Teacher):
|
||||
user: Optional[UserOut] = None
|
||||
|
||||
# Update forward refs
|
||||
TeacherWithUser.model_rebuild()
|
17
main.py
17
main.py
@ -28,8 +28,23 @@ app.include_router(api_router, prefix=settings.API_V1_STR)
|
||||
async def health_check():
|
||||
"""
|
||||
Health check endpoint
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the health status and application information
|
||||
"""
|
||||
return {"status": "healthy"}
|
||||
return {
|
||||
"status": "healthy",
|
||||
"version": "1.0.0",
|
||||
"application": settings.PROJECT_NAME,
|
||||
"environment": settings.ENVIRONMENT,
|
||||
"features": {
|
||||
"cbt": True,
|
||||
"role_based_access": True,
|
||||
"student_management": True,
|
||||
"teacher_management": True,
|
||||
"course_management": True
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
125
migrations/versions/002_add_cbt_tables.py
Normal file
125
migrations/versions/002_add_cbt_tables.py
Normal file
@ -0,0 +1,125 @@
|
||||
"""Add CBT tables
|
||||
|
||||
Revision ID: 002
|
||||
Revises: 001
|
||||
Create Date: 2023-10-15
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '002'
|
||||
down_revision: Union[str, None] = '001'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create exam table
|
||||
op.create_table(
|
||||
'exam',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('title', sa.String(length=200), nullable=False),
|
||||
sa.Column('description', sa.Text(), nullable=True),
|
||||
sa.Column('course_id', sa.Integer(), nullable=False),
|
||||
sa.Column('duration_minutes', sa.Integer(), nullable=True, default=60),
|
||||
sa.Column('passing_score', sa.Float(), nullable=True, default=50.0),
|
||||
sa.Column('is_active', sa.Boolean(), nullable=True, default=True),
|
||||
sa.Column('is_randomized', sa.Boolean(), nullable=True, default=False),
|
||||
sa.Column('created_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('updated_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('start_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('end_time', sa.DateTime(), nullable=True),
|
||||
sa.Column('created_by', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['course_id'], ['course.id'], ),
|
||||
sa.ForeignKeyConstraint(['created_by'], ['user.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_exam_id'), 'exam', ['id'], unique=False)
|
||||
|
||||
# Create question table
|
||||
op.create_table(
|
||||
'question',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('exam_id', sa.Integer(), nullable=False),
|
||||
sa.Column('text', sa.Text(), nullable=False),
|
||||
sa.Column('question_type', sa.Enum('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER', 'ESSAY', name='questiontype'), nullable=False),
|
||||
sa.Column('points', sa.Float(), nullable=True),
|
||||
sa.Column('image_url', sa.String(length=255), nullable=True),
|
||||
sa.Column('solution', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['exam_id'], ['exam.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_question_id'), 'question', ['id'], unique=False)
|
||||
|
||||
# Create question_option table
|
||||
op.create_table(
|
||||
'question_option',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('question_id', sa.Integer(), nullable=False),
|
||||
sa.Column('text', sa.Text(), nullable=False),
|
||||
sa.Column('is_correct', sa.Boolean(), nullable=True, default=False),
|
||||
sa.ForeignKeyConstraint(['question_id'], ['question.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_question_option_id'), 'question_option', ['id'], unique=False)
|
||||
|
||||
# Create exam_result table
|
||||
op.create_table(
|
||||
'exam_result',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('exam_id', sa.Integer(), nullable=False),
|
||||
sa.Column('student_id', sa.Integer(), nullable=False),
|
||||
sa.Column('score', sa.Float(), nullable=True),
|
||||
sa.Column('max_score', sa.Float(), nullable=True),
|
||||
sa.Column('started_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('completed_at', sa.DateTime(), nullable=True),
|
||||
sa.Column('is_completed', sa.Boolean(), nullable=True, default=False),
|
||||
sa.Column('remarks', sa.Text(), nullable=True),
|
||||
sa.ForeignKeyConstraint(['exam_id'], ['exam.id'], ),
|
||||
sa.ForeignKeyConstraint(['student_id'], ['student.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_exam_result_id'), 'exam_result', ['id'], unique=False)
|
||||
|
||||
# Create student_answer table
|
||||
op.create_table(
|
||||
'student_answer',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('exam_result_id', sa.Integer(), nullable=False),
|
||||
sa.Column('question_id', sa.Integer(), nullable=False),
|
||||
sa.Column('selected_option_id', sa.Integer(), nullable=True),
|
||||
sa.Column('text_answer', sa.Text(), nullable=True),
|
||||
sa.Column('is_correct', sa.Boolean(), nullable=True),
|
||||
sa.Column('points_earned', sa.Float(), nullable=True, default=0.0),
|
||||
sa.ForeignKeyConstraint(['exam_result_id'], ['exam_result.id'], ),
|
||||
sa.ForeignKeyConstraint(['question_id'], ['question.id'], ),
|
||||
sa.ForeignKeyConstraint(['selected_option_id'], ['question_option.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_student_answer_id'), 'student_answer', ['id'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop tables in reverse order of creation
|
||||
op.drop_index(op.f('ix_student_answer_id'), table_name='student_answer')
|
||||
op.drop_table('student_answer')
|
||||
|
||||
op.drop_index(op.f('ix_exam_result_id'), table_name='exam_result')
|
||||
op.drop_table('exam_result')
|
||||
|
||||
op.drop_index(op.f('ix_question_option_id'), table_name='question_option')
|
||||
op.drop_table('question_option')
|
||||
|
||||
op.drop_index(op.f('ix_question_id'), table_name='question')
|
||||
op.drop_table('question')
|
||||
|
||||
op.drop_index(op.f('ix_exam_id'), table_name='exam')
|
||||
op.drop_table('exam')
|
||||
|
||||
# Drop the enum type
|
||||
op.execute('DROP TYPE questiontype')
|
Loading…
x
Reference in New Issue
Block a user