Skip to content

Commit f4b8258

Browse files
committed
[IMP] website_event, website_event_sale: adapt JsonLd builder
1 parent b5b3f8b commit f4b8258

4 files changed

Lines changed: 162 additions & 1 deletion

File tree

addons/website_event/controllers/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,8 @@ def events(self, page=1, slug_tags=None, **searches):
187187
'slugify_tags': self._slugify_tags,
188188
'search_count': event_count,
189189
'original_search': fuzzy_search_term and search,
190-
'website': website
190+
'website': website,
191+
'structured_data': events.get_jsonLD(),
191192
}
192193

193194
return request.render("website_event.index", values)
@@ -219,6 +220,7 @@ def event_page(self, event, page, **post):
219220
page = view.key if view else page
220221
values['seo_object'] = request.website.get_template(page)
221222
values['main_object'] = event
223+
values['structured_data'] = event.get_jsonLD(is_detail_page=True)
222224
except ValueError:
223225
# page not found
224226
page = 'website.page_404'
@@ -264,6 +266,7 @@ def _prepare_event_register_values(self, event, **post):
264266
return {
265267
'event': event,
266268
'main_object': event,
269+
'structured_data': event.get_jsonLD(is_detail_page=True),
267270
'range': range,
268271
'google_url': lazy(lambda: urls.get('google_url')),
269272
'iCal_url': lazy(lambda: urls.get('iCal_url')),

addons/website_event/models/event_event.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from markupsafe import Markup
1111

1212
from odoo import api, fields, models, tools, _
13+
from odoo.addons.website.helpers.jsonld_builder import JsonLd
14+
from odoo.addons.website.tools import text_from_html
1315
from odoo.exceptions import UserError, ValidationError
1416
from odoo.fields import Domain
1517
from odoo.tools.misc import get_lang, format_date
@@ -26,6 +28,7 @@ class EventEvent(models.Model):
2628
'website.cover_properties.mixin',
2729
'website.searchable.mixin',
2830
'website.page_visibility_options.mixin',
31+
'website.structured_data.mixin',
2932
]
3033

3134
def _default_cover_properties(self):
@@ -650,3 +653,131 @@ def _search_render_results(self, fetch_fields, mapping, icon, limit):
650653
data['tag_ids'] = event.tag_ids.read(['name'])
651654
data['image_url'] = event._get_image_url()
652655
return results_data
656+
657+
def _to_structured_data_summary_event(self):
658+
"""Return summary structured data for a single event card."""
659+
self.ensure_one()
660+
if not self.address_id or self.is_ongoing or self.is_done or self.website_visibility != 'public':
661+
# Google event rich results require a public upcoming event with
662+
# a physical place.
663+
return None
664+
website = self.env['website'].get_current_website()
665+
base_url = website.get_base_url()
666+
image_jsonld = None
667+
if image_url := website.image_url(self, 'image_1920'):
668+
image_jsonld = JsonLd("ImageObject", {"url": f"{base_url}{image_url}"})
669+
address = self.address_id.sudo()
670+
street = ', '.join(part for part in (address.street, address.street2) if part)
671+
location_jsonld = JsonLd("Place", {"name": self.address_name}).add_nested({
672+
"address": JsonLd("PostalAddress", {
673+
"streetAddress": street or None,
674+
"addressLocality": address.city or None,
675+
"postalCode": address.zip or None,
676+
"addressRegion": address.state_id.code or None,
677+
"addressCountry": address.country_id.code or None,
678+
}),
679+
})
680+
return JsonLd("Event", {
681+
"name": self.name,
682+
"url": self.website_absolute_url,
683+
"startDate": JsonLd.to_iso_datetime(self.date_begin),
684+
}).add_nested({
685+
"image": image_jsonld,
686+
"location": location_jsonld,
687+
})
688+
689+
def _to_structured_data_event(self):
690+
"""Return full structured data for a single event detail page."""
691+
self.ensure_one()
692+
event_data = self._to_structured_data_summary_event()
693+
if not event_data:
694+
return None
695+
696+
description = self.subtitle or (self.description and text_from_html(self.description, True))
697+
organizer_jsonld = None
698+
if self.organizer_id:
699+
organizer_sudo = self.organizer_id.sudo()
700+
if organizer_sudo == self.env.company.partner_id:
701+
base_url = self.env['website'].get_current_website().get_base_url()
702+
organizer_jsonld = JsonLd("Organization", {"@id": f"{base_url}/#organization"})
703+
else:
704+
organizer_jsonld = JsonLd("Organization", {"name": organizer_sudo.name})
705+
if organizer_sudo.website:
706+
organizer_jsonld.set({"url": organizer_sudo.website})
707+
708+
event_status = "EventCancelled" if self.kanban_state == 'cancel' else "EventScheduled"
709+
offers_jsonld = [self._to_structured_data_ticket_offer(ticket) for ticket in self.event_ticket_ids]
710+
return event_data.set({
711+
"endDate": JsonLd.to_iso_datetime(self.date_end),
712+
"description": description,
713+
"eventStatus": f"https://schema.org/{event_status}",
714+
"eventAttendanceMode": "https://schema.org/OfflineEventAttendanceMode",
715+
}).add_nested({
716+
"organizer": organizer_jsonld,
717+
"offers": offers_jsonld,
718+
})
719+
720+
def _to_structured_data_ticket_offer(self, ticket):
721+
"""Create a JSON-LD Offer for a single event ticket."""
722+
now = fields.Datetime.now()
723+
is_expired = ticket.end_sale_datetime and ticket.end_sale_datetime < now
724+
availability = (
725+
"https://schema.org/SoldOut"
726+
if ticket.is_sold_out or is_expired
727+
else "https://schema.org/InStock"
728+
)
729+
730+
offer_jsonld = JsonLd("Offer", {
731+
"name": ticket.name,
732+
"availability": availability,
733+
"validFrom": JsonLd.to_iso_datetime(ticket.start_sale_datetime),
734+
"validThrough": JsonLd.to_iso_datetime(ticket.end_sale_datetime),
735+
})
736+
if availability == "https://schema.org/InStock":
737+
offer_jsonld.set({"url": f"{self.website_absolute_url}/register"})
738+
return offer_jsonld
739+
740+
def _to_structured_data_collectionpage(self):
741+
"""Return CollectionPage structured data for the events listing page."""
742+
website = self.env['website'].get_current_website()
743+
base_url = website.get_base_url()
744+
haspart_jsonld = [event._to_structured_data_summary_event() for event in self]
745+
ispartof_jsonld = JsonLd("Organization", {"@id": f"{base_url}/#organization"})
746+
return JsonLd("CollectionPage", {
747+
"name": self.env._("Events"),
748+
"url": f"{base_url}/event",
749+
}).add_nested({
750+
"hasPart": haspart_jsonld,
751+
"isPartOf": ispartof_jsonld,
752+
})
753+
754+
def _get_breadcrumb_items(self):
755+
"""Return breadcrumb items for a single event page."""
756+
self.ensure_one()
757+
website = self.env['website'].get_current_website()
758+
base_url = website.get_base_url()
759+
return [
760+
(website.name, base_url),
761+
(self.env._("Events"), f"{base_url}/event"),
762+
(self.name, f"{base_url}{self.website_url}"),
763+
]
764+
765+
def _build_structured_data(self, is_detail_page=False):
766+
"""Return rendered structured data for event list and detail pages."""
767+
schemas = super()._build_structured_data(is_detail_page=is_detail_page)
768+
if is_detail_page:
769+
self.ensure_one()
770+
if event_schema := self._to_structured_data_event():
771+
schemas.append(event_schema)
772+
items = self._get_breadcrumb_items()
773+
else:
774+
website = self.env['website'].get_current_website()
775+
base_url = website.get_base_url()
776+
if self:
777+
schemas.append(self._to_structured_data_collectionpage())
778+
items = [
779+
(website.name, base_url),
780+
(self.env._("Events"), f"{base_url}/event"),
781+
]
782+
schemas.append(self._build_breadcrumb_schema(items))
783+
return schemas

addons/website_event_sale/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from . import event_event
12
from . import product
23
from . import product_pricelist
34
from . import sale_order
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Part of Odoo. See LICENSE file for full copyright and licensing details.
2+
3+
from odoo import models
4+
5+
6+
class EventEvent(models.Model):
7+
_inherit = 'event.event'
8+
9+
def _to_structured_data_ticket_offer(self, ticket):
10+
"""Add ticket pricing to the structured data offer.
11+
12+
Uses the tax-included or tax-excluded price depending on the
13+
website ``show_line_subtotals_tax_selection`` setting, matching
14+
the price displayed on the event page.
15+
"""
16+
offer_jsonld = super()._to_structured_data_ticket_offer(ticket)
17+
website = self.env['website'].get_current_website()
18+
if website.show_line_subtotals_tax_selection == 'tax_excluded':
19+
price = ticket.total_price_reduce
20+
else:
21+
price = ticket.total_price_reduce_taxinc
22+
offer_jsonld.set({
23+
'price': price,
24+
'priceCurrency': self.company_id.currency_id.name,
25+
})
26+
return offer_jsonld

0 commit comments

Comments
 (0)