bump project
This commit is contained in:
12
app/models/__init__.py
Normal file
12
app/models/__init__.py
Normal 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
68
app/models/account.py
Normal 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
44
app/models/attendance.py
Normal 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
62
app/models/person.py
Normal 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])
|
||||
Reference in New Issue
Block a user