06/16 Phase 6
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user