bump project

This commit is contained in:
2026-05-01 21:24:21 +03:30
parent dc4b1a1400
commit 139b3e26da
83 changed files with 2455 additions and 18349 deletions

12
app/models/__init__.py Normal file
View File

@@ -0,0 +1,12 @@
from app.models.account import Account, Role
from app.models.attendance import AttendanceEvent, AttendanceType
from app.models.person import FaceEncoding, Person
__all__ = [
"Account",
"AttendanceEvent",
"AttendanceType",
"FaceEncoding",
"Person",
"Role",
]

68
app/models/account.py Normal file
View File

@@ -0,0 +1,68 @@
from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from flask_login import UserMixin
from sqlalchemy import Boolean, DateTime, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from werkzeug.security import check_password_hash, generate_password_hash
from app.extensions import db
def utcnow() -> datetime:
return datetime.now(timezone.utc)
class Role(str, Enum):
ADMIN = "admin"
OPERATOR = "operator"
VIEWER = "viewer"
@property
def label(self) -> str:
return self.value.title()
class Account(UserMixin, db.Model):
__tablename__ = "accounts"
id: Mapped[int] = mapped_column(primary_key=True)
username: Mapped[str] = mapped_column(String(80), unique=True, index=True, nullable=False)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
full_name: Mapped[str] = mapped_column(String(160), nullable=False)
role: Mapped[str] = mapped_column(String(20), default=Role.VIEWER.value, nullable=False)
password_hash: Mapped[str] = mapped_column(String(255), nullable=False)
active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=utcnow,
onupdate=utcnow,
nullable=False,
)
attendance_events = relationship("AttendanceEvent", back_populates="created_by")
@property
def is_active(self) -> bool:
return self.active
def set_password(self, password: str) -> None:
self.password_hash = generate_password_hash(password, method="scrypt")
def check_password(self, password: str) -> bool:
return check_password_hash(self.password_hash, password)
def has_role(self, *roles: str | Role) -> bool:
normalized = {role.value if isinstance(role, Role) else role for role in roles}
return self.role in normalized
@property
def is_admin(self) -> bool:
return self.role == Role.ADMIN.value
@property
def can_operate(self) -> bool:
return self.role in {Role.ADMIN.value, Role.OPERATOR.value}

44
app/models/attendance.py Normal file
View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from sqlalchemy import DateTime, Float, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.extensions import db
def utcnow() -> datetime:
return datetime.now(timezone.utc)
class AttendanceType(str, Enum):
CHECK_IN = "check_in"
CHECK_OUT = "check_out"
@property
def label(self) -> str:
return "Check in" if self is AttendanceType.CHECK_IN else "Check out"
class AttendanceEvent(db.Model):
__tablename__ = "attendance_events"
id: Mapped[int] = mapped_column(primary_key=True)
person_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=False, index=True)
event_type: Mapped[str] = mapped_column(String(20), nullable=False)
occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False, index=True)
source: Mapped[str] = mapped_column(String(80), default="web", nullable=False)
confidence: Mapped[float | None] = mapped_column(Float)
created_by_id: Mapped[int | None] = mapped_column(ForeignKey("accounts.id"), index=True)
person = relationship("Person", back_populates="attendance_events")
created_by = relationship("Account", back_populates="attendance_events")
@property
def type_label(self) -> str:
try:
return AttendanceType(self.event_type).label
except ValueError:
return self.event_type

62
app/models/person.py Normal file
View File

@@ -0,0 +1,62 @@
from __future__ import annotations
import json
from datetime import datetime, timezone
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.extensions import db
def utcnow() -> datetime:
return datetime.now(timezone.utc)
class Person(db.Model):
__tablename__ = "people"
id: Mapped[int] = mapped_column(primary_key=True)
full_name: Mapped[str] = mapped_column(String(160), nullable=False, index=True)
employee_id: Mapped[str] = mapped_column(String(80), unique=True, index=True, nullable=False)
notes: Mapped[str | None] = mapped_column(Text)
active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=utcnow,
onupdate=utcnow,
nullable=False,
)
face_encodings: Mapped[list["FaceEncoding"]] = relationship(
back_populates="person",
cascade="all, delete-orphan",
lazy="selectin",
)
attendance_events = relationship(
"AttendanceEvent",
back_populates="person",
cascade="all, delete-orphan",
lazy="selectin",
)
class FaceEncoding(db.Model):
__tablename__ = "face_encodings"
id: Mapped[int] = mapped_column(primary_key=True)
person_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=False, index=True)
image_slot: Mapped[int] = mapped_column(default=1, nullable=False)
vector_json: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False)
person: Mapped[Person] = relationship(back_populates="face_encodings")
@property
def vector(self) -> list[float]:
return json.loads(self.vector_json)
@vector.setter
def vector(self, value: list[float]) -> None:
self.vector_json = json.dumps([float(item) for item in value])