diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/website_event/models/event_event.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website_event/models/event_event.py')
| -rw-r--r-- | addons/website_event/models/event_event.py | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/addons/website_event/models/event_event.py b/addons/website_event/models/event_event.py new file mode 100644 index 00000000..6fe54ab8 --- /dev/null +++ b/addons/website_event/models/event_event.py @@ -0,0 +1,396 @@ +# -*- 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 + def create(self, vals): + res = super(Event, self).create(vals) + res._update_website_menus() + return res + + 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. """ + if not url: + self.env['ir.ui.view'].with_context(_force_unlink=True).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 |
