Purpose
=======
Clean the ACLs related to the Event application.
Add a new group to manage the registration in the entrance of an event. This
group should not be able to modify or remove records in Event but should be
able to create and manage the registrations.
Specifications
==============
Now, there are 3 event groups
* ``Registration Desk User``, who can manage the registrations and
read all event-related information;
* ``Event User`` who can create event, sponsor, ticket... His role is
to globally handle events on a day-to-day basis;
* ``Event Administrator`` who can create event type, sponsor type,
ticket type... His role is to manage the way events are managed withint
its company:
Each group implies all previous groups.
Compared to previous event users gain a lot of rules, allowing to update
records like tickets, registrations, ... Low-end event users should now
use the registration desk group.
Links
=====
Task ID-2204364
COM odoo/odoo#57022
ENT odoo/enterprise#13984
UPG odoo/upgrade#1897
398 lines
18 KiB
Python
398 lines
18 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import json
|
|
import werkzeug.urls
|
|
|
|
from pytz import utc
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.addons.http_routing.models.ir_http import slug
|
|
from odoo.osv import expression
|
|
|
|
GOOGLE_CALENDAR_URL = 'https://www.google.com/calendar/render?'
|
|
|
|
|
|
class Event(models.Model):
|
|
_name = 'event.event'
|
|
_inherit = [
|
|
'event.event',
|
|
'website.seo.metadata',
|
|
'website.published.multi.mixin',
|
|
'website.cover_properties.mixin'
|
|
]
|
|
|
|
def _default_cover_properties(self):
|
|
res = super()._default_cover_properties()
|
|
res['opacity'] = '0.4'
|
|
return res
|
|
|
|
# description
|
|
subtitle = fields.Char('Event Subtitle', translate=True)
|
|
# registration
|
|
is_participating = fields.Boolean("Is Participating", compute="_compute_is_participating")
|
|
# website
|
|
website_published = fields.Boolean(tracking=True)
|
|
website_menu = fields.Boolean(
|
|
string='Website Menu',
|
|
compute='_compute_website_menu', readonly=False, store=True,
|
|
help="Creates menus Introduction, Location and Register on the page "
|
|
"of the event on the website.")
|
|
menu_id = fields.Many2one('website.menu', 'Event Menu', copy=False)
|
|
menu_register_cta = fields.Boolean(
|
|
'Add Register Button', compute='_compute_menu_register_cta',
|
|
readonly=False, store=True)
|
|
community_menu = fields.Boolean(
|
|
"Community Menu", compute="_compute_community_menu",
|
|
readonly=False, store=True,
|
|
help="Display community tab on website")
|
|
community_menu_ids = fields.One2many(
|
|
"website.event.menu", "event_id", string="Event Community Menus",
|
|
domain=[("menu_type", "=", "community")])
|
|
# live information
|
|
is_ongoing = fields.Boolean(
|
|
'Is Ongoing', compute='_compute_time_data', search='_search_is_ongoing',
|
|
help="Whether event has begun")
|
|
is_done = fields.Boolean(
|
|
'Is Done', compute='_compute_time_data',
|
|
help="Whether event is finished")
|
|
start_today = fields.Boolean(
|
|
'Start Today', compute='_compute_time_data',
|
|
help="Whether event is going to start today if still not ongoing")
|
|
start_remaining = fields.Integer(
|
|
'Remaining before start', compute='_compute_time_data',
|
|
help="Remaining time before event starts (minutes)")
|
|
|
|
def _compute_is_participating(self):
|
|
"""Heuristic
|
|
|
|
* public, no visitor: not participating as we have no information;
|
|
* public and visitor: check visitor is linked to a registration. As
|
|
visitors are merged on the top parent, current visitor check is
|
|
sufficient even for successive visits;
|
|
* logged, no visitor: check partner is linked to a registration. Do
|
|
not check the email as it is not really secure;
|
|
* logged as visitor: check partner or visitor are linked to a
|
|
registration;
|
|
"""
|
|
current_visitor = self.env['website.visitor']._get_visitor_from_request(force_create=False)
|
|
if self.env.user._is_public() and not current_visitor:
|
|
events = self.env['event.event']
|
|
elif self.env.user._is_public():
|
|
events = self.env['event.registration'].sudo().search([
|
|
('event_id', 'in', self.ids),
|
|
('state', '!=', 'cancel'),
|
|
('visitor_id', '=', current_visitor.id),
|
|
]).event_id
|
|
else:
|
|
if current_visitor:
|
|
domain = [
|
|
'|',
|
|
('partner_id', '=', self.env.user.partner_id.id),
|
|
('visitor_id', '=', current_visitor.id)
|
|
]
|
|
else:
|
|
domain = [('partner_id', '=', self.env.user.partner_id.id)]
|
|
events = self.env['event.registration'].sudo().search(
|
|
expression.AND([
|
|
domain,
|
|
['&', ('event_id', 'in', self.ids), ('state', '!=', 'cancel')]
|
|
])
|
|
).event_id
|
|
|
|
for event in self:
|
|
event.is_participating = event in events
|
|
|
|
@api.depends('event_type_id')
|
|
def _compute_website_menu(self):
|
|
""" Also ensure a value for website_menu as it is a trigger notably for
|
|
track related menus. """
|
|
for event in self:
|
|
if event.event_type_id and event.event_type_id != event._origin.event_type_id:
|
|
event.website_menu = event.event_type_id.website_menu
|
|
elif not event.website_menu:
|
|
event.website_menu = False
|
|
|
|
@api.depends("event_type_id", "website_menu", "community_menu")
|
|
def _compute_community_menu(self):
|
|
""" Set False in base module. Sub modules will add their own logic
|
|
(meet or track_quiz). """
|
|
for event in self:
|
|
event.community_menu = False
|
|
|
|
@api.depends("event_type_id", "website_menu")
|
|
def _compute_menu_register_cta(self):
|
|
""" At type onchange: synchronize. At website_menu update: synchronize. """
|
|
for event in self:
|
|
if event.event_type_id and event.event_type_id != event._origin.event_type_id:
|
|
event.menu_register_cta = event.event_type_id.menu_register_cta
|
|
elif event.website_menu and (event.website_menu != event._origin.website_menu or not event.menu_register_cta):
|
|
event.menu_register_cta = True
|
|
elif not event.website_menu:
|
|
event.menu_register_cta = False
|
|
|
|
@api.depends('date_begin', 'date_end')
|
|
def _compute_time_data(self):
|
|
""" Compute start and remaining time. Do everything in UTC as we compute only
|
|
time deltas here. """
|
|
now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0))
|
|
for event in self:
|
|
date_begin_utc = utc.localize(event.date_begin, is_dst=False)
|
|
date_end_utc = utc.localize(event.date_end, is_dst=False)
|
|
event.is_ongoing = date_begin_utc <= now_utc <= date_end_utc
|
|
event.is_done = now_utc > date_end_utc
|
|
event.start_today = date_begin_utc.date() == now_utc.date()
|
|
if date_begin_utc >= now_utc:
|
|
td = date_begin_utc - now_utc
|
|
event.start_remaining = int(td.total_seconds() / 60)
|
|
else:
|
|
event.start_remaining = 0
|
|
|
|
@api.depends('name')
|
|
def _compute_website_url(self):
|
|
super(Event, self)._compute_website_url()
|
|
for event in self:
|
|
if event.id: # avoid to perform a slug on a not yet saved record in case of an onchange.
|
|
event.website_url = '/event/%s' % slug(event)
|
|
|
|
# ------------------------------------------------------------
|
|
# CRUD
|
|
# ------------------------------------------------------------
|
|
|
|
@api.model_create_multi
|
|
def create(self, vals_list):
|
|
events = super().create(vals_list)
|
|
events._update_website_menus()
|
|
return events
|
|
|
|
def write(self, vals):
|
|
menus_state_by_field = self._split_menus_state_by_field()
|
|
res = super(Event, self).write(vals)
|
|
menus_update_by_field = self._get_menus_update_by_field(menus_state_by_field, force_update=vals.keys())
|
|
self._update_website_menus(menus_update_by_field=menus_update_by_field)
|
|
return res
|
|
|
|
# ------------------------------------------------------------
|
|
# WEBSITE MENU MANAGEMENT
|
|
# ------------------------------------------------------------
|
|
|
|
def toggle_website_menu(self, val):
|
|
self.website_menu = val
|
|
|
|
def _get_menu_update_fields(self):
|
|
"""" Return a list of fields triggering a split of menu to activate /
|
|
menu to de-activate. Due to saas-13.3 improvement of menu management
|
|
this is done using side-methods to ease inheritance.
|
|
|
|
:return list: list of fields, each of which triggering a menu update
|
|
like website_menu, website_track, ... """
|
|
return ['website_menu', 'community_menu']
|
|
|
|
def _get_menu_type_field_matching(self):
|
|
return {'community': 'community_menu'}
|
|
|
|
def _split_menus_state_by_field(self):
|
|
""" For each field linked to a menu, get the set of events having this
|
|
menu activated and de-activated. Purpose is to find those whose value
|
|
changed and update the underlying menus.
|
|
|
|
:return dict: key = name of field triggering a website menu update, get {
|
|
'activated': subset of self having its menu currently set to True
|
|
'deactivated': subset of self having its menu currently set to False
|
|
} """
|
|
menus_state_by_field = dict()
|
|
for fname in self._get_menu_update_fields():
|
|
activated = self.filtered(lambda event: event[fname])
|
|
menus_state_by_field[fname] = {
|
|
'activated': activated,
|
|
'deactivated': self - activated,
|
|
}
|
|
return menus_state_by_field
|
|
|
|
def _get_menus_update_by_field(self, menus_state_by_field, force_update=None):
|
|
""" For each field linked to a menu, get the set of events requiring
|
|
this menu to be activated or de-activated based on previous recorded
|
|
value.
|
|
|
|
:param menus_state_by_field: see ``_split_menus_state_by_field``;
|
|
:param force_update: list of field to which we force update of menus. This
|
|
is used notably when a direct write to a stored editable field messes with
|
|
its pre-computed value, notably in a transient mode (aka demo for example);
|
|
|
|
:return dict: key = name of field triggering a website menu update, get {
|
|
'activated': subset of self having its menu toggled to True
|
|
'deactivated': subset of self having its menu toggled to False
|
|
} """
|
|
menus_update_by_field = dict()
|
|
for fname in self._get_menu_update_fields():
|
|
if fname in force_update:
|
|
menus_update_by_field[fname] = self
|
|
else:
|
|
menus_update_by_field[fname] = self.env['event.event']
|
|
menus_update_by_field[fname] |= menus_state_by_field[fname]['activated'].filtered(lambda event: not event[fname])
|
|
menus_update_by_field[fname] |= menus_state_by_field[fname]['deactivated'].filtered(lambda event: event[fname])
|
|
return menus_update_by_field
|
|
|
|
def _get_website_menu_entries(self):
|
|
""" Method returning menu entries to display on the website view of the
|
|
event, possibly depending on some options in inheriting modules.
|
|
|
|
Each menu entry is a tuple containing :
|
|
* name: menu item name
|
|
* url: if set, url to a route (do not use xml_id in that case);
|
|
* xml_id: template linked to the page (do not use url in that case);
|
|
* sequence: specific sequence of menu entry to be set on the menu;
|
|
* menu_type: type of menu entry (used in inheriting modules to ease
|
|
menu management; not used in this module in 13.3 due to technical
|
|
limitations);
|
|
"""
|
|
self.ensure_one()
|
|
return [
|
|
(_('Introduction'), False, 'website_event.template_intro', 1, False),
|
|
(_('Location'), False, 'website_event.template_location', 50, False),
|
|
(_('Register'), '/event/%s/register' % slug(self), False, 100, False),
|
|
]
|
|
|
|
def _get_community_menu_entries(self):
|
|
self.ensure_one()
|
|
return [(_('Community'), '/event/%s/community' % slug(self), False, 80, 'community')]
|
|
|
|
def _update_website_menus(self, menus_update_by_field=None):
|
|
""" Synchronize event configuration and its menu entries for frontend.
|
|
|
|
:param menus_update_by_field: see ``_get_menus_update_by_field``"""
|
|
for event in self:
|
|
if event.menu_id and not event.website_menu:
|
|
event.menu_id.sudo().unlink()
|
|
elif event.website_menu and not event.menu_id:
|
|
root_menu = self.env['website.menu'].sudo().create({'name': event.name, 'website_id': event.website_id.id})
|
|
event.menu_id = root_menu
|
|
if event.website_menu and (not menus_update_by_field or event in menus_update_by_field.get('website_menu')):
|
|
for name, url, xml_id, menu_sequence, menu_type in event._get_website_menu_entries():
|
|
event._create_menu(menu_sequence, name, url, xml_id, menu_type=menu_type)
|
|
if event.menu_id and (not menus_update_by_field or event in menus_update_by_field.get('community_menu')):
|
|
event._update_website_menu_entry('community_menu', 'community_menu_ids', '_get_community_menu_entries')
|
|
|
|
def _update_website_menu_entry(self, fname_bool, fname_o2m, method_name):
|
|
""" Generic method to create menu entries based on a flag on event. This
|
|
method is a bit obscure, but is due to preparation of adding new menus
|
|
entries and pages for event in a stable version, leading to some constraints
|
|
while developing.
|
|
|
|
:param fname_bool: field name (e.g. website_track)
|
|
:param fname_o2m: o2m linking towards website.event.menu matching the
|
|
boolean fields (normally an entry ot website.event.menu with type matching
|
|
the boolean field name)
|
|
:param method_name: method returning menu entries information: url, sequence, ...
|
|
"""
|
|
self.ensure_one()
|
|
new_menu = None
|
|
|
|
if self[fname_bool] and not self[fname_o2m]:
|
|
# menus not found but boolean True: get menus to create
|
|
for sequence, menu_data in enumerate(getattr(self, method_name)()):
|
|
# some modules have 4 data: name, url, xml_id, menu_type; however we
|
|
# plan to support sequence in future modules, so this hackish code is
|
|
# necessary to avoid crashing. Not nice, but stable target = meh.
|
|
if len(menu_data) == 4:
|
|
(name, url, xml_id, menu_type) = menu_data
|
|
menu_sequence = sequence
|
|
elif len(menu_data) == 5:
|
|
(name, url, xml_id, menu_sequence, menu_type) = menu_data
|
|
new_menu = self._create_menu(menu_sequence, name, url, xml_id, menu_type=menu_type)
|
|
elif not self[fname_bool]:
|
|
# will cascade delete to the website.event.menu
|
|
self[fname_o2m].mapped('menu_id').sudo().unlink()
|
|
|
|
return new_menu
|
|
|
|
def _create_menu(self, sequence, name, url, xml_id, menu_type=False):
|
|
""" If url: create a website menu. Menu leads directly to the URL that
|
|
should be a valid route. If xml_id: create a new page, take its url back
|
|
thanks to new_page of website, then link it to a menu. Template is
|
|
duplicated and linked to a new url, meaning each menu will have its own
|
|
copy of the template.
|
|
|
|
:param menu_type: type of menu. Mainly used for inheritance purpose
|
|
allowing more fine-grain tuning of menus. """
|
|
self.check_access_rights('write')
|
|
if not url:
|
|
self.env['ir.ui.view'].with_context(_force_unlink=True).sudo().search([('name', '=', name + ' ' + self.name)]).unlink()
|
|
page_result = self.env['website'].sudo().new_page(name + ' ' + self.name, template=xml_id, ispage=False)
|
|
url = "/event/" + slug(self) + "/page" + page_result['url'] # url contains starting "/"
|
|
website_menu = self.env['website.menu'].sudo().create({
|
|
'name': name,
|
|
'url': url,
|
|
'parent_id': self.menu_id.id,
|
|
'sequence': sequence,
|
|
'website_id': self.website_id.id,
|
|
})
|
|
if menu_type:
|
|
self.env['website.event.menu'].create({
|
|
'menu_id': website_menu.id,
|
|
'event_id': self.id,
|
|
'menu_type': menu_type,
|
|
})
|
|
return website_menu
|
|
|
|
# ------------------------------------------------------------
|
|
# TOOLS
|
|
# ------------------------------------------------------------
|
|
|
|
def google_map_link(self, zoom=8):
|
|
""" Temporary method for stable """
|
|
return self._google_map_link(zoom=zoom)
|
|
|
|
def _google_map_link(self, zoom=8):
|
|
self.ensure_one()
|
|
if self.address_id:
|
|
return self.sudo().address_id.google_map_link(zoom=zoom)
|
|
return None
|
|
|
|
def _track_subtype(self, init_values):
|
|
self.ensure_one()
|
|
if 'is_published' in init_values and self.is_published:
|
|
return self.env.ref('website_event.mt_event_published')
|
|
elif 'is_published' in init_values and not self.is_published:
|
|
return self.env.ref('website_event.mt_event_unpublished')
|
|
return super(Event, self)._track_subtype(init_values)
|
|
|
|
def action_open_badge_editor(self):
|
|
""" open the event badge editor : redirect to the report page of event badge report """
|
|
self.ensure_one()
|
|
return {
|
|
'type': 'ir.actions.act_url',
|
|
'target': 'new',
|
|
'url': '/report/html/%s/%s?enable_editor' % ('event.event_event_report_template_badge', self.id),
|
|
}
|
|
|
|
def _get_event_resource_urls(self):
|
|
url_date_start = self.date_begin.strftime('%Y%m%dT%H%M%SZ')
|
|
url_date_stop = self.date_end.strftime('%Y%m%dT%H%M%SZ')
|
|
params = {
|
|
'action': 'TEMPLATE',
|
|
'text': self.name,
|
|
'dates': url_date_start + '/' + url_date_stop,
|
|
'details': self.name,
|
|
}
|
|
if self.address_id:
|
|
params.update(location=self.sudo().address_id.contact_address.replace('\n', ' '))
|
|
encoded_params = werkzeug.urls.url_encode(params)
|
|
google_url = GOOGLE_CALENDAR_URL + encoded_params
|
|
iCal_url = '/event/%d/ics?%s' % (self.id, encoded_params)
|
|
return {'google_url': google_url, 'iCal_url': iCal_url}
|
|
|
|
def _default_website_meta(self):
|
|
res = super(Event, self)._default_website_meta()
|
|
event_cover_properties = json.loads(self.cover_properties)
|
|
# background-image might contain single quotes eg `url('/my/url')`
|
|
res['default_opengraph']['og:image'] = res['default_twitter']['twitter:image'] = event_cover_properties.get('background-image', 'none')[4:-1].strip("'")
|
|
res['default_opengraph']['og:title'] = res['default_twitter']['twitter:title'] = self.name
|
|
res['default_opengraph']['og:description'] = res['default_twitter']['twitter:description'] = self.subtitle
|
|
res['default_twitter']['twitter:card'] = 'summary'
|
|
res['default_meta_description'] = self.subtitle
|
|
return res
|
|
|
|
def get_backend_menu_id(self):
|
|
return self.env.ref('event.event_main_menu').id
|