06/15 Phase 4 + 5 codes
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
"""Ads service.
|
||||
|
||||
Ad serving:
|
||||
get_ad(slot, lang, state) → Ad or None
|
||||
Picks a random active ad matching slot + optional lang/state targeting.
|
||||
Falls back from targeted → untargeted if no targeted ad found.
|
||||
|
||||
Tracking:
|
||||
record_impression(ad_id) — increments ad.impressions
|
||||
record_click(ad_id) — increments ad.clicks
|
||||
Both are direct DB increments for now.
|
||||
Phase 7: batch to Redis counter, flush every N minutes.
|
||||
|
||||
Promoted search:
|
||||
promoted_listings(keyword) → [Listing, ...] ordered by priority desc
|
||||
Inserted at the top of browse results when a keyword is searched.
|
||||
|
||||
Sponsors:
|
||||
active_sponsors(tier, category_id) → [Sponsor, ...]
|
||||
"""
|
||||
import random
|
||||
from datetime import datetime
|
||||
from app.extensions import db
|
||||
from app.models.ads import Ad, Sponsor, PromotedKeyword
|
||||
from app.models.listing import Listing
|
||||
from app.utils.text import normalize
|
||||
|
||||
|
||||
def get_ad(slot: str, lang: str = None, state: str = None) -> "Ad | None":
|
||||
"""Return one active ad for the slot, targeted then untargeted fallback."""
|
||||
now = datetime.utcnow()
|
||||
base = Ad.query.filter(
|
||||
Ad.slot == slot,
|
||||
Ad.is_active == True,
|
||||
Ad.starts_at <= now,
|
||||
Ad.ends_at >= now,
|
||||
)
|
||||
|
||||
# try targeted first
|
||||
if lang or state:
|
||||
targeted = base
|
||||
if lang:
|
||||
targeted = targeted.filter(Ad.lang == lang)
|
||||
if state:
|
||||
targeted = targeted.filter(Ad.geo_state == state)
|
||||
candidates = targeted.all()
|
||||
if candidates:
|
||||
return random.choice(candidates)
|
||||
|
||||
# untargeted fallback
|
||||
candidates = base.filter(Ad.lang.is_(None), Ad.geo_state.is_(None)).all()
|
||||
if candidates:
|
||||
return random.choice(candidates)
|
||||
|
||||
# any active ad for this slot
|
||||
candidates = base.all()
|
||||
return random.choice(candidates) if candidates else None
|
||||
|
||||
|
||||
def record_impression(ad_id: int):
|
||||
"""Increment impression counter. Safe to call on every page render."""
|
||||
Ad.query.filter_by(id=ad_id).update(
|
||||
{Ad.impressions: Ad.impressions + 1})
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def record_click(ad_id: int):
|
||||
"""Increment click counter."""
|
||||
Ad.query.filter_by(id=ad_id).update(
|
||||
{Ad.clicks: Ad.clicks + 1})
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def promoted_listings(keyword: str) -> list:
|
||||
"""Return active promoted listings for keyword, priority-ordered."""
|
||||
if not keyword:
|
||||
return []
|
||||
norm = normalize(keyword)
|
||||
now = datetime.utcnow()
|
||||
rows = (PromotedKeyword.query
|
||||
.filter(PromotedKeyword.expires_at > now)
|
||||
.order_by(PromotedKeyword.priority.desc())
|
||||
.all())
|
||||
# match any keyword that is a substring of the search term (normalized)
|
||||
matched = [r for r in rows if normalize(r.keyword) in norm or norm in normalize(r.keyword)]
|
||||
listings = []
|
||||
seen = set()
|
||||
for r in matched:
|
||||
if r.listing_id not in seen and r.listing and r.listing.is_live:
|
||||
listings.append(r.listing)
|
||||
seen.add(r.listing_id)
|
||||
return listings
|
||||
|
||||
|
||||
def active_sponsors(tier: str = None, category_id: int = None) -> list:
|
||||
"""Return running sponsors, optionally filtered by tier and category."""
|
||||
now = datetime.utcnow()
|
||||
q = Sponsor.query.filter(
|
||||
Sponsor.is_active == True,
|
||||
Sponsor.starts_at <= now,
|
||||
Sponsor.ends_at >= now,
|
||||
)
|
||||
if tier:
|
||||
q = q.filter(Sponsor.tier == tier)
|
||||
if category_id is not None:
|
||||
q = q.filter(Sponsor.category_id == category_id)
|
||||
return q.order_by(Sponsor.created_at.desc()).all()
|
||||
|
||||
|
||||
def expire_promoted_keywords() -> int:
|
||||
"""Remove expired promoted keyword rows. Returns count."""
|
||||
now = datetime.utcnow()
|
||||
expired = PromotedKeyword.query.filter(
|
||||
PromotedKeyword.expires_at <= now).all()
|
||||
n = len(expired)
|
||||
for row in expired:
|
||||
db.session.delete(row)
|
||||
if n:
|
||||
db.session.commit()
|
||||
return n
|
||||
Reference in New Issue
Block a user