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