forked from jamesp/sasa-membership
stuff changed:
- ui has been made 'kinda better' (after making it worse for a while lol - ESP rfid readers are now supported [ill upload the code for them in another repo later] - admin system has been secured a bit better and seems to be working well
This commit is contained in:
+175
-33
@@ -3,9 +3,9 @@ from sqlalchemy import (
|
||||
Float, Text, ForeignKey, Date, UniqueConstraint
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from datetime import datetime
|
||||
import enum
|
||||
from ..core.database import Base
|
||||
from ..core.datetime import utc_now
|
||||
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
@@ -49,6 +49,37 @@ class RSVPStatus(str, enum.Enum):
|
||||
MAYBE = "maybe"
|
||||
|
||||
|
||||
class EspReaderType(str, enum.Enum):
|
||||
CHECKIN_CHECKOUT = "checkin_checkout"
|
||||
|
||||
|
||||
class EspReaderProvisioningStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
APPROVED = "approved"
|
||||
PROVISIONED = "provisioned"
|
||||
REJECTED = "rejected"
|
||||
|
||||
|
||||
class EspTapAction(str, enum.Enum):
|
||||
CHECK_IN = "check_in"
|
||||
CHECK_OUT = "check_out"
|
||||
DENIED = "denied"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
class AttendanceCheckoutSource(str, enum.Enum):
|
||||
USER = "user"
|
||||
SYSTEM = "system"
|
||||
|
||||
|
||||
class RfidWriteJobStatus(str, enum.Enum):
|
||||
PENDING = "pending"
|
||||
CLAIMED = "claimed"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
@@ -62,8 +93,8 @@ class User(Base):
|
||||
role = Column(SQLEnum(UserRole, values_callable=lambda x: [e.value for e in x]), default=UserRole.MEMBER, nullable=False)
|
||||
volunteer_level = Column(String(50), nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
@@ -78,6 +109,8 @@ class User(Base):
|
||||
cascade="all, delete-orphan",
|
||||
foreign_keys="UserProfileAnswer.user_id"
|
||||
)
|
||||
rfid_cards = relationship("RfidCard", back_populates="user")
|
||||
attendance_sessions = relationship("AttendanceSession", back_populates="user")
|
||||
|
||||
|
||||
class ProfileQuestion(Base):
|
||||
@@ -96,8 +129,8 @@ class ProfileQuestion(Base):
|
||||
display_order = Column(Integer, default=0, nullable=False)
|
||||
depends_on_question_id = Column(Integer, ForeignKey("profile_questions.id"), nullable=True)
|
||||
depends_on_value = Column(String(255), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
depends_on_question = relationship("ProfileQuestion", remote_side=[id], backref="dependent_questions")
|
||||
answers = relationship("UserProfileAnswer", back_populates="question", cascade="all, delete-orphan")
|
||||
@@ -114,8 +147,8 @@ class UserProfileAnswer(Base):
|
||||
question_id = Column(Integer, ForeignKey("profile_questions.id"), nullable=False, index=True)
|
||||
value_text = Column(Text, nullable=True)
|
||||
updated_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
user = relationship("User", foreign_keys=[user_id], back_populates="profile_answers")
|
||||
question = relationship("ProfileQuestion", back_populates="answers")
|
||||
@@ -131,8 +164,8 @@ class MembershipTier(Base):
|
||||
annual_fee = Column(Float, nullable=False)
|
||||
benefits = Column(Text, nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
# Relationships
|
||||
memberships = relationship("Membership", back_populates="tier")
|
||||
@@ -148,8 +181,8 @@ class Membership(Base):
|
||||
start_date = Column(Date, nullable=False)
|
||||
end_date = Column(Date, nullable=False)
|
||||
auto_renew = Column(Boolean, default=False, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="memberships")
|
||||
@@ -169,8 +202,8 @@ class Payment(Base):
|
||||
transaction_id = Column(String(255), nullable=True)
|
||||
payment_date = Column(DateTime, nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="payments")
|
||||
@@ -189,8 +222,8 @@ class Event(Base):
|
||||
max_attendees = Column(Integer, nullable=True)
|
||||
status = Column(SQLEnum(EventStatus, values_callable=lambda x: [e.value for e in x]), default=EventStatus.DRAFT, nullable=False)
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
# Relationships
|
||||
rsvps = relationship("EventRSVP", back_populates="event", cascade="all, delete-orphan")
|
||||
@@ -205,14 +238,123 @@ class EventRSVP(Base):
|
||||
status = Column(SQLEnum(RSVPStatus, values_callable=lambda x: [e.value for e in x]), default=RSVPStatus.PENDING, nullable=False)
|
||||
attended = Column(Boolean, default=False, nullable=False)
|
||||
notes = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
# Relationships
|
||||
event = relationship("Event", back_populates="rsvps")
|
||||
user = relationship("User", back_populates="event_rsvps")
|
||||
|
||||
|
||||
class EspReader(Base):
|
||||
__tablename__ = "esp_readers"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
device_id = Column(String(100), unique=True, index=True, nullable=False)
|
||||
name = Column(String(255), nullable=False)
|
||||
location = Column(String(255), nullable=True)
|
||||
reader_type = Column(SQLEnum(EspReaderType, values_callable=lambda x: [e.value for e in x]), default=EspReaderType.CHECKIN_CHECKOUT, nullable=False)
|
||||
provisioning_status = Column(SQLEnum(EspReaderProvisioningStatus, values_callable=lambda x: [e.value for e in x]), default=EspReaderProvisioningStatus.PENDING, nullable=False)
|
||||
api_key_hash = Column(String(255), nullable=True)
|
||||
pending_api_key = Column(String(255), nullable=True)
|
||||
registration_token_hash = Column(String(255), nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
can_write_cards = Column(Boolean, default=False, nullable=False)
|
||||
firmware_version = Column(String(100), nullable=True)
|
||||
last_seen_at = Column(DateTime, nullable=True)
|
||||
approved_at = Column(DateTime, nullable=True)
|
||||
provisioned_at = Column(DateTime, nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
taps = relationship("RfidTap", back_populates="reader")
|
||||
attendance_sessions = relationship("AttendanceSession", back_populates="reader")
|
||||
write_jobs = relationship("RfidCardWriteJob", back_populates="reader")
|
||||
|
||||
|
||||
class RfidCard(Base):
|
||||
__tablename__ = "rfid_cards"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
uid = Column(String(100), unique=True, index=True, nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
|
||||
label = Column(String(255), nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
user = relationship("User", back_populates="rfid_cards")
|
||||
taps = relationship("RfidTap", back_populates="card")
|
||||
|
||||
|
||||
class RfidCardWriteJob(Base):
|
||||
__tablename__ = "rfid_card_write_jobs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
reader_id = Column(Integer, ForeignKey("esp_readers.id"), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
card_id = Column(Integer, ForeignKey("rfid_cards.id"), nullable=True, index=True)
|
||||
label = Column(String(255), nullable=False)
|
||||
status = Column(SQLEnum(RfidWriteJobStatus, values_callable=lambda x: [e.value for e in x]), default=RfidWriteJobStatus.PENDING, nullable=False, index=True)
|
||||
requested_by_user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
card_uid = Column(String(100), nullable=True, index=True)
|
||||
write_payload = Column(Text, nullable=True)
|
||||
claimed_at = Column(DateTime, nullable=True)
|
||||
completed_at = Column(DateTime, nullable=True)
|
||||
error_message = Column(String(500), nullable=True)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
reader = relationship("EspReader", back_populates="write_jobs")
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
requested_by_user = relationship("User", foreign_keys=[requested_by_user_id])
|
||||
card = relationship("RfidCard")
|
||||
|
||||
|
||||
class RfidTap(Base):
|
||||
__tablename__ = "rfid_taps"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
reader_id = Column(Integer, ForeignKey("esp_readers.id"), nullable=False, index=True)
|
||||
card_id = Column(Integer, ForeignKey("rfid_cards.id"), nullable=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
|
||||
card_uid = Column(String(100), nullable=False, index=True)
|
||||
action = Column(SQLEnum(EspTapAction, values_callable=lambda x: [e.value for e in x]), default=EspTapAction.UNKNOWN, nullable=False)
|
||||
accepted = Column(Boolean, default=False, nullable=False)
|
||||
message = Column(String(255), nullable=True)
|
||||
raw_payload = Column(Text, nullable=True)
|
||||
tapped_at = Column(DateTime, default=utc_now, nullable=False, index=True)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
|
||||
reader = relationship("EspReader", back_populates="taps")
|
||||
card = relationship("RfidCard", back_populates="taps")
|
||||
user = relationship("User")
|
||||
|
||||
|
||||
class AttendanceSession(Base):
|
||||
__tablename__ = "attendance_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
reader_id = Column(Integer, ForeignKey("esp_readers.id"), nullable=False, index=True)
|
||||
check_in_tap_id = Column(Integer, ForeignKey("rfid_taps.id"), nullable=False)
|
||||
check_out_tap_id = Column(Integer, ForeignKey("rfid_taps.id"), nullable=True)
|
||||
checked_in_at = Column(DateTime, nullable=False, index=True)
|
||||
checked_out_at = Column(DateTime, nullable=True, index=True)
|
||||
checkout_source = Column(SQLEnum(AttendanceCheckoutSource, values_callable=lambda x: [e.value for e in x]), nullable=True)
|
||||
system_flag_reason = Column(String(255), nullable=True)
|
||||
duration_seconds = Column(Integer, nullable=True)
|
||||
is_open = Column(Boolean, default=True, nullable=False, index=True)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
user = relationship("User", back_populates="attendance_sessions")
|
||||
reader = relationship("EspReader", back_populates="attendance_sessions")
|
||||
check_in_tap = relationship("RfidTap", foreign_keys=[check_in_tap_id])
|
||||
check_out_tap = relationship("RfidTap", foreign_keys=[check_out_tap_id])
|
||||
|
||||
|
||||
class VolunteerRole(Base):
|
||||
__tablename__ = "volunteer_roles"
|
||||
|
||||
@@ -220,8 +362,8 @@ class VolunteerRole(Base):
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
# Relationships
|
||||
assignments = relationship("VolunteerAssignment", back_populates="role", cascade="all, delete-orphan")
|
||||
@@ -236,8 +378,8 @@ class VolunteerAssignment(Base):
|
||||
assigned_date = Column(Date, nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
notes = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="volunteer_assignments")
|
||||
@@ -256,8 +398,8 @@ class VolunteerSchedule(Base):
|
||||
location = Column(String(255), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
completed = Column(Boolean, default=False, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
# Relationships
|
||||
assignment = relationship("VolunteerAssignment", back_populates="schedules")
|
||||
@@ -275,8 +417,8 @@ class Certificate(Base):
|
||||
certificate_number = Column(String(100), nullable=True)
|
||||
file_path = Column(String(500), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="certificates")
|
||||
@@ -294,8 +436,8 @@ class File(Base):
|
||||
min_tier_id = Column(Integer, ForeignKey("membership_tiers.id"), nullable=True)
|
||||
description = Column(Text, nullable=True)
|
||||
uploaded_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
|
||||
class Notification(Base):
|
||||
@@ -308,7 +450,7 @@ class Notification(Base):
|
||||
email_sent = Column(Boolean, default=False, nullable=False)
|
||||
sent_at = Column(DateTime, nullable=True)
|
||||
error_message = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
|
||||
|
||||
class PasswordResetToken(Base):
|
||||
@@ -319,7 +461,7 @@ class PasswordResetToken(Base):
|
||||
token = Column(String(255), unique=True, nullable=False, index=True)
|
||||
expires_at = Column(DateTime, nullable=False)
|
||||
used = Column(Boolean, default=False, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", backref="password_reset_tokens")
|
||||
@@ -336,8 +478,8 @@ class EmailTemplate(Base):
|
||||
text_body = Column(Text, nullable=True)
|
||||
variables = Column(Text, nullable=True) # JSON string of available variables
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
|
||||
class BounceType(str, enum.Enum):
|
||||
@@ -357,5 +499,5 @@ class EmailBounce(Base):
|
||||
smtp2go_message_id = Column(String(255), nullable=True, index=True)
|
||||
bounce_date = Column(DateTime, nullable=False)
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=utc_now, nullable=False)
|
||||
updated_at = Column(DateTime, default=utc_now, onupdate=utc_now, nullable=False)
|
||||
|
||||
Reference in New Issue
Block a user