From b0998f81a7e9ffab6da1f92f8d84640a34f63f2f Mon Sep 17 00:00:00 2001 From: metatroncubeswdev Date: Mon, 24 Aug 2026 02:58:54 -0400 Subject: [PATCH] docs: community_classifieds standalone-reuse handover A developer-facing guide for taking community_classifieds out of this monorepo into a different project: exact dependency surface (hard: website/portal; the one soft-check on community_membership and nowhere else, verified by grepping the whole module), how to move/rename it, the recent admin-access fix and how it works, config reference, data model, public routes, cron/email jobs, and known gaps (missing static/description/banner.png, empty demo/, placeholder pricing) to close before shipping it publicly. --- docs/community_classifieds_handover.md | 167 +++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 docs/community_classifieds_handover.md diff --git a/docs/community_classifieds_handover.md b/docs/community_classifieds_handover.md new file mode 100644 index 0000000..a9805c6 --- /dev/null +++ b/docs/community_classifieds_handover.md @@ -0,0 +1,167 @@ +# `community_classifieds` — Module Handover + +A technical guide for taking this module out of the CommunityOS monorepo +and installing it in a different Odoo project. For how the module *works* +day-to-day, see the CommunityOS build plan or `README.rst` inside the +module itself — this doc is specifically about the move. + +## What it is + +A member-gated classifieds board: public listing board, a posting form +for logged-in users, an admin moderation queue (publish/reject), and +automatic expiry with warning emails. Odoo 19 Community Edition, licensed +`LGPL-3`. + +## Dependencies + +**Hard** (declared in `__manifest__.py`, both stock Odoo Community — no +Enterprise, nothing else from this suite): +```python +'depends': ['website', 'portal'], +``` + +**Soft — one runtime check, nowhere else**: `controllers/main.py` calls +`_is_module_installed(env, 'community_membership')` before gating who can +post. If `community_membership` is present *and* installed, posting +requires an active membership; if it's absent, that check is simply +skipped and posting only requires being logged in. This isn't in +`depends`, so Odoo has no idea the two modules are related — verified by +grepping the whole module for any other cross-reference to +`community_membership`, `community_theme_base`, `community_portal`, or +any other `community_*`/`event_qr_ticketing` module: there are none. +Every template it calls into (`website.layout`, `portal.portal_layout`) +comes from its own declared dependencies. + +**Net effect**: copy the folder, install it in a project with just +`website` and `portal`, and it works exactly like it does here, minus +the optional membership-gating. + +## Moving it + +1. Copy `addons/community_classifieds/` into the new project's addons + path. That's the whole module — no other file in this repo is + required by it. +2. **No client data to strip.** This is a product-layer module in the + original repo's own architecture — it's already been through that + repo's brand-leak CI check (`scripts/check_brand_leak.py`), which + greps every `community_*` module for client names/emails/colours and + fails the build if it finds any. There's nothing TNCSC-specific + anywhere in it. +3. **Consider renaming the technical prefix** before distributing it + publicly or installing it alongside another copy of the same module — + `community_classifieds` could collide with another vendor's module of + the same name. If you rename the folder, note that the Python model + name (`community.classified`) is hardcoded in `models/community_classified.py` + and referenced by XML id throughout `views/`, `security/`, and + `data/` — a folder rename alone is enough (Odoo resolves everything + by folder/technical name automatically), but a *model* rename would + need a project-wide find-replace across every file in the module. +4. Install normally: `-i community_classifieds` (or via Apps once the + addons path is registered). + +## Post-install: access & permissions + +As of `19.0.1.0.1`, **every Odoo Administrator automatically gets +moderator access** — nothing to configure. This wasn't always true: a +standalone install used to leave the installing admin unable to see the +Classifieds app or Moderation queue at all, because the +`Classifieds Moderator` group wasn't implied by anything an admin already +had. Fixed in `security/classifieds_security.xml` by making +`base.group_system` (Odoo's built-in Administrator group) imply +`group_classifieds_moderator`: +```xml + + + +``` +Verified on a from-scratch database with *only* `community_classifieds` +installed (`community_membership` and any deployment layer both +`uninstalled`): `admin.has_group('community_classifieds.group_classifieds_moderator')` +returns `True` and "Classifieds" appears in the app switcher immediately. + +**To give a non-admin staff member moderator access**: Settings → Users +& Companies → Users → open their record → check **Classifieds Moderator** +under the Classifieds privilege group. + +## Configuration + +Settings → General Settings → **Classifieds** tab: + +| Setting | Field / param | Default | +|---|---|---| +| Listing Duration (days) | `community_classifieds.expiry_days` | 30 | + +One thing *not* exposed as a setting: the expiry-warning email fires +exactly **7 days** before expiry — `DEFAULT_WARNING_DAYS_BEFORE_EXPIRY` +in `models/community_classified.py`, a hardcoded constant. Change the +constant if a different lead time is needed; it's not wired to a +`res.config.settings` field. + +## Data model + +**`community.classified`** — the listing itself: + +| Field | Type | Notes | +|---|---|---| +| `title` | Char | required | +| `category` | Selection | for_sale / housing / services / jobs / other | +| `description` | Html | | +| `image_ids` | One2many → `community.classified.image` | max 3, enforced by a `@api.constrains` | +| `contact_method` / `contact_email` / `contact_phone` | | | +| `poster_partner_id` | Many2one res.partner | readonly, set by the controller on create | +| `post_date` | Datetime | readonly, defaults to now | +| `expiry_date` | Date | readonly, computed on create/publish/renew | +| `state` | Selection | pending_review / published / expired / rejected | +| `admin_notes` | Text | | +| `view_count` | Integer | incremented via `sudo()` on each public detail-page view | + +**`community.classified.image`** — `classified_id` (required, cascade +delete), `sequence`, `image` (Binary, `attachment=True`). + +## Public routes + +| Route | Auth | Notes | +|---|---|---| +| `GET /classifieds` | public | list, `?category=` filter, only `state=published` | +| `GET /classifieds/` | public | detail page, 404s unless published | +| `GET`/`POST /classifieds/new` | portal login required | membership check if applicable (see Dependencies) | +| `GET /classifieds/my` | portal login required | poster's own listings, any state | +| `GET /classifieds//renew` | portal login required | only works if the requesting partner is the poster | + +Login-required routes use `auth='public'` plus a manual redirect check +(`_redirect_to_login_if_public`), not `auth='user'` — a documented +workaround for an Odoo 19 bug where `auth='user'` + `website=True` throws +a raw 500 instead of redirecting to login in some builds. Worth knowing +before "simplifying" that pattern back to `auth='user'`. + +## Background jobs + +Two daily `ir.cron` entries (`data/ir_cron.xml`): +- **Expire Listings** — flips `published` past `expiry_date` to `expired`. +- **Send Expiry Warnings** — emails the poster when `expiry_date` is + exactly 7 days out. + +## Email templates + +`data/mail_templates.xml`: one to moderators on every new submission +(`mail_template_new_submission`), one to the poster on approaching expiry +(`mail_template_expiry_warning`). Both are looked up via +`env.ref(..., raise_if_not_found=False)` — if you strip the data file, +the module still runs, it just silently skips sending. + +## Known gaps to fix before shipping this elsewhere + +- **`static/description/banner.png` is referenced in the manifest's + `images` key but the file doesn't exist.** The App Store / Apps-list + listing will show a broken image until a real banner asset is added. +- **`demo/` is empty** (just a `.gitkeep`) — no demo data ships with the + module. +- Manifest `price`/`currency` are placeholder zero values (`0.00 USD`) — + set real pricing before listing it for sale. + +## Tests + +`tests/test_classifieds.py` covers: new listing starts pending review, +publish makes it visible, reject, the expiry cron, renew resets expiry, +the 3-image max constraint, and the configurable expiry-days setting. +Run with `--test-enable --test-tags /community_classifieds` on install.