From b46c0eceec36cb3b196f72baab924d158a0edf25 Mon Sep 17 00:00:00 2001 From: Ivan Budakov Date: Mon, 19 Oct 2026 18:22:31 +0300 Subject: [PATCH] feat: add rule-based assistant, seed script and projects module --- app/main.py | 59 ++++++++++++++++++++------ app/models.py | 21 +++++++--- app/templates/index.html | 7 +++- app/templates/project_form.html | 54 ++++++++++++++++++++++++ app/templates/projects.html | 74 +++++++++++++++++++++++++++++++++ seed_schedule.py | 53 +++++++++++++++++++++++ 6 files changed, 247 insertions(+), 21 deletions(-) create mode 100644 app/templates/project_form.html create mode 100644 app/templates/projects.html create mode 100644 seed_schedule.py diff --git a/app/main.py b/app/main.py index ec8139d..e932ae9 100644 --- a/app/main.py +++ b/app/main.py @@ -2,14 +2,13 @@ from flask import Flask, render_template, jsonify, redirect, url_for, request, s from .db import init_db, engine from . import config, auth, assistant from sqlmodel import Session, select -from .models import User, Event, GlobalStatus +from .models import User, Event, GlobalStatus, Project from datetime import datetime, timezone from functools import wraps app = Flask(__name__) app.secret_key = config.WEB_SECRET -# Инициализация БД и дефолтного статуса with app.app_context(): init_db() with Session(engine) as db_session: @@ -18,7 +17,6 @@ with app.app_context(): db_session.add(GlobalStatus(status="free", message="Вне расписания")) db_session.commit() -# --- Декоратор проверки ролей --- def require_role(allowed_roles): if isinstance(allowed_roles, str): allowed_roles = [allowed_roles] @@ -110,6 +108,50 @@ def logout(): session.clear() return redirect(url_for("index")) +# --- ПРОЕКТЫ --- +@app.route("/projects") +def projects_list(): + with Session(engine) as db_session: + projects = db_session.exec(select(Project).order_by(Project.created_at.desc())).all() + return render_template("projects.html", projects=projects, role=session.get("role")) + +@app.route("/projects/new", methods=["GET", "POST"]) +@require_role(["admin", "professor", "lead"]) +def project_new(): + if request.method == "POST": + title = request.form.get("title") + description = request.form.get("description") + status = request.form.get("status") + gitea_url = request.form.get("gitea_url") + is_public = request.form.get("is_public") == "on" + + with Session(engine) as db_session: + new_proj = Project( + title=title, + description=description, + status=status, + gitea_url=gitea_url, + is_public=is_public, + owner_id=session.get("user_id") + ) + db_session.add(new_proj) + db_session.commit() + return redirect(url_for("projects_list")) + + return render_template("project_form.html") + +@app.route("/projects//delete", methods=["POST"]) +@require_role(["admin", "professor", "lead"]) +def project_delete(project_id): + with Session(engine) as db_session: + proj = db_session.get(Project, project_id) + if proj: + # Удалять может только админ или владелец + if session.get("role") == "admin" or proj.owner_id == session.get("user_id"): + db_session.delete(proj) + db_session.commit() + return redirect(url_for("projects_list")) + # --- АДМИНКА: ПОЛЬЗОВАТЕЛИ --- @app.route("/admin/users") @require_role("admin") @@ -132,11 +174,9 @@ def change_user_role(user_id): if user: if user.id == session.get("user_id") and new_role != "admin": return "Ошибка: нельзя лишить самого себя прав администратора", 400 - user.role = new_role db_session.add(user) db_session.commit() - return redirect(url_for("admin_users")) # --- РАСПИСАНИЕ: ПУБЛИЧНАЯ ЧАСТЬ --- @@ -144,7 +184,6 @@ def change_user_role(user_id): def public_schedule(): with Session(engine) as db_session: current_status = db_session.exec(select(GlobalStatus)).first() - now = datetime.now(timezone.utc) events = db_session.exec( select(Event) @@ -153,7 +192,6 @@ def public_schedule(): .order_by(Event.start_time) .limit(10) ).all() - return render_template("schedule.html", status=current_status, events=events) # --- АССИСТЕНТ: API --- @@ -162,10 +200,7 @@ def api_assistant(): data = request.get_json() if not data or "message" not in data: return jsonify({"error": "Нет сообщения"}), 400 - - user_message = data["message"] - reply = assistant.get_response(user_message) - + reply = assistant.get_response(data["message"]) return jsonify({"reply": reply}) # --- РАСПИСАНИЕ: АДМИН-ПАНЕЛЬ --- @@ -198,7 +233,6 @@ def update_status(): current_status.updated_at = datetime.now(timezone.utc) db_session.add(current_status) db_session.commit() - return redirect(url_for("admin_schedule")) @app.route("/admin/schedule/event/add", methods=["POST"]) @@ -227,7 +261,6 @@ def add_event(): ) db_session.add(new_event) db_session.commit() - return redirect(url_for("admin_schedule")) @app.route("/admin/schedule/event//delete", methods=["POST"]) diff --git a/app/models.py b/app/models.py index 464775a..90bf487 100644 --- a/app/models.py +++ b/app/models.py @@ -8,27 +8,36 @@ class User(SQLModel, table=True): username: str = Field(index=True) full_name: Optional[str] = None email: Optional[str] = None - role: str = Field(default="student") # admin, professor, lead, member, student + role: str = Field(default="student") is_active: bool = Field(default=True) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class Event(SQLModel, table=True): - """Событие в расписании.""" id: Optional[int] = Field(default=None, primary_key=True) title: str - event_type: str = Field(default="lecture") # lecture, workshop, meeting, personal + event_type: str = Field(default="lecture") start_time: datetime end_time: datetime location: Optional[str] = None description: Optional[str] = None - status: str = Field(default="scheduled") # scheduled, moved, cancelled, delayed + status: str = Field(default="scheduled") is_public: bool = Field(default=True) created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) class GlobalStatus(SQLModel, table=True): - """Текущий статус Морозова.""" id: Optional[int] = Field(default=None, primary_key=True) - status: str = Field(default="free") # free, on_place, delayed, away, cancelled + status: str = Field(default="free") delay_minutes: int = Field(default=0) message: Optional[str] = None updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + +# --- НОВАЯ МОДЕЛЬ --- +class Project(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + title: str + description: Optional[str] = None + status: str = Field(default="idea") # idea, in_progress, completed, frozen + gitea_url: Optional[str] = None + owner_id: int = Field(foreign_key="user.id") + is_public: bool = Field(default=True) + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) diff --git a/app/templates/index.html b/app/templates/index.html index 521a670..86710ac 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -15,6 +15,7 @@ .btn-profile { background: #1f6feb; } .btn-admin { background: #da3633; } .btn-schedule { background: #d29922; } + .btn-projects { background: #a371f7; } @@ -25,6 +26,7 @@
Мой профиль Расписание + Проекты {% if session.get('role') in ['admin', 'professor'] %} Управление расписанием @@ -35,13 +37,14 @@ {% endif %}
{% else %} -

Скелет системы успешно запущен! (Flask Edition)

+

Скелет системы успешно запущен!

{% endif %} -
Stage 2.2: Schedule Management OK
+
Stage 3.1: Projects Module OK
diff --git a/app/templates/project_form.html b/app/templates/project_form.html new file mode 100644 index 0000000..7c906af --- /dev/null +++ b/app/templates/project_form.html @@ -0,0 +1,54 @@ + + + + + Новый проект — Morozovka Portal + + + +
+ ← К проектам +

Создание нового проекта

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + diff --git a/app/templates/projects.html b/app/templates/projects.html new file mode 100644 index 0000000..cf52050 --- /dev/null +++ b/app/templates/projects.html @@ -0,0 +1,74 @@ + + + + + Проекты — Morozovka Portal + + + +
+ ← На главную +
+

Каталог проектов

+ {% if role in ['admin', 'professor', 'lead'] %} + + Добавить проект + {% endif %} +
+ +
+ {% for proj in projects %} +
+
+ + {% if proj.status == 'idea' %}Идея + {% elif proj.status == 'in_progress' %}В работе + {% elif proj.status == 'completed' %}Завершён + {% else %}Заморожен{% endif %} + +
+

{{ proj.title }}

+

{{ proj.description or 'Описание отсутствует.' }}

+ +
+ {% else %} +

Проектов пока нет.

+ {% endfor %} +
+
+ + diff --git a/seed_schedule.py b/seed_schedule.py new file mode 100644 index 0000000..8180d4d --- /dev/null +++ b/seed_schedule.py @@ -0,0 +1,53 @@ +from datetime import datetime, timezone, timedelta +from sqlmodel import Session, select +from app.db import engine +from app.models import Event, GlobalStatus + +def seed_data(): + with Session(engine) as session: + # Обновляем статус + status = session.exec(select(GlobalStatus)).first() + if status: + status.status = "on_place" + status.message = "Провожу пары по расписанию" + status.updated_at = datetime.now(timezone.utc) + session.add(status) + + # Добавляем события (относительно текущего времени) + now = datetime.now(timezone.utc) + + events = [ + Event( + title="Лекция: Введение в веб-разработку", + event_type="lecture", + start_time=now + timedelta(days=1, hours=2), + end_time=now + timedelta(days=1, hours=4), + location="Ауд. 405", + description="Разбираем основы HTTP и клиент-серверной архитектуры." + ), + Event( + title="Мастер-класс по пайке", + event_type="workshop", + start_time=now + timedelta(days=2, hours=5), + end_time=now + timedelta(days=2, hours=8), + location="Лаборатория 101", + description="Сборка простого светодиодного индикатора." + ), + Event( + title="Встреча с командой Неймарк", + event_type="meeting", + start_time=now + timedelta(days=3, hours=1), + end_time=now + timedelta(days=3, hours=2), + location="Переговорная", + is_public=False + ) + ] + + for e in events: + session.add(e) + + session.commit() + print("[+] Тестовые события успешно добавлены в БД!") + +if __name__ == "__main__": + seed_data()