Implement comprehensive school portal API with FastAPI
- Complete authentication system with JWT and role-based access control
- User management for Admin, Teacher, Student, and Parent roles
- Student management with CRUD operations
- Class management and assignment system
- Subject and grade tracking functionality
- Daily attendance marking and viewing
- Notification system for announcements
- SQLite database with Alembic migrations
- Comprehensive API documentation with Swagger/ReDoc
- Proper project structure with services, models, and schemas
- Environment variable configuration
- CORS support and security features
🤖 Generated with BackendIM
Co-Authored-By: BackendIM <noreply@anthropic.com>
This commit is contained in:
parent
ab56cbf293
commit
5a02fb8b1f
153
README.md
153
README.md
@ -1,3 +1,152 @@
|
|||||||
# FastAPI Application
|
# School Portal API
|
||||||
|
|
||||||
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
|
A comprehensive school management system built with Python FastAPI, supporting multiple user roles and complete student lifecycle management.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### User Roles
|
||||||
|
- **Admin**: Full system access, user management, class management
|
||||||
|
- **Teacher**: Student management, grade tracking, attendance marking, notifications
|
||||||
|
- **Student**: View grades, attendance, and notifications
|
||||||
|
- **Parent**: View child's grades, attendance, and notifications
|
||||||
|
|
||||||
|
### Core Functionality
|
||||||
|
- **Authentication**: JWT-based login and registration for all user roles
|
||||||
|
- **Student Management**: Create, update, and delete student profiles
|
||||||
|
- **Class Management**: Assign students to classes and teachers to classes
|
||||||
|
- **Subject & Grade Tracking**: Teachers record grades per subject, students/parents view them
|
||||||
|
- **Attendance**: Daily attendance marking and viewing
|
||||||
|
- **Notifications**: Announcements and messages to students and parents
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
schoolportalapi/
|
||||||
|
├── app/
|
||||||
|
│ ├── api/
|
||||||
|
│ │ └── v1/
|
||||||
|
│ │ ├── endpoints/
|
||||||
|
│ │ │ ├── auth.py # Authentication endpoints
|
||||||
|
│ │ │ ├── users.py # User management
|
||||||
|
│ │ │ ├── classes.py # Class management
|
||||||
|
│ │ │ ├── subjects.py # Subject management
|
||||||
|
│ │ │ ├── grades.py # Grade tracking
|
||||||
|
│ │ │ ├── attendance.py # Attendance management
|
||||||
|
│ │ │ └── notifications.py # Notification system
|
||||||
|
│ │ └── api.py # API router
|
||||||
|
│ ├── core/
|
||||||
|
│ │ ├── config.py # Configuration settings
|
||||||
|
│ │ └── security.py # JWT and password utilities
|
||||||
|
│ ├── db/
|
||||||
|
│ │ ├── base.py # SQLAlchemy base
|
||||||
|
│ │ └── session.py # Database session
|
||||||
|
│ ├── models/ # SQLAlchemy models
|
||||||
|
│ │ ├── user.py
|
||||||
|
│ │ ├── class_model.py
|
||||||
|
│ │ ├── subject.py
|
||||||
|
│ │ ├── grade.py
|
||||||
|
│ │ ├── attendance.py
|
||||||
|
│ │ └── notification.py
|
||||||
|
│ ├── schemas/ # Pydantic schemas
|
||||||
|
│ │ ├── user.py
|
||||||
|
│ │ ├── class_schema.py
|
||||||
|
│ │ ├── subject.py
|
||||||
|
│ │ ├── grade.py
|
||||||
|
│ │ ├── attendance.py
|
||||||
|
│ │ ├── notification.py
|
||||||
|
│ │ └── token.py
|
||||||
|
│ └── services/ # Business logic layer
|
||||||
|
│ ├── base.py
|
||||||
|
│ ├── user.py
|
||||||
|
│ ├── class_service.py
|
||||||
|
│ ├── subject.py
|
||||||
|
│ ├── grade.py
|
||||||
|
│ ├── attendance.py
|
||||||
|
│ └── notification.py
|
||||||
|
├── alembic/ # Database migrations
|
||||||
|
│ ├── versions/
|
||||||
|
│ └── env.py
|
||||||
|
├── main.py # FastAPI application entry point
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
└── alembic.ini # Alembic configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup Instructions
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- Python 3.8+
|
||||||
|
- pip
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
1. **Clone the repository**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd schoolportalapi
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Create virtual environment**
|
||||||
|
```bash
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate # On Windows: venv\Scripts\activate
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Install dependencies**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Set environment variables**
|
||||||
|
Create a `.env` file or set the following environment variables:
|
||||||
|
```bash
|
||||||
|
# Required Environment Variables
|
||||||
|
SECRET_KEY=your-secret-key-change-in-production
|
||||||
|
DATABASE_URL=sqlite:////app/storage/db/db.sqlite
|
||||||
|
FIRST_SUPERUSER_EMAIL=admin@schoolportal.com
|
||||||
|
FIRST_SUPERUSER_PASSWORD=admin123
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Create storage directory**
|
||||||
|
```bash
|
||||||
|
mkdir -p /app/storage/db
|
||||||
|
```
|
||||||
|
|
||||||
|
6. **Run database migrations**
|
||||||
|
```bash
|
||||||
|
alembic upgrade head
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Start the application**
|
||||||
|
```bash
|
||||||
|
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
Once the application is running, access the interactive API documentation:
|
||||||
|
|
||||||
|
- **Swagger UI**: http://localhost:8000/docs
|
||||||
|
- **ReDoc**: http://localhost:8000/redoc
|
||||||
|
- **OpenAPI JSON**: http://localhost:8000/openapi.json
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|----------|-------------|---------|
|
||||||
|
| `SECRET_KEY` | JWT secret key | `your-secret-key-change-in-production` |
|
||||||
|
| `DATABASE_URL` | Database connection URL | `sqlite:////app/storage/db/db.sqlite` |
|
||||||
|
| `FIRST_SUPERUSER_EMAIL` | Initial admin email | `admin@schoolportal.com` |
|
||||||
|
| `FIRST_SUPERUSER_PASSWORD` | Initial admin password | `admin123` |
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
- JWT-based authentication
|
||||||
|
- Password hashing using bcrypt
|
||||||
|
- Role-based access control
|
||||||
|
- CORS configuration for cross-origin requests
|
||||||
|
- Input validation using Pydantic schemas
|
||||||
|
|
||||||
|
## Health Check
|
||||||
|
|
||||||
|
The application provides a health check endpoint:
|
||||||
|
- `GET /health` - Returns application health status
|
41
alembic.ini
Normal file
41
alembic.ini
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
[alembic]
|
||||||
|
script_location = alembic
|
||||||
|
prepend_sys_path = .
|
||||||
|
version_path_separator = os
|
||||||
|
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
|
||||||
|
|
||||||
|
[post_write_hooks]
|
||||||
|
|
||||||
|
[loggers]
|
||||||
|
keys = root,sqlalchemy,alembic
|
||||||
|
|
||||||
|
[handlers]
|
||||||
|
keys = console
|
||||||
|
|
||||||
|
[formatters]
|
||||||
|
keys = generic
|
||||||
|
|
||||||
|
[logger_root]
|
||||||
|
level = WARN
|
||||||
|
handlers = console
|
||||||
|
qualname =
|
||||||
|
|
||||||
|
[logger_sqlalchemy]
|
||||||
|
level = WARN
|
||||||
|
handlers =
|
||||||
|
qualname = sqlalchemy.engine
|
||||||
|
|
||||||
|
[logger_alembic]
|
||||||
|
level = INFO
|
||||||
|
handlers =
|
||||||
|
qualname = alembic
|
||||||
|
|
||||||
|
[handler_console]
|
||||||
|
class = StreamHandler
|
||||||
|
args = (sys.stderr,)
|
||||||
|
level = NOTSET
|
||||||
|
formatter = generic
|
||||||
|
|
||||||
|
[formatter_generic]
|
||||||
|
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||||
|
datefmt = %H:%M:%S
|
49
alembic/env.py
Normal file
49
alembic/env.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from logging.config import fileConfig
|
||||||
|
from sqlalchemy import engine_from_config
|
||||||
|
from sqlalchemy import pool
|
||||||
|
from alembic import context
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.append(str(Path(__file__).resolve().parent.parent))
|
||||||
|
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
config = context.config
|
||||||
|
|
||||||
|
if config.config_file_name is not None:
|
||||||
|
fileConfig(config.config_file_name)
|
||||||
|
|
||||||
|
target_metadata = Base.metadata
|
||||||
|
|
||||||
|
def run_migrations_offline() -> None:
|
||||||
|
url = config.get_main_option("sqlalchemy.url")
|
||||||
|
context.configure(
|
||||||
|
url=url,
|
||||||
|
target_metadata=target_metadata,
|
||||||
|
literal_binds=True,
|
||||||
|
dialect_opts={"paramstyle": "named"},
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
def run_migrations_online() -> None:
|
||||||
|
connectable = engine_from_config(
|
||||||
|
config.get_section(config.config_ini_section),
|
||||||
|
prefix="sqlalchemy.",
|
||||||
|
poolclass=pool.NullPool,
|
||||||
|
)
|
||||||
|
|
||||||
|
with connectable.connect() as connection:
|
||||||
|
context.configure(
|
||||||
|
connection=connection, target_metadata=target_metadata
|
||||||
|
)
|
||||||
|
|
||||||
|
with context.begin_transaction():
|
||||||
|
context.run_migrations()
|
||||||
|
|
||||||
|
if context.is_offline_mode():
|
||||||
|
run_migrations_offline()
|
||||||
|
else:
|
||||||
|
run_migrations_online()
|
23
alembic/script.py.mako
Normal file
23
alembic/script.py.mako
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""${message}
|
||||||
|
|
||||||
|
Revision ID: ${up_revision}
|
||||||
|
Revises: ${down_revision | comma,n}
|
||||||
|
Create Date: ${create_date}
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
${imports if imports else ""}
|
||||||
|
|
||||||
|
revision = '${up_revision}'
|
||||||
|
down_revision = ${repr(down_revision)}
|
||||||
|
branch_labels = ${repr(branch_labels)}
|
||||||
|
depends_on = ${repr(depends_on)}
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
${upgrades if upgrades else "pass"}
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
${downgrades if downgrades else "pass"}
|
147
alembic/versions/001_initial_migration.py
Normal file
147
alembic/versions/001_initial_migration.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"""Initial migration
|
||||||
|
|
||||||
|
Revision ID: 001
|
||||||
|
Revises:
|
||||||
|
Create Date: 2024-01-01 00:00:00.000000
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = '001'
|
||||||
|
down_revision = None
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table('classes',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('grade_level', sa.String(), nullable=False),
|
||||||
|
sa.Column('academic_year', sa.String(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_classes_id'), 'classes', ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('users',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('email', sa.String(), nullable=False),
|
||||||
|
sa.Column('hashed_password', sa.String(), nullable=False),
|
||||||
|
sa.Column('first_name', sa.String(), nullable=False),
|
||||||
|
sa.Column('last_name', sa.String(), nullable=False),
|
||||||
|
sa.Column('role', sa.Enum('admin', 'teacher', 'student', 'parent', name='userrole'), nullable=False),
|
||||||
|
sa.Column('is_active', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column('parent_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('class_id', sa.Integer(), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['class_id'], ['classes.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['parent_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
|
||||||
|
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('subjects',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('name', sa.String(), nullable=False),
|
||||||
|
sa.Column('code', sa.String(), nullable=False),
|
||||||
|
sa.Column('description', sa.String(), nullable=True),
|
||||||
|
sa.Column('class_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('teacher_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['class_id'], ['classes.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['teacher_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id'),
|
||||||
|
sa.UniqueConstraint('code')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_subjects_id'), 'subjects', ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('teacher_classes',
|
||||||
|
sa.Column('teacher_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('class_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['class_id'], ['classes.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['teacher_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('teacher_id', 'class_id')
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table('attendance',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('student_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('class_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('date', sa.Date(), nullable=False),
|
||||||
|
sa.Column('is_present', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('remarks', sa.String(), nullable=True),
|
||||||
|
sa.Column('marked_by', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['class_id'], ['classes.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['marked_by'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['student_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_attendance_id'), 'attendance', ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('grades',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('student_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('subject_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('score', sa.Float(), nullable=False),
|
||||||
|
sa.Column('max_score', sa.Float(), nullable=False),
|
||||||
|
sa.Column('grade_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('description', sa.String(), nullable=True),
|
||||||
|
sa.Column('graded_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['student_id'], ['users.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['subject_id'], ['subjects.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_grades_id'), 'grades', ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('notifications',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('title', sa.String(), nullable=False),
|
||||||
|
sa.Column('message', sa.Text(), nullable=False),
|
||||||
|
sa.Column('sender_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('notification_type', sa.String(), nullable=False),
|
||||||
|
sa.Column('priority', sa.String(), nullable=True),
|
||||||
|
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=True),
|
||||||
|
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['sender_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
op.create_index(op.f('ix_notifications_id'), 'notifications', ['id'], unique=False)
|
||||||
|
|
||||||
|
op.create_table('notification_recipients',
|
||||||
|
sa.Column('notification_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('user_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('is_read', sa.Boolean(), nullable=True),
|
||||||
|
sa.Column('read_at', sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('notification_id', 'user_id')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table('notification_recipients')
|
||||||
|
op.drop_index(op.f('ix_notifications_id'), table_name='notifications')
|
||||||
|
op.drop_table('notifications')
|
||||||
|
op.drop_index(op.f('ix_grades_id'), table_name='grades')
|
||||||
|
op.drop_table('grades')
|
||||||
|
op.drop_index(op.f('ix_attendance_id'), table_name='attendance')
|
||||||
|
op.drop_table('attendance')
|
||||||
|
op.drop_table('teacher_classes')
|
||||||
|
op.drop_index(op.f('ix_subjects_id'), table_name='subjects')
|
||||||
|
op.drop_table('subjects')
|
||||||
|
op.drop_index(op.f('ix_users_id'), table_name='users')
|
||||||
|
op.drop_index(op.f('ix_users_email'), table_name='users')
|
||||||
|
op.drop_table('users')
|
||||||
|
op.drop_index(op.f('ix_classes_id'), table_name='classes')
|
||||||
|
op.drop_table('classes')
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
0
app/api/__init__.py
Normal file
0
app/api/__init__.py
Normal file
62
app/api/deps.py
Normal file
62
app/api/deps.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from typing import Generator
|
||||||
|
from fastapi import Depends, HTTPException, status
|
||||||
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.db.session import SessionLocal
|
||||||
|
from app.core import security
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.services.user import user_service
|
||||||
|
|
||||||
|
security_scheme = HTTPBearer()
|
||||||
|
|
||||||
|
def get_db() -> Generator:
|
||||||
|
try:
|
||||||
|
db = SessionLocal()
|
||||||
|
yield db
|
||||||
|
finally:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
def get_current_user(
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
credentials: HTTPAuthorizationCredentials = Depends(security_scheme)
|
||||||
|
) -> User:
|
||||||
|
token = credentials.credentials
|
||||||
|
user_id = security.verify_token(token)
|
||||||
|
if not user_id:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Could not validate credentials",
|
||||||
|
)
|
||||||
|
user = user_service.get(db, id=int(user_id))
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
if not user.is_active:
|
||||||
|
raise HTTPException(status_code=400, detail="Inactive user")
|
||||||
|
return user
|
||||||
|
|
||||||
|
def get_current_active_user(
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
) -> User:
|
||||||
|
if not current_user.is_active:
|
||||||
|
raise HTTPException(status_code=400, detail="Inactive user")
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
def get_current_admin_user(
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
) -> User:
|
||||||
|
if current_user.role != UserRole.ADMIN:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough permissions"
|
||||||
|
)
|
||||||
|
return current_user
|
||||||
|
|
||||||
|
def get_current_teacher_or_admin(
|
||||||
|
current_user: User = Depends(get_current_user),
|
||||||
|
) -> User:
|
||||||
|
if current_user.role not in [UserRole.TEACHER, UserRole.ADMIN]:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Not enough permissions"
|
||||||
|
)
|
||||||
|
return current_user
|
0
app/api/v1/__init__.py
Normal file
0
app/api/v1/__init__.py
Normal file
11
app/api/v1/api.py
Normal file
11
app/api/v1/api.py
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
from fastapi import APIRouter
|
||||||
|
from app.api.v1.endpoints import auth, users, classes, subjects, grades, attendance, notifications
|
||||||
|
|
||||||
|
api_router = APIRouter()
|
||||||
|
api_router.include_router(auth.router, prefix="/auth", tags=["authentication"])
|
||||||
|
api_router.include_router(users.router, prefix="/users", tags=["users"])
|
||||||
|
api_router.include_router(classes.router, prefix="/classes", tags=["classes"])
|
||||||
|
api_router.include_router(subjects.router, prefix="/subjects", tags=["subjects"])
|
||||||
|
api_router.include_router(grades.router, prefix="/grades", tags=["grades"])
|
||||||
|
api_router.include_router(attendance.router, prefix="/attendance", tags=["attendance"])
|
||||||
|
api_router.include_router(notifications.router, prefix="/notifications", tags=["notifications"])
|
0
app/api/v1/endpoints/__init__.py
Normal file
0
app/api/v1/endpoints/__init__.py
Normal file
83
app/api/v1/endpoints/attendance.py
Normal file
83
app/api/v1/endpoints/attendance.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
from typing import Any, List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from datetime import date
|
||||||
|
from app.api import deps
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.schemas.attendance import Attendance, AttendanceCreate, AttendanceUpdate
|
||||||
|
from app.services.attendance import attendance_service
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Attendance])
|
||||||
|
def read_attendance(
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
student_id: int = None,
|
||||||
|
class_id: int = None,
|
||||||
|
date: date = None,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
if current_user.role == UserRole.STUDENT:
|
||||||
|
attendance = attendance_service.get_by_student(db, student_id=current_user.id)
|
||||||
|
elif current_user.role == UserRole.PARENT:
|
||||||
|
if not student_id:
|
||||||
|
raise HTTPException(status_code=400, detail="Parent must specify student_id")
|
||||||
|
attendance = attendance_service.get_by_student(db, student_id=student_id)
|
||||||
|
elif student_id and date:
|
||||||
|
attendance = [attendance_service.get_by_student_and_date(db, student_id=student_id, date=date)]
|
||||||
|
elif student_id:
|
||||||
|
attendance = attendance_service.get_by_student(db, student_id=student_id)
|
||||||
|
elif class_id:
|
||||||
|
attendance = attendance_service.get_by_class(db, class_id=class_id)
|
||||||
|
elif date:
|
||||||
|
attendance = attendance_service.get_by_date(db, date=date)
|
||||||
|
else:
|
||||||
|
attendance = attendance_service.get_multi(db, skip=skip, limit=limit)
|
||||||
|
|
||||||
|
return [a for a in attendance if a is not None]
|
||||||
|
|
||||||
|
@router.post("/", response_model=Attendance)
|
||||||
|
def create_attendance(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
attendance_in: AttendanceCreate,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
attendance = attendance_service.create_with_teacher(
|
||||||
|
db, obj_in=attendance_in, teacher_id=current_user.id
|
||||||
|
)
|
||||||
|
return attendance
|
||||||
|
|
||||||
|
@router.put("/{attendance_id}", response_model=Attendance)
|
||||||
|
def update_attendance(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
attendance_id: int,
|
||||||
|
attendance_in: AttendanceUpdate,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
attendance = attendance_service.get(db, id=attendance_id)
|
||||||
|
if not attendance:
|
||||||
|
raise HTTPException(status_code=404, detail="Attendance not found")
|
||||||
|
attendance = attendance_service.update(db, db_obj=attendance, obj_in=attendance_in)
|
||||||
|
return attendance
|
||||||
|
|
||||||
|
@router.get("/{attendance_id}", response_model=Attendance)
|
||||||
|
def read_attendance_record(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
attendance_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
attendance = attendance_service.get(db, id=attendance_id)
|
||||||
|
if not attendance:
|
||||||
|
raise HTTPException(status_code=404, detail="Attendance not found")
|
||||||
|
|
||||||
|
if current_user.role == UserRole.STUDENT and attendance.student_id != current_user.id:
|
||||||
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||||
|
elif current_user.role == UserRole.PARENT and attendance.student.parent_id != current_user.id:
|
||||||
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||||
|
|
||||||
|
return attendance
|
60
app/api/v1/endpoints/auth.py
Normal file
60
app/api/v1/endpoints/auth.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
from datetime import timedelta
|
||||||
|
from typing import Any
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, status
|
||||||
|
from fastapi.security import OAuth2PasswordRequestForm
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.api import deps
|
||||||
|
from app.core import security
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.schemas.token import Token
|
||||||
|
from app.schemas.user import User, UserCreate
|
||||||
|
from app.services.user import user_service
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/login", response_model=Token)
|
||||||
|
def login_for_access_token(
|
||||||
|
db: Session = Depends(deps.get_db), form_data: OAuth2PasswordRequestForm = Depends()
|
||||||
|
) -> Any:
|
||||||
|
user = user_service.authenticate(
|
||||||
|
db, email=form_data.username, password=form_data.password
|
||||||
|
)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Incorrect email or password",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
elif not user_service.is_active(user):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user"
|
||||||
|
)
|
||||||
|
access_token_expires = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||||
|
return {
|
||||||
|
"access_token": security.create_access_token(
|
||||||
|
user.id, expires_delta=access_token_expires
|
||||||
|
),
|
||||||
|
"token_type": "bearer",
|
||||||
|
}
|
||||||
|
|
||||||
|
@router.post("/register", response_model=User)
|
||||||
|
def register_user(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
user_in: UserCreate,
|
||||||
|
) -> Any:
|
||||||
|
user = user_service.get_by_email(db, email=user_in.email)
|
||||||
|
if user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="The user with this email already exists in the system.",
|
||||||
|
)
|
||||||
|
user = user_service.create(db, obj_in=user_in)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@router.get("/me", response_model=User)
|
||||||
|
def read_users_me(
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
return current_user
|
68
app/api/v1/endpoints/classes.py
Normal file
68
app/api/v1/endpoints/classes.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from typing import Any, List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.api import deps
|
||||||
|
from app.models.user import User
|
||||||
|
from app.schemas.class_schema import Class, ClassCreate, ClassUpdate
|
||||||
|
from app.services.class_service import class_service
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Class])
|
||||||
|
def read_classes(
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
classes = class_service.get_multi(db, skip=skip, limit=limit)
|
||||||
|
return classes
|
||||||
|
|
||||||
|
@router.post("/", response_model=Class)
|
||||||
|
def create_class(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
class_in: ClassCreate,
|
||||||
|
current_user: User = Depends(deps.get_current_admin_user),
|
||||||
|
) -> Any:
|
||||||
|
class_obj = class_service.create(db, obj_in=class_in)
|
||||||
|
return class_obj
|
||||||
|
|
||||||
|
@router.put("/{class_id}", response_model=Class)
|
||||||
|
def update_class(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
class_id: int,
|
||||||
|
class_in: ClassUpdate,
|
||||||
|
current_user: User = Depends(deps.get_current_admin_user),
|
||||||
|
) -> Any:
|
||||||
|
class_obj = class_service.get(db, id=class_id)
|
||||||
|
if not class_obj:
|
||||||
|
raise HTTPException(status_code=404, detail="Class not found")
|
||||||
|
class_obj = class_service.update(db, db_obj=class_obj, obj_in=class_in)
|
||||||
|
return class_obj
|
||||||
|
|
||||||
|
@router.get("/{class_id}", response_model=Class)
|
||||||
|
def read_class(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
class_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
class_obj = class_service.get(db, id=class_id)
|
||||||
|
if not class_obj:
|
||||||
|
raise HTTPException(status_code=404, detail="Class not found")
|
||||||
|
return class_obj
|
||||||
|
|
||||||
|
@router.delete("/{class_id}", response_model=Class)
|
||||||
|
def delete_class(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
class_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_admin_user),
|
||||||
|
) -> Any:
|
||||||
|
class_obj = class_service.get(db, id=class_id)
|
||||||
|
if not class_obj:
|
||||||
|
raise HTTPException(status_code=404, detail="Class not found")
|
||||||
|
class_obj = class_service.remove(db, id=class_id)
|
||||||
|
return class_obj
|
90
app/api/v1/endpoints/grades.py
Normal file
90
app/api/v1/endpoints/grades.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
from typing import Any, List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.api import deps
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.schemas.grade import Grade, GradeCreate, GradeUpdate
|
||||||
|
from app.services.grade import grade_service
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Grade])
|
||||||
|
def read_grades(
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
student_id: int = None,
|
||||||
|
subject_id: int = None,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
if current_user.role == UserRole.STUDENT:
|
||||||
|
grades = grade_service.get_by_student(db, student_id=current_user.id)
|
||||||
|
elif current_user.role == UserRole.PARENT:
|
||||||
|
if not student_id:
|
||||||
|
raise HTTPException(status_code=400, detail="Parent must specify student_id")
|
||||||
|
grades = grade_service.get_by_student(db, student_id=student_id)
|
||||||
|
elif student_id and subject_id:
|
||||||
|
grades = grade_service.get_by_student_and_subject(db, student_id=student_id, subject_id=subject_id)
|
||||||
|
elif student_id:
|
||||||
|
grades = grade_service.get_by_student(db, student_id=student_id)
|
||||||
|
elif subject_id:
|
||||||
|
grades = grade_service.get_by_subject(db, subject_id=subject_id)
|
||||||
|
else:
|
||||||
|
grades = grade_service.get_multi(db, skip=skip, limit=limit)
|
||||||
|
|
||||||
|
return grades
|
||||||
|
|
||||||
|
@router.post("/", response_model=Grade)
|
||||||
|
def create_grade(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
grade_in: GradeCreate,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
grade = grade_service.create(db, obj_in=grade_in)
|
||||||
|
return grade
|
||||||
|
|
||||||
|
@router.put("/{grade_id}", response_model=Grade)
|
||||||
|
def update_grade(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
grade_id: int,
|
||||||
|
grade_in: GradeUpdate,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
grade = grade_service.get(db, id=grade_id)
|
||||||
|
if not grade:
|
||||||
|
raise HTTPException(status_code=404, detail="Grade not found")
|
||||||
|
grade = grade_service.update(db, db_obj=grade, obj_in=grade_in)
|
||||||
|
return grade
|
||||||
|
|
||||||
|
@router.get("/{grade_id}", response_model=Grade)
|
||||||
|
def read_grade(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
grade_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
grade = grade_service.get(db, id=grade_id)
|
||||||
|
if not grade:
|
||||||
|
raise HTTPException(status_code=404, detail="Grade not found")
|
||||||
|
|
||||||
|
if current_user.role == UserRole.STUDENT and grade.student_id != current_user.id:
|
||||||
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||||
|
elif current_user.role == UserRole.PARENT and grade.student.parent_id != current_user.id:
|
||||||
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||||
|
|
||||||
|
return grade
|
||||||
|
|
||||||
|
@router.delete("/{grade_id}", response_model=Grade)
|
||||||
|
def delete_grade(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
grade_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
grade = grade_service.get(db, id=grade_id)
|
||||||
|
if not grade:
|
||||||
|
raise HTTPException(status_code=404, detail="Grade not found")
|
||||||
|
grade = grade_service.remove(db, id=grade_id)
|
||||||
|
return grade
|
82
app/api/v1/endpoints/notifications.py
Normal file
82
app/api/v1/endpoints/notifications.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
from typing import Any, List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.api import deps
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.schemas.notification import Notification, NotificationCreate, NotificationUpdate
|
||||||
|
from app.services.notification import notification_service
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Notification])
|
||||||
|
def read_notifications(
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
if current_user.role in [UserRole.STUDENT, UserRole.PARENT]:
|
||||||
|
notifications = notification_service.get_by_recipient(db, recipient_id=current_user.id)
|
||||||
|
else:
|
||||||
|
notifications = notification_service.get_multi(db, skip=skip, limit=limit)
|
||||||
|
|
||||||
|
return notifications
|
||||||
|
|
||||||
|
@router.post("/", response_model=Notification)
|
||||||
|
def create_notification(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
notification_in: NotificationCreate,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
notification = notification_service.create_with_recipients(
|
||||||
|
db, obj_in=notification_in, sender_id=current_user.id
|
||||||
|
)
|
||||||
|
return notification
|
||||||
|
|
||||||
|
@router.put("/{notification_id}", response_model=Notification)
|
||||||
|
def update_notification(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
notification_id: int,
|
||||||
|
notification_in: NotificationUpdate,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
notification = notification_service.get(db, id=notification_id)
|
||||||
|
if not notification:
|
||||||
|
raise HTTPException(status_code=404, detail="Notification not found")
|
||||||
|
|
||||||
|
if notification.sender_id != current_user.id and current_user.role != UserRole.ADMIN:
|
||||||
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||||
|
|
||||||
|
notification = notification_service.update(db, db_obj=notification, obj_in=notification_in)
|
||||||
|
return notification
|
||||||
|
|
||||||
|
@router.get("/{notification_id}", response_model=Notification)
|
||||||
|
def read_notification(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
notification_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
notification = notification_service.get(db, id=notification_id)
|
||||||
|
if not notification:
|
||||||
|
raise HTTPException(status_code=404, detail="Notification not found")
|
||||||
|
return notification
|
||||||
|
|
||||||
|
@router.delete("/{notification_id}", response_model=Notification)
|
||||||
|
def delete_notification(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
notification_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
notification = notification_service.get(db, id=notification_id)
|
||||||
|
if not notification:
|
||||||
|
raise HTTPException(status_code=404, detail="Notification not found")
|
||||||
|
|
||||||
|
if notification.sender_id != current_user.id and current_user.role != UserRole.ADMIN:
|
||||||
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||||
|
|
||||||
|
notification = notification_service.remove(db, id=notification_id)
|
||||||
|
return notification
|
68
app/api/v1/endpoints/subjects.py
Normal file
68
app/api/v1/endpoints/subjects.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
from typing import Any, List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.api import deps
|
||||||
|
from app.models.user import User
|
||||||
|
from app.schemas.subject import Subject, SubjectCreate, SubjectUpdate
|
||||||
|
from app.services.subject import subject_service
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[Subject])
|
||||||
|
def read_subjects(
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
subjects = subject_service.get_multi(db, skip=skip, limit=limit)
|
||||||
|
return subjects
|
||||||
|
|
||||||
|
@router.post("/", response_model=Subject)
|
||||||
|
def create_subject(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
subject_in: SubjectCreate,
|
||||||
|
current_user: User = Depends(deps.get_current_admin_user),
|
||||||
|
) -> Any:
|
||||||
|
subject = subject_service.create(db, obj_in=subject_in)
|
||||||
|
return subject
|
||||||
|
|
||||||
|
@router.put("/{subject_id}", response_model=Subject)
|
||||||
|
def update_subject(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
subject_id: int,
|
||||||
|
subject_in: SubjectUpdate,
|
||||||
|
current_user: User = Depends(deps.get_current_admin_user),
|
||||||
|
) -> Any:
|
||||||
|
subject = subject_service.get(db, id=subject_id)
|
||||||
|
if not subject:
|
||||||
|
raise HTTPException(status_code=404, detail="Subject not found")
|
||||||
|
subject = subject_service.update(db, db_obj=subject, obj_in=subject_in)
|
||||||
|
return subject
|
||||||
|
|
||||||
|
@router.get("/{subject_id}", response_model=Subject)
|
||||||
|
def read_subject(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
subject_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
subject = subject_service.get(db, id=subject_id)
|
||||||
|
if not subject:
|
||||||
|
raise HTTPException(status_code=404, detail="Subject not found")
|
||||||
|
return subject
|
||||||
|
|
||||||
|
@router.delete("/{subject_id}", response_model=Subject)
|
||||||
|
def delete_subject(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
subject_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_admin_user),
|
||||||
|
) -> Any:
|
||||||
|
subject = subject_service.get(db, id=subject_id)
|
||||||
|
if not subject:
|
||||||
|
raise HTTPException(status_code=404, detail="Subject not found")
|
||||||
|
subject = subject_service.remove(db, id=subject_id)
|
||||||
|
return subject
|
115
app/api/v1/endpoints/users.py
Normal file
115
app/api/v1/endpoints/users.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
from typing import Any, List
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.api import deps
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.schemas.user import User as UserSchema, UserCreate, UserUpdate
|
||||||
|
from app.services.user import user_service
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.get("/", response_model=List[UserSchema])
|
||||||
|
def read_users(
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
current_user: User = Depends(deps.get_current_admin_user),
|
||||||
|
) -> Any:
|
||||||
|
users = user_service.get_multi(db, skip=skip, limit=limit)
|
||||||
|
return users
|
||||||
|
|
||||||
|
@router.post("/", response_model=UserSchema)
|
||||||
|
def create_user(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
user_in: UserCreate,
|
||||||
|
current_user: User = Depends(deps.get_current_admin_user),
|
||||||
|
) -> Any:
|
||||||
|
user = user_service.get_by_email(db, email=user_in.email)
|
||||||
|
if user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=400,
|
||||||
|
detail="The user with this email already exists in the system",
|
||||||
|
)
|
||||||
|
user = user_service.create(db, obj_in=user_in)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@router.put("/{user_id}", response_model=UserSchema)
|
||||||
|
def update_user(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
user_id: int,
|
||||||
|
user_in: UserUpdate,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
user = user_service.get(db, id=user_id)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="The user with this id does not exist in the system",
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_user.role == UserRole.TEACHER and user_in.role and user_in.role != UserRole.STUDENT:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Teachers can only modify student profiles"
|
||||||
|
)
|
||||||
|
|
||||||
|
user = user_service.update(db, db_obj=user, obj_in=user_in)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@router.get("/{user_id}", response_model=UserSchema)
|
||||||
|
def read_user(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
user_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_active_user),
|
||||||
|
) -> Any:
|
||||||
|
user = user_service.get(db, id=user_id)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
if current_user.role not in [UserRole.ADMIN, UserRole.TEACHER] and current_user.id != user_id:
|
||||||
|
raise HTTPException(status_code=403, detail="Not enough permissions")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
@router.delete("/{user_id}", response_model=UserSchema)
|
||||||
|
def delete_user(
|
||||||
|
*,
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
user_id: int,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
user = user_service.get(db, id=user_id)
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=404, detail="User not found")
|
||||||
|
|
||||||
|
if current_user.role == UserRole.TEACHER and user.role != UserRole.STUDENT:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=403,
|
||||||
|
detail="Teachers can only delete student profiles"
|
||||||
|
)
|
||||||
|
|
||||||
|
user = user_service.remove(db, id=user_id)
|
||||||
|
return user
|
||||||
|
|
||||||
|
@router.get("/students/", response_model=List[UserSchema])
|
||||||
|
def read_students(
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
current_user: User = Depends(deps.get_current_teacher_or_admin),
|
||||||
|
) -> Any:
|
||||||
|
students = user_service.get_students(db, skip=skip, limit=limit)
|
||||||
|
return students
|
||||||
|
|
||||||
|
@router.get("/teachers/", response_model=List[UserSchema])
|
||||||
|
def read_teachers(
|
||||||
|
db: Session = Depends(deps.get_db),
|
||||||
|
skip: int = 0,
|
||||||
|
limit: int = 100,
|
||||||
|
current_user: User = Depends(deps.get_current_admin_user),
|
||||||
|
) -> Any:
|
||||||
|
teachers = user_service.get_teachers(db, skip=skip, limit=limit)
|
||||||
|
return teachers
|
0
app/core/__init__.py
Normal file
0
app/core/__init__.py
Normal file
20
app/core/config.py
Normal file
20
app/core/config.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import os
|
||||||
|
from pydantic import BaseSettings
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
PROJECT_NAME: str = "School Portal API"
|
||||||
|
VERSION: str = "1.0.0"
|
||||||
|
API_V1_STR: str = "/api/v1"
|
||||||
|
|
||||||
|
SECRET_KEY: str = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
|
||||||
|
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8 # 8 days
|
||||||
|
|
||||||
|
DATABASE_URL: str = os.getenv("DATABASE_URL", "sqlite:////app/storage/db/db.sqlite")
|
||||||
|
|
||||||
|
FIRST_SUPERUSER_EMAIL: str = os.getenv("FIRST_SUPERUSER_EMAIL", "admin@schoolportal.com")
|
||||||
|
FIRST_SUPERUSER_PASSWORD: str = os.getenv("FIRST_SUPERUSER_PASSWORD", "admin123")
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
case_sensitive = True
|
||||||
|
|
||||||
|
settings = Settings()
|
38
app/core/security.py
Normal file
38
app/core/security.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Optional, Union
|
||||||
|
from jose import JWTError, jwt
|
||||||
|
from passlib.context import CryptContext
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||||
|
|
||||||
|
ALGORITHM = "HS256"
|
||||||
|
|
||||||
|
def create_access_token(
|
||||||
|
subject: Union[str, int], expires_delta: Optional[timedelta] = None
|
||||||
|
):
|
||||||
|
if expires_delta:
|
||||||
|
expire = datetime.utcnow() + expires_delta
|
||||||
|
else:
|
||||||
|
expire = datetime.utcnow() + timedelta(
|
||||||
|
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||||
|
)
|
||||||
|
to_encode = {"exp": expire, "sub": str(subject)}
|
||||||
|
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
||||||
|
return encoded_jwt
|
||||||
|
|
||||||
|
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||||
|
return pwd_context.verify(plain_password, hashed_password)
|
||||||
|
|
||||||
|
def get_password_hash(password: str) -> str:
|
||||||
|
return pwd_context.hash(password)
|
||||||
|
|
||||||
|
def verify_token(token: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[ALGORITHM])
|
||||||
|
user_id: str = payload.get("sub")
|
||||||
|
if user_id is None:
|
||||||
|
return None
|
||||||
|
return user_id
|
||||||
|
except JWTError:
|
||||||
|
return None
|
0
app/db/__init__.py
Normal file
0
app/db/__init__.py
Normal file
3
app/db/base.py
Normal file
3
app/db/base.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
|
||||||
|
Base = declarative_base()
|
15
app/db/session.py
Normal file
15
app/db/session.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
DB_DIR = Path("/app/storage/db")
|
||||||
|
DB_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite"
|
||||||
|
|
||||||
|
engine = create_engine(
|
||||||
|
SQLALCHEMY_DATABASE_URL,
|
||||||
|
connect_args={"check_same_thread": False}
|
||||||
|
)
|
||||||
|
|
||||||
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
8
app/models/__init__.py
Normal file
8
app/models/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
from .user import User
|
||||||
|
from .class_model import Class
|
||||||
|
from .subject import Subject
|
||||||
|
from .grade import Grade
|
||||||
|
from .attendance import Attendance
|
||||||
|
from .notification import Notification
|
||||||
|
|
||||||
|
__all__ = ["User", "Class", "Subject", "Grade", "Attendance", "Notification"]
|
21
app/models/attendance.py
Normal file
21
app/models/attendance.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Date, Boolean
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
class Attendance(Base):
|
||||||
|
__tablename__ = "attendance"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
student_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
class_id = Column(Integer, ForeignKey("classes.id"), nullable=False)
|
||||||
|
date = Column(Date, nullable=False)
|
||||||
|
is_present = Column(Boolean, default=False)
|
||||||
|
remarks = Column(String)
|
||||||
|
marked_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
student = relationship("User", foreign_keys=[student_id], back_populates="attendance_records")
|
||||||
|
class_ref = relationship("Class", back_populates="attendance_records")
|
||||||
|
teacher = relationship("User", foreign_keys=[marked_by])
|
26
app/models/class_model.py
Normal file
26
app/models/class_model.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, Table, ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
teacher_classes = Table(
|
||||||
|
"teacher_classes",
|
||||||
|
Base.metadata,
|
||||||
|
Column("teacher_id", Integer, ForeignKey("users.id"), primary_key=True),
|
||||||
|
Column("class_id", Integer, ForeignKey("classes.id"), primary_key=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Class(Base):
|
||||||
|
__tablename__ = "classes"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
grade_level = Column(String, nullable=False)
|
||||||
|
academic_year = Column(String, nullable=False)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
students = relationship("User", back_populates="assigned_class")
|
||||||
|
teachers = relationship("User", secondary=teacher_classes, back_populates="taught_classes")
|
||||||
|
subjects = relationship("Subject", back_populates="class_ref")
|
||||||
|
attendance_records = relationship("Attendance", back_populates="class_ref")
|
21
app/models/grade.py
Normal file
21
app/models/grade.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
class Grade(Base):
|
||||||
|
__tablename__ = "grades"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
student_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
subject_id = Column(Integer, ForeignKey("subjects.id"), nullable=False)
|
||||||
|
score = Column(Float, nullable=False)
|
||||||
|
max_score = Column(Float, nullable=False, default=100.0)
|
||||||
|
grade_type = Column(String, nullable=False) # quiz, exam, assignment, etc.
|
||||||
|
description = Column(String)
|
||||||
|
graded_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
student = relationship("User", back_populates="grades")
|
||||||
|
subject = relationship("Subject", back_populates="grades")
|
28
app/models/notification.py
Normal file
28
app/models/notification.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean, Text, Table
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
notification_recipients = Table(
|
||||||
|
"notification_recipients",
|
||||||
|
Base.metadata,
|
||||||
|
Column("notification_id", Integer, ForeignKey("notifications.id"), primary_key=True),
|
||||||
|
Column("user_id", Integer, ForeignKey("users.id"), primary_key=True),
|
||||||
|
Column("is_read", Boolean, default=False),
|
||||||
|
Column("read_at", DateTime(timezone=True))
|
||||||
|
)
|
||||||
|
|
||||||
|
class Notification(Base):
|
||||||
|
__tablename__ = "notifications"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
title = Column(String, nullable=False)
|
||||||
|
message = Column(Text, nullable=False)
|
||||||
|
sender_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
notification_type = Column(String, nullable=False) # announcement, message, alert
|
||||||
|
priority = Column(String, default="normal") # low, normal, high, urgent
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
sender = relationship("User", foreign_keys=[sender_id], back_populates="sent_notifications")
|
||||||
|
recipients = relationship("User", secondary=notification_recipients, back_populates="received_notifications")
|
20
app/models/subject.py
Normal file
20
app/models/subject.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
class Subject(Base):
|
||||||
|
__tablename__ = "subjects"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
name = Column(String, nullable=False)
|
||||||
|
code = Column(String, unique=True, nullable=False)
|
||||||
|
description = Column(String)
|
||||||
|
class_id = Column(Integer, ForeignKey("classes.id"), nullable=False)
|
||||||
|
teacher_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
class_ref = relationship("Class", back_populates="subjects")
|
||||||
|
teacher = relationship("User")
|
||||||
|
grades = relationship("Grade", back_populates="subject")
|
35
app/models/user.py
Normal file
35
app/models/user.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, Enum, ForeignKey
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
import enum
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
class UserRole(str, enum.Enum):
|
||||||
|
ADMIN = "admin"
|
||||||
|
TEACHER = "teacher"
|
||||||
|
STUDENT = "student"
|
||||||
|
PARENT = "parent"
|
||||||
|
|
||||||
|
class User(Base):
|
||||||
|
__tablename__ = "users"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
|
email = Column(String, unique=True, index=True, nullable=False)
|
||||||
|
hashed_password = Column(String, nullable=False)
|
||||||
|
first_name = Column(String, nullable=False)
|
||||||
|
last_name = Column(String, nullable=False)
|
||||||
|
role = Column(Enum(UserRole), nullable=False)
|
||||||
|
is_active = Column(Boolean, default=True)
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
updated_at = Column(DateTime(timezone=True), onupdate=func.now())
|
||||||
|
|
||||||
|
parent_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||||
|
class_id = Column(Integer, ForeignKey("classes.id"), nullable=True)
|
||||||
|
|
||||||
|
parent = relationship("User", remote_side=[id], backref="children")
|
||||||
|
assigned_class = relationship("Class", back_populates="students")
|
||||||
|
taught_classes = relationship("Class", secondary="teacher_classes", back_populates="teachers")
|
||||||
|
grades = relationship("Grade", back_populates="student")
|
||||||
|
attendance_records = relationship("Attendance", back_populates="student")
|
||||||
|
sent_notifications = relationship("Notification", foreign_keys="Notification.sender_id", back_populates="sender")
|
||||||
|
received_notifications = relationship("Notification", secondary="notification_recipients", back_populates="recipients")
|
17
app/schemas/__init__.py
Normal file
17
app/schemas/__init__.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from .user import User, UserCreate, UserUpdate, UserInDB
|
||||||
|
from .class_schema import Class, ClassCreate, ClassUpdate
|
||||||
|
from .subject import Subject, SubjectCreate, SubjectUpdate
|
||||||
|
from .grade import Grade, GradeCreate, GradeUpdate
|
||||||
|
from .attendance import Attendance, AttendanceCreate, AttendanceUpdate
|
||||||
|
from .notification import Notification, NotificationCreate, NotificationUpdate
|
||||||
|
from .token import Token, TokenPayload
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"User", "UserCreate", "UserUpdate", "UserInDB",
|
||||||
|
"Class", "ClassCreate", "ClassUpdate",
|
||||||
|
"Subject", "SubjectCreate", "SubjectUpdate",
|
||||||
|
"Grade", "GradeCreate", "GradeUpdate",
|
||||||
|
"Attendance", "AttendanceCreate", "AttendanceUpdate",
|
||||||
|
"Notification", "NotificationCreate", "NotificationUpdate",
|
||||||
|
"Token", "TokenPayload"
|
||||||
|
]
|
29
app/schemas/attendance.py
Normal file
29
app/schemas/attendance.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime, date
|
||||||
|
|
||||||
|
class AttendanceBase(BaseModel):
|
||||||
|
student_id: int
|
||||||
|
class_id: int
|
||||||
|
date: date
|
||||||
|
is_present: bool = False
|
||||||
|
remarks: Optional[str] = None
|
||||||
|
|
||||||
|
class AttendanceCreate(AttendanceBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class AttendanceUpdate(BaseModel):
|
||||||
|
is_present: Optional[bool] = None
|
||||||
|
remarks: Optional[str] = None
|
||||||
|
|
||||||
|
class AttendanceInDBBase(AttendanceBase):
|
||||||
|
id: int
|
||||||
|
marked_by: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class Attendance(AttendanceInDBBase):
|
||||||
|
pass
|
27
app/schemas/class_schema.py
Normal file
27
app/schemas/class_schema.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class ClassBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
grade_level: str
|
||||||
|
academic_year: str
|
||||||
|
|
||||||
|
class ClassCreate(ClassBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ClassUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
grade_level: Optional[str] = None
|
||||||
|
academic_year: Optional[str] = None
|
||||||
|
|
||||||
|
class ClassInDBBase(ClassBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class Class(ClassInDBBase):
|
||||||
|
pass
|
32
app/schemas/grade.py
Normal file
32
app/schemas/grade.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class GradeBase(BaseModel):
|
||||||
|
student_id: int
|
||||||
|
subject_id: int
|
||||||
|
score: float
|
||||||
|
max_score: float = 100.0
|
||||||
|
grade_type: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
class GradeCreate(GradeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class GradeUpdate(BaseModel):
|
||||||
|
score: Optional[float] = None
|
||||||
|
max_score: Optional[float] = None
|
||||||
|
grade_type: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
|
||||||
|
class GradeInDBBase(GradeBase):
|
||||||
|
id: int
|
||||||
|
graded_at: datetime
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class Grade(GradeInDBBase):
|
||||||
|
pass
|
30
app/schemas/notification.py
Normal file
30
app/schemas/notification.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from typing import Optional, List
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class NotificationBase(BaseModel):
|
||||||
|
title: str
|
||||||
|
message: str
|
||||||
|
notification_type: str
|
||||||
|
priority: str = "normal"
|
||||||
|
|
||||||
|
class NotificationCreate(NotificationBase):
|
||||||
|
recipient_ids: List[int]
|
||||||
|
|
||||||
|
class NotificationUpdate(BaseModel):
|
||||||
|
title: Optional[str] = None
|
||||||
|
message: Optional[str] = None
|
||||||
|
notification_type: Optional[str] = None
|
||||||
|
priority: Optional[str] = None
|
||||||
|
|
||||||
|
class NotificationInDBBase(NotificationBase):
|
||||||
|
id: int
|
||||||
|
sender_id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class Notification(NotificationInDBBase):
|
||||||
|
pass
|
31
app/schemas/subject.py
Normal file
31
app/schemas/subject.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class SubjectBase(BaseModel):
|
||||||
|
name: str
|
||||||
|
code: str
|
||||||
|
description: Optional[str] = None
|
||||||
|
class_id: int
|
||||||
|
teacher_id: int
|
||||||
|
|
||||||
|
class SubjectCreate(SubjectBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class SubjectUpdate(BaseModel):
|
||||||
|
name: Optional[str] = None
|
||||||
|
code: Optional[str] = None
|
||||||
|
description: Optional[str] = None
|
||||||
|
class_id: Optional[int] = None
|
||||||
|
teacher_id: Optional[int] = None
|
||||||
|
|
||||||
|
class SubjectInDBBase(SubjectBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class Subject(SubjectInDBBase):
|
||||||
|
pass
|
9
app/schemas/token.py
Normal file
9
app/schemas/token.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
class Token(BaseModel):
|
||||||
|
access_token: str
|
||||||
|
token_type: str
|
||||||
|
|
||||||
|
class TokenPayload(BaseModel):
|
||||||
|
sub: Optional[int] = None
|
40
app/schemas/user.py
Normal file
40
app/schemas/user.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel, EmailStr
|
||||||
|
from datetime import datetime
|
||||||
|
from app.models.user import UserRole
|
||||||
|
|
||||||
|
class UserBase(BaseModel):
|
||||||
|
email: EmailStr
|
||||||
|
first_name: str
|
||||||
|
last_name: str
|
||||||
|
role: UserRole
|
||||||
|
is_active: bool = True
|
||||||
|
parent_id: Optional[int] = None
|
||||||
|
class_id: Optional[int] = None
|
||||||
|
|
||||||
|
class UserCreate(UserBase):
|
||||||
|
password: str
|
||||||
|
|
||||||
|
class UserUpdate(BaseModel):
|
||||||
|
email: Optional[EmailStr] = None
|
||||||
|
first_name: Optional[str] = None
|
||||||
|
last_name: Optional[str] = None
|
||||||
|
role: Optional[UserRole] = None
|
||||||
|
is_active: Optional[bool] = None
|
||||||
|
parent_id: Optional[int] = None
|
||||||
|
class_id: Optional[int] = None
|
||||||
|
password: Optional[str] = None
|
||||||
|
|
||||||
|
class UserInDBBase(UserBase):
|
||||||
|
id: int
|
||||||
|
created_at: datetime
|
||||||
|
updated_at: Optional[datetime] = None
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
orm_mode = True
|
||||||
|
|
||||||
|
class User(UserInDBBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class UserInDB(UserInDBBase):
|
||||||
|
hashed_password: str
|
0
app/services/__init__.py
Normal file
0
app/services/__init__.py
Normal file
33
app/services/attendance.py
Normal file
33
app/services/attendance.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from typing import List
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from datetime import date
|
||||||
|
from app.models.attendance import Attendance
|
||||||
|
from app.schemas.attendance import AttendanceCreate, AttendanceUpdate
|
||||||
|
from app.services.base import CRUDBase
|
||||||
|
|
||||||
|
class CRUDAttendance(CRUDBase[Attendance, AttendanceCreate, AttendanceUpdate]):
|
||||||
|
def get_by_student(self, db: Session, *, student_id: int) -> List[Attendance]:
|
||||||
|
return db.query(Attendance).filter(Attendance.student_id == student_id).all()
|
||||||
|
|
||||||
|
def get_by_class(self, db: Session, *, class_id: int) -> List[Attendance]:
|
||||||
|
return db.query(Attendance).filter(Attendance.class_id == class_id).all()
|
||||||
|
|
||||||
|
def get_by_date(self, db: Session, *, date: date) -> List[Attendance]:
|
||||||
|
return db.query(Attendance).filter(Attendance.date == date).all()
|
||||||
|
|
||||||
|
def get_by_student_and_date(self, db: Session, *, student_id: int, date: date) -> Attendance:
|
||||||
|
return db.query(Attendance).filter(
|
||||||
|
Attendance.student_id == student_id,
|
||||||
|
Attendance.date == date
|
||||||
|
).first()
|
||||||
|
|
||||||
|
def create_with_teacher(self, db: Session, *, obj_in: AttendanceCreate, teacher_id: int) -> Attendance:
|
||||||
|
obj_in_data = obj_in.dict()
|
||||||
|
obj_in_data["marked_by"] = teacher_id
|
||||||
|
db_obj = Attendance(**obj_in_data)
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
attendance_service = CRUDAttendance(Attendance)
|
55
app/services/base.py
Normal file
55
app/services/base.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
|
||||||
|
from fastapi.encoders import jsonable_encoder
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.db.base import Base
|
||||||
|
|
||||||
|
ModelType = TypeVar("ModelType", bound=Base)
|
||||||
|
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
|
||||||
|
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
|
||||||
|
|
||||||
|
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
||||||
|
def __init__(self, model: Type[ModelType]):
|
||||||
|
self.model = model
|
||||||
|
|
||||||
|
def get(self, db: Session, id: Any) -> Optional[ModelType]:
|
||||||
|
return db.query(self.model).filter(self.model.id == id).first()
|
||||||
|
|
||||||
|
def get_multi(
|
||||||
|
self, db: Session, *, skip: int = 0, limit: int = 100
|
||||||
|
) -> List[ModelType]:
|
||||||
|
return db.query(self.model).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
|
||||||
|
obj_in_data = jsonable_encoder(obj_in)
|
||||||
|
db_obj = self.model(**obj_in_data)
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self,
|
||||||
|
db: Session,
|
||||||
|
*,
|
||||||
|
db_obj: ModelType,
|
||||||
|
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
|
||||||
|
) -> ModelType:
|
||||||
|
obj_data = jsonable_encoder(db_obj)
|
||||||
|
if isinstance(obj_in, dict):
|
||||||
|
update_data = obj_in
|
||||||
|
else:
|
||||||
|
update_data = obj_in.dict(exclude_unset=True)
|
||||||
|
for field in obj_data:
|
||||||
|
if field in update_data:
|
||||||
|
setattr(db_obj, field, update_data[field])
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def remove(self, db: Session, *, id: int) -> ModelType:
|
||||||
|
obj = db.query(self.model).get(id)
|
||||||
|
db.delete(obj)
|
||||||
|
db.commit()
|
||||||
|
return obj
|
14
app/services/class_service.py
Normal file
14
app/services/class_service.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from typing import List
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.class_model import Class
|
||||||
|
from app.schemas.class_schema import ClassCreate, ClassUpdate
|
||||||
|
from app.services.base import CRUDBase
|
||||||
|
|
||||||
|
class CRUDClass(CRUDBase[Class, ClassCreate, ClassUpdate]):
|
||||||
|
def get_by_grade_level(self, db: Session, *, grade_level: str) -> List[Class]:
|
||||||
|
return db.query(Class).filter(Class.grade_level == grade_level).all()
|
||||||
|
|
||||||
|
def get_by_academic_year(self, db: Session, *, academic_year: str) -> List[Class]:
|
||||||
|
return db.query(Class).filter(Class.academic_year == academic_year).all()
|
||||||
|
|
||||||
|
class_service = CRUDClass(Class)
|
20
app/services/grade.py
Normal file
20
app/services/grade.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from typing import List
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.grade import Grade
|
||||||
|
from app.schemas.grade import GradeCreate, GradeUpdate
|
||||||
|
from app.services.base import CRUDBase
|
||||||
|
|
||||||
|
class CRUDGrade(CRUDBase[Grade, GradeCreate, GradeUpdate]):
|
||||||
|
def get_by_student(self, db: Session, *, student_id: int) -> List[Grade]:
|
||||||
|
return db.query(Grade).filter(Grade.student_id == student_id).all()
|
||||||
|
|
||||||
|
def get_by_subject(self, db: Session, *, subject_id: int) -> List[Grade]:
|
||||||
|
return db.query(Grade).filter(Grade.subject_id == subject_id).all()
|
||||||
|
|
||||||
|
def get_by_student_and_subject(self, db: Session, *, student_id: int, subject_id: int) -> List[Grade]:
|
||||||
|
return db.query(Grade).filter(
|
||||||
|
Grade.student_id == student_id,
|
||||||
|
Grade.subject_id == subject_id
|
||||||
|
).all()
|
||||||
|
|
||||||
|
grade_service = CRUDGrade(Grade)
|
35
app/services/notification.py
Normal file
35
app/services/notification.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from typing import List
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.notification import Notification
|
||||||
|
from app.schemas.notification import NotificationCreate, NotificationUpdate
|
||||||
|
from app.services.base import CRUDBase
|
||||||
|
|
||||||
|
class CRUDNotification(CRUDBase[Notification, NotificationCreate, NotificationUpdate]):
|
||||||
|
def create_with_recipients(self, db: Session, *, obj_in: NotificationCreate, sender_id: int) -> Notification:
|
||||||
|
recipient_ids = obj_in.recipient_ids
|
||||||
|
obj_in_data = obj_in.dict(exclude={"recipient_ids"})
|
||||||
|
obj_in_data["sender_id"] = sender_id
|
||||||
|
|
||||||
|
db_obj = Notification(**obj_in_data)
|
||||||
|
db.add(db_obj)
|
||||||
|
db.flush()
|
||||||
|
|
||||||
|
for recipient_id in recipient_ids:
|
||||||
|
db.execute(
|
||||||
|
"INSERT INTO notification_recipients (notification_id, user_id) VALUES (?, ?)",
|
||||||
|
(db_obj.id, recipient_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def get_by_sender(self, db: Session, *, sender_id: int) -> List[Notification]:
|
||||||
|
return db.query(Notification).filter(Notification.sender_id == sender_id).all()
|
||||||
|
|
||||||
|
def get_by_recipient(self, db: Session, *, recipient_id: int) -> List[Notification]:
|
||||||
|
return db.query(Notification).join(
|
||||||
|
Notification.recipients
|
||||||
|
).filter_by(id=recipient_id).all()
|
||||||
|
|
||||||
|
notification_service = CRUDNotification(Notification)
|
14
app/services/subject.py
Normal file
14
app/services/subject.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from typing import List
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.models.subject import Subject
|
||||||
|
from app.schemas.subject import SubjectCreate, SubjectUpdate
|
||||||
|
from app.services.base import CRUDBase
|
||||||
|
|
||||||
|
class CRUDSubject(CRUDBase[Subject, SubjectCreate, SubjectUpdate]):
|
||||||
|
def get_by_class(self, db: Session, *, class_id: int) -> List[Subject]:
|
||||||
|
return db.query(Subject).filter(Subject.class_id == class_id).all()
|
||||||
|
|
||||||
|
def get_by_teacher(self, db: Session, *, teacher_id: int) -> List[Subject]:
|
||||||
|
return db.query(Subject).filter(Subject.teacher_id == teacher_id).all()
|
||||||
|
|
||||||
|
subject_service = CRUDSubject(Subject)
|
63
app/services/user.py
Normal file
63
app/services/user.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
from typing import Any, Dict, Optional, Union, List
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from app.core.security import get_password_hash, verify_password
|
||||||
|
from app.models.user import User, UserRole
|
||||||
|
from app.schemas.user import UserCreate, UserUpdate
|
||||||
|
from app.services.base import CRUDBase
|
||||||
|
|
||||||
|
class CRUDUser(CRUDBase[User, UserCreate, UserUpdate]):
|
||||||
|
def get_by_email(self, db: Session, *, email: str) -> Optional[User]:
|
||||||
|
return db.query(User).filter(User.email == email).first()
|
||||||
|
|
||||||
|
def create(self, db: Session, *, obj_in: UserCreate) -> User:
|
||||||
|
db_obj = User(
|
||||||
|
email=obj_in.email,
|
||||||
|
hashed_password=get_password_hash(obj_in.password),
|
||||||
|
first_name=obj_in.first_name,
|
||||||
|
last_name=obj_in.last_name,
|
||||||
|
role=obj_in.role,
|
||||||
|
is_active=obj_in.is_active,
|
||||||
|
parent_id=obj_in.parent_id,
|
||||||
|
class_id=obj_in.class_id,
|
||||||
|
)
|
||||||
|
db.add(db_obj)
|
||||||
|
db.commit()
|
||||||
|
db.refresh(db_obj)
|
||||||
|
return db_obj
|
||||||
|
|
||||||
|
def update(
|
||||||
|
self, db: Session, *, db_obj: User, obj_in: Union[UserUpdate, Dict[str, Any]]
|
||||||
|
) -> User:
|
||||||
|
if isinstance(obj_in, dict):
|
||||||
|
update_data = obj_in
|
||||||
|
else:
|
||||||
|
update_data = obj_in.dict(exclude_unset=True)
|
||||||
|
|
||||||
|
if "password" in update_data:
|
||||||
|
hashed_password = get_password_hash(update_data["password"])
|
||||||
|
del update_data["password"]
|
||||||
|
update_data["hashed_password"] = hashed_password
|
||||||
|
|
||||||
|
return super().update(db, db_obj=db_obj, obj_in=update_data)
|
||||||
|
|
||||||
|
def authenticate(self, db: Session, *, email: str, password: str) -> Optional[User]:
|
||||||
|
user = self.get_by_email(db, email=email)
|
||||||
|
if not user:
|
||||||
|
return None
|
||||||
|
if not verify_password(password, user.hashed_password):
|
||||||
|
return None
|
||||||
|
return user
|
||||||
|
|
||||||
|
def is_active(self, user: User) -> bool:
|
||||||
|
return user.is_active
|
||||||
|
|
||||||
|
def get_students(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[User]:
|
||||||
|
return db.query(User).filter(User.role == UserRole.STUDENT).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
def get_teachers(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[User]:
|
||||||
|
return db.query(User).filter(User.role == UserRole.TEACHER).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
def get_parents(self, db: Session, *, skip: int = 0, limit: int = 100) -> List[User]:
|
||||||
|
return db.query(User).filter(User.role == UserRole.PARENT).offset(skip).limit(limit).all()
|
||||||
|
|
||||||
|
user_service = CRUDUser(User)
|
37
main.py
Normal file
37
main.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from app.api.v1.api import api_router
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title=settings.PROJECT_NAME,
|
||||||
|
version=settings.VERSION,
|
||||||
|
description="School Portal API - A comprehensive school management system",
|
||||||
|
openapi_url="/openapi.json",
|
||||||
|
docs_url="/docs",
|
||||||
|
redoc_url="/redoc"
|
||||||
|
)
|
||||||
|
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(api_router, prefix="/api/v1")
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {
|
||||||
|
"title": settings.PROJECT_NAME,
|
||||||
|
"version": settings.VERSION,
|
||||||
|
"description": "School Portal API - A comprehensive school management system",
|
||||||
|
"documentation": "/docs",
|
||||||
|
"health": "/health"
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
return {"status": "healthy", "service": "School Portal API"}
|
9
requirements.txt
Normal file
9
requirements.txt
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
fastapi==0.104.1
|
||||||
|
uvicorn[standard]==0.24.0
|
||||||
|
sqlalchemy==2.0.23
|
||||||
|
alembic==1.12.1
|
||||||
|
pydantic[email]==2.5.0
|
||||||
|
python-jose[cryptography]==3.3.0
|
||||||
|
passlib[bcrypt]==1.7.4
|
||||||
|
python-multipart==0.0.6
|
||||||
|
ruff==0.1.6
|
Loading…
x
Reference in New Issue
Block a user