import os from typing import Optional, Union, List from datetime import datetime import sqlalchemy as sa from werkzeug.security import check_password_hash, generate_password_hash from flask_login import UserMixin from flask import url_for from flask_sqlalchemy import SQLAlchemy from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy import String, ForeignKey, DateTime, Text, Integer, MetaData class Base(DeclarativeBase): metadata = MetaData(naming_convention={ "ix": 'ix_%(column_0_label)s', "uq": "uq_%(table_name)s_%(column_0_name)s", "ck": "ck_%(table_name)s_%(constraint_name)s", "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", "pk": "pk_%(table_name)s" }) db = SQLAlchemy(model_class=Base) class Category(Base): __tablename__ = 'categories' id = mapped_column(Integer, primary_key=True) name: Mapped[str] = mapped_column(String(100)) parent_id: Mapped[Optional[int]] = mapped_column(ForeignKey("categories.id")) def __repr__(self): return '' % self.name class User(Base, UserMixin): __tablename__ = 'users' id: Mapped[int] = mapped_column(primary_key=True) first_name: Mapped[str] = mapped_column(String(100)) last_name: Mapped[str] = mapped_column(String(100)) middle_name: Mapped[Optional[str]] = mapped_column(String(100)) login: Mapped[str] = mapped_column(String(100), unique=True) password_hash: Mapped[str] = mapped_column(String(200)) created_at: Mapped[datetime] = mapped_column(default=datetime.now) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) @property def full_name(self): return ' '.join([self.last_name, self.first_name, self.middle_name or '']) def __repr__(self): return '' % self.login class Course(Base): __tablename__ = 'courses' id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(100)) short_desc: Mapped[str] = mapped_column(Text) full_desc: Mapped[str] = mapped_column(Text) rating_sum: Mapped[int] = mapped_column(default=0) rating_num: Mapped[int] = mapped_column(default=0) category_id: Mapped[int] = mapped_column(ForeignKey("categories.id")) author_id: Mapped[int] = mapped_column(ForeignKey("users.id")) background_image_id: Mapped[str] = mapped_column(ForeignKey("images.id")) created_at: Mapped[datetime] = mapped_column(default=datetime.now) author: Mapped["User"] = relationship() category: Mapped["Category"] = relationship(lazy=False) bg_image: Mapped["Image"] = relationship() # Связь с отзывами reviews: Mapped[List["Review"]] = relationship(back_populates='course') def __repr__(self): return '' % self.name @property def rating(self): if self.rating_num > 0: return self.rating_sum / self.rating_num return 0 class Image(db.Model): __tablename__ = 'images' id: Mapped[str] = mapped_column(String(100), primary_key=True) file_name: Mapped[str] = mapped_column(String(100)) mime_type: Mapped[str] = mapped_column(String(100)) md5_hash: Mapped[str] = mapped_column(String(100), unique=True) object_id: Mapped[Optional[int]] object_type: Mapped[Optional[str]] = mapped_column(String(100)) created_at: Mapped[datetime] = mapped_column(default=datetime.now) def __repr__(self): return '' % self.file_name @property def storage_filename(self): _, ext = os.path.splitext(self.file_name) return self.id + ext @property def url(self): return url_for('image', image_id=self.id) # Модель отзыва class Review(Base): __tablename__ = 'reviews' id: Mapped[int] = mapped_column(Integer, primary_key=True) rating: Mapped[int] = mapped_column(Integer, nullable=False) text: Mapped[str] = mapped_column(Text, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) course_id: Mapped[int] = mapped_column(ForeignKey('courses.id')) user_id: Mapped[int] = mapped_column(ForeignKey('users.id')) # Связи с курсом и пользователем course: Mapped['Course'] = relationship(back_populates='reviews') user: Mapped['User'] = relationship()