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
+98 -1
View File
@@ -9,10 +9,14 @@ from app.models.listing import Listing
from app.models.plan import Plan
from app.models.trust import TrustEvent
from app.models.audit import AuditLog
from app.models.enums import UserStatus, TrustEventType
from app.models.report import Report
from app.models.enums import UserStatus, TrustEventType, ListingStatus
from app.utils import admin_required
from app.services import admin_users as usvc
from app.services import admin_dashboard as dash
from app.services import moderation as msvc
from app.services.settings import get_setting, set_setting
from app.services import audit
admin_bp = Blueprint("admin", __name__)
@@ -125,3 +129,96 @@ def trust_adjust(user_id):
db.session.commit()
flash(_("Trust adjusted."), "success")
return redirect(url_for("admin.user_detail", user_id=user.id))
# --- listing moderation ---
@admin_bp.route("/admin/listings")
@admin_required
def listings():
page = request.args.get("page", 1, type=int)
pagination = msvc.flag_queue(page=page, per_page=PER_PAGE)
flag_threshold = get_setting("flag_threshold", 5)
keyword_blocklist = get_setting("keyword_blocklist", [])
return render_template("admin/listings.html", pagination=pagination,
flag_threshold=flag_threshold,
keyword_blocklist=keyword_blocklist)
@admin_bp.route("/admin/listings/settings", methods=["POST"])
@admin_required
def listings_settings():
threshold = request.form.get("flag_threshold", type=int) or 5
raw_blocklist = request.form.get("keyword_blocklist", "")
blocklist = [line.strip() for line in raw_blocklist.splitlines() if line.strip()]
set_setting("flag_threshold", threshold)
set_setting("keyword_blocklist", blocklist)
audit.log_action(current_user, "settings.updated", "setting", None,
meta={"flag_threshold": threshold, "keyword_blocklist": blocklist})
db.session.commit()
flash(_("Moderation settings updated."), "success")
return redirect(url_for("admin.listings"))
@admin_bp.route("/admin/listings/<int:listing_id>/approve", methods=["POST"])
@admin_required
def approve_listing(listing_id):
listing = Listing.query.get_or_404(listing_id)
msvc.approve(listing, actor=current_user)
db.session.commit()
flash(_("Listing approved."), "success")
return redirect(url_for("admin.listings"))
@admin_bp.route("/admin/listings/<int:listing_id>/hide", methods=["POST"])
@admin_required
def hide_listing(listing_id):
listing = Listing.query.get_or_404(listing_id)
msvc.hide(listing, actor=current_user)
db.session.commit()
flash(_("Listing hidden."), "warning")
return redirect(url_for("admin.listings"))
@admin_bp.route("/admin/listings/<int:listing_id>/remove", methods=["POST"])
@admin_required
def remove_listing(listing_id):
listing = Listing.query.get_or_404(listing_id)
msvc.remove(listing, actor=current_user)
db.session.commit()
flash(_("Listing removed."), "danger")
return redirect(url_for("admin.listings"))
# --- reports queue ---
@admin_bp.route("/admin/reports")
@admin_required
def reports():
candidates = (Report.query.join(Listing, Report.listing_id == Listing.id)
.filter(Listing.status == ListingStatus.flagged)
.order_by(Report.created_at.desc()).all())
dismissed_ids = {a.target_id for a in
AuditLog.query.filter_by(target_type="report", action="report.dismissed").all()}
open_reports = [r for r in candidates if r.id not in dismissed_ids]
return render_template("admin/reports.html", reports=open_reports)
@admin_bp.route("/admin/reports/<int:report_id>/dismiss", methods=["POST"])
@admin_required
def dismiss_report(report_id):
report = Report.query.get_or_404(report_id)
audit.log_action(current_user, "report.dismissed", "report", report.id,
meta={"listing_id": report.listing_id, "reporter_id": report.reporter_id})
db.session.commit()
flash(_("Report dismissed."), "info")
return redirect(url_for("admin.reports"))
@admin_bp.route("/admin/reports/<int:report_id>/escalate", methods=["POST"])
@admin_required
def escalate_report(report_id):
report = Report.query.get_or_404(report_id)
audit.log_action(current_user, "report.escalated", "report", report.id,
meta={"listing_id": report.listing_id, "reporter_id": report.reporter_id})
db.session.commit()
flash(_("Report escalated."), "warning")
return redirect(url_for("admin.reports"))
+24 -1
View File
@@ -9,10 +9,11 @@ from flask_babel import gettext as _
from app.extensions import db, limiter
from app.models.category import Category
from app.models.listing import Listing, ListingImage
from app.models.enums import ListingStatus
from app.models.enums import ListingStatus, ReportReason
from app.services import listings as svc
from app.services.geo import geocode_zip
from app.services.images import process_upload, delete_image_files, ImageError
from app.services import reports as rsvc
from app.blueprints.listings.forms import ListingForm, ImageUploadForm
listings_bp = Blueprint("listings", __name__)
@@ -220,6 +221,28 @@ def mark_sold(listing_id):
return redirect(url_for("listings.detail", listing_id=listing.id))
# --- report ---
@listings_bp.route("/listings/<int:listing_id>/report", methods=["POST"])
@login_required
@limiter.limit("20 per hour", methods=["POST"])
def report(listing_id):
listing = Listing.query.get_or_404(listing_id)
reason_raw = request.form.get("reason", type=str)
note = request.form.get("note", type=str)
try:
reason = ReportReason(reason_raw)
except ValueError:
flash(_("Invalid report reason."), "danger")
return redirect(url_for("listings.detail", listing_id=listing.id))
try:
rsvc.create_report(listing, current_user, reason, note=note)
except rsvc.ReportError as e:
flash(str(e), "warning")
else:
flash(_("Thanks — this listing has been reported for review."), "success")
return redirect(url_for("listings.detail", listing_id=listing.id))
# --- my listings ---
@listings_bp.route("/my/listings")
@login_required