setup update 3

This commit is contained in:
Alejandro Vazquez
2026-03-26 20:14:35 -03:00
parent 9fb21c4a29
commit 192d1dd41c
12 changed files with 1928 additions and 595 deletions
+309 -256
View File
@@ -1,277 +1,330 @@
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
from flask_login import login_required, current_user
from app import db
from app.models.reservation import Reservation, ReservationStatus
from app.models.classroom import Classroom
from app.models.subject import Subject, Commission
from app.models.subject import Commission
from app.forms.reservation import ReservationForm
from datetime import datetime, timedelta
from sqlalchemy import func, or_
from app import db
from datetime import datetime, timedelta, date
from wtforms import SelectField, StringField, TextAreaField, IntegerField, SubmitField, DateTimeLocalField
from wtforms.validators import DataRequired, Length, NumberRange, Optional
schedule_bp = Blueprint('schedule', __name__, url_prefix='/schedule')
schedule_bp = Blueprint('schedule', __name__)
@schedule_bp.route('/')
@schedule_bp.route('/add', methods=['GET', 'POST'])
@login_required
def calendar_view():
"""Calendar view of reservations"""
return render_template('schedule/calendar.html')
def add_reservation():
"""Add a new reservation"""
form = ReservationForm()
# Populate form choices
form.classroom_id.choices = [(c.id, f"{c.code_display} (Capacity: {c.capacity})")
for c in Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all()]
form.commission_id.choices = [(c.id, c.get_full_code())
for c in Commission.query.filter(Commission.active == True).order_by(Commission.code).all()]
if form.validate_on_submit():
try:
# Check for time conflicts
conflicting_reservations = Reservation.find_conflicts(
form.classroom_id.data,
form.start_time.data,
form.end_time.data
)
if conflicting_reservations:
flash('This classroom is already reserved for the selected time slot.', 'error')
# Get classrooms for the template
classroom_list = Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all()
return render_template('schedule/add.html', form=form, classroom_list=classroom_list, title='New Reservation')
# Check capacity
classroom = Classroom.query.get(form.classroom_id.data)
if form.expected_attendees.data > classroom.capacity:
flash(f'Expected attendees ({form.expected_attendees.data}) exceeds classroom capacity ({classroom.capacity}).', 'error')
# Get classrooms for the template
classroom_list = Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all()
return render_template('schedule/add.html', form=form, classroom_list=classroom_list, title='New Reservation')
# Create reservation
reservation = Reservation(
classroom_id=form.classroom_id.data,
commission_id=form.commission_id.data,
user_id=current_user.id,
start_time=form.start_time.data,
end_time=form.end_time.data,
purpose=form.purpose.data,
expected_attendees=form.expected_attendees.data,
notes=form.notes.data
)
db.session.add(reservation)
db.session.commit()
flash('Reservation created successfully! It will be confirmed after review.', 'success')
return redirect(url_for('schedule.list_reservations'))
except Exception as e:
db.session.rollback()
flash(f'Error creating reservation: {str(e)}', 'error')
# Get classrooms for the template
classroom_list = Classroom.query.filter_by(is_active=True).order_by(Classroom.building, Classroom.code).all()
return render_template('schedule/add.html', form=form, classroom_list=classroom_list, title='New Reservation')
@schedule_bp.route('/list')
@login_required
def list_reservations():
"""List all reservations"""
page = request.args.get('page', 1, type=int)
status_filter = request.args.get('status', '')
classroom_filter = request.args.get('classroom', 0, type=int)
date_filter = request.args.get('date', '')
query = Reservation.query
# Apply filters
if status_filter:
query = query.filter(Reservation.status == status_filter)
if classroom_filter:
query = query.filter(Reservation.classroom_id == classroom_filter)
if date_filter:
try:
filter_date = datetime.fromisoformat(date_filter).date()
query = query.filter(func.date(Reservation.start_time) == filter_date)
except ValueError:
pass
reservations = query.order_by(Reservation.start_time.desc()).paginate(
page=page,
per_page=20,
error_out=False
)
# Get filter options
classrooms = Classroom.query.filter_by(is_active=True).all()
return render_template('schedule/list.html',
reservations=reservations,
classrooms=classrooms,
status_filter=status_filter,
classroom_filter=classroom_filter,
date_filter=date_filter)
@schedule_bp.route('/add', methods=['GET', 'POST'])
@login_required
def add_reservation():
"""Add new reservation"""
form = ReservationForm()
# Populate classroom choices
classrooms = Classroom.query.filter_by(is_active=True).all()
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, f'{c.get_full_code()} - {c.teacher_name}') for c in commissions]
if form.validate_on_submit():
# Check for conflicts
conflicts = Reservation.find_conflicts(
form.classroom_id.data,
form.start_time.data,
form.end_time.data
)
if conflicts:
flash('Classroom is already booked for this time slot.', 'error')
return render_template('schedule/add.html', form=form)
# Check if end time is after start time
if form.end_time.data <= form.start_time.data:
flash('End time must be after start time.', 'error')
return render_template('schedule/add.html', form=form)
# Check capacity
classroom = Classroom.query.get(form.classroom_id.data)
if form.expected_attendees.data > classroom.capacity:
flash('Number of attendees exceeds classroom capacity.', 'error')
return render_template('schedule/add.html', form=form)
reservation = Reservation(
classroom_id=form.classroom_id.data,
commission_id=form.commission_id.data,
user_id=current_user.id,
start_time=form.start_time.data,
end_time=form.end_time.data,
purpose=form.purpose.data,
expected_attendees=form.expected_attendees.data,
notes=form.notes.data,
status=ReservationStatus.PENDING
)
try:
db.session.add(reservation)
db.session.commit()
flash('Reservation created successfully!', 'success')
return redirect(url_for('schedule.view_reservation', Id=reservation.id))
except Exception as e:
db.session.rollback()
flash('Error creating reservation.', 'error')
return render_template('schedule/add.html', form=form)
@schedule_bp.route('/view/<int:Id>')
@login_required
def view_reservation(Id):
"""View reservation details"""
reservation = Reservation.query.get_or_404(Id)
return render_template('schedule/view.html', reservation=reservation)
@schedule_bp.route('/edit/<int:Id>', methods=['GET', 'POST'])
@login_required
def edit_reservation(Id):
"""Edit reservation"""
reservation = Reservation.query.get_or_404(Id)
# Only allow editing pending reservations
if reservation.status != ReservationStatus.PENDING:
flash('Only pending reservations can be edited.', 'warning')
return redirect(url_for('schedule.view_reservation', Id=Id))
form = ReservationForm(obj=reservation)
# Populate choices
classrooms = Classroom.query.filter_by(is_active=True).all()
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, f'{c.get_full_code()} - {c.teacher_name}') for c in commissions]
if form.validate_on_submit():
# Check for conflicts (excluding this reservation)
conflicts = Reservation.find_conflicts(
form.classroom_id.data,
form.start_time.data,
form.end_time.data,
exclude_reservation_id=Id
)
if conflicts:
flash('Classroom is already booked for this time slot.', 'error')
return render_template('schedule/edit.html', form=form, reservation=reservation)
# Update reservation
reservation.classroom_id = form.classroom_id.data
reservation.commission_id = form.commission_id.data
reservation.start_time = form.start_time.data
reservation.end_time = form.end_time.data
reservation.purpose = form.purpose.data
reservation.expected_attendees = form.expected_attendees.data
reservation.notes = form.notes.data
reservation.updated_at = datetime.utcnow()
try:
db.session.commit()
flash('Reservation updated successfully!', 'success')
return redirect(url_for('schedule.view_reservation', Id=Id))
except Exception as e:
db.session.rollback()
flash('Error updating reservation.', 'error')
return render_template('schedule/edit.html', form=form, reservation=reservation)
@schedule_bp.route('/confirm/<int:Id>')
@login_required
def confirm_reservation(Id):
"""Confirm a reservation"""
reservation = Reservation.query.get_or_404(Id)
if reservation.status != ReservationStatus.PENDING:
flash('Only pending reservations can be confirmed.', 'warning')
return redirect(url_for('schedule.view_reservation', Id=Id))
try:
reservation.status = ReservationStatus.CONFIRMED
reservation.updated_at = datetime.utcnow()
db.session.commit()
flash('Reservation confirmed successfully!', 'success')
page = request.args.get('page', 1, type=int)
status_filter = request.args.get('status', 'all')
date_from = request.args.get('date_from')
date_to = request.args.get('date_to')
# Build query
query = Reservation.query
# Apply filters
if status_filter != 'all':
try:
query = query.filter_by(status=ReservationStatus[status_filter.upper()])
except KeyError:
pass
if date_from:
try:
from_date = datetime.strptime(date_from, '%Y-%m-%d')
query = query.filter(Reservation.start_time >= from_date)
except ValueError:
pass
if date_to:
try:
to_date = datetime.strptime(date_to, '%Y-%m-%d') + timedelta(days=1)
query = query.filter(Reservation.start_time < to_date)
except ValueError:
pass
# Order by start time descending
reservations = query.order_by(Reservation.start_time.desc()).paginate(
page=page, per_page=20, error_out=False
)
return render_template('schedule/list.html',
reservations=reservations,
status_filter=status_filter,
date_from=date_from,
date_to=date_to,
title='Reservations')
except Exception as e:
db.session.rollback()
flash('Error confirming reservation.', 'error')
return redirect(url_for('schedule.view_reservation', Id=Id))
@schedule_bp.route('/cancel/<int:Id>')
@login_required
def cancel_reservation(Id):
"""Cancel a reservation"""
reservation = Reservation.query.get_or_404(Id)
if reservation.status not in [ReservationStatus.PENDING, ReservationStatus.CONFIRMED]:
flash('Cannot cancel this reservation.', 'warning')
return redirect(url_for('schedule.view_reservation', Id=Id))
try:
reservation.status = ReservationStatus.CANCELLED
reservation.updated_at = datetime.utcnow()
db.session.commit()
flash('Reservation cancelled successfully!', 'success')
except Exception as e:
db.session.rollback()
flash('Error cancelling reservation.', 'error')
return redirect(url_for('schedule.view_reservation', Id=Id))
@schedule_bp.route('/calendar-data')
@login_required
def calendar_data():
"""API endpoint for calendar data"""
start = request.args.get('start')
end = request.args.get('end')
if not start or not end:
return jsonify([])
try:
start_date = datetime.fromisoformat(start)
end_date = datetime.fromisoformat(end)
except ValueError:
return jsonify([])
reservations = Reservation.query.filter(
Reservation.start_time >= start_date,
Reservation.start_time <= end_date,
Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING])
).all()
events = []
for reservation in reservations:
color = {
ReservationStatus.CONFIRMED: '#28a745',
ReservationStatus.PENDING: '#ffc107'
}.get(reservation.status, '#6c757d')
events.append({
'id': reservation.id,
'title': f'{reservation.classroom.code} - {reservation.commission.get_full_code()}',
'start': reservation.start_time.isoformat(),
'end': reservation.end_time.isoformat(),
'url': url_for('schedule.view_reservation', Id=reservation.id),
'backgroundColor': color,
'borderColor': color
})
return jsonify(events)
flash(f'Error loading reservations: {str(e)}', 'error')
return render_template('schedule/list.html', reservations=None, title='Reservations')
@schedule_bp.route('/today')
@login_required
def today_schedule():
"""Show today's schedule"""
today = datetime.utcnow().date()
today_start = datetime.combine(today, datetime.min.time())
today_end = datetime.combine(today, datetime.max.time())
try:
today_start = datetime.combine(date.today(), datetime.min.time())
today_end = datetime.combine(date.today(), datetime.max.time())
# Get today's reservations
reservations = Reservation.query.filter(
Reservation.start_time >= today_start,
Reservation.start_time <= today_end
).order_by(Reservation.start_time).all()
# Group by time blocks for better display
time_blocks = {}
for reservation in reservations:
hour_key = reservation.start_time.hour
if hour_key not in time_blocks:
time_blocks[hour_key] = []
time_blocks[hour_key].append({
'reservation': reservation,
'classroom': reservation.classroom,
'commission': reservation.commission,
'user': reservation.user
})
# Get quick stats
stats = {
'total_reservations': len(reservations),
'confirmed': len([r for r in reservations if r.status == ReservationStatus.CONFIRMED]),
'pending': len([r for r in reservations if r.status == ReservationStatus.PENDING]),
'cancelled': len([r for r in reservations if r.status == ReservationStatus.CANCELLED]),
'total_classrooms': Classroom.query.filter_by(is_active=True).count(),
'occupied_classrooms': len(set(r.classroom_id for r in reservations
if r.status == ReservationStatus.CONFIRMED))
}
return render_template('schedule/today.html',
time_blocks=time_blocks,
reservations=reservations,
stats=stats,
today=date.today(),
title='Today\'s Schedule')
except Exception as e:
flash(f'Error loading today\'s schedule: {str(e)}', 'error')
return render_template('schedule/today.html',
time_blocks={},
reservations=[],
stats={},
today=date.today(),
title='Today\'s Schedule')
@schedule_bp.route('/<int:id>/confirm', methods=['POST'])
@login_required
def confirm_reservation(id):
"""Confirm a reservation"""
reservation = Reservation.query.get_or_404(id)
reservations = Reservation.query.filter(
Reservation.start_time >= today_start,
Reservation.start_time <= today_end,
Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING])
).order_by(Reservation.start_time).all()
try:
# Check if reservation can be confirmed
if reservation.status != ReservationStatus.PENDING:
flash('Only pending reservations can be confirmed.', 'error')
return redirect(request.referrer)
# Check for conflicts again before confirming
conflicts = Reservation.find_conflicts(
reservation.classroom_id,
reservation.start_time,
reservation.end_time
)
if conflicts:
flash('Cannot confirm: Time slot is already occupied.', 'error')
return redirect(request.referrer)
reservation.status = ReservationStatus.CONFIRMED
db.session.commit()
flash('Reservation confirmed successfully!', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error confirming reservation: {str(e)}', 'error')
return render_template('schedule/today.html', reservations=reservations)
return redirect(request.referrer)
@schedule_bp.route('/<int:id>/cancel', methods=['POST'])
@login_required
def cancel_reservation(id):
"""Cancel a reservation"""
reservation = Reservation.query.get_or_404(id)
try:
# Check if reservation can be cancelled
if not reservation.can_be_cancelled():
flash('Reservations can only be cancelled at least 24 hours before start time.', 'error')
return redirect(request.referrer)
reservation.status = ReservationStatus.CANCELLED
db.session.commit()
flash('Reservation cancelled successfully!', 'success')
except Exception as e:
db.session.rollback()
flash(f'Error cancelling reservation: {str(e)}', 'error')
return redirect(request.referrer)
@schedule_bp.route('/calendar')
@login_required
def calendar_view():
"""Show calendar view"""
return render_template('schedule/calendar.html', title='Calendar View')
# API endpoints
@schedule_bp.route('/calendar-data')
@login_required
def calendar_data():
"""Get calendar events data for FullCalendar"""
try:
# Get date range from query parameters
start = request.args.get('start')
end = request.args.get('end')
if start and end:
from datetime import datetime
start_date = datetime.fromisoformat(start)
end_date = datetime.fromisoformat(end)
else:
# Default to current month
today = date.today()
start_date = datetime.combine(today.replace(day=1), datetime.min.time())
next_month = today.replace(day=28) + timedelta(days=4) # Next month
end_date = datetime.combine(next_month.replace(day=1), datetime.max.time())
# Get reservations in the date range
reservations = Reservation.query.filter(
Reservation.start_time >= start_date,
Reservation.start_time <= end_date,
Reservation.status.in_([ReservationStatus.CONFIRMED, ReservationStatus.PENDING])
).order_by(Reservation.start_time).all()
events = []
for reservation in reservations:
events.append({
'id': reservation.id,
'title': f"{reservation.commission.get_full_code() if reservation.commission else 'No Commission'} - {reservation.classroom.code_display}",
'start': reservation.start_time.isoformat(),
'end': reservation.end_time.isoformat(),
'url': f"/schedule/{reservation.id}", # For event clicking
'className': f'fc-event-{reservation.status.value.lower()}',
'extendedProps': {
'classroom': reservation.classroom.code_display if reservation.classroom else 'Unknown',
'commission': reservation.commission.get_full_code() if reservation.commission else 'No Commission',
'purpose': reservation.purpose or 'No purpose',
'attendees': reservation.expected_attendees,
'status': reservation.status.value,
'notes': reservation.notes or ''
}
})
return jsonify(events)
except Exception as e:
return jsonify({'error': str(e)}), 500
@schedule_bp.route('/api/today-events')
@login_required
def api_today_events():
"""Get today's events as JSON"""
try:
today_start = datetime.combine(date.today(), datetime.min.time())
today_end = datetime.combine(date.today(), datetime.max.time())
reservations = Reservation.query.filter(
Reservation.start_time >= today_start,
Reservation.start_time <= today_end,
Reservation.status == ReservationStatus.CONFIRMED
).order_by(Reservation.start_time).all()
events = []
for reservation in reservations:
events.append({
'id': reservation.id,
'title': f"{reservation.commission.get_full_code()} - {reservation.classroom.code_display}",
'start': reservation.start_time.isoformat(),
'end': reservation.end_time.isoformat(),
'extendedProps': {
'classroom': reservation.classroom.code_display,
'purpose': reservation.purpose,
'attendees': reservation.expected_attendees,
'status': reservation.status.value
}
})
return jsonify({'events': events})
except Exception as e:
return jsonify({'error': str(e)}), 500