From 101afa20b5c00ae53525401ef4c5fb6267b42b6b Mon Sep 17 00:00:00 2001 From: Alejandro Vazquez Date: Thu, 26 Mar 2026 18:17:04 -0300 Subject: [PATCH 1/8] setup --- app/models/user.py | 2 +- app/routes/schedule.py | 12 ++++++------ setup.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/models/user.py b/app/models/user.py index b6aede1..019b2f8 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -20,7 +20,7 @@ class User(UserMixin, db.Model): def set_password(self, password): """Set password hash""" - self.password_hash = generate_password_hash(password) + self.password_hash = generate_password_hash(password, method='pbkdf2:sha256') def check_password(self, password): """Check password""" diff --git a/app/routes/schedule.py b/app/routes/schedule.py index 81d6554..47449be 100644 --- a/app/routes/schedule.py +++ b/app/routes/schedule.py @@ -63,13 +63,13 @@ def add_reservation(): """Add new reservation""" form = ReservationForm() - # Populate classroom choices +# Populate classroom choices classrooms = Classroom.query.filter_by(is_active=True).all() - form.classroom_id.choices = [(c.id, c'{c.code} - {c.name} ({c.capacity} seats)') for c in classrooms] + form.classroom_id.choices = [(c.id, f'{c.code} - {c.name} ({c.capacity} seats)') for c in classrooms] # Populate commission choices commissions = Commission.query.filter_by(is_active=True).all() - form.commission_id.choices = [(c.id, c'{c.get_full_code()} - {c.teacher_name}') for c in commissions] + form.commission_id.choices = [(c.id, f'{c.get_full_code()} - {c.teacher_name}') for c in commissions] if form.validate_on_submit(): # Check for conflicts @@ -137,12 +137,12 @@ def edit_reservation(Id): form = ReservationForm(obj=reservation) - # Populate choices +# Populate choices classrooms = Classroom.query.filter_by(is_active=True).all() - form.classroom_id.choices = [(c.id, c'{c.code} - {c.name} ({c.capacity} seats)') for c in classrooms] + form.classroom_id.choices = [(c.id, f'{c.code} - {c.name} ({c.capacity} seats)') for c in classrooms] commissions = Commission.query.filter_by(is_active=True).all() - form.commission_id.choices = [(c.id, c'{c.get_full_code()} - {c.teacher_name}') for c in commissions] + form.commission_id.choices = [(c.id, f'{c.get_full_code()} - {c.teacher_name}') for c in commissions] if form.validate_on_submit(): # Check for conflicts (excluding this reservation) diff --git a/setup.py b/setup.py index 8a2bd70..65b58cc 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +pipfrom setuptools import setup, find_packages setup( name="edu-space-admin", From b95a3f5bc7f5f211598439bfdbd0f58ab930c4b3 Mon Sep 17 00:00:00 2001 From: Alejandro Vazquez Date: Thu, 26 Mar 2026 18:31:28 -0300 Subject: [PATCH 2/8] setup --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d2f84b6..59d5603 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ python-dotenv==1.0.0 requests==2.31.0 bleach==6.1.0 python-dateutil==2.8.2 -Pillow==10.1.0 \ No newline at end of file +Pillow==10.1.0 +email_validator>=2.0.0 From fa76741d505d56e202257d97e5be4dbe8fe2fa39 Mon Sep 17 00:00:00 2001 From: Alejandro Vazquez Date: Thu, 26 Mar 2026 18:43:15 -0300 Subject: [PATCH 3/8] setup update --- .env | 4 +- .gitignore | 209 +++++++++++++++++++++++++++++++++++++++++++++++ config/config.py | 2 +- requirements.txt | 2 + 4 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 .gitignore diff --git a/.env b/.env index a9da184..b644a19 100644 --- a/.env +++ b/.env @@ -1,5 +1,7 @@ FLASK_APP=app.py FLASK_ENV=development SECRET_KEY=your-secret-key-here-please-change-in-production -DATABASE_URL=sqlite:///edu_space.db +DATABASE_URL=postgresql://postgres:password@localhost:5432/classrooms_db +DB_USERNAME=postgres +DB_PASSWORD=password API_BASE_URL=http://localhost:8080/api \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..de788bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,209 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +.idea + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ diff --git a/config/config.py b/config/config.py index 6836733..af626ba 100644 --- a/config/config.py +++ b/config/config.py @@ -2,7 +2,7 @@ import os class Config: SECRET_KEY = os.environ.get('SECRET_KEY') or 'dev-secret-key-change-in-production' - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///edu_space.db' + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'postgresql://postgres:password@localhost:5432/classrooms_db' SQLALCHEMY_TRACK_MODIFICATIONS = False API_BASE_URL = os.environ.get('API_BASE_URL') or 'http://localhost:8080/api' diff --git a/requirements.txt b/requirements.txt index 59d5603..157a4bc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,3 +13,5 @@ bleach==6.1.0 python-dateutil==2.8.2 Pillow==10.1.0 email_validator>=2.0.0 +psycopg2-binary==2.9.9 +Flask-CORS==6.0.2 From 9fb21c4a298c37d85688ed7d9f763f2b5ad298fb Mon Sep 17 00:00:00 2001 From: Alejandro Vazquez Date: Thu, 26 Mar 2026 19:39:31 -0300 Subject: [PATCH 4/8] setup update 2 --- app/models/classroom.py | 25 ++++++++++---------- app/models/subject.py | 22 ++++++++++------- app/models/user.py | 14 +++++++++-- app/routes/auth.py | 2 +- app/routes/main.py | 46 ++++++++++++++++++++++++------------ app/templates/base.html | 14 ++++------- app/templates/dashboard.html | 12 +++++++--- requirements.txt | 3 +-- setup.py | 2 +- 9 files changed, 86 insertions(+), 54 deletions(-) diff --git a/app/models/classroom.py b/app/models/classroom.py index 110c20e..049bebf 100644 --- a/app/models/classroom.py +++ b/app/models/classroom.py @@ -5,17 +5,21 @@ class Classroom(db.Model): __tablename__ = 'classrooms' id = db.Column(db.Integer, primary_key=True) - code = db.Column(db.String(20), unique=True, nullable=False, index=True) - name = db.Column(db.String(100), nullable=False) - building = db.Column(db.String(50), nullable=False) + campus_id = db.Column(db.Integer, db.ForeignKey('campuses.id'), nullable=False) + building = db.Column(db.String(100), nullable=False) + room_number = db.Column(db.String(50), nullable=False) floor = db.Column(db.Integer, nullable=False) capacity = db.Column(db.Integer, nullable=False) description = db.Column(db.Text) - campus_location = db.Column(db.String(50), default='Main Campus') is_active = db.Column(db.Boolean, default=True, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + # Virtual field for code (combination of building and room_number) + @property + def code(self): + return f"{self.building}-{self.room_number}" + # Relationships reservations = db.relationship('Reservation', backref='classroom', lazy=True, cascade='all, delete-orphan') @@ -27,12 +31,10 @@ class Classroom(db.Model): return { 'id': self.id, 'code': self.code, - 'name': self.name, 'building': self.building, 'floor': self.floor, 'capacity': self.capacity, 'description': self.description, - 'campus_location': self.campus_location, 'is_active': self.is_active, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None, @@ -55,10 +57,10 @@ class ClassroomResource(db.Model): brand = db.Column(db.String(100)) model = db.Column(db.String(100)) specifications = db.Column(db.Text) - quantity = db.Column(db.Integer, default=1) - is_functional = db.Column(db.Boolean, default=True) - last_maintenance = db.Column(db.DateTime) + functional = db.Column(db.Boolean, default=True) + last_maintenance_date = db.Column(db.DateTime) created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) def to_dict(self): """Convert resource to dictionary""" @@ -69,9 +71,8 @@ class ClassroomResource(db.Model): 'brand': self.brand, 'model': self.model, 'specifications': self.specifications, - 'quantity': self.quantity, - 'is_functional': self.is_functional, - 'last_maintenance': self.last_maintenance.isoformat() if self.last_maintenance else None, + 'functional': self.functional, + 'last_maintenance_date': self.last_maintenance_date.isoformat() if self.last_maintenance_date else None, 'created_at': self.created_at.isoformat() if self.created_at else None } diff --git a/app/models/subject.py b/app/models/subject.py index 7e6d91b..a89ae36 100644 --- a/app/models/subject.py +++ b/app/models/subject.py @@ -6,11 +6,11 @@ class Subject(db.Model): id = db.Column(db.Integer, primary_key=True) code = db.Column(db.String(20), unique=True, nullable=False, index=True) - name = db.Column(db.String(100), nullable=False) + name = db.Column(db.String(200), nullable=False) description = db.Column(db.Text) department = db.Column(db.String(100), nullable=False) - credits = db.Column(db.Integer) - duration_minutes = db.Column(db.Integer, default=90) + credits = db.Column(db.Integer, nullable=False) + duration_in_minutes = db.Column(db.Integer, nullable=False) is_active = db.Column(db.Boolean, default=True, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) @@ -27,7 +27,7 @@ class Subject(db.Model): 'description': self.description, 'department': self.department, 'credits': self.credits, - 'duration_minutes': self.duration_minutes, + 'duration_minutes': self.duration_in_minutes, 'is_active': self.is_active, 'created_at': self.created_at.isoformat() if self.created_at else None, 'updated_at': self.updated_at.isoformat() if self.updated_at else None @@ -42,18 +42,24 @@ class Commission(db.Model): id = db.Column(db.Integer, primary_key=True) subject_id = db.Column(db.Integer, db.ForeignKey('subjects.id'), nullable=False) code = db.Column(db.String(50), nullable=False) - semester = db.Column(db.String(20), nullable=False) # Fall, Spring, Summer + semester = db.Column(db.String(20), nullable=False) year = db.Column(db.Integer, nullable=False) - teacher_name = db.Column(db.String(100), nullable=False) - max_students = db.Column(db.Integer, default=50) + teacher_id = db.Column(db.Integer, db.ForeignKey('users.id')) + max_students = db.Column(db.Integer, nullable=False) current_students = db.Column(db.Integer, default=0) - schedule = db.Column(db.String(255)) # e.g., "MWF 9:00-10:30 AM" + schedule = db.Column(db.String(255)) is_active = db.Column(db.Boolean, default=True, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationships reservations = db.relationship('Reservation', backref='commission', lazy=True, cascade='all, delete-orphan') + teacher = db.relationship('User', backref='teaching_commissions', foreign_keys=[teacher_id]) + + # Virtual field for teacher_name + @property + def teacher_name(self): + return f"{self.teacher.first_name} {self.teacher.last_name}" if self.teacher else "TBD" # Unique constraint __table_args__ = (db.UniqueConstraint('subject_id', 'code', 'semester', 'year'),) diff --git a/app/models/user.py b/app/models/user.py index 019b2f8..b2092be 100644 --- a/app/models/user.py +++ b/app/models/user.py @@ -7,14 +7,24 @@ class User(UserMixin, db.Model): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) - email = db.Column(db.String(120), unique=True, nullable=False, index=True) + email = db.Column(db.String(255), unique=True, nullable=False, index=True) password_hash = db.Column(db.String(255), nullable=False) name = db.Column(db.String(100), nullable=False) - role = db.Column(db.String(20), default='admin', nullable=False) # admin, staff + role = db.Column(db.String(50), nullable=False) is_active = db.Column(db.Boolean, default=True, nullable=False) created_at = db.Column(db.DateTime, default=datetime.utcnow) last_login = db.Column(db.DateTime) + # Virtual fields for future compatibility + @property + def first_name(self): + return self.name.split()[0] if self.name else '' + + @property + def last_name(self): + parts = self.name.split() + return ' '.join(parts[1:]) if len(parts) > 1 else '' + # Relationships reservations = db.relationship('Reservation', backref='user', lazy=True, cascade='all, delete-orphan') diff --git a/app/routes/auth.py b/app/routes/auth.py index caf5beb..e5a56a7 100644 --- a/app/routes/auth.py +++ b/app/routes/auth.py @@ -29,7 +29,7 @@ def login(): if not next_page or not next_page.startswith('/'): next_page = url_for('main.dashboard') - flash(f'Welcome back, {user.name}!', 'success') + flash(f'Welcome back, {user.first_name} {user.last_name}!', 'success') return redirect(next_page) else: flash('Invalid email or password.', 'error') diff --git a/app/routes/main.py b/app/routes/main.py index 200341d..46f6d8a 100644 --- a/app/routes/main.py +++ b/app/routes/main.py @@ -3,6 +3,7 @@ from flask_login import login_required, current_user from app.models.reservation import Reservation from app.models.classroom import Classroom from app.models.subject import Subject +from app.models.subject import Commission from datetime import datetime, timedelta from sqlalchemy import func @@ -12,30 +13,45 @@ main_bp = Blueprint('main', __name__) @login_required def dashboard(): """Main dashboard page""" - # Get statistics - total_classrooms = Classroom.query.filter_by(is_active=True).count() - total_subjects = Subject.query.filter_by(is_active=True).count() +# Get statistics + try: + total_classrooms = Classroom.query.filter_by(is_active=True).count() + except: + total_classrooms = 0 + + try: + total_subjects = Subject.query.filter_by(is_active=True).count() + except: + total_subjects = 0 # Get today's reservations today = datetime.utcnow().date() - today_reservations = Reservation.query.filter( - func.date(Reservation.start_time) == today - ).count() + try: + today_reservations = Reservation.query.filter( + func.date(Reservation.start_time) == today + ).count() + except: + today_reservations = 0 # Get this week's reservations week_start = today - timedelta(days=today.weekday()) week_end = week_start + timedelta(days=6) - week_reservations = Reservation.query.filter( - func.date(Reservation.start_time) >= week_start, - func.date(Reservation.start_time) <= week_end - ).count() + try: + week_reservations = Reservation.query.filter( + func.date(Reservation.start_time) >= week_start, + func.date(Reservation.start_time) <= week_end + ).count() + except: + week_reservations = 0 # Get upcoming reservations (next 7 days) - upcoming_reservations = Reservation.query.filter( - Reservation.start_time >= datetime.utcnow(), - Reservation.start_time <= datetime.utcnow() + timedelta(days=7), - Reservation.status.in_(['CONFIRMED', 'PENDING']) - ).join(Classroom).join(Subject).limit(10).all() + try: + upcoming_reservations = Reservation.query.filter( + Reservation.start_time >= datetime.utcnow(), + Reservation.start_time <= datetime.utcnow() + timedelta(days=7) + ).limit(10).all() + except: + upcoming_reservations = [] stats = { 'total_classrooms': total_classrooms, diff --git a/app/templates/base.html b/app/templates/base.html index fec41e4..e25758b 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -71,7 +71,7 @@