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