community_classifieds: a member-gated classifieds board. community.classified (title/category/description/up-to-3-images via a child .image model/contact info/state) with a public board at /classifieds, detail pages that track view_count, portal posting at /classifieds/new (soft-detects community_membership - blocks non-active-members only if that module is installed, otherwise anyone logged in can post), a "my listings" portal page with self-service renew, and an admin moderation queue (publish/reject). Daily crons expire past-due listings and send a 7-day expiry warning; a new-submission notice goes out to the Classifieds Moderator group. Listing duration is configurable (Settings), never hardcoded. community_benefits: community.benefit.partner (benefit centres) offer community.benefit entitlements scoped to specific community_membership tiers via tier_ids. community.benefit.redemption logs a redemption but its create() is guarded by a constraint that re-checks the same active-membership condition the membership QR verification page uses (membership_state in active/renewal_due) plus tier entitlement, refusing the redemption otherwise. Public directory at /benefits, portal page at /my/benefits listing only benefits the member's tier actually entitles them to. Two more real Odoo 19 API changes hit here: search-view <group> elements for "Group By" sections no longer accept a `string` attribute (must use `name` only - same fix as community_membership's partner search view, applied here to a fresh module), and res.groups.users was renamed to user_ids. Verified against a live Odoo 19 + Postgres 16 container: 7+4 automated tests pass, plus a full manual live run covering the Phase 4 gate exactly - confirmed a non-member is blocked from /classifieds/new, activated a real membership, posted a classified (pending_review), published it via the moderator action, confirmed it appears on the public board and detail page with view_count incrementing, entitled a tier to a benefit, logged a redemption for the active member, confirmed it shows on /my/benefits, and confirmed redemption creation is refused for a non-member. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
85 lines
3.8 KiB
Python
85 lines
3.8 KiB
Python
import base64
|
|
|
|
from odoo import http
|
|
from odoo.http import request
|
|
|
|
MAX_IMAGES = 3
|
|
|
|
|
|
def _is_module_installed(env, module_name):
|
|
return bool(env['ir.module.module'].sudo().search_count(
|
|
[('name', '=', module_name), ('state', '=', 'installed')]
|
|
))
|
|
|
|
|
|
def _has_active_membership(partner):
|
|
"""Soft-check: only meaningful if community_membership is installed."""
|
|
if 'membership_state' not in partner._fields:
|
|
return True
|
|
return partner.membership_state in ('active', 'renewal_due')
|
|
|
|
|
|
class ClassifiedsController(http.Controller):
|
|
|
|
@http.route(['/classifieds'], type='http', auth='public', website=True, sitemap=True)
|
|
def classifieds_list(self, category=None, **kwargs):
|
|
domain = [('state', '=', 'published')]
|
|
if category:
|
|
domain.append(('category', '=', category))
|
|
listings = request.env['community.classified'].sudo().search(domain)
|
|
return request.render('community_classifieds.classifieds_list_page', {
|
|
'listings': listings,
|
|
'category': category,
|
|
})
|
|
|
|
@http.route(['/classifieds/<int:classified_id>'], type='http', auth='public', website=True, sitemap=False)
|
|
def classifieds_detail(self, classified_id, **kwargs):
|
|
listing = request.env['community.classified'].sudo().browse(classified_id)
|
|
if not listing.exists() or listing.state != 'published':
|
|
return request.not_found()
|
|
listing._increment_view_count()
|
|
return request.render('community_classifieds.classifieds_detail_page', {'listing': listing})
|
|
|
|
@http.route(['/classifieds/new'], type='http', auth='user', website=True)
|
|
def classifieds_new(self, **kwargs):
|
|
partner = request.env.user.partner_id
|
|
if _is_module_installed(request.env, 'community_membership') and not _has_active_membership(partner):
|
|
return request.render('community_classifieds.classifieds_membership_required', {})
|
|
|
|
if request.httprequest.method == 'POST':
|
|
image_ids = []
|
|
for field_name in ('image1', 'image2', 'image3'):
|
|
upload = kwargs.get(field_name)
|
|
if upload and getattr(upload, 'filename', None):
|
|
image_ids.append((0, 0, {'image': base64.b64encode(upload.read())}))
|
|
|
|
listing = request.env['community.classified'].sudo().create({
|
|
'title': kwargs.get('title', '').strip(),
|
|
'category': kwargs.get('category', 'other'),
|
|
'description': kwargs.get('description', ''),
|
|
'contact_method': kwargs.get('contact_method', 'email'),
|
|
'contact_email': kwargs.get('contact_email', '').strip(),
|
|
'contact_phone': kwargs.get('contact_phone', '').strip(),
|
|
'poster_partner_id': partner.id,
|
|
'image_ids': image_ids[:MAX_IMAGES],
|
|
})
|
|
return request.redirect(f'/classifieds/my?posted={listing.id}')
|
|
|
|
return request.render('community_classifieds.classifieds_new_page', {})
|
|
|
|
@http.route(['/classifieds/my'], type='http', auth='user', website=True)
|
|
def classifieds_my(self, **kwargs):
|
|
partner = request.env.user.partner_id
|
|
listings = request.env['community.classified'].sudo().search([('poster_partner_id', '=', partner.id)])
|
|
return request.render('community_classifieds.classifieds_my_page', {'listings': listings})
|
|
|
|
@http.route(['/classifieds/<int:classified_id>/renew'], type='http', auth='user', website=True)
|
|
def classifieds_renew(self, classified_id, **kwargs):
|
|
partner = request.env.user.partner_id
|
|
listing = request.env['community.classified'].sudo().search([
|
|
('id', '=', classified_id), ('poster_partner_id', '=', partner.id),
|
|
], limit=1)
|
|
if listing:
|
|
listing.action_renew()
|
|
return request.redirect('/classifieds/my')
|