Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions website_event_hidden_by_password/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

================================
Website Event Hidden by Password
================================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:de39157b2d18e9098cbca7d99c039325ba1a17de4d0355a1c9fb9aeeed9b570f
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fevent-lightgray.png?logo=github
:target: https://github.com/OCA/event/tree/19.0/website_event_hidden_by_password
:alt: OCA/event
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/event-19-0/event-19-0-website_event_hidden_by_password
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/event&target_branch=19.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module lets each website event be made **private**, hidden behind a
shared access password.

A private event:

- is **redacted** on the website events listing: its card shows no name,
date, location or image, only a lock, "Private" and an optional public
**codename** (the slug-free gate link keeps the name out of the URL
too);
- is **not searchable by its real name, subtitle or venue**, and never
leaks through the search facet counts. A visitor can only find it by
typing its **codename**, and then it surfaces as the same redacted
card, shown as ``{codename}`` (both on the events page and in the
search autocomplete);
- shows an **info-free password wall** when opened: the event page (and
its sub-pages) only render once the visitor enters the correct
password;
- keeps its name out of every direct entry point. Guessing the integer
id (``/event/42``), the registration page or the iCal download
(``/ics``) sends the visitor to the gate instead of leaking the name,
and the event is left out of ``sitemap.xml``;
- keeps its password on the event record itself, next to the *Private*
toggle, with a **Generate** button for a random one.

The password is a *shared secret* (like a webinar password), not a user
account credential: it is stored and shown in clear text and is only
readable by event managers. Managers always bypass the wall so they can
preview and edit the page.

Scope and limitations
---------------------

This module is designed to close **every entry point in Odoo core**
through which a private event could otherwise be discovered or have its
details exposed: the events listing, the search, the search autocomplete
and the filter counts, the canonical-URL redirect, direct page access,
the iCal exports and ``sitemap.xml``. The access guard is applied at the
routing layer, so it also covers the standard event add-ons (tracks,
agenda, booth) without this module depending on them.

It cannot, however, protect entry points it has no knowledge of. If you
install additional modules, or otherwise make events reachable through a
custom route, report, API, feed, export or third-party integration,
securing those new entry points is **your responsibility**, typically in
a small custom bridge module (this module deliberately avoids hard
dependencies on optional add-ons).

If you discover an entry point in **Odoo core** that still exposes a
private event's name or details, please open an issue on the project's
GitHub tracker and tag the maintainer (``@odrakirmusic``) so it can be
closed in this module.

**Table of contents**

.. contents::
:local:

Configuration
=============

To make an event private:

1. Go to *Events* and open (or create) an event.
2. In the **Private Access** group, switch on **Private**. A random
password is proposed automatically.
3. Keep the proposed **Access Password**, type your own, or click
**Generate** for a new random one.
4. Optionally set a **Codename**, a public, non-secret hint (e.g.
*Aurora*) shown under "Private" on the redacted card, so the people
you invite can tell which event to open.
5. Save and publish the event.

Share the password (and codename, if any) with the people who should be
able to open the event.

Usage
=====

On the website:

- The events listing shows private events redacted: no name, date,
location or image, only a lock and "Private".
- Opening a private event displays an info-free password wall instead of
the event page.
- Once a visitor enters the correct password, the event page and its
sub-pages are shown for the rest of the browsing session.
- Event managers (group *Event / User*) skip the wall, so they can
preview and edit private events as usual.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/event/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/event/issues/new?body=module:%20website_event_hidden_by_password%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Riccardo Fiore

Contributors
------------

- Riccardo Fiore (Odrakir) <odrakirmusic@gmail.com>

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

.. |maintainer-odrakirmusic| image:: https://github.com/odrakirmusic.png?size=40px
:target: https://github.com/odrakirmusic
:alt: odrakirmusic

Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-odrakirmusic|

This module is part of the `OCA/event <https://github.com/OCA/event/tree/19.0/website_event_hidden_by_password>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
2 changes: 2 additions & 0 deletions website_event_hidden_by_password/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import controllers
from . import models
25 changes: 25 additions & 0 deletions website_event_hidden_by_password/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2026 Riccardo Fiore
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Website Event Hidden by Password",
"summary": "Hide website events behind a per-event access password",
"version": "19.0.1.0.0",
"category": "Marketing/Events",
"author": "Riccardo Fiore, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/event",
"license": "AGPL-3",
"development_status": "Beta",
"maintainers": ["odrakirmusic"],
"depends": ["website_event"],
"data": [
"views/event_event_views.xml",
"views/website_event_templates.xml",
],
"assets": {
"web.assets_frontend": [
"website_event_hidden_by_password/static/src/scss/website_event_hidden_by_password.scss",
],
},
"installable": True,
"application": False,
}
1 change: 1 addition & 0 deletions website_event_hidden_by_password/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import main
142 changes: 142 additions & 0 deletions website_event_hidden_by_password/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Copyright 2026 Riccardo Fiore
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import http
from odoo.fields import Domain
from odoo.http import request

from odoo.addons.website.models.ir_http import sitemap_qs2dom
from odoo.addons.website_event.controllers.main import WebsiteEventController

from ..utils import SESSION_KEY, event_visitor_unlocked


def sitemap_event_private(env, rule, qs):
"""Sitemap for the ``/event/<event>`` detail route.

Yields the published events' canonical URLs like the core default would,
but skips private events so their real name (the slug) never appears in
``sitemap.xml``.
"""
Event = env["event.event"]
# The public sitemap env already restricts to published events via the
# record rule; we only add the privacy exclusion.
dom = sitemap_qs2dom(qs, "/event", Event._rec_name) & Domain(
"is_private", "=", False
)
for event in Event.search(dom):
loc = "/event/{}".format(env["ir.http"]._slug(event))
if not qs or qs.lower() in loc.lower():
yield {"loc": loc}


class WebsiteEventPrivate(WebsiteEventController):
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def _event_private_is_unlocked(self, event):
"""Whether the current visitor may see this (possibly private) event."""
return event_visitor_unlocked(request.env, request.session, event)

def _event_private_mark_unlocked(self, event):
unlocked = request.session.get(SESSION_KEY) or []
if event.id not in unlocked:
# Reassign (not append in place) so the session is marked dirty.
request.session[SESSION_KEY] = unlocked + [event.id]

def _event_private_safe_redirect(self, event, redirect):
"""Only allow redirects back to this event's own pages."""
prefix = f"/event/{event.id}"
if redirect and redirect.startswith(prefix):
return redirect
return prefix

def _event_private_render_gate(self, event, redirect=None, password_error=False):
values = {
"event": event,
"redirect": self._event_private_safe_redirect(event, redirect),
"password_error": password_error,
}
return request.render(
"website_event_hidden_by_password.event_private_gate", values
)

# ------------------------------------------------------------------
# Gated pages
#
# These override routed endpoints of the parent controller. The empty
# ``@http.route()`` re-declares them as routes that inherit the parent's
# routing config, which is the documented way to extend a routed method
# (without it Odoo warns that the override is "not decorated by @route").
# ------------------------------------------------------------------
@http.route()
def event_register(self, event, **post):
if not self._event_private_is_unlocked(event):
return self._event_private_render_gate(
event, redirect=request.httprequest.path
)
return super().event_register(event, **post)

@http.route()
def event_page(self, event, page, **post):
if not self._event_private_is_unlocked(event):
return self._event_private_render_gate(
event, redirect=request.httprequest.path
)
return super().event_page(event, page, **post)

@http.route(
["""/event/<model("event.event"):event>"""],
type="http",
auth="public",
website=True,
sitemap=sitemap_event_private,
readonly=True,
)
def event(self, event, **post):
# Re-declared only to swap the core ``sitemap=True`` for one that
# omits private events (so their name never lands in sitemap.xml);
# the behaviour is otherwise the core redirect. Locked visitors are
# already sent to the gate by the ir.http pre-dispatch guard.
return super().event(event, **post)

# ------------------------------------------------------------------
# Unlock endpoint
# ------------------------------------------------------------------
@http.route(
["/event/<int:event_id>/private"],
type="http",
auth="public",
website=True,
sitemap=False,
)
def event_private_locked(self, event_id, **post):
"""Slug-free gate page for a private event.

Reached from the redacted listing card. Using a plain ``<int>`` (not
the ``<model>`` slug converter) keeps the event name out of the URL —
otherwise Odoo would 301 to the canonical ``/event/<name>-<id>`` URL
and leak it. Managers / already-unlocked visitors are sent straight to
the real page.
"""
event = request.env["event.event"].browse(event_id).sudo().exists()
if not event or not event.is_private:
return request.redirect("/event")
if self._event_private_is_unlocked(event):
return request.redirect(f"/event/{event.id}")
return self._event_private_render_gate(event, redirect=f"/event/{event.id}")

@http.route(
['/event/<model("event.event"):event>/unlock'],
type="http",
auth="public",
website=True,
methods=["POST"],
sitemap=False,
)
def event_private_unlock(self, event, password=None, redirect=None, **post):
if event.is_private and event.sudo()._is_access_password_valid(password):
self._event_private_mark_unlocked(event)
return request.redirect(self._event_private_safe_redirect(event, redirect))
return self._event_private_render_gate(
event, redirect=redirect, password_error=True
)
Loading