06/15 Phase 3 codes

This commit is contained in:
2026-06-15 14:19:14 -04:00
parent c2064b84b4
commit 60526262f0
20 changed files with 802 additions and 7 deletions
+2 -2
View File
@@ -101,7 +101,7 @@ def verify_email(token):
if user_id is None:
flash(_("Verification link is invalid or expired."), "danger")
return redirect(url_for("auth.login"))
user = User.query.get(user_id)
user = db.session.get(User, user_id)
if user is None:
abort(404)
if not user.email_verified:
@@ -136,7 +136,7 @@ def reset_password(token):
if user_id is None:
flash(_("Reset link is invalid or expired."), "danger")
return redirect(url_for("auth.reset_request"))
user = User.query.get(user_id)
user = db.session.get(User, user_id)
if user is None:
abort(404)
form = ResetForm()
+2 -2
View File
@@ -109,7 +109,7 @@ def create():
category = None
if request.method == "POST" and form.category_id.data:
category = Category.query.get(form.category_id.data)
category = db.session.get(Category, form.category_id.data)
if form.validate_on_submit() and category:
raw_attrs = _parse_attributes(category)
@@ -156,7 +156,7 @@ def edit(listing_id):
form.price.data = (listing.price_cents / 100) if listing.price_cents else None
form.zip.data = listing.zip
category = Category.query.get(form.category_id.data or listing.category_id)
category = db.session.get(Category, form.category_id.data or listing.category_id)
if form.validate_on_submit() and category:
raw_attrs = _parse_attributes(category)
+11
View File
@@ -0,0 +1,11 @@
"""Messaging form."""
from flask_wtf import FlaskForm
from wtforms import TextAreaField, SubmitField
from wtforms.validators import DataRequired, Length
from flask_babel import lazy_gettext as _l
class MessageForm(FlaskForm):
body = TextAreaField(_l("Message"),
validators=[DataRequired(), Length(1, 4000)])
submit = SubmitField(_l("Send"))
+122
View File
@@ -0,0 +1,122 @@
"""Messaging + Favorites routes."""
from flask import (Blueprint, render_template, redirect, url_for,
flash, abort, request, jsonify)
from flask_login import login_required, current_user
from flask_babel import gettext as _
from app.extensions import db, limiter
from app.models.listing import Listing
from app.models.messaging import Conversation
from app.services import messaging as msvc
from app.services import favorites as fsvc
from app.services.contact import contact_revealed, mask_body
from app.blueprints.messaging.forms import MessageForm
messaging_bp = Blueprint("messaging", __name__)
# ── Inbox ──────────────────────────────────────────────────────────────────
@messaging_bp.route("/messages")
@login_required
def inbox():
page = request.args.get("page", 1, type=int)
pagination = msvc.inbox(current_user, page=page)
return render_template("messaging/inbox.html",
pagination=pagination,
unread_total=msvc.total_unread(current_user))
# ── Conversation thread ────────────────────────────────────────────────────
@messaging_bp.route("/messages/<int:conv_id>", methods=["GET", "POST"])
@login_required
def conversation(conv_id):
conv = db.session.get(Conversation, conv_id)
if conv is None:
abort(404)
if current_user.id not in (conv.buyer_id, conv.seller_id):
abort(403)
msvc.mark_conversation_read(conv, current_user)
reveal = contact_revealed(current_user)
form = MessageForm()
if form.validate_on_submit():
try:
msvc.send_message(conv, current_user, form.body.data)
return redirect(url_for("messaging.conversation", conv_id=conv.id))
except msvc.MessagingError as e:
flash(str(e), "danger")
# mask contact info in messages for low-trust users
masked_messages = [
(msg, mask_body(msg.body, reveal=reveal))
for msg in conv.messages
]
return render_template("messaging/conversation.html",
conv=conv, form=form,
masked_messages=masked_messages,
reveal=reveal,
other=conv.other_party(current_user))
# ── Start conversation from listing detail ─────────────────────────────────
@messaging_bp.route("/listings/<int:listing_id>/contact", methods=["GET", "POST"])
@login_required
@limiter.limit("20 per hour", methods=["POST"])
def start_conversation(listing_id):
listing = db.session.get(Listing, listing_id)
if listing is None:
abort(404)
if listing.user_id == current_user.id:
flash(_("You cannot message your own listing."), "warning")
return redirect(url_for("listings.detail", listing_id=listing_id))
try:
conv, created = msvc.get_or_create_conversation(listing, current_user)
except msvc.MessagingError as e:
flash(str(e), "danger")
return redirect(url_for("listings.detail", listing_id=listing_id))
form = MessageForm()
if form.validate_on_submit():
try:
msvc.send_message(conv, current_user, form.body.data)
flash(_("Message sent."), "success")
return redirect(url_for("messaging.conversation", conv_id=conv.id))
except msvc.MessagingError as e:
flash(str(e), "danger")
return render_template("messaging/start.html",
listing=listing, conv=conv, form=form,
created=created)
# ── Favorites ──────────────────────────────────────────────────────────────
@messaging_bp.route("/listings/<int:listing_id>/favorite", methods=["POST"])
@login_required
def toggle_favorite(listing_id):
listing = db.session.get(Listing, listing_id)
if listing is None:
abort(404)
now_fav = fsvc.toggle_favorite(current_user.id, listing_id)
# AJAX or form fallback
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
return jsonify(favorited=now_fav)
flash(_("Saved.") if now_fav else _("Removed from saved."), "info")
return redirect(request.referrer or url_for("listings.detail",
listing_id=listing_id))
@messaging_bp.route("/my/favorites")
@login_required
def my_favorites():
page = request.args.get("page", 1, type=int)
pagination = fsvc.user_favorites(current_user.id, page=page)
return render_template("messaging/favorites.html", pagination=pagination)
# ── Unread count API (for nav badge polling) ───────────────────────────────
@messaging_bp.route("/api/unread")
@login_required
def unread_count():
return jsonify(unread=msvc.total_unread(current_user))