diff options
| author | Thibault Delavallée <tde@odoo.com> | 2020-08-20 09:16:38 +0000 |
|---|---|---|
| committer | Thibault Delavallée <tde@odoo.com> | 2020-08-21 17:30:13 +0000 |
| commit | 1163013ed6bf191a9e1548866a4df699f72b5bf4 (patch) | |
| tree | 8d54ebf3fe6a76ff69ad163541870eac938c0c7c /addons/website_event_track/controllers | |
| parent | b0845c0f2b48d6c7029a34ae50fc95876630e7ed (diff) | |
[REM] website_event_track_online: merge into website_event_track
PURPOSE
Clean organization and models linked to Event Online feature introduced in
semi stable saas-13.3 at odoo/odoo@981f95bbf46658eda9f2d33751b9310cc67d86aa and odoo/enterprise@14722028ae0773c3710ea48ce48ff44703e83b5a
Also clean website event menu not being available in website event but only
in website event track, which implies some extra-code to manage it.
In short: merge website_event_online in website_event, website_event_track
_(online/session) in website_event_track.
RATIONALE
_online modules have been added to extend content of website_event and
website_event_track without having any impact on those module. First step
of cleaning is to move this content directly in base module.
track_session module is mainly a rewrite of track module. Second step of
cleaning is to move its content directly in website_event_track.
SPECIFICATIONS
Move remaining content of website event track online to website event track.
Agenda notably is completely replaced by the new one developed within
track_online module.
Update dependencies accordingly.
LINKS
Task ID-2319779
COM odoo/odoo#56067
ENT odoo/enterprise#12520
UPG odoo/upgrade#1654
Diffstat (limited to 'addons/website_event_track/controllers')
| -rw-r--r-- | addons/website_event_track/controllers/__init__.py | 3 | ||||
| -rw-r--r-- | addons/website_event_track/controllers/event_track.py | 332 | ||||
| -rw-r--r-- | addons/website_event_track/controllers/main.py | 185 | ||||
| -rw-r--r-- | addons/website_event_track/controllers/webmanifest.py | 58 |
4 files changed, 392 insertions, 186 deletions
diff --git a/addons/website_event_track/controllers/__init__.py b/addons/website_event_track/controllers/__init__.py index 5d4b25db9c0..8779baf6109 100644 --- a/addons/website_event_track/controllers/__init__.py +++ b/addons/website_event_track/controllers/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import main +from . import event_track +from . import webmanifest diff --git a/addons/website_event_track/controllers/event_track.py b/addons/website_event_track/controllers/event_track.py new file mode 100644 index 00000000000..e338f384580 --- /dev/null +++ b/addons/website_event_track/controllers/event_track.py @@ -0,0 +1,332 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta +from werkzeug.exceptions import Forbidden, NotFound + +import babel +import babel.dates +import base64 +import pytz + +from odoo import exceptions, http, fields +from odoo.http import request +from odoo.tools import plaintext2html, html2plaintext + + +class EventTrackController(http.Controller): + + # ------------------------------------------------------------ + # TRACK LIST VIEW + # ------------------------------------------------------------ + + @http.route([ + '''/event/<model("event.event"):event>/track''', + '''/event/<model("event.event"):event>/track/tag/<model("event.track.tag"):tag>''' + ], type='http', auth="public", website=True, sitemap=False) + def event_tracks(self, event, tag=None, **post): + if not event.can_access_from_current_website() or (tag and tag.color == 0): + raise NotFound() + + event = event.with_context(tz=event.date_tz or 'UTC') + searches = {} + if tag: + searches.update(tag=tag.id) + tracks = event.track_ids.filtered(lambda track: tag in track.tag_ids) + else: + tracks = event.track_ids + + values = { + 'event': event, + 'main_object': event, + 'tracks': tracks, + 'tags': event.tracks_tag_ids, + 'searches': searches, + 'html2plaintext': html2plaintext + } + return request.render("website_event_track.tracks", values) + + # ------------------------------------------------------------ + # AGENDA VIEW + # ------------------------------------------------------------ + + @http.route(['''/event/<model("event.event"):event>/agenda'''], type='http', auth="public", website=True, sitemap=False) + def event_agenda(self, event, tag=None, **post): + if not event.can_access_from_current_website(): + raise NotFound() + + event = event.with_context(tz=event.date_tz or 'UTC') + vals = { + 'event': event, + 'main_object': event, + 'tag': tag, + 'user_event_manager': request.env.user.has_group('event.group_event_manager'), + } + + vals.update(self._prepare_calendar_values(event)) + + return request.render("website_event_track.agenda_online", vals) + + def _prepare_calendar_values(self, event): + """ + Override that should completely replace original method in v14. + + This methods slit the day (max end time - min start time) into 15 minutes time slots. + For each time slot, we assign the tracks that start at this specific time slot, and we add the number + of time slot that the track covers (track duration / 15 min) + The calendar will be divided into rows of 15 min, and the talks will cover the corresponding number of rows + (15 min slots). + """ + event = event.with_context(tz=event.date_tz or 'UTC') + local_tz = pytz.timezone(event.date_tz or 'UTC') + lang_code = request.env.context.get('lang') + event_track_ids = self._event_agenda_get_tracks(event) + + locations = list(set(track.location_id for track in event_track_ids)) + locations.sort(key=lambda x: x.id) + + # First split day by day (based on start time) + time_slots_by_tracks = {track: self._split_track_by_days(track, local_tz) for track in event_track_ids} + + # extract all the tracks time slots + track_time_slots = set().union(*(time_slot.keys() for time_slot in [time_slots for time_slots in time_slots_by_tracks.values()])) + + # extract unique days + days = list(set(time_slot.date() for time_slot in track_time_slots)) + days.sort() + + # Create the dict that contains the tracks at the correct time_slots / locations coordinates + tracks_by_days = dict.fromkeys(days, 0) + time_slots_by_day = dict((day, dict(start=set(), end=set())) for day in days) + tracks_by_rounded_times = dict((time_slot, dict((location, {}) for location in locations)) for time_slot in track_time_slots) + for track, time_slots in time_slots_by_tracks.items(): + start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz) + end_date = start_date + timedelta(hours=(track.duration or 0.25)) + + for time_slot, duration in time_slots.items(): + tracks_by_rounded_times[time_slot][track.location_id][track] = { + 'rowspan': duration, # rowspan + 'start_date': self._get_locale_time(start_date, lang_code), + 'end_date': self._get_locale_time(end_date, lang_code), + 'occupied_cells': self._get_occupied_cells(track, duration, locations, local_tz) + } + + # get all the time slots by day to determine the max duration of a day. + day = time_slot.date() + time_slots_by_day[day]['start'].add(time_slot) + time_slots_by_day[day]['end'].add(time_slot+timedelta(minutes=15*duration)) + tracks_by_days[day] += 1 + + # split days into 15 minutes time slots + global_time_slots_by_day = dict((day, {}) for day in days) + for day, time_slots in time_slots_by_day.items(): + start_time_slot = min(time_slots['start']) + end_time_slot = max(time_slots['end']) + + time_slots_count = int(((end_time_slot - start_time_slot).total_seconds() / 3600) * 4) + current_time_slot = start_time_slot + for i in range(0, time_slots_count + 1): + global_time_slots_by_day[day][current_time_slot] = tracks_by_rounded_times.get(current_time_slot, {}) + global_time_slots_by_day[day][current_time_slot]['formatted_time'] = self._get_locale_time(current_time_slot, lang_code) + current_time_slot = current_time_slot + timedelta(minutes=15) + + # count the number of tracks by days + tracks_by_days = dict.fromkeys(days, 0) + for track in event_track_ids: + track_day = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz).date() + tracks_by_days[track_day] += 1 + + return { + 'days': days, + 'tracks_by_days': tracks_by_days, + 'time_slots': global_time_slots_by_day, + 'locations': locations + } + + def _event_agenda_get_tracks(self, event): + tracks_sudo = event.sudo().track_ids.filtered(lambda track: track.date) + if not request.env.user.has_group('event.group_event_manager'): + tracks_sudo = tracks_sudo.filtered(lambda track: track.is_published or track.stage_id.is_accepted) + return tracks_sudo + + def _get_locale_time(self, dt_time, lang_code): + """ Get locale time from datetime object + + :param dt_time: datetime object + :param lang_code: language code (eg. en_US) + """ + locale = babel.Locale.parse(lang_code) + return babel.dates.format_time(dt_time, format='short', locale=locale) + + def time_slot_rounder(self, time, rounded_minutes): + """ Rounds to nearest hour by adding a timedelta hour if minute >= rounded_minutes + E.g. : If rounded_minutes = 15 -> 09:26:00 becomes 09:30:00 + 09:17:00 becomes 09:15:00 + """ + return (time.replace(second=0, microsecond=0, minute=0, hour=time.hour) + + timedelta(minutes=rounded_minutes * (time.minute // rounded_minutes))) + + def _split_track_by_days(self, track, local_tz): + """ + Based on the track start_date and the duration, + split the track duration into : + start_time by day : number of time slot (15 minutes) that the track takes on that day. + E.g. : start date = 01-01-2000 10:00 PM and duration = 3 hours + return { + 01-01-2000 10:00:00 PM: 8 (2 * 4), + 01-02-2000 00:00:00 AM: 4 (1 * 4) + } + Also return a set of all the time slots + """ + start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz) + start_datetime = self.time_slot_rounder(start_date, 15) + end_datetime = self.time_slot_rounder(start_datetime + timedelta(hours=(track.duration or 0.25)), 15) + time_slots_count = int(((end_datetime - start_datetime).total_seconds() / 3600) * 4) + + time_slots_by_day_start_time = {start_datetime: 0} + for i in range(0, time_slots_count): + # If the new time slot is still on the current day + next_day = (start_datetime + timedelta(days=1)).date() + if (start_datetime + timedelta(minutes=15*i)).date() <= next_day: + time_slots_by_day_start_time[start_datetime] += 1 + else: + start_datetime = next_day.datetime() + time_slots_by_day_start_time[start_datetime] = 0 + + return time_slots_by_day_start_time + + def _get_occupied_cells(self, track, rowspan, locations, local_tz): + """ + In order to use only once the cells that the tracks will occupy, we need to reserve those cells + (time_slot, location) coordinate. Those coordinated will be given to the template to avoid adding + blank cells where already occupied by a track. + """ + occupied_cells = [] + + start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz) + start_date = self.time_slot_rounder(start_date, 15) + for i in range(0, rowspan): + time_slot = start_date + timedelta(minutes=15*i) + if track.location_id: + occupied_cells.append((time_slot, track.location_id)) + # when no location, reserve all locations + else: + occupied_cells += [(time_slot, location) for location in locations if location] + + return occupied_cells + + # ------------------------------------------------------------ + # TRACK PAGE VIEW + # ------------------------------------------------------------ + + @http.route(['''/event/<model("event.event"):event>/track/<model("event.track", "[('event_id','=',event.id)]"):track>'''], type='http', auth="public", website=True, sitemap=True) + def event_track_view(self, event, track, **post): + if not event.can_access_from_current_website(): + raise NotFound() + + track = track.sudo().with_context(tz=event.date_tz or 'UTC') + values = {'track': track, 'event': track.event_id, 'main_object': track} + return request.render("website_event_track.track_view", values) + + @http.route("/event/track/toggle_reminder", type="json", auth="public", website=True) + def track_reminder_toggle(self, track_id, set_reminder_on): + """ Set a reminder a track for current visitor. Track visitor is created or updated + if it already exists. Exception made if un-wishlisting and no track_visitor + record found (should not happen unless manually done). + + :param boolean set_reminder_on: + If True, set as a wishlist, otherwise un-wishlist track; + If the track is a Key Track (wishlisted_by_default): + if set_reminder_on = False, blacklist the track_partner + otherwise, un-blacklist the track_partner + """ + track = self._fetch_track(track_id, allow_is_accepted=True) + force_create = set_reminder_on or track.wishlisted_by_default + event_track_partner = track._get_event_track_visitors(force_create=force_create) + visitor_sudo = event_track_partner.visitor_id + + if not track.wishlisted_by_default: + if not event_track_partner or event_track_partner.is_wishlisted == set_reminder_on: # ignore if new state = old state + return {'error': 'ignored'} + event_track_partner.is_wishlisted = set_reminder_on + else: + if not event_track_partner or event_track_partner.is_blacklisted != set_reminder_on: # ignore if new state = old state + return {'error': 'ignored'} + event_track_partner.is_blacklisted = not set_reminder_on + + result = {'reminderOn': set_reminder_on} + if request.httprequest.cookies.get('visitor_uuid', '') != visitor_sudo.access_token: + result['visitor_uuid'] = visitor_sudo.access_token + + return result + + # ------------------------------------------------------------ + # TRACK PROPOSAL + # ------------------------------------------------------------ + + @http.route(['''/event/<model("event.event"):event>/track_proposal'''], type='http', auth="public", website=True, sitemap=False) + def event_track_proposal(self, event, **post): + if not event.can_access_from_current_website(): + raise NotFound() + + return request.render("website_event_track.event_track_proposal", {'event': event, 'main_object': event}) + + @http.route(['''/event/<model("event.event"):event>/track_proposal/post'''], type='http', auth="public", methods=['POST'], website=True) + def event_track_proposal_post(self, event, **post): + if not event.can_access_from_current_website(): + raise NotFound() + + tags = [] + for tag in event.allowed_track_tag_ids: + if post.get('tag_' + str(tag.id)): + tags.append(tag.id) + + track = request.env['event.track'].sudo().create({ + 'name': post['track_name'], + 'partner_name': post['partner_name'], + 'partner_email': post['email_from'], + 'partner_phone': post['phone'], + 'partner_biography': plaintext2html(post['biography']), + 'event_id': event.id, + 'tag_ids': [(6, 0, tags)], + 'user_id': False, + 'description': plaintext2html(post['description']), + 'image': base64.b64encode(post['image'].read()) if post.get('image') else False + }) + if request.env.user != request.website.user_id: + track.sudo().message_subscribe(partner_ids=request.env.user.partner_id.ids) + else: + partner = request.env['res.partner'].sudo().search([('email', '=', post['email_from'])]) + if partner: + track.sudo().message_subscribe(partner_ids=partner.ids) + return request.render("website_event_track.event_track_proposal", {'track': track, 'event': event}) + + # ------------------------------------------------------------ + # TOOLS + # ------------------------------------------------------------ + + def _fetch_track(self, track_id, allow_is_accepted=False): + track = request.env['event.track'].browse(track_id).exists() + if not track: + raise NotFound() + try: + track.check_access_rights('read') + track.check_access_rule('read') + except exceptions.AccessError: + track_sudo = track.sudo() + if allow_is_accepted and track_sudo.is_accepted: + track = track_sudo + else: + raise Forbidden() + + event = track.event_id + # JSON RPC have no website in requests + if hasattr(request, 'website_id') and not event.can_access_from_current_website(): + raise NotFound() + try: + event.check_access_rights('read') + event.check_access_rule('read') + except exceptions.AccessError: + raise Forbidden() + + return track diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py deleted file mode 100644 index 13477d422b4..00000000000 --- a/addons/website_event_track/controllers/main.py +++ /dev/null @@ -1,185 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. - -import babel -import babel.dates -import collections -import datetime -import pytz -import base64 -from werkzeug.exceptions import NotFound - -from odoo import fields, http -from odoo.http import request -from odoo.tools import plaintext2html, html2plaintext - - -class WebsiteEventTrackController(http.Controller): - - # ------------------------------------------------------------ - # TRACK LIST VIEW - # ------------------------------------------------------------ - - @http.route([ - '''/event/<model("event.event"):event>/track''', - '''/event/<model("event.event"):event>/track/tag/<model("event.track.tag"):tag>''' - ], type='http', auth="public", website=True, sitemap=False) - def event_tracks(self, event, tag=None, **post): - if not event.can_access_from_current_website() or (tag and tag.color == 0): - raise NotFound() - - event = event.with_context(tz=event.date_tz or 'UTC') - searches = {} - if tag: - searches.update(tag=tag.id) - tracks = event.track_ids.filtered(lambda track: tag in track.tag_ids) - else: - tracks = event.track_ids - - values = { - 'event': event, - 'main_object': event, - 'tracks': tracks, - 'tags': event.tracks_tag_ids, - 'searches': searches, - 'html2plaintext': html2plaintext - } - return request.render("website_event_track.tracks", values) - - # ------------------------------------------------------------ - # AGENDA VIEW - # ------------------------------------------------------------ - - def _get_locale_time(self, dt_time, lang_code): - """ Get locale time from datetime object - - :param dt_time: datetime object - :param lang_code: language code (eg. en_US) - """ - locale = babel.Locale.parse(lang_code) - return babel.dates.format_time(dt_time, format='short', locale=locale) - - def _prepare_calendar(self, event, event_track_ids): - local_tz = pytz.timezone(event.date_tz or 'UTC') - lang_code = request.env.context.get('lang') - locations = {} # { location: [track, start_date, end_date, rowspan]} - dates = [] # [ (date, {}) ] - for track in event_track_ids: - locations.setdefault(track.location_id or False, []) - - forcetr = True - for track in event_track_ids: - start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz) - end_date = start_date + datetime.timedelta(hours=(track.duration or 0.5)) - location = track.location_id or False - locations.setdefault(location, []) - - # New TR, align all events - if forcetr or (start_date>dates[-1][0]) or not location: - formatted_time = self._get_locale_time(start_date, lang_code) - dates.append((start_date, {}, bool(location), formatted_time)) - for loc in list(locations): - if locations[loc] and (locations[loc][-1][2] > start_date): - locations[loc][-1][3] += 1 - elif not locations[loc] or locations[loc][-1][2] <= start_date: - locations[loc].append([False, locations[loc] and locations[loc][-1][2] or dates[0][0], start_date, 1]) - dates[-1][1][loc] = locations[loc][-1] - forcetr = not bool(location) - - # Add event - if locations[location] and locations[location][-1][1] > start_date: - locations[location][-1][3] -= 1 - locations[location].append([track, start_date, end_date, 1]) - dates[-1][1][location] = locations[location][-1] - locations = collections.OrderedDict(sorted(locations.items(), key=lambda t: t[0].id if t[0] else 0)) - return { - 'locations': locations, - 'dates': dates - } - - @http.route(['''/event/<model("event.event"):event>/agenda'''], type='http', auth="public", website=True, sitemap=False) - def event_agenda(self, event, tag=None, **post): - if not event.can_access_from_current_website(): - raise NotFound() - - event = event.with_context(tz=event.date_tz or 'UTC') - local_tz = pytz.timezone(event.date_tz or 'UTC') - days_tracks = collections.defaultdict(lambda: []) - event_track_ids = self._event_agenda_get_tracks(event) - for track in event_track_ids.sorted(lambda track: (bool(track.date), track.date, bool(track.location_id))): - if not track.date: - continue - date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz) - days_tracks[str(date)[:10]].append(track) - - days = {} - tracks_by_days = {} - for day, tracks in days_tracks.items(): - tracks_by_days[day] = tracks - days[day] = self._prepare_calendar(event, tracks) - - return request.render("website_event_track.agenda", { - 'event': event, - 'main_object': event, - 'days': days, - 'tracks_by_days': tracks_by_days, - 'tag': tag, - 'user_event_manager': request.env.user.has_group('event.group_event_manager'), - }) - - def _event_agenda_get_tracks(self, event): - return event.track_ids - - # ------------------------------------------------------------ - # TRACK PAGE VIEW - # ------------------------------------------------------------ - - @http.route(['''/event/<model("event.event"):event>/track/<model("event.track", "[('event_id','=',event.id)]"):track>'''], type='http', auth="public", website=True, sitemap=True) - def event_track_view(self, event, track, **post): - if not event.can_access_from_current_website(): - raise NotFound() - - track = track.sudo().with_context(tz=event.date_tz or 'UTC') - values = {'track': track, 'event': track.event_id, 'main_object': track} - return request.render("website_event_track.track_view", values) - - # ------------------------------------------------------------ - # TRACK PROPOSAL - # ------------------------------------------------------------ - - @http.route(['''/event/<model("event.event"):event>/track_proposal'''], type='http', auth="public", website=True, sitemap=False) - def event_track_proposal(self, event, **post): - if not event.can_access_from_current_website(): - raise NotFound() - - return request.render("website_event_track.event_track_proposal", {'event': event, 'main_object': event}) - - @http.route(['''/event/<model("event.event"):event>/track_proposal/post'''], type='http', auth="public", methods=['POST'], website=True) - def event_track_proposal_post(self, event, **post): - if not event.can_access_from_current_website(): - raise NotFound() - - tags = [] - for tag in event.allowed_track_tag_ids: - if post.get('tag_' + str(tag.id)): - tags.append(tag.id) - - track = request.env['event.track'].sudo().create({ - 'name': post['track_name'], - 'partner_name': post['partner_name'], - 'partner_email': post['email_from'], - 'partner_phone': post['phone'], - 'partner_biography': plaintext2html(post['biography']), - 'event_id': event.id, - 'tag_ids': [(6, 0, tags)], - 'user_id': False, - 'description': plaintext2html(post['description']), - 'image': base64.b64encode(post['image'].read()) if post.get('image') else False - }) - if request.env.user != request.website.user_id: - track.sudo().message_subscribe(partner_ids=request.env.user.partner_id.ids) - else: - partner = request.env['res.partner'].sudo().search([('email', '=', post['email_from'])]) - if partner: - track.sudo().message_subscribe(partner_ids=partner.ids) - return request.render("website_event_track.event_track_proposal", {'track': track, 'event': event}) diff --git a/addons/website_event_track/controllers/webmanifest.py b/addons/website_event_track/controllers/webmanifest.py new file mode 100644 index 00000000000..a777ff10303 --- /dev/null +++ b/addons/website_event_track/controllers/webmanifest.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import json +import pytz + +from odoo import http +from odoo.addons.http_routing.models.ir_http import url_for +from odoo.http import request +from odoo.modules.module import get_module_resource +from odoo.tools import ustr +from odoo.tools.translate import _ + + +class TrackManifest(http.Controller): + + @http.route('/event/manifest.webmanifest', type='http', auth='public', methods=['GET'], website=True, sitemap=False) + def webmanifest(self): + """ Returns a WebManifest describing the metadata associated with a web application. + Using this metadata, user agents can provide developers with means to create user + experiences that are more comparable to that of a native application. + """ + company = request.env.company + website = request.website + manifest = { + 'name': _('%s Online Events') % company.name, + 'short_name': company.name, + 'description': _('%s Online Events') % company.name, + 'scope': url_for('/event'), + 'start_url': url_for('/event'), + 'display': 'standalone', + 'background_color': '#ffffff', + 'theme_color': '#875A7B', + } + icon_sizes = ['192x192', '512x512'] + manifest['icons'] = [{ + 'src': website.image_url(website, 'app_icon', size=size), + 'sizes': size, + 'type': 'image/png', + } for size in icon_sizes] + body = json.dumps(manifest, default=ustr) + response = request.make_response(body, [ + ('Content-Type', 'application/manifest+json'), + ]) + return response + + @http.route('/event/service-worker.js', type='http', auth='public', methods=['GET'], website=True, sitemap=False) + def service_worker(self): + """ Returns a ServiceWorker javascript file scoped for website_event + """ + sw_file = get_module_resource('website_event_track', 'static/src/js/service_worker.js') + with open(sw_file, 'rb') as fp: + body = fp.read() + response = request.make_response(body, [ + ('Content-Type', 'text/javascript'), + ('Service-Worker-Allowed', url_for('/event')), + ]) + return response |
