This commit implements a way to expose any model publicly on the website. Both manual and existing models can create such pages called 'Model Pages'. A manual model is a model that has been created by the user at runtime, via the technical menu, via studio, or with an xml declaration. The main interface to easily create such page is in Studio, in the tab Website Integration. But a partner or developer could easily import those without studio installed. That's the reason most of the code to handle the display of such pages is present in the website module directly. The listing can handle two display modes (Grid or List). Once the user changes the display mode, the latest value is set in the session to be remembered for the next visit of the page. A default_layout can be set and is customizable in the website editor or from the backend of Website. This value is linked to the page to display, so each listing page can use a different layout by default. The route on which the models are exposed is: "/model/<string:page_name_slugified>", With the derived routes: "/model/<string:page_name_slugified>/page/<int:page_number>", "/model/<string:page_name_slugified>/<string:record_slug>" task-id-3231144 Part-of: odoo/odoo#130544 Co-authored-by: Florent Dardenne (dafl) <dafl@odoo.com> Co-authored-by: Luca Vitali (luvi) <luvi@odoo.com>
135 lines
5.1 KiB
Python
135 lines
5.1 KiB
Python
import ast
|
|
|
|
import werkzeug
|
|
|
|
from odoo.addons.http_routing.models.ir_http import slug, unslug
|
|
from odoo.http import Controller, request, route
|
|
from odoo.osv.expression import AND, OR
|
|
|
|
|
|
class ModelPageController(Controller):
|
|
pager_step = 20
|
|
|
|
@route([
|
|
"/model/<string:page_name_slugified>",
|
|
"/model/<string:page_name_slugified>/page/<int:page_number>",
|
|
"/model/<string:page_name_slugified>/<string:record_slug>",
|
|
], website=True, auth="public")
|
|
def generic_model(self, page_name_slugified=None, page_number=1, record_slug=None, **searches):
|
|
if not page_name_slugified:
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
website = request.website
|
|
|
|
page_type = "listing"
|
|
if record_slug is not None:
|
|
page_type = "single"
|
|
|
|
website_page_domain = AND([
|
|
[("page_type", "=", page_type)],
|
|
[("name_slugified", "=", page_name_slugified)],
|
|
[("website_published", "=", True)],
|
|
website.website_domain(),
|
|
])
|
|
|
|
page = request.env["website.controller.page"].search(website_page_domain, limit=1)
|
|
if not page:
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
view = page.sudo().view_id
|
|
if not view:
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
target_model_name = page.sudo().model_id.model
|
|
Model = request.env[target_model_name]
|
|
if not Model.check_access_rights("read", raise_exception=False):
|
|
raise werkzeug.exceptions.Forbidden()
|
|
|
|
rec_domain = ast.literal_eval(page.record_domain or "[]")
|
|
domains = [rec_domain]
|
|
|
|
if record_slug:
|
|
_, res_id = unslug(record_slug)
|
|
record = Model.browse(res_id).filtered_domain(AND(domains))
|
|
# We check for slug matching because we are not entirely sure
|
|
# that we end up seeing record for the right model
|
|
# i.e. in case of a redirect when a "single" page doesn't match the listing
|
|
if not record.exists() or record_slug != slug(record):
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
listing = request.env["website.controller.page"].search(AND([
|
|
[("name_slugified", "=", page_name_slugified)],
|
|
[("page_type", "=", "listing")],
|
|
[("model", "=", target_model_name)],
|
|
website.website_domain(),
|
|
]))
|
|
|
|
render_context = {
|
|
"main_object": page.sudo(), # The template reads some fields that are actually on view
|
|
"record": record,
|
|
"listing": {
|
|
'href': '.',
|
|
'name': listing.page_name
|
|
} if listing else False
|
|
}
|
|
return request.render(view.key, render_context)
|
|
|
|
layout_mode = request.session.get(f'website_{view.id}_layout_mode')
|
|
if not layout_mode:
|
|
# use the default layout set for this page
|
|
layout_mode = page.default_layout
|
|
|
|
searches.setdefault("search", "")
|
|
searches.setdefault("order", "create_date desc")
|
|
|
|
single_record_pages = request.env["website.controller.page"].search(AND([
|
|
[("page_type", "=", "single")],
|
|
[("model", "=", target_model_name)],
|
|
website.website_domain(),
|
|
]))
|
|
|
|
single_record_page = single_record_pages.filtered(lambda rec: rec.name_slugified == page_name_slugified)
|
|
if single_record_page:
|
|
single_record_page = single_record_page[0]
|
|
else:
|
|
single_record_page = single_record_pages[:1]
|
|
|
|
def record_to_url(record):
|
|
if not single_record_page:
|
|
return None
|
|
return "/model/%s/%s" % (single_record_page.name_slugified, slug(record))
|
|
|
|
if searches["search"]:
|
|
# _name_search doesn't take offset, we reimplement the logic that builds the name domain here
|
|
search_fnames = set(Model._rec_names_search or ([Model._rec_name] if Model._rec_name else []))
|
|
if "seo_name" in Model._fields:
|
|
search_fnames.add("seo_name")
|
|
if search_fnames:
|
|
name_domain = OR([[(name_field, "ilike", searches["search"])] for name_field in search_fnames])
|
|
domains.append(name_domain)
|
|
|
|
search_count = Model.search_count(AND(domains))
|
|
pager = website.pager(
|
|
url=f"/model/{page.name_slugified}",
|
|
url_args=searches,
|
|
total=search_count,
|
|
page=page_number,
|
|
step=self.pager_step,
|
|
scope=5,
|
|
)
|
|
|
|
records = Model.search(AND(domains), limit=self.pager_step, offset=self.pager_step * (page_number - 1), order=searches["order"])
|
|
|
|
render_context = {
|
|
"order_by": searches["order"],
|
|
"search": searches["search"],
|
|
"search_count": search_count,
|
|
"pager": pager,
|
|
"records": records,
|
|
"record_to_url": record_to_url,
|
|
"layout_mode": layout_mode,
|
|
"view_id": view.id,
|
|
"main_object": page.sudo(), # The template reads some fields that are actually on view
|
|
}
|
|
return request.render(view.key, render_context)
|