wendajia/backend/app/models.py

480 lines
27 KiB
Python

from datetime import datetime
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, LargeBinary, String, Text, UniqueConstraint
from sqlalchemy.dialects.mysql import MEDIUMBLOB
from sqlalchemy.orm import Mapped, mapped_column, relationship
from .database import Base
class RefundPaymentAccount(Base):
__tablename__ = "refund_payment_accounts"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(128))
alipay_account: Mapped[str] = mapped_column(String(128), unique=True)
balance_fen: Mapped[int] = mapped_column(Integer, default=0)
single_limit_fen: Mapped[int] = mapped_column(Integer)
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class RefundRecord(Base):
"""Immutable confirmation snapshot; deliberately independent of task deletion."""
__tablename__ = "refund_records"
__table_args__ = (UniqueConstraint("target_kind", "target_id", name="uq_refund_record_target"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
target_kind: Mapped[str] = mapped_column(String(32))
target_id: Mapped[int] = mapped_column(Integer)
owner_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
shop_name: Mapped[str] = mapped_column(String(255), default="")
order_no: Mapped[str] = mapped_column(String(64), index=True)
payee_name: Mapped[str] = mapped_column(String(64), default="")
payee_account: Mapped[str] = mapped_column(String(128), default="")
amount_fen: Mapped[int] = mapped_column(Integer)
payer_account: Mapped[str] = mapped_column(String(128), default="")
operator_name: Mapped[str] = mapped_column(String(64))
alipay_trade_no: Mapped[str] = mapped_column(String(128), default="")
transferred_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
class AlipayRefundOrder(Base):
"""Idempotency and provider status record for a real Alipay transfer."""
__tablename__ = "alipay_transfer_attempts"
__table_args__ = (UniqueConstraint("target_kind", "target_id", name="uq_alipay_transfer_attempt_target"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
target_kind: Mapped[str] = mapped_column(String(32), index=True)
target_id: Mapped[int] = mapped_column(Integer, index=True)
out_biz_no: Mapped[str] = mapped_column(String(64), unique=True, index=True)
amount_fen: Mapped[int] = mapped_column(Integer)
order_no: Mapped[str] = mapped_column(String(64), default="")
shop_name: Mapped[str] = mapped_column(String(255), default="")
owner_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
account: Mapped[str] = mapped_column(String(128))
payee_name: Mapped[str] = mapped_column(String(64))
payer_account: Mapped[str] = mapped_column(String(128), default="")
created_by_id: Mapped[int] = mapped_column(Integer, index=True)
operator_name: Mapped[str] = mapped_column(String(64), default="")
status: Mapped[str] = mapped_column(String(24), default="pending", index=True)
alipay_trade_no: Mapped[str] = mapped_column(String(128), default="")
error_code: Mapped[str] = mapped_column(String(128), default="")
error_message: Mapped[str] = mapped_column(String(500), default="")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class RefundRecipient(Base):
__tablename__ = "refund_recipients"
__table_args__ = (UniqueConstraint("target_kind", "target_id", name="uq_refund_recipient_target"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
target_kind: Mapped[str] = mapped_column(String(32))
target_id: Mapped[int] = mapped_column(Integer)
account: Mapped[str] = mapped_column(String(128))
payee_name: Mapped[str] = mapped_column(String(64))
amount_fen: Mapped[int | None] = mapped_column(Integer, nullable=True)
class AdminUser(Base):
__tablename__ = "admin_users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
username: Mapped[str] = mapped_column(String(64), unique=True, index=True)
password_hash: Mapped[str] = mapped_column(String(255))
display_name: Mapped[str] = mapped_column(String(64))
role_code: Mapped[str] = mapped_column(String(32), default="operator", index=True)
refund_payment_account_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
dingtalk_user_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
dingtalk_union_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
dingtalk_corp_id: Mapped[str | None] = mapped_column(String(128), nullable=True, index=True)
dingtalk_dept_id: Mapped[str | None] = mapped_column(String(64), nullable=True, index=True)
dingtalk_dept_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
dingtalk_dept_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
dingtalk_title: Mapped[str | None] = mapped_column(String(255), nullable=True)
is_dept_manager: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
# Stable public entry point for answer-task submissions created by this
# operator. The token is intentionally separate from the admin session
# cookie and is safe to embed in a long-lived URL.
answer_public_token: Mapped[str | None] = mapped_column(String(96), unique=True, index=True, nullable=True)
last_answer_shop_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
last_login_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
login_logs: Mapped[list["AdminLoginLog"]] = relationship(back_populates="user")
ask_tasks: Mapped[list["AskTask"]] = relationship(back_populates="owner")
class AdminLoginLog(Base):
__tablename__ = "admin_login_logs"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int | None] = mapped_column(ForeignKey("admin_users.id"), nullable=True, index=True)
username: Mapped[str] = mapped_column(String(64), index=True)
success: Mapped[bool] = mapped_column(Boolean, index=True)
ip_address: Mapped[str | None] = mapped_column(String(45), nullable=True)
user_agent: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
user: Mapped[AdminUser | None] = relationship(back_populates="login_logs")
class AdminUsageEntry(Base):
"""One deduplicated entry into the operations console."""
__tablename__ = "admin_usage_entries"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
user_id: Mapped[int] = mapped_column(Integer, index=True)
username: Mapped[str] = mapped_column(String(64), index=True)
display_name: Mapped[str] = mapped_column(String(64), index=True)
department_name: Mapped[str] = mapped_column(String(255), default="", index=True)
entered_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
class QALibraryEntry(Base):
__tablename__ = "qa_library_entries"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
fingerprint: Mapped[str] = mapped_column(String(64), unique=True, index=True)
product_name: Mapped[str] = mapped_column(String(255), default="", index=True)
category: Mapped[str] = mapped_column(String(100), default="", index=True)
benefits: Mapped[str] = mapped_column(Text, default="")
selling_points: Mapped[str] = mapped_column(Text, default="")
question_text: Mapped[str] = mapped_column(Text)
answers_json: Mapped[str] = mapped_column(Text)
source: Mapped[str] = mapped_column(String(64), default="AI生成")
created_by: Mapped[str] = mapped_column(String(64), default="运营后台")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
class QABulkGenerationJob(Base):
"""Persistent progress for a bulk AI-answer generation request."""
__tablename__ = "qa_bulk_generation_jobs"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
public_id: Mapped[str] = mapped_column(String(64), unique=True, index=True)
# Keep this as an indexed ownership value without a database FK so the
# table remains compatible with existing MySQL installations whose admin
# primary key may use a different integer width.
owner_id: Mapped[int] = mapped_column(Integer, index=True)
payload_json: Mapped[str] = mapped_column(Text)
answers_per_question: Mapped[int] = mapped_column(Integer)
status: Mapped[str] = mapped_column(String(32), default="queued", index=True)
total_count: Mapped[int] = mapped_column(Integer, default=0)
completed_count: Mapped[int] = mapped_column(Integer, default=0)
failed_count: Mapped[int] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
items: Mapped[list["QABulkGenerationItem"]] = relationship(
back_populates="job",
cascade="all, delete-orphan",
lazy="selectin",
order_by="QABulkGenerationItem.sequence_no",
)
class QABulkGenerationItem(Base):
__tablename__ = "qa_bulk_generation_items"
__table_args__ = (
UniqueConstraint("job_id", "sequence_no", name="uq_qa_bulk_generation_item_sequence"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
job_id: Mapped[int] = mapped_column(ForeignKey("qa_bulk_generation_jobs.id"), index=True)
sequence_no: Mapped[int] = mapped_column(Integer)
question_text: Mapped[str] = mapped_column(Text)
answers_json: Mapped[str | None] = mapped_column(Text, nullable=True)
status: Mapped[str] = mapped_column(String(24), default="pending", index=True)
attempts: Mapped[int] = mapped_column(Integer, default=0)
error_message: Mapped[str | None] = mapped_column(String(500), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
job: Mapped[QABulkGenerationJob] = relationship(back_populates="items")
class AnswerUser(Base):
__tablename__ = "answer_users"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
public_id: Mapped[str] = mapped_column(String(32), unique=True, index=True)
phone: Mapped[str] = mapped_column(String(20), unique=True, index=True)
taobao_id: Mapped[str | None] = mapped_column(String(64), unique=True, index=True, nullable=True)
auth_token: Mapped[str] = mapped_column(String(64), unique=True, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
last_login_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
submissions: Mapped[list["AnswerSubmission"]] = relationship(back_populates="user")
ask_assignments: Mapped[list["AskAssignment"]] = relationship(back_populates="user")
class AnswerSubmission(Base):
__tablename__ = "answer_submissions"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
submission_no: Mapped[str] = mapped_column(String(40), unique=True, index=True)
user_id: Mapped[int] = mapped_column(ForeignKey("answer_users.id"), index=True)
owner_id: Mapped[int | None] = mapped_column(ForeignKey("admin_users.id"), nullable=True, index=True)
# Store name is entered by the operator when this answer task is
# dispatched. It is kept on the submission because one operator may
# handle multiple stores and the same user can submit through different
# operator links.
shop_name: Mapped[str] = mapped_column(String(255), default="")
# 用户上传受邀截图后立即进入运营筛选队列。
status: Mapped[str] = mapped_column(String(32), default="待筛选", index=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user: Mapped[AnswerUser] = relationship(back_populates="submissions", lazy="selectin")
owner: Mapped[AdminUser | None] = relationship()
questions: Mapped[list["AnswerRecognizedQuestion"]] = relationship(back_populates="submission", cascade="all, delete-orphan", lazy="selectin")
screenshots: Mapped[list["AnswerScreenshot"]] = relationship(back_populates="submission", cascade="all, delete-orphan")
review_decisions: Mapped[list["AnswerReviewDecision"]] = relationship(back_populates="submission", cascade="all, delete-orphan", lazy="selectin")
class AnswerScreenshot(Base):
__tablename__ = "answer_screenshots"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
submission_id: Mapped[int] = mapped_column(ForeignKey("answer_submissions.id"), index=True)
stage: Mapped[str] = mapped_column(String(20), index=True) # invited/completion
filename: Mapped[str] = mapped_column(String(255))
content_type: Mapped[str] = mapped_column(String(64))
image_data: Mapped[bytes | None] = mapped_column(LargeBinary(length=16_777_215), nullable=True)
file_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
submission: Mapped[AnswerSubmission] = relationship(back_populates="screenshots")
class AnswerRecognizedQuestion(Base):
__tablename__ = "answer_recognized_questions"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
submission_id: Mapped[int] = mapped_column(ForeignKey("answer_submissions.id"), index=True)
source_screenshot_id: Mapped[int | None] = mapped_column(ForeignKey("answer_screenshots.id"), nullable=True)
sequence_no: Mapped[int] = mapped_column(Integer)
product_title: Mapped[str] = mapped_column(String(255))
product_image_screenshot_id: Mapped[int | None] = mapped_column(ForeignKey("answer_screenshots.id"), nullable=True)
operator_image_screenshot_id: Mapped[int | None] = mapped_column(ForeignKey("answer_screenshots.id"), nullable=True)
operator_image_screenshot_ids: Mapped[str | None] = mapped_column(Text, nullable=True)
question_text: Mapped[str] = mapped_column(Text)
answer_text: Mapped[str] = mapped_column(Text, default="")
answer_status: Mapped[str] = mapped_column(String(32), default="已生成")
selected: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
submission: Mapped[AnswerSubmission] = relationship(back_populates="questions")
class AnswerReviewDecision(Base):
__tablename__ = "answer_review_decisions"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
submission_id: Mapped[int] = mapped_column(ForeignKey("answer_submissions.id"), index=True)
approved: Mapped[bool] = mapped_column(Boolean, index=True)
reason_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
reason_text: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
submission: Mapped[AnswerSubmission] = relationship(back_populates="review_decisions")
class AskTask(Base):
__tablename__ = "ask_tasks"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
task_no: Mapped[str] = mapped_column(String(40), unique=True, index=True)
public_token: Mapped[str | None] = mapped_column(String(64), unique=True, index=True, nullable=True)
owner_id: Mapped[int | None] = mapped_column(ForeignKey("admin_users.id"), nullable=True, index=True)
product_link: Mapped[str] = mapped_column(String(1000))
# QR images are capped at 5 MB by the API. MEDIUMBLOB keeps fresh MySQL
# installs above the 64 KB BLOB limit while the generic type remains
# compatible with SQLite development databases.
product_qr_image_data: Mapped[bytes | None] = mapped_column(
LargeBinary().with_variant(MEDIUMBLOB, "mysql"), nullable=True
)
product_qr_image_content_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
product_qr_image_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
shop_name: Mapped[str] = mapped_column(String(255), default="")
product_name: Mapped[str] = mapped_column(String(255))
category: Mapped[str] = mapped_column(String(100), default="")
benefits: Mapped[str] = mapped_column(Text, default="")
selling_points: Mapped[str] = mapped_column(Text, default="")
language_style: Mapped[str] = mapped_column(String(40), default="口语(亲切随性)")
target_group: Mapped[str] = mapped_column(String(100), default="女性")
usage_scene: Mapped[str] = mapped_column(String(100), default="居家")
purchase_channel: Mapped[str] = mapped_column(String(100), default="电商平台")
claim_questions_per_user: Mapped[int] = mapped_column(Integer, default=5, nullable=False)
status: Mapped[str] = mapped_column(String(32), default="草稿", index=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
questions: Mapped[list["AskQuestion"]] = relationship(back_populates="task", cascade="all, delete-orphan", lazy="selectin")
assignments: Mapped[list["AskAssignment"]] = relationship(back_populates="task", cascade="all, delete-orphan")
owner: Mapped[AdminUser | None] = relationship(back_populates="ask_tasks")
class AskQuestion(Base):
__tablename__ = "ask_questions"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
task_id: Mapped[int] = mapped_column(ForeignKey("ask_tasks.id"), index=True)
sequence_no: Mapped[int] = mapped_column(Integer)
question_text: Mapped[str] = mapped_column(Text)
# Keep the origin so the draft UI can distinguish AI-generated questions
# from questions added manually or imported from the Q&A library.
source: Mapped[str] = mapped_column(String(32), default="AI生成", nullable=False)
selected: Mapped[bool] = mapped_column(Boolean, default=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
task: Mapped[AskTask] = relationship(back_populates="questions", lazy="selectin")
assignment_items: Mapped[list["AskAssignmentQuestion"]] = relationship(back_populates="question")
class AskAssignment(Base):
__tablename__ = "ask_assignments"
__table_args__ = (UniqueConstraint("task_id", "user_id", name="uq_ask_assignment_task_user"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
task_id: Mapped[int] = mapped_column(ForeignKey("ask_tasks.id"), index=True)
user_id: Mapped[int] = mapped_column(ForeignKey("answer_users.id"), index=True)
status: Mapped[str] = mapped_column(String(32), default="待选择", index=True)
# The current reservation window is independent from the assignment's
# lifetime. One assignment can retain several submitted batches while a
# later, unsubmitted selection expires and is released safely.
reservation_started_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
task: Mapped[AskTask] = relationship(back_populates="assignments", lazy="selectin")
user: Mapped[AnswerUser] = relationship(back_populates="ask_assignments", lazy="selectin")
items: Mapped[list["AskAssignmentQuestion"]] = relationship(back_populates="assignment", cascade="all, delete-orphan", lazy="selectin")
used_questions: Mapped[list["AskAssignmentUsedQuestion"]] = relationship(back_populates="assignment", cascade="all, delete-orphan", lazy="selectin")
screenshots: Mapped[list["AskAssignmentScreenshot"]] = relationship(back_populates="assignment", cascade="all, delete-orphan")
review_decisions: Mapped[list["AskAssignmentReview"]] = relationship(back_populates="assignment", cascade="all, delete-orphan", lazy="selectin")
batches: Mapped[list["AskAssignmentBatch"]] = relationship(back_populates="assignment", cascade="all, delete-orphan", lazy="selectin")
class AskAssignmentBatch(Base):
"""One submission/review batch for an ask-task assignment.
A user may submit the five assigned questions in multiple batches. The
parent assignment remains the question pool, while this table preserves
each submission independently for the admin review queue.
"""
__tablename__ = "ask_assignment_batches"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
assignment_id: Mapped[int] = mapped_column(ForeignKey("ask_assignments.id"), index=True)
status: Mapped[str] = mapped_column(String(32), default="pending", index=True)
selected_question_ids: Mapped[str] = mapped_column(Text, default="")
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, index=True)
assignment: Mapped[AskAssignment] = relationship(back_populates="batches")
screenshots: Mapped[list["AskAssignmentBatchScreenshot"]] = relationship(back_populates="batch", cascade="all, delete-orphan")
review_decisions: Mapped[list["AskAssignmentBatchReview"]] = relationship(back_populates="batch", cascade="all, delete-orphan", lazy="selectin")
class AskAssignmentBatchScreenshot(Base):
__tablename__ = "ask_assignment_batch_screenshots"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
batch_id: Mapped[int] = mapped_column(ForeignKey("ask_assignment_batches.id"), index=True)
stage: Mapped[str] = mapped_column(String(20), index=True)
filename: Mapped[str] = mapped_column(String(255))
content_type: Mapped[str] = mapped_column(String(64))
image_data: Mapped[bytes | None] = mapped_column(LargeBinary(length=16_777_215), nullable=True)
file_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
batch: Mapped[AskAssignmentBatch] = relationship(back_populates="screenshots")
class AskAssignmentBatchReview(Base):
__tablename__ = "ask_assignment_batch_reviews"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
batch_id: Mapped[int] = mapped_column(ForeignKey("ask_assignment_batches.id"), index=True)
approved: Mapped[bool] = mapped_column(Boolean, index=True)
reason_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
reason_text: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
batch: Mapped[AskAssignmentBatch] = relationship(back_populates="review_decisions")
class AskAssignmentQuestion(Base):
__tablename__ = "ask_assignment_questions"
__table_args__ = (
UniqueConstraint("assignment_id", "question_id", name="uq_ask_assignment_question"),
# One published question can only be allocated to one user. The
# question already belongs to a single task, so a global constraint on
# question_id also guarantees uniqueness within that task.
UniqueConstraint("question_id", name="uq_ask_assignment_question_global"),
)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
assignment_id: Mapped[int] = mapped_column(ForeignKey("ask_assignments.id"), index=True)
question_id: Mapped[int] = mapped_column(ForeignKey("ask_questions.id"), index=True)
selected: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
assignment: Mapped[AskAssignment] = relationship(back_populates="items", lazy="selectin")
question: Mapped[AskQuestion] = relationship(back_populates="assignment_items", lazy="selectin")
class AskAssignmentUsedQuestion(Base):
"""Questions already consumed by a user after an approved submission.
This is kept in a separate table so existing installations do not need an
ALTER TABLE on ``ask_assignment_questions`` when the feature is enabled.
"""
__tablename__ = "ask_assignment_used_questions"
__table_args__ = (UniqueConstraint("assignment_id", "question_id", name="uq_ask_assignment_used_question"),)
id: Mapped[int] = mapped_column(Integer, primary_key=True)
assignment_id: Mapped[int] = mapped_column(ForeignKey("ask_assignments.id"), index=True)
question_id: Mapped[int] = mapped_column(ForeignKey("ask_questions.id"), index=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
assignment: Mapped[AskAssignment] = relationship(back_populates="used_questions")
question: Mapped[AskQuestion] = relationship()
class AskAssignmentScreenshot(Base):
__tablename__ = "ask_assignment_screenshots"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
assignment_id: Mapped[int] = mapped_column(ForeignKey("ask_assignments.id"), index=True)
stage: Mapped[str] = mapped_column(String(20), index=True) # question/payment
filename: Mapped[str] = mapped_column(String(255))
content_type: Mapped[str] = mapped_column(String(64))
image_data: Mapped[bytes | None] = mapped_column(LargeBinary(length=16_777_215), nullable=True)
file_path: Mapped[str | None] = mapped_column(String(1000), nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
assignment: Mapped[AskAssignment] = relationship(back_populates="screenshots")
class AskAssignmentReview(Base):
__tablename__ = "ask_assignment_reviews"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
assignment_id: Mapped[int] = mapped_column(ForeignKey("ask_assignments.id"), index=True)
approved: Mapped[bool] = mapped_column(Boolean, index=True)
reason_code: Mapped[str | None] = mapped_column(String(64), nullable=True)
reason_text: Mapped[str | None] = mapped_column(Text, nullable=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, index=True)
assignment: Mapped[AskAssignment] = relationship(back_populates="review_decisions")