480 lines
27 KiB
Python
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")
|