| name | everjust-website-forum |
| description | Operate the Q&A Forum (website_forum) app of a live everjust.app tenant via the Odoo MCP/ORM — the full ask→answer→accept→close/validate lifecycle plus votes, tags, the karma economy, and moderation. Use when the task is to create/configure a forum.forum, post a question or an answer (answers are forum.post rows with a parent_id, NOT comments), accept the correct answer (is_correct), up/downvote a post, add/retag tags, moderate (validate a pending post, close/reopen, flag, mark offensive), grant a user forum karma, or read forum state. This is STOCK Odoo website_forum + gamification (karma is res.users.karma, a STORED field driven by gamification.karma.tracking) — NO custom everjust forum addon; every write is karma-gated by the forum's karma_* fields and you act as one Odoo user. The public /forum pages are QWeb edited via the website_* tools, NOT this ORM. Cross-references [[everjust-platform]], [[everjust-agent-mcp]], and siblings [[everjust-events]], [[everjust-mail-ops]]. |
EVERJUST Website Forum — Agent Skill
Operate the Q&A Forum of a live everjust.app tenant as an agent: create and configure a
forum, ask questions and post answers, accept the correct answer, vote, tag, and moderate — all
through the Odoo MCP / ORM (search, get, create, update, call, describe_model; see
[[everjust-agent-mcp]] for opening the session against the right tenant DB). The public
/forum/... pages are stock QWeb; edit those with the website_* tools (see the last
recipe), not this model layer.
The crucial everjust fact: there is NO custom everjust forum addon. This is stock Odoo
website_forum riding on gamification. Read the field set from the live model
(describe_model / fields_get), not from an everjust addon. What you must internalize is
everything else on this page: the lifecycle is expressed as forum.post rows (a question is
a root post; an answer is a forum.post with parent_id set — an answer is NOT a
mail.message comment), the "accept" is a boolean is_correct, and every mutation is
karma-gated by the owning forum.forum's karma_* fields against the acting user's
res.users.karma. website_forum is optional per tenant — it is installed on connectdomain
(one live forum, Connect Domain Community), but on a tenant without it forum.forum is a
KeyError; check first (see Pitfalls).
When to use this skill
- Configure a forum — create a
forum.forum, set mode (questions vs discussions),
privacy, default sort, welcome/FAQ, and (importantly) tune the karma_* economy.
- Ask a question — create a root
forum.post (parent_id unset).
- Answer a question — create a child
forum.post with parent_id = <question id>.
- Accept the correct answer — set the answer's
is_correct = True (awards karma).
- Vote — up/downvote a post via its
vote() method (never write forum.post.vote raw).
- Tag / retag — create
forum.tag rows and attach them to a question's tag_ids.
- Moderate — validate a
pending question, close/reopen with a reason, flag, or
mark offensive; read the moderation queues (count_posts_waiting_validation,
count_flagged_posts).
- Grant karma — bootstrap a user so they clear the
karma_* gates (_add_karma /
writing res.users.karma).
- Edit the public forum pages / layout — QWeb, via the website_* MCP tools.
Do NOT use this skill for, and stop if the task is really:
- Odoo Discuss / chatter / general
message_post — forum comments are mail.message
on a post, but the Q&A objects (question/answer) are forum.post. Don't model an answer as a
chatter comment (see Pitfalls) and don't reach for [[everjust-mail-ops]] (that's the separate
webmail stack).
- The
/forum page HTML/design — that's stock QWeb served by the website_forum
controllers; rewrite it with website_edit_page (COW-safe), not by writing ir.ui.view.
- A tenant without
website_forum installed — installing a module is platform-ops; see
[[everjust-platform]].
Architecture (the model map)
website_forum layers on mail.thread + website.* mixins + gamification. Everything below
is stock Odoo 19 (verified live on connectdomain).
| Model | Role | Key fields |
|---|
forum.forum | A forum container. Holds the entire karma economy — the karma_gen_* (earnings) and karma_* (action thresholds) fields the whole app reads. | name, mode (questions=1 answer|discussions=multiple), privacy (public|connected|private + authorized_group_id), default_order, faq, welcome_message, tag_ids, post_ids, can_moderate (computed: user.karma >= karma_moderate), count_posts_waiting_validation, count_flagged_posts, plus the karma fields below. |
forum.post | The Q&A object — both questions AND answers. A question is a root post (parent_id unset); an answer is a post with parent_id = the question. | name (title; answers usually blank/Re:), content (Html), forum_id (required), parent_id (→ question; set = this is an answer), state (active|pending|close|offensive|flagged), is_correct (the "accepted answer" flag), active, tag_ids, vote_count (store), child_count, has_validated_answer, closed_reason_id/uid/date, moderator_id, flag_user_id, and a large family of can_*/karma_* computed rights (can_answer, can_accept, can_close, can_moderate, karma_accept, …). |
Forum URL shape (all computed): forum /forum/<slug>-<id>, question
/forum/<forum-slug>/<post-slug>, answer adds #answer_<id>, tag
/forum/<forum-slug>/tag/<tag-slug>/questions.
The karma economy (this is the whole app)
Two families of Integer fields on forum.forum (defaults shown; live connectdomain
values differ — e.g. that forum runs karma_ask=3, karma_answer=3, karma_upvote=5, karma_downvote=50, karma_tag_create=30, karma_moderate=1000, karma_answer_accept_own=20, karma_answer_accept_all=500 — always read the real forum, never assume defaults):
- Earnings —
karma_gen_* (added to a user's karma when an event fires):
karma_gen_question_new (+2 ask), karma_gen_question_upvote (+5), karma_gen_question_downvote
(−2), karma_gen_answer_upvote (+10), karma_gen_answer_downvote (−2),
karma_gen_answer_accept (+2 to the accepter), karma_gen_answer_accepted (+15 to the answer
author when their answer is accepted), karma_gen_answer_flagged (−100, spam/offensive
penalty).
- Action thresholds —
karma_* (min karma the acting user needs, else AccessError):
karma_ask (3), karma_answer (3), karma_upvote (5), karma_downvote (50),
karma_answer_accept_own (20) / karma_answer_accept_all (500), karma_edit_own (1) /
karma_edit_all (300), karma_edit_retag (75), karma_close_own (100) / karma_close_all
(500), karma_unlink_own (500) / karma_unlink_all (1000), karma_tag_create (30),
karma_flag (500), karma_moderate (1000), karma_post (100 — below it, a new question lands
pending and needs moderation), karma_editor (30 — below it you can't post images/links),
karma_dofollow (500 — below it your links get rel=nofollow), karma_comment_*,
karma_comment_convert_*, karma_comment_unlink_*.
The own-vs-all pattern is everywhere: acting on your own post needs the _own threshold;
acting on anyone's needs the (higher) _all threshold. The post exposes the resolved value as
computed fields — post.karma_accept, post.karma_close, post.karma_edit, post.karma_unlink
— and the boolean gate as post.can_accept, post.can_close, post.can_edit, etc. Read those
can_* booleans before you attempt the write — they already fold in own/all AND admin bypass
(env.is_admin() skips all karma gates).
Recipes
Route each through the Odoo MCP (open an env on the tenant DB — see [[everjust-agent-mcp]]).
Prefer the generic search/get/create/update tools; use call for the model methods
(vote, close, validate, reopen). Non-read call and update on structural things need
confirm:true. Because writes are karma-gated, the agent's own res.users.karma matters —
if you're acting as a low-karma service user, either bootstrap its karma (last recipe) or act as
an admin user (admin bypasses every gate).
0. Confirm the app + read the forum's live karma settings (do this first)
"forum.forum" in env
forum = env["forum.forum"].search([], limit=1)
forum.read(["name", "mode", "privacy", "default_order",
"karma_ask", "karma_answer", "karma_upvote", "karma_downvote",
"karma_tag_create", "karma_post", "karma_moderate",
"karma_answer_accept_own", "karma_answer_accept_all",
"karma_gen_question_new", "karma_gen_answer_accepted", "karma_gen_answer_flagged"])
env.user.karma
forum.can_moderate
Never assume the module defaults — this forum may have been re-tuned. mode='questions'
enforces one answer per question in the UI flow; discussions allows many.
1. Ask a question (root post) and answer it (child post)
qid = env["forum.post"].create({
"forum_id": forum.id,
"name": "How does Connect Domain auto-provision SSL?",
"content": "<p>What issues the cert, and when does it renew?</p>",
"tag_ids": [(6, 0, [])],
}).id
aid = env["forum.post"].create({
"forum_id": forum.id,
"parent_id": qid,
"content": "<p>ACM issues it via DNS-01; it renews ~30 days before expiry.</p>",
}).id
An answer's name is typically empty or Re: <question>; the body is content. Do not
create an answer as a chatter comment — an answer is a first-class forum.post and only rows
with a parent_id count as answers (child_count, has_validated_answer, the QAPage schema).
2. Accept the correct answer (is_correct) — the karma-bearing "solve"
answer = env["forum.post"].browse(aid)
answer.can_accept
env["forum.post"].write([aid], {"is_correct": True})
env["forum.post"].browse(qid).read(["has_validated_answer", "child_count"])
is_correct is the accepted-answer flag; the question's _order floats correct answers to the
top (is_correct DESC, vote_count DESC, ...). In questions mode there is conceptually one
accepted answer.
3. Vote — always via the vote() method (never write forum.post.vote directly)
env["forum.post"].call(qid, "vote", kwargs={"upvote": True})
env["forum.post"].call(aid, "vote", kwargs={"upvote": False})
Gates the method enforces for you: you cannot vote on your own post (UserError);
upvoting needs karma_upvote, downvoting needs the (much higher) karma_downvote — unless you're
toggling off an existing opposite vote. Voting moves karma to the post's author
(recipient_id) per the forum's karma_gen_*_upvote/downvote. Writing a forum.post.vote row by
hand bypasses the own-post check, the karma gate, AND the karma award — don't.
4. Tags: create + attach + retag
tag = env["forum.tag"].create({"name": "ssl", "forum_id": forum.id})
env["forum.post"].write([qid], {"tag_ids": [(6, 0, [tag.id])]})
env["forum.post"].write([qid], {"tag_ids": [(4, tag.id)]})
env["forum.tag"].search_read([("forum_id", "=", forum.id)], ["name", "posts_count"])
posts_count only counts state='active' posts (it's in the m2m domain). The forum also
exposes tag_most_used_ids / tag_unused_ids.
5. Moderate: validate a pending question, close/reopen, flag, mark offensive
forum.read(["count_posts_waiting_validation", "count_flagged_posts"])
pending = env["forum.post"].search([("forum_id","=",forum.id), ("state","=","pending")])
flagged = env["forum.post"].search([("forum_id","=",forum.id), ("state","=","flagged")])
env["forum.post"].call(pending.ids, "validate")
reason_off = env.ref("website_forum.reason_7").id
reason_spam = env.ref("website_forum.reason_8").id
reason_dupe = env.ref("website_forum.reason_1").id
env["forum.post"].call(qid, "close", args=[reason_dupe])
env["forum.post"].call(qid, "reopen")
env[].call(aid, )
env[].call(aid, , args=[reason_off])
close/reopen/validate/_flag/_mark_as_offensive are model methods (use the call tool
with confirm:true). They each re-check the karma gate server-side and raise AccessError if the
acting user is short — so if you're a low-karma service user, moderate as an admin or grant
karma_moderate first.
6. Grant a user forum karma (so they clear the gates)
res.users.karma is a stored computed field backed by the karma ledger — writing it directly
works (it logs a tracking row via res.users.write), but the idiomatic path is _add_karma:
u = env["res.users"].browse(uid)
u.read(["login", "karma"])
env["res.users"].call([uid], "_add_karma", args=[1000], kwargs={"reason": "Forum bootstrap"})
env["res.users"].write([uid], {"karma": 1000})
Karma is per-user, tenant-wide (not per-forum) — it's a res.users field, so a user's karma
applies across every forum on that tenant. Grant enough to clear the specific karma_* gate the
task needs (e.g. karma_moderate to run recipe 5). Do not hand-edit
gamification.karma.tracking rows.
7. Edit the PUBLIC forum pages (QWeb) — website_* tools, not this ORM
The /forum list, question, and tag pages are stock website_forum QWeb served by its
controllers. To restyle/copy-tweak them, go through the website tools (COW-safe — Odoo forks
the module view into a site-specific ir.ui.view so upgrades never clobber your edit):
website_pages # find the /forum-related pages + their view ids
website_edit_page(url="/forum", arch=<full QWeb XML>) # read current arch FIRST; keep the idiom
website_publish(url="/forum", published=True, indexed=True)
website_menu(name="Community", url="/forum") # add a nav entry (there's already one)
On everjust the marketing pages are Tailwind-utility QWeb wrapped in
<t t-call="website.layout">; the forum templates are stock Bootstrap QWeb — either way, read
the current arch before rewriting and reuse the existing classes. Editing forum pages needs the
website.group_website_designer role (the everjust Administrator role includes it —
see [[everjust-platform]]). Never hand-write the ir.ui.view directly; use website_edit_page.
Pitfalls
-
An answer is a forum.post with parent_id, NOT a chatter comment. The Q&A hierarchy is
parent (question) → children (answers), all forum.post. mail.message comments exist on a
post too, but they are comments, and only real answers count toward child_count,
has_validated_answer, acceptance, and the QAPage schema. Don't model "answer" as
message_post — and don't confuse this with the [[everjust-mail-ops]] webmail stack.
-
is_correct is the "accept," and self-acceptance grants no karma. Setting
is_correct=True on an answer awards karma_gen_answer_accepted to the author and
karma_gen_answer_accept to the accepter — unless the accepter is the author, in which case
no karma moves. Flipping it back reverses the award. Deleting an already-accepted answer also
claws back the karma.
-
Every write is karma-gated against env.user.karma — read can_* first. create on a
post checks can_ask/can_answer; write checks can_edit/can_close/can_accept/
can_unlink/retag; vote/close/validate/_flag/_mark_as_offensive each re-check. A
low-karma service user gets AccessError. Either act as an admin (bypasses all gates via
env.is_admin()) or grant the user the needed karma (recipe 6) — don't try to force the write.
-
Never write forum.post.vote directly — call vote(). The own-post ban, the
up/downvote karma gate, and the karma award to the author all live in the vote model's
create/write and in the toggle logic. A raw row skips all three and corrupts the karma ledger.
-
karma_post decides pending vs active on a NEW question. Below karma_post (default 100),
a newly asked question is saved state='pending' and is invisible publicly until a moderator
validate()s it (which then awards the withheld ask-karma). So "I created it but it's not on
the site" usually means it's pending, not failed.