06/16 Phase 6 (continue)

This commit is contained in:
2026-06-17 17:51:03 -04:00
parent 68a842d6b6
commit d1383a9835
11 changed files with 846 additions and 5 deletions
+352
View File
@@ -1,5 +1,6 @@
"""Admin backend: dashboard KPIs + user management. Admin-only."""
import json
from datetime import datetime, timedelta
from flask import Blueprint, render_template, request, redirect, url_for, flash, abort
from flask_login import current_user
from flask_babel import gettext as _
@@ -13,6 +14,7 @@ from app.models.trust import TrustEvent
from app.models.audit import AuditLog
from app.models.report import Report
from app.models.payments import Transaction
from app.models.ads import Ad, Sponsor, PromotedKeyword
from app.models.enums import UserStatus, TrustEventType, ListingStatus
from app.utils import admin_required
from app.services import admin_users as usvc
@@ -402,3 +404,353 @@ def plan_edit(plan_id):
config_str = json.dumps(plan.config or {}, indent=2)
return render_template("admin/plan_edit.html", plan=plan,
config_str=config_str, error=error)
# ---------------------------------------------------------------------------
# Ads management
# ---------------------------------------------------------------------------
def _parse_dt(s, fallback=None):
"""Parse 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM' strings into datetime."""
if not s:
return fallback
for fmt in ("%Y-%m-%dT%H:%M", "%Y-%m-%d"):
try:
return datetime.strptime(s.strip(), fmt)
except ValueError:
continue
return fallback
@admin_bp.route("/admin/ads")
@admin_required
def admin_ads():
ad_list = Ad.query.order_by(Ad.created_at.desc()).all()
return render_template("admin/ads.html", ad_list=ad_list)
@admin_bp.route("/admin/ads/new", methods=["GET", "POST"])
@admin_required
def admin_ad_new():
error = None
if request.method == "POST":
try:
ad = Ad(
advertiser_name=request.form["advertiser_name"].strip(),
slot=request.form["slot"],
target_url=request.form["target_url"].strip(),
alt_text=request.form.get("alt_text", "").strip() or None,
creative_path=request.form.get("creative_path", "").strip() or None,
lang=request.form.get("lang", "").strip() or None,
geo_state=request.form.get("geo_state", "").strip().upper() or None,
starts_at=_parse_dt(request.form.get("starts_at"),
datetime.utcnow()),
ends_at=_parse_dt(request.form.get("ends_at"),
datetime.utcnow() + timedelta(days=30)),
is_active=request.form.get("is_active") == "on",
)
db.session.add(ad)
audit.log_action(current_user, "ad.created", "ad", None,
meta={"advertiser": ad.advertiser_name,
"slot": ad.slot})
db.session.commit()
flash(_("Ad created."), "success")
return redirect(url_for("admin.admin_ads"))
except Exception as exc:
db.session.rollback()
error = str(exc)
return render_template("admin/ad_edit.html", ad=None, error=error)
@admin_bp.route("/admin/ads/<int:ad_id>", methods=["GET", "POST"])
@admin_required
def admin_ad_edit(ad_id):
ad = db.get_or_404(Ad, ad_id)
error = None
if request.method == "POST":
try:
ad.advertiser_name = request.form["advertiser_name"].strip()
ad.slot = request.form["slot"]
ad.target_url = request.form["target_url"].strip()
ad.alt_text = request.form.get("alt_text", "").strip() or None
ad.creative_path = request.form.get("creative_path", "").strip() or None
ad.lang = request.form.get("lang", "").strip() or None
ad.geo_state = request.form.get("geo_state", "").strip().upper() or None
ad.starts_at = _parse_dt(request.form.get("starts_at"), ad.starts_at)
ad.ends_at = _parse_dt(request.form.get("ends_at"), ad.ends_at)
ad.is_active = request.form.get("is_active") == "on"
audit.log_action(current_user, "ad.updated", "ad", ad.id,
meta={"advertiser": ad.advertiser_name})
db.session.commit()
flash(_("Ad updated."), "success")
return redirect(url_for("admin.admin_ads"))
except Exception as exc:
db.session.rollback()
error = str(exc)
return render_template("admin/ad_edit.html", ad=ad, error=error)
@admin_bp.route("/admin/ads/<int:ad_id>/delete", methods=["POST"])
@admin_required
def admin_ad_delete(ad_id):
ad = db.get_or_404(Ad, ad_id)
audit.log_action(current_user, "ad.deleted", "ad", ad.id,
meta={"advertiser": ad.advertiser_name})
db.session.delete(ad)
db.session.commit()
flash(_("Ad deleted."), "info")
return redirect(url_for("admin.admin_ads"))
@admin_bp.route("/admin/ads/<int:ad_id>/toggle", methods=["POST"])
@admin_required
def admin_ad_toggle(ad_id):
ad = db.get_or_404(Ad, ad_id)
ad.is_active = not ad.is_active
audit.log_action(current_user, "ad.toggled", "ad", ad.id,
meta={"is_active": ad.is_active})
db.session.commit()
flash(_("Ad %(state)s.", state=_("enabled") if ad.is_active else _("disabled")),
"success")
return redirect(url_for("admin.admin_ads"))
# ---------------------------------------------------------------------------
# Sponsors management
# ---------------------------------------------------------------------------
@admin_bp.route("/admin/sponsors")
@admin_required
def admin_sponsors():
sponsors = Sponsor.query.order_by(Sponsor.created_at.desc()).all()
return render_template("admin/sponsors.html", sponsors=sponsors)
@admin_bp.route("/admin/sponsors/new", methods=["GET", "POST"])
@admin_required
def admin_sponsor_new():
cats = Category.query.filter_by(parent_id=None, is_active=True).order_by(
Category.sort_order, Category.name).all()
error = None
if request.method == "POST":
try:
cat_id = request.form.get("category_id", type=int) or None
sp = Sponsor(
name=request.form["name"].strip(),
url=request.form["url"].strip(),
tagline=request.form.get("tagline", "").strip() or None,
logo_path=request.form.get("logo_path", "").strip() or None,
tier=request.form.get("tier", "directory"),
category_id=cat_id,
starts_at=_parse_dt(request.form.get("starts_at"), datetime.utcnow()),
ends_at=_parse_dt(request.form.get("ends_at"),
datetime.utcnow() + timedelta(days=30)),
is_active=request.form.get("is_active") == "on",
)
db.session.add(sp)
audit.log_action(current_user, "sponsor.created", "sponsor", None,
meta={"name": sp.name})
db.session.commit()
flash(_("Sponsor created."), "success")
return redirect(url_for("admin.admin_sponsors"))
except Exception as exc:
db.session.rollback()
error = str(exc)
return render_template("admin/sponsor_edit.html", sponsor=None,
categories=cats, error=error)
@admin_bp.route("/admin/sponsors/<int:sponsor_id>", methods=["GET", "POST"])
@admin_required
def admin_sponsor_edit(sponsor_id):
sp = db.get_or_404(Sponsor, sponsor_id)
cats = Category.query.filter_by(parent_id=None, is_active=True).order_by(
Category.sort_order, Category.name).all()
error = None
if request.method == "POST":
try:
sp.name = request.form["name"].strip()
sp.url = request.form["url"].strip()
sp.tagline = request.form.get("tagline", "").strip() or None
sp.logo_path = request.form.get("logo_path", "").strip() or None
sp.tier = request.form.get("tier", "directory")
sp.category_id = request.form.get("category_id", type=int) or None
sp.starts_at = _parse_dt(request.form.get("starts_at"), sp.starts_at)
sp.ends_at = _parse_dt(request.form.get("ends_at"), sp.ends_at)
sp.is_active = request.form.get("is_active") == "on"
audit.log_action(current_user, "sponsor.updated", "sponsor", sp.id,
meta={"name": sp.name})
db.session.commit()
flash(_("Sponsor updated."), "success")
return redirect(url_for("admin.admin_sponsors"))
except Exception as exc:
db.session.rollback()
error = str(exc)
return render_template("admin/sponsor_edit.html", sponsor=sp,
categories=cats, error=error)
@admin_bp.route("/admin/sponsors/<int:sponsor_id>/delete", methods=["POST"])
@admin_required
def admin_sponsor_delete(sponsor_id):
sp = db.get_or_404(Sponsor, sponsor_id)
audit.log_action(current_user, "sponsor.deleted", "sponsor", sp.id,
meta={"name": sp.name})
db.session.delete(sp)
db.session.commit()
flash(_("Sponsor deleted."), "info")
return redirect(url_for("admin.admin_sponsors"))
# ---------------------------------------------------------------------------
# Promoted keywords
# ---------------------------------------------------------------------------
@admin_bp.route("/admin/promoted-keywords")
@admin_required
def admin_promoted_keywords():
pks = (PromotedKeyword.query
.order_by(PromotedKeyword.expires_at.desc()).all())
return render_template("admin/promoted_keywords.html", pks=pks)
@admin_bp.route("/admin/promoted-keywords/new", methods=["GET", "POST"])
@admin_required
def admin_promoted_keyword_new():
error = None
if request.method == "POST":
listing_id = request.form.get("listing_id", type=int)
keyword = request.form.get("keyword", "").strip()
priority = request.form.get("priority", 0, type=int)
expires_at = _parse_dt(request.form.get("expires_at"),
datetime.utcnow() + timedelta(days=7))
listing = db.session.get(Listing, listing_id) if listing_id else None
if not listing:
error = "Listing not found."
elif not keyword:
error = "Keyword required."
else:
try:
pk = PromotedKeyword(keyword=keyword, listing_id=listing_id,
priority=priority, expires_at=expires_at)
db.session.add(pk)
audit.log_action(current_user, "promoted_keyword.created",
"promoted_keyword", None,
meta={"keyword": keyword, "listing_id": listing_id})
db.session.commit()
flash(_("Promoted keyword added."), "success")
return redirect(url_for("admin.admin_promoted_keywords"))
except Exception as exc:
db.session.rollback()
error = str(exc)
return render_template("admin/promoted_keyword_new.html", error=error)
@admin_bp.route("/admin/promoted-keywords/<int:pk_id>/delete", methods=["POST"])
@admin_required
def admin_promoted_keyword_delete(pk_id):
pk = db.get_or_404(PromotedKeyword, pk_id)
audit.log_action(current_user, "promoted_keyword.deleted",
"promoted_keyword", pk.id,
meta={"keyword": pk.keyword})
db.session.delete(pk)
db.session.commit()
flash(_("Promoted keyword removed."), "info")
return redirect(url_for("admin.admin_promoted_keywords"))
# ---------------------------------------------------------------------------
# Analytics
# ---------------------------------------------------------------------------
@admin_bp.route("/admin/analytics")
@admin_required
def analytics():
since_30 = datetime.utcnow() - timedelta(days=30)
# signups per day (last 30 days)
signups_raw = (db.session.query(
db.func.date(User.created_at).label("day"),
db.func.count().label("n"))
.filter(User.created_at >= since_30)
.group_by(db.func.date(User.created_at))
.order_by(db.func.date(User.created_at))
.all())
# listings posted per day (last 30 days)
listings_raw = (db.session.query(
db.func.date(Listing.created_at).label("day"),
db.func.count().label("n"))
.filter(Listing.created_at >= since_30)
.group_by(db.func.date(Listing.created_at))
.order_by(db.func.date(Listing.created_at))
.all())
# revenue per day (last 30 days)
revenue_raw = (db.session.query(
db.func.date(Transaction.created_at).label("day"),
db.func.sum(Transaction.amount_cents).label("cents"))
.filter(Transaction.created_at >= since_30,
Transaction.status == "succeeded")
.group_by(db.func.date(Transaction.created_at))
.order_by(db.func.date(Transaction.created_at))
.all())
# top categories by listing count (active)
top_cats = (db.session.query(
Category.name,
db.func.count(Listing.id).label("n"))
.join(Listing, Listing.category_id == Category.id)
.filter(Listing.status == ListingStatus.active)
.group_by(Category.id, Category.name)
.order_by(db.func.count(Listing.id).desc())
.limit(10).all())
# ad performance summary
ad_stats = (db.session.query(
db.func.sum(Ad.impressions).label("total_impressions"),
db.func.sum(Ad.clicks).label("total_clicks"))
.filter(Ad.is_active == True)
.first())
return render_template("admin/analytics.html",
signups=signups_raw,
listings_chart=listings_raw,
revenue_chart=revenue_raw,
top_cats=top_cats,
ad_stats=ad_stats)
# ---------------------------------------------------------------------------
# Refund action
# ---------------------------------------------------------------------------
@admin_bp.route("/admin/transactions/<int:txn_id>/refund", methods=["POST"])
@admin_required
def refund_transaction(txn_id):
txn = db.get_or_404(Transaction, txn_id)
if txn.status != "succeeded":
flash(_("Only succeeded transactions can be refunded."), "danger")
return redirect(url_for("admin.transactions"))
from app.services.billing import stripe_enabled
if stripe_enabled():
import stripe as stripe_lib
try:
stripe_lib.Refund.create(payment_intent=txn.stripe_object_id)
except stripe_lib.error.StripeError as exc:
flash(_("Stripe refund failed: %(m)s", m=str(exc)), "danger")
return redirect(url_for("admin.transactions"))
# record local refund transaction
refund_txn = Transaction(
user_id=txn.user_id,
type="refund",
amount_cents=-abs(txn.amount_cents),
currency=txn.currency or "usd",
stripe_object_id=txn.stripe_object_id,
status="succeeded",
meta={"refunded_txn_id": txn.id},
)
db.session.add(refund_txn)
audit.log_action(current_user, "transaction.refunded", "transaction", txn.id,
meta={"amount_cents": txn.amount_cents,
"user_id": txn.user_id})
db.session.commit()
flash(_("Refund recorded."), "success")
return redirect(url_for("admin.transactions"))