06/15 Phase 1 + 2 codes

This commit is contained in:
2026-06-15 11:23:05 -04:00
commit c2064b84b4
62 changed files with 2937 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
"""Listing model.
Portability notes (see README "MySQL upgrades"):
- Location stored as lat/lng floats; radius search uses a bounding-box prefilter
+ haversine refine. On MySQL you may add a generated POINT + SPATIAL INDEX.
- Hot filter fields (condition/job_type/salary) are denormalized onto indexed
columns, populated in the service layer from `attributes`. On MySQL these can
instead be GENERATED ALWAYS columns over the JSON.
- Free-text search uses the accent-insensitive `title_norm` + body LIKE. On
MySQL add a FULLTEXT(title, body) index and switch to MATCH ... AGAINST.
"""
from datetime import datetime
from sqlalchemy import JSON, Index
from app.extensions import db
from app.models.enums import ListingStatus, Lang
class Listing(db.Model):
__tablename__ = "listings"
id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
primary_key=True, autoincrement=True)
user_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
db.ForeignKey("users.id"), nullable=False, index=True)
category_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
db.ForeignKey("categories.id"), nullable=False, index=True)
title = db.Column(db.String(140), nullable=False)
title_norm = db.Column(db.String(140), nullable=False, index=True) # accent-stripped
body = db.Column(db.Text, nullable=False)
lang = db.Column(db.Enum(Lang), nullable=False, default=Lang.en)
price_cents = db.Column(db.Integer, nullable=True, index=True)
# location
zip = db.Column(db.String(12), nullable=True, index=True)
city = db.Column(db.String(80), nullable=True)
state = db.Column(db.String(2), nullable=True, index=True)
lat = db.Column(db.Float, nullable=True)
lng = db.Column(db.Float, nullable=True)
# category-specific values
attributes = db.Column(JSON, nullable=False, default=dict)
# denormalized hot filter columns (populated from attributes on save)
attr_condition = db.Column(db.String(40), nullable=True, index=True)
attr_job_type = db.Column(db.String(40), nullable=True, index=True)
attr_salary_min = db.Column(db.Integer, nullable=True, index=True)
attr_salary_max = db.Column(db.Integer, nullable=True, index=True)
status = db.Column(db.Enum(ListingStatus), nullable=False,
default=ListingStatus.active, index=True)
is_featured = db.Column(db.Boolean, nullable=False, default=False)
bump_at = db.Column(db.DateTime, nullable=True)
flag_count = db.Column(db.Integer, nullable=False, default=0)
view_count = db.Column(db.Integer, nullable=False, default=0)
expires_at = db.Column(db.DateTime, nullable=False, index=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow,
onupdate=datetime.utcnow, nullable=False)
user = db.relationship("User", backref=db.backref("listings", lazy="dynamic"))
category = db.relationship("Category", back_populates="listings")
images = db.relationship("ListingImage", back_populates="listing",
order_by="ListingImage.sort_order",
cascade="all, delete-orphan", lazy="selectin")
__table_args__ = (
Index("ix_listing_browse", "status", "expires_at"),
Index("ix_listing_sort", "is_featured", "bump_at"),
)
@property
def is_live(self):
return (self.status == ListingStatus.active
and self.expires_at > datetime.utcnow())
@property
def price_display(self):
if self.price_cents is None:
return None
return f"${self.price_cents / 100:,.2f}"
@property
def cover(self):
return self.images[0] if self.images else None
def __repr__(self):
return f"<Listing {self.id} {self.title[:24]!r}>"
class ListingImage(db.Model):
__tablename__ = "listing_images"
id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
primary_key=True, autoincrement=True)
listing_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"),
db.ForeignKey("listings.id"), nullable=False, index=True)
path = db.Column(db.String(255), nullable=False)
thumb_path = db.Column(db.String(255), nullable=True)
sort_order = db.Column(db.Integer, nullable=False, default=0)
width = db.Column(db.Integer, nullable=True)
height = db.Column(db.Integer, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
listing = db.relationship("Listing", back_populates="images")
def __repr__(self):
return f"<ListingImage {self.id} of L{self.listing_id}>"