"""Ad and Sponsor models. Ad: internal ad server. Slots: header, sidebar, inline, footer. Targeting: lang (nullable = all), geo_state (nullable = all). Impression/click counters on the model; Redis batching added in Phase 7. Sponsor: paid directory listing + optional category sponsorship. Category sponsor FK wired to categories.sponsor_id (set separately). PromotedKeyword: pinned listing for a search keyword (promoted search). """ from datetime import datetime from app.utils.time import utcnow from app.extensions import db class Ad(db.Model): __tablename__ = "ads" SLOTS = ["header", "sidebar", "inline", "footer"] id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"), primary_key=True, autoincrement=True) advertiser_name = db.Column(db.String(120), nullable=False) slot = db.Column(db.Enum("header", "sidebar", "inline", "footer", name="ad_slot"), nullable=False) creative_path = db.Column(db.String(255), nullable=True) # image file rel path target_url = db.Column(db.String(512), nullable=False) alt_text = db.Column(db.String(200), nullable=True) # targeting (null = match all) lang = db.Column(db.String(5), nullable=True) geo_state = db.Column(db.String(2), nullable=True) starts_at = db.Column(db.DateTime, nullable=False, index=True) ends_at = db.Column(db.DateTime, nullable=False, index=True) impressions = db.Column(db.Integer, nullable=False, default=0) clicks = db.Column(db.Integer, nullable=False, default=0) is_active = db.Column(db.Boolean, nullable=False, default=True) created_at = db.Column(db.DateTime, default=utcnow, nullable=False) updated_at = db.Column(db.DateTime, default=utcnow, onupdate=utcnow, nullable=False) @property def ctr(self): if not self.impressions: return 0.0 return round(self.clicks / self.impressions * 100, 2) @property def is_running(self): now = utcnow() return self.is_active and self.starts_at <= now <= self.ends_at def __repr__(self): return f"" class Sponsor(db.Model): __tablename__ = "sponsors" TIERS = ["directory", "category"] id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"), primary_key=True, autoincrement=True) name = db.Column(db.String(120), nullable=False) logo_path = db.Column(db.String(255), nullable=True) url = db.Column(db.String(512), nullable=False) tagline = db.Column(db.String(160), nullable=True) tier = db.Column(db.Enum("directory", "category", name="sponsor_tier"), nullable=False, default="directory") category_id = db.Column( db.BigInteger().with_variant(db.Integer, "sqlite"), db.ForeignKey("categories.id"), nullable=True) starts_at = db.Column(db.DateTime, nullable=False) ends_at = db.Column(db.DateTime, nullable=False) is_active = db.Column(db.Boolean, nullable=False, default=True) created_at = db.Column(db.DateTime, default=utcnow, nullable=False) category = db.relationship("Category", backref=db.backref("sponsors", lazy="selectin")) @property def is_running(self): now = utcnow() return self.is_active and self.starts_at <= now <= self.ends_at def __repr__(self): return f"" class PromotedKeyword(db.Model): """A listing pinned to the top of search results for a given keyword.""" __tablename__ = "promoted_keywords" __table_args__ = ( db.UniqueConstraint("keyword", "listing_id", name="uq_pk_kw_listing"), ) id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"), primary_key=True, autoincrement=True) keyword = db.Column(db.String(80), nullable=False, index=True) listing_id = db.Column(db.BigInteger().with_variant(db.Integer, "sqlite"), db.ForeignKey("listings.id"), nullable=False) priority = 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=utcnow, nullable=False) listing = db.relationship("Listing", backref=db.backref("promoted_keywords", lazy="selectin")) @property def is_active(self): return self.expires_at > utcnow() def __repr__(self): return f""