06/16 Phase 6

This commit is contained in:
2026-06-16 13:29:32 -04:00
parent 2e9025fe55
commit 479afde2fa
16 changed files with 527 additions and 8 deletions
+2 -1
View File
@@ -41,4 +41,5 @@ def revenue_30d_cents():
def flag_queue_depth():
return Listing.query.filter(Listing.flag_count > 0).count()
from app.services.moderation import QUEUE_FILTER
return Listing.query.filter(QUEUE_FILTER).count()
+14
View File
@@ -8,6 +8,7 @@ from app.models.enums import ListingStatus, Lang
from app.utils.text import normalize
from app.services.geo import geocode_zip, bounding_box, haversine_mi
from app.services.field_schema import validate_attributes, hot_values
from app.services.settings import get_setting
class ListingError(ValueError):
@@ -48,6 +49,15 @@ def _life_days(user):
return _limit(user, "listing_life_days", 14)
def _blocklist_hit(title, body):
"""Accent-insensitive check against the admin-configured keyword blocklist."""
blocklist = get_setting("keyword_blocklist", [])
if not blocklist:
return False
hay = normalize(f"{title} {body}")
return any(normalize(term) in hay for term in blocklist if term)
# --- create / update ---
def create_listing(user, category, *, title, body, lang, price_cents,
zip_code, raw_attributes):
@@ -71,6 +81,8 @@ def create_listing(user, category, *, title, body, lang, price_cents,
expires_at=datetime.utcnow() + timedelta(days=_life_days(user)),
**hot_values(cleaned),
)
if _blocklist_hit(title, body):
listing.status = ListingStatus.flagged
_apply_location(listing, zip_code)
db.session.add(listing)
db.session.commit()
@@ -91,6 +103,8 @@ def update_listing(listing, category, *, title, body, lang, price_cents,
listing.attributes = cleaned
for col, val in hot_values(cleaned).items():
setattr(listing, col, val)
if listing.status == ListingStatus.active and _blocklist_hit(title, body):
listing.status = ListingStatus.flagged
_apply_location(listing, zip_code)
db.session.commit()
return listing
+43
View File
@@ -0,0 +1,43 @@
"""Admin listing moderation: flag queue + approve/hide/remove actions.
Each action mutates the listing + calls audit.log_action; caller commits
(mirrors services/admin_users.py).
"""
from app.extensions import db
from app.models.listing import Listing
from app.models.enums import ListingStatus
from app.services import audit
# A listing is in the moderation queue if it's already flagged, or it's
# still active but has accumulated reports below the auto-flag threshold.
QUEUE_FILTER = db.or_(
Listing.status == ListingStatus.flagged,
db.and_(Listing.status == ListingStatus.active, Listing.flag_count > 0),
)
def flag_queue(*, page=1, per_page=25):
return (Listing.query.filter(QUEUE_FILTER)
.order_by(Listing.flag_count.desc(), Listing.updated_at.desc())
.paginate(page=page, per_page=per_page, error_out=False))
def approve(listing, *, actor):
"""Clear flags, restore to active. Caller commits."""
old = listing.flag_count
listing.status = ListingStatus.active
listing.flag_count = 0
audit.log_action(actor, "listing.approved", "listing", listing.id,
meta={"cleared_flags": old})
def hide(listing, *, actor):
"""Manually flag for review without removing. Caller commits."""
listing.status = ListingStatus.flagged
audit.log_action(actor, "listing.hidden", "listing", listing.id)
def remove(listing, *, actor):
"""Permanently remove from the marketplace (status flip, not a hard delete). Caller commits."""
listing.status = ListingStatus.removed
audit.log_action(actor, "listing.removed", "listing", listing.id)
+32
View File
@@ -0,0 +1,32 @@
"""User-submitted listing reports + flag-count threshold auto-flag."""
from sqlalchemy.exc import IntegrityError
from app.extensions import db
from app.models.report import Report
from app.models.enums import ListingStatus, ReportReason
from app.services.settings import get_setting
class ReportError(ValueError):
pass
def create_report(listing, reporter, reason: ReportReason, note=None):
"""Record a report, bump flag_count, auto-flag at threshold. Commits."""
if listing.user_id == reporter.id:
raise ReportError("cannot report your own listing")
report = Report(listing_id=listing.id, reporter_id=reporter.id,
reason=reason, note=(note or "").strip() or None)
db.session.add(report)
try:
db.session.commit()
except IntegrityError:
db.session.rollback()
raise ReportError("you have already reported this listing")
listing.flag_count = (listing.flag_count or 0) + 1
threshold = get_setting("flag_threshold", 5)
if listing.flag_count >= threshold and listing.status == ListingStatus.active:
listing.status = ListingStatus.flagged
db.session.commit()
return report