BuildError
werkzeug.routing.exceptions.BuildError: Could not build url for endpoint 'auth.login'. Did you mean 'login' instead?

Traceback (most recent call last)
File "c:\Program Files\Python312\Lib\site-packages\flask\app.py", line 1498, in __call__
return self.wsgi_app(environ, start_response)
       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "c:\Program Files\Python312\Lib\site-packages\flask\app.py", line 1476, in wsgi_app # app.py

from flask import Flask, render_template, redirect, url_for, flash, request, current_app, send_file
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
from flask_bcrypt import Bcrypt
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField, SelectField, FileField, BooleanField, FormField, FieldList
from wtforms.validators import DataRequired, Length
from werkzeug.utils import secure_filename
import os
import uuid
import pandas as pd
from datetime import datetime

# Инициализация приложения и расширений
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'  # Замените на случайный ключ
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///data/students.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data', 'uploads')
app.config['ALLOWED_EXTENSIONS'] = {'csv', 'xlsx'}

db = SQLAlchemy(app)
bcrypt = Bcrypt(app)

login_manager = LoginManager(app)
login_manager.login_view = 'login'

# Создание папки для загрузок, если ее нет
if not os.path.exists(app.config['UPLOAD_FOLDER']):
    os.makedirs(app.config['UPLOAD_FOLDER'])

# Модели
class User(UserMixin, db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(150), unique=True, nullable=False)
    password_hash = db.Column(db.String(128), nullable=False)
    name = db.Column(db.String(255), nullable=False)
    role = db.Column(db.String(50), nullable=False)  # 'admin' или 'teacher'

    objects = db.relationship('Object', backref='teacher', lazy=True)

    def set_password(self, password):
        self.password_hash = bcrypt.generate_password_hash(password).decode('utf-8')

    def check_password(self, password):
        return bcrypt.check_password_hash(self.password_hash, password)

class Discipline(db.Model):
    __tablename__ = 'disciplines'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False, unique=True)
    objects = db.relationship('Object', backref='discipline', lazy=True)

class Group(db.Model):
    __tablename__ = 'groups'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(50), nullable=False, unique=True)
    students = db.relationship('Student', backref='group', lazy=True)
    objects = db.relationship('Object', backref='group', lazy=True)

class Student(db.Model):
    __tablename__ = 'students'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    group_id = db.Column(db.Integer, db.ForeignKey('groups.id'), nullable=False)
    attendance = db.relationship('Attendance', backref='student', lazy=True)

class Object(db.Model):
    __tablename__ = 'objects'
    id = db.Column(db.Integer, primary_key=True)
    teacher_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
    discipline_id = db.Column(db.Integer, db.ForeignKey('disciplines.id'), nullable=False)
    group_id = db.Column(db.Integer, db.ForeignKey('groups.id'), nullable=False)
    sessions = db.relationship('Session', backref='object', lazy=True)

class Session(db.Model):
    __tablename__ = 'sessions'
    id = db.Column(db.Integer, primary_key=True)
    object_id = db.Column(db.Integer, db.ForeignKey('objects.id'), nullable=False)
    date = db.Column(db.Date, nullable=False)
    time = db.Column(db.Time, nullable=True)
    attendance = db.relationship('Attendance', backref='session', lazy=True)

class Attendance(db.Model):
    __tablename__ = 'attendance'
    id = db.Column(db.Integer, primary_key=True)
    session_id = db.Column(db.Integer, db.ForeignKey('sessions.id'), nullable=False)
    student_id = db.Column(db.Integer, db.ForeignKey('students.id'), nullable=False)
    status = db.Column(db.String(50), nullable=False)  # 'present' или 'absent'

# Загрузка пользователя
@login_manager.user_loader
def load_user(user_id):
    return User.query.get(int(user_id))

# Формы
class LoginForm(FlaskForm):
    username = StringField('Логин', validators=[DataRequired()])
    password = PasswordField('Пароль', validators=[DataRequired()])
    submit = SubmitField('Войти')

class UploadForm(FlaskForm):
    file = FileField('Загрузить файл', validators=[DataRequired()])
    submit = SubmitField('Загрузить')

class AddTeacherForm(FlaskForm):
    name = StringField('ФИО преподавателя', validators=[DataRequired(), Length(min=2, max=255)])
    submit = SubmitField('Добавить')

class CreateObjectForm(FlaskForm):
    discipline = SelectField('Дисциплина', coerce=int, validators=[DataRequired()])
    group = SelectField('Группа', coerce=int, validators=[DataRequired()])
    submit = SubmitField('Создать объект')

class AttendanceForm(FlaskForm):
    student_id = StringField('Студент')
    present = BooleanField('Присутствует')

class MarkAttendanceForm(FlaskForm):
    date = StringField('Дата', validators=[DataRequired()])
    time = StringField('Время', validators=[DataRequired()])
    attendances = FieldList(FormField(AttendanceForm))
    submit = SubmitField('Сохранить посещаемость')

# Вспомогательные функции
def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']

# Маршруты для авторизации
@app.route('/login', methods=['GET', 'POST'])
def login():
    if current_user.is_authenticated:
        if current_user.role == 'admin':
            return redirect(url_for('admin_dashboard'))
        else:
            return redirect(url_for('teacher_dashboard'))

    form = LoginForm()
    if form.validate_on_submit():
        user = User.query.filter_by(username=form.username.data).first()
        if user and user.check_password(form.password.data):
            login_user(user)
            next_page = request.args.get('next')
            if user.role == 'admin':
                return redirect(next_page or url_for('admin_dashboard'))
            else:
                return redirect(next_page or url_for('teacher_dashboard'))
        else:
            flash('Неверный логин или пароль.', 'danger')
    return render_template('login.html', form=form)

@app.route('/logout')
@login_required
def logout():
    logout_user()
    return redirect(url_for('login'))

# Админские маршруты
@app.route('/admin/dashboard')
@login_required
def admin_dashboard():
    if current_user.role != 'admin':
        flash('Доступ запрещен.', 'danger')
        return redirect(url_for('login'))
    return render_template('admin/dashboard.html')

@app.route('/admin/upload', methods=['GET', 'POST'])
@login_required
def admin_upload():
    if current_user.role != 'admin':
        flash('Доступ запрещен.', 'danger')
        return redirect(url_for('login'))

    form = UploadForm()
    if form.validate_on_submit():
        file = form.file.data
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            unique_filename = f"{uuid.uuid4().hex}_{filename}"
            filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
            file.save(filepath)
            # Обработка файла
            try:
                if filename.endswith('.csv'):
                    df = pd.read_csv(filepath)
                elif filename.endswith('.xlsx'):
                    df = pd.read_excel(filepath)
                else:
                    flash('Неподдерживаемый формат файла.', 'danger')
                    return redirect(request.url)

                # Пример обработки (загрузка групп, дисциплин, студентов)
                if 'Группа' in df.columns or 'groups' in df.columns:
                    for index, row in df.iterrows():
                        group_name = row.get('name') or row.get('Группа') or row.get('group')
                        if group_name and not Group.query.filter_by(name=group_name).first():
                            new_group = Group(name=group_name)
                            db.session.add(new_group)
                    db.session.commit()
                    flash('Группы успешно загружены.', 'success')

                elif 'Дисциплина' in df.columns or 'discipline' in df.columns:
                    for index, row in df.iterrows():
                        discipline_name = row.get('name') or row.get('Дисциплина') or row.get('discipline')
                        if discipline_name and not Discipline.query.filter_by(name=discipline_name).first():
                            new_discipline = Discipline(name=discipline_name)
                            db.session.add(new_discipline)
                    db.session.commit()
                    flash('Дисциплины успешно загружены.', 'success')

                elif 'ФИО' in df.columns or 'name' in df.columns:
                    for index, row in df.iterrows():
                        student_name = row.get('name') or row.get('ФИО') or row.get('student_name')
                        group_name = row.get('group') or row.get('Группа') or row.get('group_name')
                        if student_name and group_name:
                            group = Group.query.filter_by(name=group_name).first()
                            if group and not Student.query.filter_by(name=student_name, group_id=group.id).first():
                                new_student = Student(name=student_name, group_id=group.id)
                                db.session.add(new_student)
                    db.session.commit()
                    flash('Студенты успешно загружены.', 'success')

                else:
                    flash('Неподдерживаемый формат содержимого файла.', 'danger')
            except Exception as e:
                flash(f'Ошибка при обработке файла: {str(e)}', 'danger')
            finally:
                os.remove(filepath)  # Удаление файла после обработки
            return redirect(url_for('admin_dashboard'))
        else:
            flash('Неподдерживаемый формат файла.', 'danger')

    return render_template('admin/upload.html', form=form)

@app.route('/admin/manage_teachers', methods=['GET', 'POST'])
@login_required
def manage_teachers():
    if current_user.role != 'admin':
        flash('Доступ запрещен.', 'danger')
        return redirect(url_for('login'))

    form = AddTeacherForm()
    if form.validate_on_submit():
        name = form.name.data.strip()
        if not name:
            flash('Введите корректное имя.', 'danger')
            return redirect(request.url)
        # Генерация логина и пароля
        username = name.lower().replace(' ', '.')  # Простая генерация логина
        password = uuid.uuid4().hex[:8]  # Случайный пароль из 8 символов

        if User.query.filter_by(username=username).first():
            flash('Преподаватель с таким логином уже существует.', 'danger')
            return redirect(request.url)

        new_teacher = User(username=username, name=name, role='teacher')
        new_teacher.set_password(password)
        db.session.add(new_teacher)
        db.session.commit()

        # Здесь можно добавить функционал отправки логина и пароля преподавателю (например, по email)
        flash(f'Преподаватель добавлен. Логин: {username}, Пароль: {password}', 'success')
        return redirect(url_for('manage_teachers'))

    teachers = User.query.filter_by(role='teacher').all()
    return render_template('admin/manage_teachers.html', form=form, teachers=teachers)

@app.route('/admin/delete_teacher/<int:teacher_id>', methods=['POST'])
@login_required
def delete_teacher(teacher_id):
    if current_user.role != 'admin':
        flash('Доступ запрещен.', 'danger')
        return redirect(url_for('login'))

    teacher = User.query.get_or_404(teacher_id)
    if teacher.role != 'teacher':
        flash('Можно удалить только преподавателей.', 'danger')
        return redirect(url_for('manage_teachers'))

    db.session.delete(teacher)
    db.session.commit()
    flash('Преподаватель успешно удален.', 'success')
    return redirect(url_for('manage_teachers'))

@app.route('/admin/export_attendance')
@login_required
def export_attendance():
    if current_user.role != 'admin':
        flash('Доступ запрещен.', 'danger')
        return redirect(url_for('login'))

    try:
        attendance_data = db.session.query(
            Discipline.name.label('Discipline'),
            Group.name.label('Group'),
            Session.date,
            Session.time,
            Student.name.label('Student'),
            Attendance.status
        ).join(Object, Object.discipline_id == Discipline.id)\
         .join(Session, Session.object_id == Object.id)\
         .join(Attendance, Attendance.session_id == Session.id)\
         .join(Student, Student.id == Attendance.student_id)\
         .join(Group, Group.id == Student.group_id)\
         .filter(Object.teacher_id == current_user.id)\
         .all()

        # Преобразование в DataFrame
        df = pd.DataFrame(attendance_data, columns=['Discipline', 'Group', 'Date', 'Time', 'Student', 'Status'])

        # Сохранение в CSV
        export_path = os.path.join(app.config['UPLOAD_FOLDER'], 'attendance_export.csv')
        df.to_csv(export_path, index=False)

        return send_file(export_path, as_attachment=True)
    except Exception as e:
        flash(f'Ошибка при экспорте данных: {str(e)}', 'danger')
        return redirect(url_for('admin_dashboard'))

# Маршруты для преподавателей
@app.route('/teacher/dashboard')
@login_required
def teacher_dashboard():
    if current_user.role != 'teacher':
        flash('Доступ запрещен.', 'danger')
        return redirect(url_for('login'))
    objects = Object.query.filter_by(teacher_id=current_user.id).all()
    return render_template('teacher/dashboard.html', objects=objects)

@app.route('/teacher/create_object', methods=['GET', 'POST'])
@login_required
def create_object():
    if current_user.role != 'teacher':
        flash('Доступ запрещен.', 'danger')
        return redirect(url_for('login'))

    form = CreateObjectForm()
    form.discipline.choices = [(d.id, d.name) for d in Discipline.query.order_by('name')]
    form.group.choices = [(g.id, g.name) for g in Group.query.order_by('name')]

    if form.validate_on_submit():
        discipline_id = form.discipline.data
        group_id = form.group.data

        # Проверка на существование объекта
        existing_object = Object.query.filter_by(teacher_id=current_user.id, discipline_id=discipline_id, group_id=group_id).first()
        if existing_object:
            flash('Этот объект уже существует.', 'warning')
            return redirect(url_for('teacher_dashboard'))

        new_object = Object(teacher_id=current_user.id, discipline_id=discipline_id, group_id=group_id)
        db.session.add(new_object)
        db.session.commit()
        flash('Объект успешно создан.', 'success')
        return redirect(url_for('teacher_dashboard'))

    return render_template('teacher/create_object.html', form=form)

@app.route('/teacher/object/<int:object_id>/attendance', methods=['GET', 'POST'])
@login_required
def mark_attendance(object_id):
    if current_user.role != 'teacher':
        flash('Доступ запрещен.', 'danger')
        return redirect(url_for('login'))

    object_ = Object.query.filter_by(id=object_id, teacher_id=current_user.id).first_or_404()
    students = Student.query.filter_by(group_id=object_.group_id).order_by(Student.name).all()

    form = MarkAttendanceForm()
    if request.method == 'GET':
        # Инициализация формы
        form.date.data = datetime.now().strftime('%Y-%m-%d')
        form.time.data = datetime.now().strftime('%H:%M')
        for student in students:
            attendance_form = AttendanceForm()
            attendance_form.student_id.data = student.name
            form.attendances.append_entry(attendance_form)

    if form.validate_on_submit():
        date = datetime.strptime(form.date.data, '%Y-%m-%d').date()
        time = datetime.strptime(form.time.data, '%H:%M').time()

        # Создаем или получаем сессию
        session = Session.query.filter_by(object_id=object_id, date=date, time=time).first()
        if not session:
            session = Session(object_id=object_id, date=date, time=time)
            db.session.add(session)
            db.session.commit()

        # Обновление посещаемости
        for attendance_form in form.attendances.entries:
            student_name = attendance_form.student_id.data
            student = Student.query.filter_by(name=student_name, group_id=object_.group_id).first()
            if student:
                status = 'present' if attendance_form.present.data else 'absent'
                attendance_record = Attendance.query.filter_by(session_id=session.id, student_id=student.id).first()
                if attendance_record:
                    attendance_record.status = status
                else:
                    attendance_record = Attendance(session_id=session.id, student_id=student.id, status=status)
                    db.session.add(attendance_record)

        db.session.commit()
        flash('Посещаемость успешно сохранена.', 'success')
        return redirect(url_for('teacher_dashboard'))

    return render_template('teacher/mark_attendance.html', object_=object_, form=form, students=students)

# Главная страница
@app.route('/')
def index():
    return redirect(url_for('login'))

# Запуск приложения
if __name__ == '__main__':
    app.run(debug=True)