summaryrefslogtreecommitdiff
path: root/addons/website_event_track
diff options
context:
space:
mode:
authorThibault Delavallée <tde@odoo.com>2020-08-20 09:16:38 +0000
committerThibault Delavallée <tde@odoo.com>2020-08-21 17:30:13 +0000
commit1163013ed6bf191a9e1548866a4df699f72b5bf4 (patch)
tree8d54ebf3fe6a76ff69ad163541870eac938c0c7c /addons/website_event_track
parentb0845c0f2b48d6c7029a34ae50fc95876630e7ed (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')
-rw-r--r--addons/website_event_track/__manifest__.py5
-rw-r--r--addons/website_event_track/controllers/__init__.py3
-rw-r--r--addons/website_event_track/controllers/event_track.py332
-rw-r--r--addons/website_event_track/controllers/main.py185
-rw-r--r--addons/website_event_track/controllers/webmanifest.py58
-rw-r--r--addons/website_event_track/data/event_track_data.xml5
-rw-r--r--addons/website_event_track/data/event_track_demo.xml4
-rw-r--r--addons/website_event_track/data/event_track_tag_demo.xml21
-rw-r--r--addons/website_event_track/data/event_track_visitor_demo.xml15
-rw-r--r--addons/website_event_track/models/__init__.py4
-rw-r--r--addons/website_event_track/models/event_sponsor.py66
-rw-r--r--addons/website_event_track/models/event_sponsor_type.py5
-rw-r--r--addons/website_event_track/models/event_track.py192
-rw-r--r--addons/website_event_track/models/event_track_stage.py8
-rw-r--r--addons/website_event_track/models/event_track_tag.py4
-rw-r--r--addons/website_event_track/models/event_track_tag_category.py14
-rw-r--r--addons/website_event_track/models/event_track_visitor.py32
-rw-r--r--addons/website_event_track/models/website.py28
-rw-r--r--addons/website_event_track/models/website_visitor.py75
-rw-r--r--addons/website_event_track/security/ir.model.access.csv4
-rw-r--r--addons/website_event_track/static/lib/idb-keyval/idb-keyval.js81
-rw-r--r--addons/website_event_track/static/src/js/event_track_reminder.js88
-rw-r--r--addons/website_event_track/static/src/js/service_worker.js280
-rw-r--r--addons/website_event_track/static/src/js/website_event_pwa_widget.js208
-rw-r--r--addons/website_event_track/static/src/js/website_event_track_set_customize_options.js94
-rw-r--r--addons/website_event_track/static/src/scss/event_track_templates.scss222
-rw-r--r--addons/website_event_track/static/src/scss/pwa_frontend.scss35
-rw-r--r--addons/website_event_track/static/src/xml/customize_options.xml23
-rw-r--r--addons/website_event_track/static/src/xml/website_event_pwa.xml11
-rw-r--r--addons/website_event_track/tests/__init__.py1
-rw-r--r--addons/website_event_track/tests/common.py28
-rw-r--r--addons/website_event_track/views/assets.xml10
-rw-r--r--addons/website_event_track/views/event_event_views.xml6
-rw-r--r--addons/website_event_track/views/event_menus.xml14
-rw-r--r--addons/website_event_track/views/event_sponsor_views.xml32
-rw-r--r--addons/website_event_track/views/event_templates.xml44
-rw-r--r--addons/website_event_track/views/event_track_stage_views.xml4
-rw-r--r--addons/website_event_track/views/event_track_tag_views.xml44
-rw-r--r--addons/website_event_track/views/event_track_templates_agenda.xml234
-rw-r--r--addons/website_event_track/views/event_track_templates_list.xml16
-rw-r--r--addons/website_event_track/views/event_track_templates_misc.xml35
-rw-r--r--addons/website_event_track/views/event_track_templates_page.xml16
-rw-r--r--addons/website_event_track/views/event_track_templates_reminder.xml27
-rw-r--r--addons/website_event_track/views/event_track_views.xml460
-rw-r--r--addons/website_event_track/views/event_track_visitor_views.xml70
-rw-r--r--addons/website_event_track/views/website_visitor_views.xml51
46 files changed, 2581 insertions, 613 deletions
diff --git a/addons/website_event_track/__manifest__.py b/addons/website_event_track/__manifest__.py
index bdcf7efc16c..74618521e33 100644
--- a/addons/website_event_track/__manifest__.py
+++ b/addons/website_event_track/__manifest__.py
@@ -14,9 +14,11 @@
'data/event_data.xml',
'data/event_track_data.xml',
'views/assets.xml',
+ 'views/event_templates.xml',
'views/event_track_templates_agenda.xml',
'views/event_track_templates_list.xml',
'views/event_track_templates_misc.xml',
+ 'views/event_track_templates_reminder.xml',
'views/event_track_templates_page.xml',
'views/event_track_templates_proposal.xml',
'views/website_templates.xml',
@@ -24,9 +26,11 @@
'views/event_track_location_views.xml',
'views/event_track_tag_views.xml',
'views/event_track_stage_views.xml',
+ 'views/event_track_visitor_views.xml',
'views/event_sponsor_views.xml',
'views/event_event_views.xml',
'views/event_type_views.xml',
+ 'views/website_visitor_views.xml',
'views/event_menus.xml',
],
'demo': [
@@ -36,5 +40,6 @@
'data/event_track_tag_demo.xml',
'data/event_track_demo.xml',
'data/event_track_demo_description.xml',
+ 'data/event_track_visitor_demo.xml',
],
}
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
diff --git a/addons/website_event_track/data/event_track_data.xml b/addons/website_event_track/data/event_track_data.xml
index 707cb8255d2..33cf0fb5fad 100644
--- a/addons/website_event_track/data/event_track_data.xml
+++ b/addons/website_event_track/data/event_track_data.xml
@@ -33,14 +33,17 @@
<record id="event_sponsor_type1" model="event.sponsor.type">
<field name="name">Bronze</field>
<field name="sequence">3</field>
+ <field name="display_ribbon_style">Bronze</field>
</record>
<record id="event_sponsor_type2" model="event.sponsor.type">
<field name="name">Silver</field>
<field name="sequence">2</field>
+ <field name="display_ribbon_style">Silver</field>
</record>
<record id="event_sponsor_type3" model="event.sponsor.type">
<field name="name">Gold</field>
<field name="sequence">1</field>
+ <field name="display_ribbon_style">Gold</field>
</record>
<record id="event_track_stage0" model="event.track.stage">
@@ -58,11 +61,13 @@
<field name="name">Announced</field>
<field name="sequence">3</field>
<field name="color">3</field>
+ <field name="is_accepted" eval="True"/>
</record>
<record id="event_track_stage3" model="event.track.stage">
<field name="name">Published</field>
<field name="sequence">4</field>
<field name="color">4</field>
+ <field name="is_accepted" eval="True"/>
<field name="is_done" eval="True"/>
</record>
<record id="event_track_stage4" model="event.track.stage">
diff --git a/addons/website_event_track/data/event_track_demo.xml b/addons/website_event_track/data/event_track_demo.xml
index 62e6f0a7af5..4eeead8b65f 100644
--- a/addons/website_event_track/data/event_track_demo.xml
+++ b/addons/website_event_track/data/event_track_demo.xml
@@ -333,6 +333,7 @@
<field name="color">1</field>
<field name="event_id" ref="event.event_7"/>
<field name="stage_id" ref="event_track_stage3"/>
+ <field name="wishlisted_by_default" eval="True"/>
<field name="date" eval="(DateTime.now() - timedelta(days=1)).strftime('%Y-%m-%d 06:00:00')"></field>
<field name="tag_ids" eval="[
(4, ref('website_event_track.event_track_tag1')),
@@ -474,6 +475,7 @@
<field name="color">1</field>
<field name="event_id" ref="event.event_7"/>
<field name="stage_id" ref="event_track_stage3"/>
+ <field name="wishlisted_by_default" eval="True"/>
<field name="date" eval="DateTime.now().strftime('%Y-%m-%d 06:00:00')"></field>
<field name="tag_ids" eval="[
(4, ref('website_event_track.event_track_tag1')),
@@ -519,6 +521,7 @@
<field name="color">2</field>
<field name="event_id" ref="event.event_7"/>
<field name="stage_id" ref="event_track_stage3"/>
+ <field name="wishlisted_by_default" eval="True"/>
<field name="location_id" ref="website_event_track.event_track_location_online_1"/>
<field name="date" eval="DateTime.now().strftime('%Y-%m-%d 12:00:00')"></field>
<field name="tag_ids" eval="[(4, ref('website_event_track.event_track_tag3'))]"/>
@@ -572,6 +575,7 @@
<field name="color">7</field>
<field name="event_id" ref="event.event_7"/>
<field name="stage_id" ref="event_track_stage3"/>
+ <field name="wishlisted_by_default" eval="True"/>
<field name="location_id" ref="website_event_track.event_track_location_online_2"/>
<field name="date" eval="DateTime.now().strftime('%Y-%m-%d 13:30:00')"></field>
<field name="tag_ids" eval="[(4, ref('website_event_track.event_track_tag1')), (4, ref('website_event_track.event_track_tag12'))]"/>
diff --git a/addons/website_event_track/data/event_track_tag_demo.xml b/addons/website_event_track/data/event_track_tag_demo.xml
index 1d4b4234e3e..bffa1d6cd05 100644
--- a/addons/website_event_track/data/event_track_tag_demo.xml
+++ b/addons/website_event_track/data/event_track_tag_demo.xml
@@ -1,28 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
+ <record id="event_track_tag_category_1" model="event.track.tag.category">
+ <field name="name">Audience</field>
+ <field name="sequence">10</field>
+ </record>
+ <record id="event_track_tag_category_2" model="event.track.tag.category">
+ <field name="name">Format</field>
+ <field name="sequence">20</field>
+ </record>
+
<record id="event_track_tag1" model="event.track.tag">
<field name="name">Consumers</field>
<field name="color">1</field>
+ <field name="category_id" ref="event_track_tag_category_1"/>
+ <field name="sequence">1</field>
</record>
<record id="event_track_tag2" model="event.track.tag">
<field name="name">Sales</field>
<field name="color">2</field>
+ <field name="category_id" ref="event_track_tag_category_1"/>
+ <field name="sequence">2</field>
</record>
<record id="event_track_tag3" model="event.track.tag">
<field name="name">Research</field>
<field name="color">3</field>
+ <field name="category_id" ref="event_track_tag_category_1"/>
+ <field name="sequence">3</field>
</record>
<record id="event_track_tag11" model="event.track.tag">
<field name="name">Lightning Talks</field>
<field name="color">4</field>
+ <field name="category_id" ref="event_track_tag_category_2"/>
+ <field name="sequence">11</field>
</record>
<record id="event_track_tag12" model="event.track.tag">
<field name="name">Round Table</field>
<field name="color">5</field>
+ <field name="category_id" ref="event_track_tag_category_2"/>
+ <field name="sequence">12</field>
</record>
<record id="event_track_tag13" model="event.track.tag">
<field name="name">Keynote</field>
<field name="color">6</field>
+ <field name="category_id" ref="event_track_tag_category_2"/>
+ <field name="sequence">13</field>
</record>
</odoo>
diff --git a/addons/website_event_track/data/event_track_visitor_demo.xml b/addons/website_event_track/data/event_track_visitor_demo.xml
new file mode 100644
index 00000000000..b343ae4f282
--- /dev/null
+++ b/addons/website_event_track/data/event_track_visitor_demo.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo><data>
+
+ <record id="event_7_track_2_visitor_0" model="event.track.visitor">
+ <field name="track_id" ref="website_event_track.event_7_track_2"/>
+ <field name="visitor_id" ref="website_event.website_visitor_event_0"/>
+ <field name="is_wishlisted" eval="True"/>
+ </record>
+ <record id="event_7_track_2_visitor_2" model="event.track.visitor">
+ <field name="track_id" ref="website_event_track.event_7_track_2"/>
+ <field name="visitor_id" ref="website_event.website_visitor_event_2"/>
+ <field name="is_wishlisted" eval="True"/>
+ </record>
+
+</data></odoo>
diff --git a/addons/website_event_track/models/__init__.py b/addons/website_event_track/models/__init__.py
index eb261d9cca2..a9c5599582c 100644
--- a/addons/website_event_track/models/__init__.py
+++ b/addons/website_event_track/models/__init__.py
@@ -8,6 +8,10 @@ from . import event_track
from . import event_track_location
from . import event_track_stage
from . import event_track_tag
+from . import event_track_tag_category
+from . import event_track_visitor
from . import event_type
+from . import website
from . import website_event_menu
from . import website_menu
+from . import website_visitor
diff --git a/addons/website_event_track/models/event_sponsor.py b/addons/website_event_track/models/event_sponsor.py
index 5966d183dec..0fca8a53a07 100644
--- a/addons/website_event_track/models/event_sponsor.py
+++ b/addons/website_event_track/models/event_sponsor.py
@@ -2,27 +2,40 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
+from odoo.modules.module import get_resource_path
class Sponsor(models.Model):
_name = "event.sponsor"
_description = 'Event Sponsor'
_order = "sequence, sponsor_type_id"
- _rec_name = 'partner_name'
+ _rec_name = 'name'
_inherit = ['mail.thread', 'mail.activity.mixin']
event_id = fields.Many2one('event.event', 'Event', required=True)
sponsor_type_id = fields.Many2one('event.sponsor.type', 'Sponsoring Type', required=True)
+ url = fields.Char('Sponsor Website', compute='_compute_url', readonly=False, store=True)
+ sequence = fields.Integer('Sequence')
+ active = fields.Boolean(default=True)
+ # contact information
partner_id = fields.Many2one('res.partner', 'Sponsor/Customer', required=True)
partner_name = fields.Char('Name', related='partner_id.name')
partner_email = fields.Char('Email', related='partner_id.email')
partner_phone = fields.Char('Phone', related='partner_id.phone')
partner_mobile = fields.Char('Mobile', related='partner_id.mobile')
- url = fields.Char('Sponsor Website', compute='_compute_url', readonly=False, store=True)
- sequence = fields.Integer('Sequence')
- image_128 = fields.Image(
- string="Logo", related='partner_id.image_128', store=True, readonly=False)
- active = fields.Boolean(default=True)
+ name = fields.Char('Sponsor Name', compute='_compute_name', readonly=False, store=True)
+ email = fields.Char('Sponsor Email', compute='_compute_email', readonly=False, store=True)
+ phone = fields.Char('Sponsor Phone', compute='_compute_phone', readonly=False, store=True)
+ mobile = fields.Char('Sponsor Mobile', compute='_compute_mobile', readonly=False, store=True)
+ # image
+ image_512 = fields.Image(
+ string="Logo", max_width=512, max_height=512,
+ compute='_compute_image_512', readonly=False, store=True)
+ image_256 = fields.Image("Image 256", related="image_512", max_width=256, max_height=256, store=False)
+ image_128 = fields.Image("Image 128", related="image_512", max_width=128, max_height=128, store=False)
+ website_image_url = fields.Char(
+ string='Image URL', max_width=256, max_height=256,
+ compute='_compute_website_image_url', compute_sudo=True, store=False)
@api.depends('partner_id')
def _compute_url(self):
@@ -30,6 +43,47 @@ class Sponsor(models.Model):
if sponsor.partner_id.website or not sponsor.url:
sponsor.url = sponsor.partner_id.website
+ @api.depends('partner_id')
+ def _compute_name(self):
+ self._synchronize_with_partner('name')
+
+ @api.depends('partner_id')
+ def _compute_email(self):
+ self._synchronize_with_partner('email')
+
+ @api.depends('partner_id')
+ def _compute_phone(self):
+ self._synchronize_with_partner('phone')
+
+ @api.depends('partner_id')
+ def _compute_mobile(self):
+ self._synchronize_with_partner('mobile')
+
+ @api.depends('partner_id')
+ def _compute_image_512(self):
+ self._synchronize_with_partner('image_512')
+
+ @api.depends('image_256', 'partner_id.image_256')
+ def _compute_website_image_url(self):
+ for sponsor in self:
+ if sponsor.image_256:
+ sponsor.website_image_url = self.env['website'].image_url(sponsor, 'image_256', size=256)
+ elif sponsor.partner_id.image_256:
+ sponsor.website_image_url = self.env['website'].image_url(sponsor.partner_id, 'image_256', size=256)
+ else:
+ sponsor.website_image_url = get_resource_path('website_event_track', 'static/src/img', 'event_sponsor_default_%d.png' % (sponsor.id % 1))
+
+ def _synchronize_with_partner(self, fname):
+ """ Synchronize with partner if not set. Setting a value does not write
+ on partner as this may be event-specific information. """
+ for sponsor in self:
+ if not sponsor[fname]:
+ sponsor[fname] = sponsor.partner_id[fname]
+
+ # ------------------------------------------------------------
+ # MESSAGING
+ # ------------------------------------------------------------
+
def _message_get_suggested_recipients(self):
recipients = super(Sponsor, self)._message_get_suggested_recipients()
for sponsor in self:
diff --git a/addons/website_event_track/models/event_sponsor_type.py b/addons/website_event_track/models/event_sponsor_type.py
index 3d3cd5749c8..8eb3eb71fbd 100644
--- a/addons/website_event_track/models/event_sponsor_type.py
+++ b/addons/website_event_track/models/event_sponsor_type.py
@@ -11,3 +11,8 @@ class SponsorType(models.Model):
name = fields.Char('Sponsor Type', required=True, translate=True)
sequence = fields.Integer('Sequence')
+ display_ribbon_style = fields.Selection([
+ ('no_ribbon', 'No Ribbon'),
+ ('Gold', 'Gold'),
+ ('Silver', 'Silver'),
+ ('Bronze', 'Bronze')], string='Ribbon Style')
diff --git a/addons/website_event_track/models/event_track.py b/addons/website_event_track/models/event_track.py
index 04d2d9e6514..2ec57bddde4 100644
--- a/addons/website_event_track/models/event_track.py
+++ b/addons/website_event_track/models/event_track.py
@@ -4,8 +4,10 @@
from datetime import timedelta
from odoo import api, fields, models
-from odoo.tools.translate import _, html_translate
from odoo.addons.http_routing.models.ir_http import slug
+from odoo.osv import expression
+from odoo.tools.mail import is_html_empty
+from odoo.tools.translate import _, html_translate
class Track(models.Model):
@@ -21,7 +23,6 @@ class Track(models.Model):
# description
name = fields.Char('Title', required=True, translate=True)
event_id = fields.Many2one('event.event', 'Event', required=True)
- image = fields.Image("Image", max_width=128, max_height=128)
active = fields.Boolean(default=True)
user_id = fields.Many2one('res.users', 'Responsible', tracking=True, default=lambda self: self.env.user)
company_id = fields.Many2one('res.company', related='event_id.company_id')
@@ -38,6 +39,7 @@ class Track(models.Model):
index=True, copy=False, default=_get_default_stage_id,
group_expand='_read_group_stage_ids',
required=True, tracking=True)
+ is_accepted = fields.Boolean('Is Accepted', related='stage_id.is_accepted', readonly=True)
kanban_state = fields.Selection([
('normal', 'Grey'),
('done', 'Green'),
@@ -58,12 +60,46 @@ class Track(models.Model):
partner_phone = fields.Char(
string='Phone', compute='_compute_partner_info',
readonly=False, store=True, tracking=30)
- partner_biography = fields.Html(string='Biography')
+ partner_biography = fields.Html(
+ string='Biography', compute='_compute_partner_biography',
+ readonly=False, store=True)
+ partner_function = fields.Char(
+ 'Job Position', related='partner_id.function',
+ compute_sudo=True, readonly=True)
+ partner_company_name = fields.Char(
+ 'Company Name', related='partner_id.parent_name',
+ compute_sudo=True, readonly=True)
+ image = fields.Image(
+ string="Speaker Photo", compute="_compute_speaker_image",
+ readonly=False, store=True,
+ max_width=256, max_height=256)
location_id = fields.Many2one('event.track.location', 'Location')
# time information
date = fields.Datetime('Track Date')
date_end = fields.Datetime('Track End Date', compute='_compute_end_date', store=True)
duration = fields.Float('Duration', default=1.5, help="Track duration in hours.")
+ # frontend description
+ website_image = fields.Image(string="Website Image", max_width=1024, max_height=1024)
+ website_image_url = fields.Char(
+ string='Image URL', compute='_compute_website_image_url',
+ compute_sudo=True, store=False)
+ # wishlist / visitors management
+ event_track_visitor_ids = fields.One2many(
+ 'event.track.visitor', 'track_id', string="Track Visitors",
+ groups="event.group_event_user")
+ is_reminder_on = fields.Boolean('Is Reminder On', compute='_compute_is_reminder_on')
+ wishlist_visitor_ids = fields.Many2many(
+ 'website.visitor', string="Visitor Wishlist",
+ compute="_compute_wishlist_visitor_ids", compute_sudo=True,
+ search="_search_wishlist_visitor_ids",
+ groups="event.group_event_user")
+ wishlist_visitor_count = fields.Integer(
+ string="# Wishlisted",
+ compute="_compute_wishlist_visitor_ids", compute_sudo=True,
+ groups="event.group_event_user")
+ wishlisted_by_default = fields.Boolean(
+ string='Always Wishlisted',
+ help="""If set, the talk will be starred for each attendee registered to the event. The attendee won't be able to un-star this talk.""")
@api.depends('name')
def _compute_website_url(self):
@@ -72,6 +108,8 @@ class Track(models.Model):
if track.id:
track.website_url = '/event/%s/track/%s' % (slug(track.event_id), slug(track))
+ # SPEAKER
+
@api.depends('partner_id')
def _compute_partner_info(self):
for track in self:
@@ -80,6 +118,23 @@ class Track(models.Model):
track.partner_email = track.partner_id.email
track.partner_phone = track.partner_id.phone
+ @api.depends('partner_id')
+ def _compute_partner_biography(self):
+ for track in self:
+ if not track.partner_biography:
+ track.partner_biography = track.partner_id.website_description
+ elif track.partner_id and is_html_empty(track.partner_biography) and \
+ not is_html_empty(track.partner_id.website_description):
+ track.partner_biography = track.partner_id.website_description
+
+ @api.depends('partner_id')
+ def _compute_speaker_image(self):
+ for track in self:
+ if not track.image:
+ track.image = track.partner_id.image_256
+
+ # TIME
+
@api.depends('date', 'duration')
def _compute_end_date(self):
for track in self:
@@ -89,6 +144,85 @@ class Track(models.Model):
else:
track.date_end = False
+
+ # FRONTEND DESCRIPTION
+
+ @api.depends('image', 'partner_id.image_256')
+ def _compute_website_image_url(self):
+ for track in self:
+ if track.website_image:
+ track.website_image_url = self.env['website'].image_url(track, 'website_image', size=1024)
+ else:
+ track.website_image_url = '/website_event_track/static/src/img/event_track_default_%d.jpeg' % (track.id % 2)
+
+ # WISHLIST / VISITOR MANAGEMENT
+
+ @api.depends('wishlisted_by_default', 'event_track_visitor_ids.visitor_id',
+ 'event_track_visitor_ids.partner_id', 'event_track_visitor_ids.is_wishlisted',
+ 'event_track_visitor_ids.is_blacklisted')
+ @api.depends_context('uid')
+ def _compute_is_reminder_on(self):
+ current_visitor = self.env['website.visitor']._get_visitor_from_request(force_create=False)
+ if self.env.user._is_public() and not current_visitor:
+ for track in self:
+ track.is_reminder_on = track.wishlisted_by_default
+ else:
+ if self.env.user._is_public():
+ domain = [('visitor_id', '=', current_visitor.id)]
+ elif 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)]
+
+ event_track_visitors = self.env['event.track.visitor'].sudo().search_read(
+ expression.AND([
+ domain,
+ [('track_id', 'in', self.ids)]
+ ]), fields=['track_id', 'is_wishlisted', 'is_blacklisted']
+ )
+
+ wishlist_map = {
+ track_visitor['track_id'][0]: {
+ 'is_wishlisted': track_visitor['is_wishlisted'],
+ 'is_blacklisted': track_visitor['is_blacklisted']
+ } for track_visitor in event_track_visitors
+ }
+ for track in self:
+ if wishlist_map.get(track.id):
+ track.is_reminder_on = wishlist_map.get(track.id)['is_wishlisted'] or (track.wishlisted_by_default and not wishlist_map[track.id]['is_blacklisted'])
+ else:
+ track.is_reminder_on = track.wishlisted_by_default
+
+ @api.depends('event_track_visitor_ids.visitor_id', 'event_track_visitor_ids.is_wishlisted')
+ def _compute_wishlist_visitor_ids(self):
+ results = self.env['event.track.visitor'].read_group(
+ [('track_id', 'in', self.ids), ('is_wishlisted', '=', True)],
+ ['track_id', 'visitor_id:array_agg'],
+ ['track_id']
+ )
+ visitor_ids_map = {result['track_id'][0]: result['visitor_id'] for result in results}
+ for track in self:
+ track.wishlist_visitor_ids = visitor_ids_map.get(track.id, [])
+ track.wishlist_visitor_count = len(visitor_ids_map.get(track.id, []))
+
+ def _search_wishlist_visitor_ids(self, operator, operand):
+ if operator == "not in":
+ raise NotImplementedError("Unsupported 'Not In' operation on track wishlist visitors")
+
+ track_visitors = self.env['event.track.visitor'].sudo().search([
+ ('visitor_id', operator, operand),
+ ('is_wishlisted', '=', True)
+ ])
+ return [('id', 'in', track_visitors.track_id.ids)]
+
+ # ------------------------------------------------------------
+ # CRUD
+ # ------------------------------------------------------------
+
@api.model_create_multi
def create(self, vals_list):
tracks = super(Track, self).create(vals_list)
@@ -100,12 +234,16 @@ class Track(models.Model):
subject=track.name,
subtype_id=self.env.ref('website_event_track.mt_event_track').id,
)
+ track._synchronize_with_stage(track.stage_id)
return tracks
def write(self, vals):
if 'stage_id' in vals and 'kanban_state' not in vals:
vals['kanban_state'] = 'normal'
+ if vals.get('stage_id'):
+ stage = self.env['event.track.stage'].browse(vals['stage_id'])
+ self._synchronize_with_stage(stage)
res = super(Track, self).write(vals)
if vals.get('partner_id'):
self.message_subscribe([vals['partner_id']])
@@ -116,6 +254,14 @@ class Track(models.Model):
""" Always display all stages """
return stages.search([], order=order)
+ def _synchronize_with_stage(self, stage):
+ if stage.is_done:
+ self.is_published = True
+
+ # ------------------------------------------------------------
+ # MESSAGING
+ # ------------------------------------------------------------
+
def _track_template(self, changes):
res = super(Track, self)._track_template(changes)
track = self[0]
@@ -157,6 +303,10 @@ class Track(models.Model):
]).write({'partner_id': new_partner.id})
return super(Track, self)._message_post_after_hook(message, msg_vals)
+ # ------------------------------------------------------------
+ # ACTION
+ # ------------------------------------------------------------
+
def open_track_speakers_list(self):
return {
'name': _('Speakers'),
@@ -166,3 +316,39 @@ class Track(models.Model):
'view_id': False,
'type': 'ir.actions.act_window',
}
+
+ # ------------------------------------------------------------
+ # TOOLS
+ # ------------------------------------------------------------
+
+ def _get_event_track_visitors(self, force_create=False):
+ self.ensure_one()
+
+ force_visitor_create = self.env.user._is_public()
+ visitor_sudo = self.env['website.visitor']._get_visitor_from_request(force_create=force_visitor_create)
+ if visitor_sudo:
+ visitor_sudo._update_visitor_last_visit()
+
+ if self.env.user._is_public():
+ domain = [('visitor_id', '=', visitor_sudo.id)]
+ elif visitor_sudo:
+ domain = [
+ '|',
+ ('partner_id', '=', self.env.user.partner_id.id),
+ ('visitor_id', '=', visitor_sudo.id)
+ ]
+ else:
+ domain = [('partner_id', '=', self.env.user.partner_id.id)]
+
+ track_visitors = self.env['event.track.visitor'].sudo().search(
+ expression.AND([domain, [('track_id', 'in', self.ids)]])
+ )
+ missing = self - track_visitors.track_id
+ if missing and force_create:
+ track_visitors += self.env['event.track.visitor'].sudo().create([{
+ 'visitor_id': visitor_sudo.id,
+ 'partner_id': self.env.user.partner_id.id if not self.env.user._is_public() else False,
+ 'track_id': track.id,
+ } for track in missing])
+
+ return track_visitors
diff --git a/addons/website_event_track/models/event_track_stage.py b/addons/website_event_track/models/event_track_stage.py
index 022237db4f5..9e1a177c0f3 100644
--- a/addons/website_event_track/models/event_track_stage.py
+++ b/addons/website_event_track/models/event_track_stage.py
@@ -18,6 +18,12 @@ class TrackStage(models.Model):
fold = fields.Boolean(
string='Folded in Kanban',
help='This stage is folded in the kanban view when there are no records in that stage to display.')
- is_done = fields.Boolean(string='Done Stage')
+ is_accepted = fields.Boolean(
+ string='Accepted Stage',
+ help='Accepted tracks are displayed in agenda views but not accessible.')
+ is_done = fields.Boolean(
+ string='Done Stage',
+ help='Done tracks are automatically published so that they are available in frontend.')
is_cancel = fields.Boolean(string='Canceled Stage')
+ is_done = fields.Boolean()
color = fields.Integer(string='Color')
diff --git a/addons/website_event_track/models/event_track_tag.py b/addons/website_event_track/models/event_track_tag.py
index 997a2c72f46..6efcba1e138 100644
--- a/addons/website_event_track/models/event_track_tag.py
+++ b/addons/website_event_track/models/event_track_tag.py
@@ -9,7 +9,7 @@ from odoo import fields, models
class TrackTag(models.Model):
_name = "event.track.tag"
_description = 'Event Track Tag'
- _order = 'name'
+ _order = "category_id, sequence, name"
def _default_color(self):
return randint(1, 11)
@@ -19,6 +19,8 @@ class TrackTag(models.Model):
color = fields.Integer(
string='Color Index', default=lambda self: self._default_color(),
help="Note that colorless tags won't be available on the website.")
+ sequence = fields.Integer('Sequence', default=10)
+ category_id = fields.Many2one('event.track.tag.category', string="Category", ondelete="set null")
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
diff --git a/addons/website_event_track/models/event_track_tag_category.py b/addons/website_event_track/models/event_track_tag_category.py
new file mode 100644
index 00000000000..05f14e7abed
--- /dev/null
+++ b/addons/website_event_track/models/event_track_tag_category.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import fields, models
+
+
+class TrackTagCategory(models.Model):
+ _name = "event.track.tag.category"
+ _description = 'Event Track Tag Category'
+ _order = "sequence"
+
+ name = fields.Char("Name", required=True, translate=True)
+ sequence = fields.Integer('Sequence', default=10)
+ tag_ids = fields.One2many('event.track.tag', 'category_id', string="Tags")
diff --git a/addons/website_event_track/models/event_track_visitor.py b/addons/website_event_track/models/event_track_visitor.py
new file mode 100644
index 00000000000..b84d7c6a4d2
--- /dev/null
+++ b/addons/website_event_track/models/event_track_visitor.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class TrackVisitor(models.Model):
+ """ Table linking track and visitors. """
+ _name = 'event.track.visitor'
+ _description = 'Track / Visitor Link'
+ _table = 'event_track_visitor'
+ _rec_name = 'track_id'
+ _order = 'track_id'
+
+ partner_id = fields.Many2one(
+ 'res.partner', string='Partner', compute='_compute_partner_id',
+ index=True, ondelete='set null', readonly=False, store=True)
+ visitor_id = fields.Many2one(
+ 'website.visitor', string='Visitor', index=True, ondelete='cascade')
+ track_id = fields.Many2one(
+ 'event.track', string='Track',
+ index=True, required=True, ondelete='cascade')
+ is_wishlisted = fields.Boolean(string="Is Wishlisted")
+ is_blacklisted = fields.Boolean(string="Is reminder off", help="As key track cannot be un-wishlisted, this field store the partner choice to remove the reminder for key tracks.")
+
+ @api.depends('visitor_id')
+ def _compute_partner_id(self):
+ for track_visitor in self:
+ if track_visitor.visitor_id.partner_id and not track_visitor.partner_id:
+ track_visitor.partner_id = track_visitor.visitor_id.partner_id
+ elif not track_visitor.partner_id:
+ track_visitor.partner_id = False
diff --git a/addons/website_event_track/models/website.py b/addons/website_event_track/models/website.py
new file mode 100644
index 00000000000..e00fde4b94f
--- /dev/null
+++ b/addons/website_event_track/models/website.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+
+from PIL import Image
+
+from odoo import api, fields, models
+from odoo.tools import ImageProcess
+
+
+class Website(models.Model):
+ _inherit = "website"
+
+ app_icon = fields.Image(string='Website App Icon', compute='_compute_app_icon', store=True, readonly=True, help='This field holds the image used as mobile app icon on the website (PNG format).')
+
+ @api.depends('favicon')
+ def _compute_app_icon(self):
+ """ Computes a squared image based on the favicon to be used as mobile webapp icon.
+ App Icon should be in PNG format and size of at least 512x512.
+ """
+ for website in self:
+ image = ImageProcess(website.favicon)
+ w, h = image.image.size
+ square_size = w if w > h else h
+ image.crop_resize(square_size, square_size)
+ image.image = image.image.resize((512, 512))
+ image.operationsCount += 1
+ website.app_icon = image.image_base64(output_format='PNG')
diff --git a/addons/website_event_track/models/website_visitor.py b/addons/website_event_track/models/website_visitor.py
new file mode 100644
index 00000000000..18072c86d11
--- /dev/null
+++ b/addons/website_event_track/models/website_visitor.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo import api, fields, models
+
+
+class WebsiteVisitor(models.Model):
+ _name = 'website.visitor'
+ _inherit = ['website.visitor']
+
+ event_track_visitor_ids = fields.One2many(
+ 'event.track.visitor', 'visitor_id', string="Track Visitors",
+ groups='event.group_event_user')
+ event_track_wishlisted_ids = fields.Many2many(
+ 'event.track', string="Wishlisted Tracks",
+ compute="_compute_event_track_wishlisted_ids", compute_sudo=True,
+ search="_search_event_track_wishlisted_ids",
+ groups="event.group_event_user")
+ event_track_wishlisted_count = fields.Integer(
+ string="# Wishlisted",
+ compute="_compute_event_track_wishlisted_ids", compute_sudo=True,
+ groups='event.group_event_user')
+
+ @api.depends('parent_id', 'event_track_visitor_ids.track_id', 'event_track_visitor_ids.is_wishlisted')
+ def _compute_event_track_wishlisted_ids(self):
+ # include parent's track visitors in a visitor o2m field. We don't add
+ # child one as child should not have track visitors (moved to the parent)
+ all_visitors = self + self.parent_id
+ results = self.env['event.track.visitor'].read_group(
+ [('visitor_id', 'in', all_visitors.ids), ('is_wishlisted', '=', True)],
+ ['visitor_id', 'track_id:array_agg'],
+ ['visitor_id']
+ )
+ track_ids_map = {result['visitor_id'][0]: result['track_id'] for result in results}
+ for visitor in self:
+ visitor_track_ids = track_ids_map.get(visitor.id, [])
+ parent_track_ids = track_ids_map.get(visitor.parent_id.id, [])
+ visitor.event_track_wishlisted_ids = visitor_track_ids + [track_id for track_id in parent_track_ids if track_id not in visitor_track_ids]
+ visitor.event_track_wishlisted_count = len(visitor.event_track_wishlisted_ids)
+
+ def _search_event_track_wishlisted_ids(self, operator, operand):
+ """ Search visitors with terms on wishlisted tracks. E.g. [('event_track_wishlisted_ids',
+ 'in', [1, 2])] should return visitors having wishlisted tracks 1, 2 as
+ well as their children for notification purpose. """
+ if operator == "not in":
+ raise NotImplementedError("Unsupported 'Not In' operation on track wishlist visitors")
+
+ track_visitors = self.env['event.track.visitor'].sudo().search([
+ ('track_id', operator, operand),
+ ('is_wishlisted', '=', True)
+ ])
+ if track_visitors:
+ visitors = track_visitors.visitor_id
+ # search children, even archived one, to contact them
+ children = self.env['website.visitor'].with_context(
+ active_test=False
+ ).sudo().search([('parent_id', 'in', visitors.ids)])
+ visitor_ids = (visitors + children).ids
+ else:
+ visitor_ids = []
+
+ return [('id', 'in', visitor_ids)]
+
+ def _link_to_partner(self, partner, update_values=None):
+ """ Propagate partner update to track_visitor records """
+ if partner:
+ track_visitor_wo_partner = self.event_track_visitor_ids.filtered(lambda track_visitor: not track_visitor.partner_id)
+ if track_visitor_wo_partner:
+ track_visitor_wo_partner.partner_id = partner
+ super(WebsiteVisitor, self)._link_to_partner(partner, update_values=update_values)
+
+ def _link_to_visitor(self, target, keep_unique=True):
+ """ Override linking process to link wishlist to the final visitor. """
+ self.event_track_visitor_ids.visitor_id = target.id
+ return super(WebsiteVisitor, self)._link_to_visitor(target, keep_unique=keep_unique)
diff --git a/addons/website_event_track/security/ir.model.access.csv b/addons/website_event_track/security/ir.model.access.csv
index 9a52bad509d..0cc16c6a31b 100644
--- a/addons/website_event_track/security/ir.model.access.csv
+++ b/addons/website_event_track/security/ir.model.access.csv
@@ -12,3 +12,7 @@ access_event_track_sponsor_manager,event.track.sponsor.type.public,model_event_s
access_event_track_sponsor_public,event.track.sponsor.public,model_event_sponsor,,1,0,0,0
access_event_track_stage_all,event.track.stage.all,model_event_track_stage,,1,0,0,0
access_event_track_stage_event_manager,event.track.stage.event.manager,model_event_track_stage,event.group_event_manager,1,1,1,1
+event_track_visitor_access_all,event.track.visitor.access.all,model_event_track_visitor,,0,0,0,0
+event_track_visitor_access_manager,event.track.visitor.access.manager,model_event_track_visitor,event.group_event_manager,1,1,1,1
+event_track_tag_category_access_public,event.track.tag.category.access,model_event_track_tag_category,,1,0,0,0
+event_track_tag_category_access_manager,event.track.tag.category.access.manager,model_event_track_tag_category,event.group_event_manager,1,1,1,1 \ No newline at end of file
diff --git a/addons/website_event_track/static/lib/idb-keyval/idb-keyval.js b/addons/website_event_track/static/lib/idb-keyval/idb-keyval.js
new file mode 100644
index 00000000000..c08909dc79a
--- /dev/null
+++ b/addons/website_event_track/static/lib/idb-keyval/idb-keyval.js
@@ -0,0 +1,81 @@
+// idb-keyval.js 3.2.0
+// https://github.com/jakearchibald/idb-keyval
+// Copyright 2016, Jake Archibald
+// Licensed under the Apache License, Version 2.0
+
+var idbKeyval = (function (exports) {
+ 'use strict';
+
+ class Store {
+ constructor(dbName = 'keyval-store', storeName = 'keyval') {
+ this.storeName = storeName;
+ this._dbp = new Promise((resolve, reject) => {
+ const openreq = indexedDB.open(dbName, 1);
+ openreq.onerror = () => reject(openreq.error);
+ openreq.onsuccess = () => resolve(openreq.result);
+ // First time setup: create an empty object store
+ openreq.onupgradeneeded = () => {
+ openreq.result.createObjectStore(storeName);
+ };
+ });
+ }
+ _withIDBStore(type, callback) {
+ return this._dbp.then(db => new Promise((resolve, reject) => {
+ const transaction = db.transaction(this.storeName, type);
+ transaction.oncomplete = () => resolve();
+ transaction.onabort = transaction.onerror = () => reject(transaction.error);
+ callback(transaction.objectStore(this.storeName));
+ }));
+ }
+ }
+ let store;
+ function getDefaultStore() {
+ if (!store)
+ store = new Store();
+ return store;
+ }
+ function get(key, store = getDefaultStore()) {
+ let req;
+ return store._withIDBStore('readonly', store => {
+ req = store.get(key);
+ }).then(() => req.result);
+ }
+ function set(key, value, store = getDefaultStore()) {
+ return store._withIDBStore('readwrite', store => {
+ store.put(value, key);
+ });
+ }
+ function del(key, store = getDefaultStore()) {
+ return store._withIDBStore('readwrite', store => {
+ store.delete(key);
+ });
+ }
+ function clear(store = getDefaultStore()) {
+ return store._withIDBStore('readwrite', store => {
+ store.clear();
+ });
+ }
+ function keys(store = getDefaultStore()) {
+ const keys = [];
+ return store._withIDBStore('readonly', store => {
+ // This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
+ // And openKeyCursor isn't supported by Safari.
+ (store.openKeyCursor || store.openCursor).call(store).onsuccess = function () {
+ if (!this.result)
+ return;
+ keys.push(this.result.key);
+ this.result.continue();
+ };
+ }).then(() => keys);
+ }
+
+ exports.Store = Store;
+ exports.get = get;
+ exports.set = set;
+ exports.del = del;
+ exports.clear = clear;
+ exports.keys = keys;
+
+ return exports;
+
+ }({}));
diff --git a/addons/website_event_track/static/src/js/event_track_reminder.js b/addons/website_event_track/static/src/js/event_track_reminder.js
new file mode 100644
index 00000000000..a4d86dd9452
--- /dev/null
+++ b/addons/website_event_track/static/src/js/event_track_reminder.js
@@ -0,0 +1,88 @@
+odoo.define('website_event_track.website_event_track_reminder', function (require) {
+'use strict';
+
+var core = require('web.core');
+var _t = core._t;
+var utils = require('web.utils');
+var publicWidget = require('web.public.widget');
+
+publicWidget.registry.websiteEventTrackReminder = publicWidget.Widget.extend({
+ selector: '.o_wetrack_js_reminder',
+ events: {
+ 'click': '_onReminderToggleClick',
+ },
+
+ /**
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this._onReminderToggleClick = _.debounce(this._onReminderToggleClick, 500, true);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //-------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {Event} ev
+ */
+ _onReminderToggleClick: function (ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ var self = this;
+ var $trackLink = $(ev.currentTarget).find('i');
+
+ if (this.reminderOn === undefined) {
+ this.reminderOn = $trackLink.data('reminderOn');
+ }
+
+ var reminderOnValue = !this.reminderOn;
+
+ this._rpc({
+ route: '/event/track/toggle_reminder',
+ params: {
+ track_id: $trackLink.data('trackId'),
+ set_reminder_on: reminderOnValue,
+ },
+ }).then(function (result) {
+ if (result.error && result.error === 'ignored') {
+ self.displayNotification({
+ type: 'info',
+ title: _t('Error'),
+ message: _.str.sprintf(_t('Talk already in your Favorites')),
+ });
+ } else {
+ self.reminderOn = reminderOnValue;
+ var reminderText = self.reminderOn ? _t('Reminder On') : _t('Set Reminder');
+ self.$('.o_wetrack_js_reminder_text').text(reminderText);
+ self._updateDisplay();
+ var message = self.reminderOn ? _t('Talk added to your Favorites') : _t('Talk removed from your Favorites');
+ self.displayNotification({
+ type: 'info',
+ title: message
+ });
+ }
+ if (result.visitor_uuid) {
+ utils.set_cookie('visitor_uuid', result.visitor_uuid);
+ }
+ });
+ },
+
+ _updateDisplay: function () {
+ var $trackLink = this.$el.find('i');
+ if (this.reminderOn) {
+ $trackLink.addClass('fa-bell').removeClass('fa-bell-o');
+ $trackLink.attr('title', _t('Reminder On'));
+ } else {
+ $trackLink.addClass('fa-bell-o').removeClass('fa-bell');
+ $trackLink.attr('title', _t('Set Reminder'));
+ }
+ },
+
+});
+
+return publicWidget.registry.websiteEventTrackReminder;
+
+});
diff --git a/addons/website_event_track/static/src/js/service_worker.js b/addons/website_event_track/static/src/js/service_worker.js
new file mode 100644
index 00000000000..a994c2123c4
--- /dev/null
+++ b/addons/website_event_track/static/src/js/service_worker.js
@@ -0,0 +1,280 @@
+importScripts("/website_event_track/static/lib/idb-keyval/idb-keyval.js");
+
+const PREFIX = "odoo-event";
+const SYNCABLE_ROUTES = ["/event/track/toggle_reminder"];
+const CACHABLE_ROUTES = ["/web/webclient/version_info"];
+
+const { Store, set, get, del } = idbKeyval;
+const pendingRequestsQueueName = `${PREFIX}-pending-requests`;
+const cacheName = `${PREFIX}-cache`;
+const syncStore = new Store(`${PREFIX}-sync-db`, `${PREFIX}-sync-store`);
+const cacheStore = new Store(`${PREFIX}-cache-db`, `${PREFIX}-cache-store`);
+
+/**
+ *
+ * @param {string} url
+ * @returns {string}
+ */
+const urlPathname = (url) => new URL(url).pathname;
+
+/**
+ *
+ * @param {Array} whitelist
+ * @returns {Function}
+ */
+const canHandleRoutes = (whitelist) => (url) => whitelist.includes(urlPathname(url));
+
+/**
+ *
+ * @param {Request} request
+ * @returns {boolean}
+ */
+const isGET = (request) => request.method === "GET";
+
+/**
+ *
+ * @returns {Function}
+ */
+const isSyncableURL = canHandleRoutes(SYNCABLE_ROUTES);
+
+/**
+ *
+ * @returns {Function}
+ */
+const isCachableURL = canHandleRoutes(CACHABLE_ROUTES);
+
+/**
+ *
+ * @param {Request} req
+ * @returns {Promise<Object>}
+ */
+const serializeRequest = async (req) => ({
+ url: req.url,
+ method: req.method,
+ headers: Object.fromEntries(req.headers.entries()),
+ body: await req.text(),
+ mode: req.mode,
+ credentials: req.credentials,
+ cache: req.cache,
+ redirect: req.redirect,
+ referrer: req.referrer,
+ integrity: req.integrity,
+});
+
+/**
+ *
+ * @param {Object} requestData
+ * @returns {Request}
+ */
+const deserializeRequest = (requestData) => {
+ const { url } = requestData;
+ delete requestData.url;
+ return new Request(url, requestData);
+};
+
+/**
+ *
+ * @param {Response} res
+ * @returns {Promise<Object>}
+ */
+const serializeResponse = async (res) => ({
+ body: await res.text(),
+ status: res.status,
+ statusText: res.statusText,
+ headers: Object.fromEntries(res.headers.entries()),
+});
+
+/**
+ *
+ * @param {Object} responseData
+ * @returns {Response}
+ */
+const deserializeResponse = (responseData) => {
+ const { body } = responseData;
+ delete responseData.body;
+ return new Response(body, responseData);
+};
+
+/**
+ *
+ * @param {Object} serializedRequest
+ * @returns {string}
+ */
+const buildCacheKey = ({ url, body: { method, params } }) =>
+ JSON.stringify({
+ url,
+ method,
+ params,
+ });
+
+/**
+ *
+ * @returns {int}
+ */
+const uniqueRequestId = () => Math.floor(Math.random() * 1000 * 1000 * 1000);
+
+/**
+ *
+ * @returns {Response}
+ */
+const buildEmptyResponse = () => new Response(JSON.stringify({ jsonrpc: "2.0", id: uniqueRequestId(), result: {} }));
+
+/**
+ *
+ * @param {Request} request
+ * @param {Response} response
+ * @returns {Promise}
+ */
+const cacheRequest = async (request, response) => {
+ if (isGET(request)) {
+ const cache = await caches.open(cacheName);
+ await cache.put(request, response.clone());
+ } else if (isCachableURL(request.url)) {
+ const serializedRequest = await serializeRequest(request);
+ const serializedResponse = await serializeResponse(response.clone());
+ await set(buildCacheKey(serializedRequest), serializedResponse, cacheStore);
+ }
+};
+
+/**
+ *
+ * @param {Request} request
+ * @returns {boolean}
+ */
+const isCachableRequest = (request) => isGET(request) || isCachableURL(request.url);
+
+/**
+ *
+ * @param {Request} request
+ * @returns {Promise<Response|null>}
+ */
+const matchCache = async (request) => {
+ if (isGET(request)) {
+ const cache = await caches.open(cacheName);
+ const response = await cache.match(request.url);
+ if (response) {
+ return deserializeResponse(await serializeResponse(response.clone()));
+ }
+ }
+ if (isCachableURL(request.url)) {
+ const serializedRequest = await serializeRequest(request);
+ const cachedResponse = await get(buildCacheKey(serializedRequest), cacheStore);
+ if (cachedResponse) {
+ return deserializeResponse(cachedResponse);
+ }
+ }
+ return null;
+};
+
+/**
+ *
+ * @param {FetchEvent} param0
+ * @returns {Promise<Response>}
+ */
+const processFetchEvent = async ({ request }) => {
+ const requestCopy = request.clone();
+ let response;
+ try {
+ response = await fetch(request);
+ await cacheRequest(request, response);
+ } catch (requestError) {
+ if (isCachableRequest(requestCopy)) {
+ response = await matchCache(requestCopy);
+ } else if (isSyncableURL(requestCopy.url)) {
+ const pendingRequests = (await get(pendingRequestsQueueName, syncStore)) || [];
+ const serializedRequest = await serializeRequest(requestCopy);
+ await set(pendingRequestsQueueName, [...pendingRequests, serializedRequest], syncStore);
+ if (self.registration.sync) {
+ await self.registration.sync.register(pendingRequestsQueueName).catch((err) => {
+ console.warn("Cannot use BackgroundSync", err);
+ throw requestError;
+ });
+ }
+ return buildEmptyResponse();
+ } else {
+ console.warn(`Offline ${requestCopy.method} request currently not supported`, requestCopy);
+ }
+
+ if (!response) {
+ throw requestError;
+ }
+ }
+ return response;
+};
+
+/**
+ *
+ * @returns {Promise}
+ */
+const processPendingRequests = async () => {
+ const pendingRequests = (await get(pendingRequestsQueueName, syncStore)) || [];
+ if (!pendingRequests.length) {
+ console.info("Nothing to sync!");
+ return;
+ }
+ let pendingRequest;
+ while ((pendingRequest = pendingRequests.shift())) {
+ const request = deserializeRequest(pendingRequest);
+ await fetch(request);
+ await set(pendingRequestsQueueName, pendingRequests, syncStore);
+ }
+};
+
+/**
+ * Add given urls to the Cache, skipping the ones already present
+ * @param {Array<string>} urls
+ */
+const prefetchUrls = async (urls = []) => {
+ const cache = await caches.open(cacheName);
+ let urlsToCache = new Set(urls);
+ for (let url of urlsToCache) {
+ (await cache.match(url)) ? urlsToCache.delete(url) : undefined;
+ }
+ return cache.addAll([...urlsToCache]);
+};
+
+/**
+ * Handle the message sent to the Worker (using the postMessage() method).
+ * The message is defined by the name of the action to perform and its associated parameters (optional).
+ *
+ * Actions:
+ * - prefetch-pages: add {Array} urls with their "alternative url" to the Cache (if not already present).
+ * - prefetch-assets: add {Array} urls to the Cache (if not already present).
+ *
+ * @param {Object} data
+ * @param {string} data.action action's name
+ * @param {*} data.* action's parameter(s)
+ * @returns {Promise}
+ */
+const processMessage = (data) => {
+ const { action } = data;
+ switch (action) {
+ case "prefetch-pages":
+ const { urls: pagesUrls } = data;
+ // To prevent redirection cached by the browser (cf. 301 Permanently Moved) from breaking the offline cache
+ // we also add alternative urls with the following rule:
+ // * if original url has a trailing "/", adds url with striped trailing "/"
+ // * if original url doesn't end with "/", adds url without the trailing "/"
+ const maybeRedirectedUrl = pagesUrls.map((url) => (url.endsWith("/") ? url.slice(0, -1) : url));
+ return prefetchUrls([...pagesUrls, ...maybeRedirectedUrl]);
+ case "prefetch-assets":
+ const { urls: assetsUrls } = data;
+ return prefetchUrls(assetsUrls);
+ }
+ throw new Error(`Action '${action}' not found.`);
+};
+
+self.addEventListener("fetch", (event) => {
+ event.respondWith(processFetchEvent(event));
+});
+
+self.addEventListener("sync", (event) => {
+ console.info(`Syncing pending requests...`);
+ if (event.tag === pendingRequestsQueueName) {
+ event.waitUntil(processPendingRequests());
+ }
+});
+
+self.addEventListener("message", (event) => {
+ event.waitUntil(processMessage(event.data));
+});
diff --git a/addons/website_event_track/static/src/js/website_event_pwa_widget.js b/addons/website_event_track/static/src/js/website_event_pwa_widget.js
new file mode 100644
index 00000000000..00c2a8ede33
--- /dev/null
+++ b/addons/website_event_track/static/src/js/website_event_pwa_widget.js
@@ -0,0 +1,208 @@
+odoo.define("website_event_track.website_event_pwa_widget", function (require) {
+ "use strict";
+
+ var config = require("web.config");
+ var publicWidget = require("web.public.widget");
+ var utils = require("web.utils");
+
+ var PWAInstallBanner = publicWidget.Widget.extend({
+ xmlDependencies: ["/website_event_track/static/src/xml/website_event_pwa.xml"],
+ template: "pwa_install_banner",
+ events: {
+ "click .o_btn_install": "_onClickInstall",
+ "click .o_btn_close": "_onClickClose",
+ },
+
+ /**
+ * @private
+ */
+ _onClickClose: function () {
+ this.trigger_up("prompt_close_bar");
+ },
+
+ /**
+ * @private
+ */
+ _onClickInstall: function () {
+ this.trigger_up("prompt_install");
+ },
+ });
+
+ publicWidget.registry.WebsiteEventPWAWidget = publicWidget.Widget.extend({
+ selector: "#wrapwrap.event",
+ custom_events: {
+ prompt_install: "_onPromptInstall",
+ prompt_close_bar: "_onPromptCloseBar",
+ },
+
+ /**
+ *
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.beforeInstallPromptHandler = this._onBeforeInstallPrompt.bind(this);
+ },
+
+ /**
+ *
+ * @override
+ */
+ start: function () {
+ var superProm = this._super.apply(this, arguments);
+ window.addEventListener("beforeinstallprompt", this.beforeInstallPromptHandler);
+ return superProm.then(this._registerServiceWorker.bind(this)).then(this._prefetch.bind(this));
+ },
+
+ /**
+ *
+ * @override
+ */
+ destroy: function () {
+ window.removeEventListener("beforeinstallprompt", this.beforeInstallPromptHandler);
+ this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Returns the website's language
+ * @private
+ * @return {String}
+ */
+ _getLangPrefix: function () {
+ var lang = utils.get_cookie("frontend_lang");
+ if (lang !== undefined && window.location.href.indexOf("/" + lang + "/") >= 0) {
+ return "/" + lang;
+ }
+ return "";
+ },
+
+ /**
+ * Returns the PWA's scope
+ * @private
+ * @returns {String}
+ */
+ _getScope: function () {
+ return this._getLangPrefix() + "/event";
+ },
+
+ /**
+ * @private
+ */
+ _hideInstallBanner: function () {
+ this.installBanner ? this.installBanner.destroy() : undefined;
+ $(".o_livechat_button").css("bottom", "0");
+ },
+
+ /**
+ * Parse the current page for first-level children pages and ask the ServiceWorker
+ * to already fetch them to populate the cache.
+ * @private
+ */
+ _prefetch: function () {
+ if (!("serviceWorker" in navigator)) {
+ return;
+ }
+ var assetsUrls = Array.from(document.querySelectorAll('link[rel="stylesheet"], script[src]')).map(function (el) {
+ return el.href || el.src;
+ });
+ navigator.serviceWorker.ready.then(function (registration) {
+ registration.active.postMessage({
+ action: "prefetch-assets",
+ urls: assetsUrls,
+ });
+ });
+ var currentPageUrl = window.location.href;
+ var childrenPagesUrls = Array.from(document.querySelectorAll('a[href^="' + this._getScope() + '/"]')).map(function (el) {
+ return el.href;
+ });
+ navigator.serviceWorker.ready.then(function (registration) {
+ registration.active.postMessage({
+ action: "prefetch-pages",
+ urls: childrenPagesUrls.concat(currentPageUrl),
+ });
+ });
+ },
+
+ /**
+ *
+ * @private
+ */
+ _registerServiceWorker: function () {
+ if (!("serviceWorker" in navigator)) {
+ return;
+ }
+ var scope = this._getScope();
+ return navigator.serviceWorker
+ .register(scope + "/service-worker.js", { scope: scope })
+ .then(function (registration) {
+ console.info("Registration successful, scope is:", registration.scope);
+ })
+ .catch(function (error) {
+ console.error("Service worker registration failed, error:", error);
+ });
+ },
+
+ /**
+ * @private
+ */
+ _showInstallBanner: function () {
+ var self = this;
+ this.installBanner = new PWAInstallBanner(this);
+ this.installBanner.appendTo(this.$el).then(function () {
+ // If Livechat available, It should be placed above the PWA banner.
+ var height = self.$(".o_pwa_install_banner").outerHeight(true);
+ $(".o_livechat_button").css("bottom", height + "px");
+ });
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param ev {Event}
+ */
+ _onBeforeInstallPrompt: function (ev) {
+ if (!config.device.isMobile) {
+ return;
+ }
+ ev.preventDefault();
+ this.deferredPrompt = ev;
+ this._showInstallBanner();
+ },
+ /**
+ * @private
+ * @param ev {Event}
+ */
+ _onPromptCloseBar: function (ev) {
+ ev.stopPropagation();
+ this._hideInstallBanner();
+ },
+ /**
+ * @private
+ * @param ev {Event}
+ */
+ _onPromptInstall: function (ev) {
+ ev.stopPropagation();
+ this.deferredPrompt.prompt();
+ this._hideInstallBanner();
+ this.deferredPrompt.userChoice.then(function (choiceResult) {
+ if (choiceResult.outcome === "accepted") {
+ console.log("User accepted the install prompt");
+ } else {
+ console.log("User dismissed the install prompt");
+ }
+ });
+ },
+ });
+
+ return {
+ PWAInstallBanner: PWAInstallBanner,
+ WebsiteEventPWAWidget: publicWidget.registry.WebsiteEventPWAWidget,
+ };
+});
diff --git a/addons/website_event_track/static/src/js/website_event_track_set_customize_options.js b/addons/website_event_track/static/src/js/website_event_track_set_customize_options.js
deleted file mode 100644
index c193b16b0c6..00000000000
--- a/addons/website_event_track/static/src/js/website_event_track_set_customize_options.js
+++ /dev/null
@@ -1,94 +0,0 @@
-odoo.define('website_event_track.set_customize_options', function (require) {
-"use strict";
-
-var EventSpecificOptions = require('website_event.set_customize_options').EventSpecificOptions;
-
-EventSpecificOptions.include({
- xmlDependencies: (EventSpecificOptions.prototype.xmlDependencies || [])
- .concat([
- '/website_event_track/static/src/xml/customize_options.xml',
- ]),
-
- events: _.extend({}, EventSpecificOptions.prototype.events, {
- 'change #display-showcase-talks': '_onDisplayTalksChange',
- 'change #allow-talk-proposal': '_onAllowTalkProposalChange',
- }),
-
- start: function () {
- this.$displayTalksInput = this.$('#display-showcase-talks');
- this.$allowTalksInput = this.$('#allow-talk-proposal');
- this._super.apply(this, arguments);
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- _onAllowTalkProposalChange: function () {
- var checkboxValue = this.$allowTalksInput.is(':checked');
- this._toggleTalkProposal(checkboxValue);
- },
-
- _onDisplayTalksChange: function () {
- var checkboxValue = this.$displayTalksInput.is(':checked');
- this._toggleDisplayTalks(checkboxValue);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- _getCheckboxFields: function () {
- var fields = this._super();
- fields = _.union(fields, ['website_track', 'website_track_proposal']);
- return fields;
- },
-
- _getCheckboxFieldMatch: function (checkboxField) {
- if (checkboxField === 'website_track') {
- return this.$displayTalksInput;
- }
- if (checkboxField === 'website_track_proposal') {
- return this.$allowTalksInput;
- }
- return this._super(checkboxField);
- },
-
- _initCheckboxCallback: function (rpcData) {
- this._super(rpcData);
- var submenuInput;
- if (rpcData[0]['website_track']) {
- submenuInput = this._getCheckboxFieldMatch('website_track');
- submenuInput.attr('checked', 'checked');
- }
- if (rpcData[0]['website_track_proposal']) {
- submenuInput = this._getCheckboxFieldMatch('website_track_proposal');
- submenuInput.attr('checked', 'checked');
- }
- },
-
- _toggleTalkProposal: function (val) {
- var self = this;
- this._rpc({
- model: this.modelName,
- method: 'toggle_website_track_proposal',
- args: [[this.eventId], val],
- }).then(function () {
- self._reloadEventPage();
- });
- },
-
- _toggleDisplayTalks: function (val) {
- var self = this;
- this._rpc({
- model: this.modelName,
- method: 'toggle_website_track',
- args: [[this.eventId], val],
- }).then(function () {
- self._reloadEventPage();
- });
- }
-
-});
-
-});
diff --git a/addons/website_event_track/static/src/scss/event_track_templates.scss b/addons/website_event_track/static/src/scss/event_track_templates.scss
new file mode 100644
index 00000000000..692fd0cd186
--- /dev/null
+++ b/addons/website_event_track/static/src/scss/event_track_templates.scss
@@ -0,0 +1,222 @@
+/*
+ * EVENT TOOL: REMINDER WIDGET
+ */
+.o_wevent_event .o_wetrack_js_reminder {
+ // Icon only
+ &.btn-link {
+ padding: 0;
+ }
+
+ // Ensure width for size coherency
+ &:not(.btn-link) {
+ min-width: 100px;
+ }
+
+ i {
+ &.fa-bell {
+ color: gold;
+ }
+ &.fa-bell-o {
+ color: black;
+ }
+ }
+}
+
+/*
+ * AGENDA
+ */
+.o_we_online_agenda {
+ overflow-x: scroll;
+
+ table {
+ border-collapse: separate;
+ border-spacing: 0em 0em;
+ tr {
+ height: 15px;
+ line-height: 1em;
+ &.active {
+ td.active {
+ padding: 0em 0.5em;
+ font-size: smaller;
+ border-top: 1px solid lightgrey;
+ }
+ }
+ }
+ th.active, td:not(.active) {
+ background-color: rgba(211, 211, 211, 0.1);
+ border: 0px;
+ border-right: 1em solid white;
+ vertical-align: middle;
+ span {
+ word-break: break-word;
+ }
+ }
+ th:not(.active), td.active {
+ width: 100px;
+ }
+ th.position-sticky {
+ left: 0;
+ }
+ td {
+ height: 0px;
+ border: 0px;
+
+ @for $size from 1 through 20 {
+ @if #{$size} != 1 {
+ &.o_location_size_#{$size} {
+ width: calc(100% / (#{$size} - 1));
+ min-width: 150px;
+ }
+ } @else {
+ width: calc(100%);
+ }
+ }
+
+ &.active {
+ position: sticky;
+ left: 0;
+ min-width: 100px;
+ background-color: white;
+ }
+ div.o_we_agenda_card_content {
+ height: 100%;
+ span {
+ cursor: pointer;
+ }
+ .o_we_agenda_card_title, small {
+ word-break: break-word;
+ }
+ }
+ .badge {
+ height: fit-content;
+ max-width: 100%;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ font-size: xx-small;
+ padding: 0.2em 0.5em;
+ border-radius: 1em;
+ line-height: 10px;
+ }
+ &.invisible {
+ visibility: visible !important;
+ opacity: 0.3;
+ }
+ &.o_we_agenda_time_slot_main, &.o_we_agenda_time_slot_half {
+ padding: 0;
+ }
+ &.o_we_agenda_time_slot_main > div {
+ padding: 0.3em;
+ border-top: 1px solid lightgrey;
+ }
+ &.o_we_agenda_time_slot_half > div {
+ padding: 0.3em;
+ border-top: 1px dashed lightgrey;
+ }
+ &.event_track {
+ padding: 0;
+ > div {
+ padding: 0.3em;
+ border-top: 1px solid lightgrey;
+ }
+
+ }
+ &.event_color_0 {
+ background-color: rgba(211, 211, 211, 0.5);
+ }
+ &.event_color_1 {
+ background-color: rgba(240, 96, 80, 0.2);
+ }
+ &.event_color_2 {
+ background-color: rgba(244, 164, 96, 0.2);
+ }
+ &.event_color_3 {
+ background-color: rgba(247, 205, 31, 0.2);
+ }
+ &.event_color_4 {
+ background-color: rgba(108,193,237,0.2);
+ }
+ &.event_color_5 {
+ background-color: rgba(129,73,104,0.2);
+ }
+ &.event_color_6 {
+ background-color: rgba(235,126,127,0.2);
+ }
+ &.event_color_7 {
+ background-color: rgba(44,131,151,0.2);
+ }
+ &.event_color_8 {
+ background-color: rgba(71,85,119,0.2);
+ }
+ &.event_color_9 {
+ background-color: rgba(214,20,95,0.2);
+ }
+ &.event_color_10 {
+ background-color: rgba(48,195,129,0.2);
+ }
+ &.event_color_11 {
+ background-color: rgba(147,101,184,0.2);
+ }
+ }
+ }
+}
+
+/*
+ * EVENT TOOL: SPONSOR WIDGET
+ */
+.o_wevent_event .o_wevent_sponsor_card {
+ position: relative;
+ width: 100px;
+ height: 100px;
+ border: 1px solid $gray-200;
+ background-color: $white;
+ margin: 0 -1px -1px 0;
+
+ .ribbon-wrapper {
+ width: 50px;
+ height: 50px;
+ }
+
+ .ribbon {
+ font: bold 10px Sans-Serif;
+ padding: 2px 0;
+ top: 10px;
+ width: 70px;
+ background-image: none; // not needed if colors for ribbons (Gold,Silver,Bronze) is removed in website_event_track.css
+
+ &.ribbon_Gold {
+ background-color: #e3aa24;
+ color:$white;
+ }
+
+ &.ribbon_Silver {
+ background-color: #adb5bd;
+ color: $white;
+ }
+
+ &.ribbon_Bronze {
+ background-color: #c7632a;
+ color: $white;
+ }
+ }
+
+ &:before {
+ content: "";
+ display: block;
+ @include o-position-absolute(0,0,0,0);
+ background-color: $black;
+ opacity: 0;
+ transition: opacity .3s;
+ }
+
+ &:hover:before {
+ opacity:.1;
+ }
+}
+
+/*
+ * EVENT TOOL: DATE
+ */
+.o_wevent_event .o_we_track_day_header > div > span.h1 {
+ font-size: xx-large;
+}
diff --git a/addons/website_event_track/static/src/scss/pwa_frontend.scss b/addons/website_event_track/static/src/scss/pwa_frontend.scss
new file mode 100644
index 00000000000..f132890229d
--- /dev/null
+++ b/addons/website_event_track/static/src/scss/pwa_frontend.scss
@@ -0,0 +1,35 @@
+@media all and (display-mode: standalone) {
+ #wrapwrap.event {
+ #top,
+ #bottom {
+ display: none;
+ }
+ }
+}
+
+#wrapwrap.event {
+ .o_pwa_install_banner {
+ position: fixed;
+ display: flex;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ margin: 0;
+ justify-content: space-between;
+ align-items: center;
+ background-color: rgba(255, 255, 255, 1);
+ border-top: 1px solid $gray-200;
+ box-shadow: 0 -0.125rem 0.25rem rgba(0, 0, 0, 0.075);
+ font-size: 1rem;
+ z-index: $zindex-tooltip;
+
+ .o_btn_close {
+ margin-left: 0;
+ margin-right: auto;
+ }
+
+ .o_btn_install {
+ margin-left: auto;
+ }
+ }
+}
diff --git a/addons/website_event_track/static/src/xml/customize_options.xml b/addons/website_event_track/static/src/xml/customize_options.xml
deleted file mode 100644
index b7d64bca0db..00000000000
--- a/addons/website_event_track/static/src/xml/customize_options.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<templates xml:space="preserve">
-
- <t t-name="website_event.customize_options" t-extend="website_event.customize_options">
- <t t-jquery="a[name='display-website-menu']" t-operation="after">
- <a href="#" name="display-showcase-talks" class="dropdown-item" role="menuitem">
- <label class="o_switch" for="display-showcase-talks">
- <input id="display-showcase-talks" type="checkbox"/>
- <span/>
- Showcase Talks
- </label>
- </a>
- <a href="#" name="allow-talk-proposal" class="dropdown-item" role="menuitem">
- <label class="o_switch" for="allow-talk-proposal">
- <input id="allow-talk-proposal" type="checkbox"/>
- <span/>
- Allow Talks Proposals
- </label>
- </a>
- </t>
- </t>
-
-</templates>
diff --git a/addons/website_event_track/static/src/xml/website_event_pwa.xml b/addons/website_event_track/static/src/xml/website_event_pwa.xml
new file mode 100644
index 00000000000..1105c4b382b
--- /dev/null
+++ b/addons/website_event_track/static/src/xml/website_event_pwa.xml
@@ -0,0 +1,11 @@
+<templates>
+
+ <t t-name="pwa_install_banner">
+ <div class="alert alert-default o_pwa_install_banner">
+ <button class="btn o_btn_close">x</button>
+ <strong>Install Application</strong>
+ <button class="btn btn-primary o_btn_install">Install</button>
+ </div>
+ </t>
+
+</templates>
diff --git a/addons/website_event_track/tests/__init__.py b/addons/website_event_track/tests/__init__.py
index 3773e715842..12b9da864c8 100644
--- a/addons/website_event_track/tests/__init__.py
+++ b/addons/website_event_track/tests/__init__.py
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import common
from . import test_website_event
diff --git a/addons/website_event_track/tests/common.py b/addons/website_event_track/tests/common.py
new file mode 100644
index 00000000000..5215f620950
--- /dev/null
+++ b/addons/website_event_track/tests/common.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from odoo.addons.website_event.tests.common import TestEventOnlineCommon
+
+
+class TestEventTrackOnlineCommon(TestEventOnlineCommon):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TestEventTrackOnlineCommon, cls).setUpClass()
+
+ cls.sponsor_type_0 = cls.env['event.sponsor.type'].create({
+ 'name': 'GigaTop',
+ 'sequence': 1,
+ })
+ cls.sponsor_0_partner = cls.env['res.partner'].create({
+ 'name': 'EventSponsor',
+ 'country_id': cls.env.ref('base.be').id,
+ 'email': 'event.sponsor@example.com',
+ 'phone': '04856112233',
+ })
+
+ cls.sponsor_0 = cls.env['event.sponsor'].create({
+ 'partner_id': cls.sponsor_0_partner.id,
+ 'event_id': cls.event_0.id,
+ 'sponsor_type_id': cls.sponsor_type_0.id,
+ })
diff --git a/addons/website_event_track/views/assets.xml b/addons/website_event_track/views/assets.xml
index d580f4e0106..120f828839d 100644
--- a/addons/website_event_track/views/assets.xml
+++ b/addons/website_event_track/views/assets.xml
@@ -2,10 +2,16 @@
<odoo>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Website Event Track Assets">
- <xpath expr="." position="inside">
+ <xpath expr="//link[last()]" position="after">
<link rel="stylesheet" href="/website_event_track/static/src/css/website_event_track.css"/>
+ <link rel="stylesheet" type="text/scss" href="/website_event_track/static/src/scss/event_track_templates.scss"/>
+ <link rel="stylesheet" type="text/scss" href="/website_event_track/static/src/scss/pwa_frontend.scss"/>
+ </xpath>
+ <xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website_event_track/static/src/js/website_event_track.js"></script>
- <script type="text/javascript" src="/website_event_track/static/src/js/website_event_track_set_customize_options.js"></script>
+ <script type="text/javascript" src="/website_event_track/static/src/js/event_track_reminder.js"></script>
+ <script type="text/javascript" src="/website_event_track/static/src/js/website_event_pwa_widget.js"/>
+ <script type="text/javascript" src="/website_event_track/static/lib/idb-keyval/idb-keyval.js"/>
</xpath>
</template>
diff --git a/addons/website_event_track/views/event_event_views.xml b/addons/website_event_track/views/event_event_views.xml
index 236f47a910b..a06ca4581ad 100644
--- a/addons/website_event_track/views/event_event_views.xml
+++ b/addons/website_event_track/views/event_event_views.xml
@@ -22,6 +22,12 @@
<field name="sponsor_count" string="Sponsors" widget="statinfo"/>
</button>
</xpath>
+ <xpath expr="//field[@name='website_menu']" position="after">
+ <label for="website_track" string="Showcase Tracks"/>
+ <field name="website_track"/>
+ <label for="website_track_proposal" string="Allow Track Proposals"/>
+ <field name="website_track_proposal"/>
+ </xpath>
</field>
</record>
diff --git a/addons/website_event_track/views/event_menus.xml b/addons/website_event_track/views/event_menus.xml
index cec7203d774..8ec7b7b2d95 100644
--- a/addons/website_event_track/views/event_menus.xml
+++ b/addons/website_event_track/views/event_menus.xml
@@ -24,6 +24,13 @@
groups="base.group_no_one"
sequence="32"/>
+ <menuitem id="event_track_tag_category_menu"
+ name="Track Tag Categories"
+ action="event_track_tag_category_action"
+ parent="event.menu_event_configuration"
+ groups="base.group_no_one"
+ sequence="33"/>
+
<menuitem
id="menu_event_track_tag"
name="Track Tags"
@@ -32,6 +39,13 @@
groups="base.group_no_one"
sequence="34"/>
+ <menuitem id="event_track_visitor_menu"
+ name="Track Visitors"
+ sequence="38"
+ action="event_track_visitor_action"
+ parent="event.menu_event_configuration"
+ groups="base.group_no_one"/>
+
<menuitem id="menu_event_sponsor_type"
name="Sponsor Types"
action="event_sponsor_type_action"
diff --git a/addons/website_event_track/views/event_sponsor_views.xml b/addons/website_event_track/views/event_sponsor_views.xml
index a7ee5442014..b38459122d5 100644
--- a/addons/website_event_track/views/event_sponsor_views.xml
+++ b/addons/website_event_track/views/event_sponsor_views.xml
@@ -10,6 +10,7 @@
<sheet>
<group>
<field name="name"/>
+ <field name="display_ribbon_style"/>
<field name="sequence" groups="base.group_no_one"/>
</group>
</sheet>
@@ -24,6 +25,7 @@
<tree editable="bottom" string="Event Sponsor Type">
<field name="sequence" widget="handle"/>
<field name="name"/>
+ <field name="display_ribbon_style"/>
</tree>
</field>
</record>
@@ -40,6 +42,9 @@
<search string="Event Sponsors">
<field name="partner_id"/>
<field name="event_id"/>
+ <field name="name"/>
+ <field name="email"/>
+ <field name="phone"/>
<filter string="Archived" name="archived" domain="[('active', '=', False)]"/>
<group string="Group By" expand="0">
<filter string="Event" name="group_by_event_id" domain="[]" context="{'group_by': 'event_id'}"/>
@@ -57,18 +62,24 @@
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
<field name="active" invisible="1"/>
- <field name="image_128" widget="image" class="oe_avatar"/>
- <h1><field name="partner_id"/></h1>
+ <field name="image_512" widget="image" class="oe_avatar"/>
+ <div class="oe_title">
+ <label for="name" class="oe_edit_only" string="Sponsor Name"/>
+ <h1><field name="name" placeholder="e.g. : OpenWood Decoration"/></h1>
+ </div>
<group>
<group>
<field name="event_id"/>
- <field name="partner_email" widget="email"/>
- <field name="url" widget="url" string="Website"/>
+ <field name="partner_id" string="Partner"/>
+ <field name="email" widget="email" string="Email"
+ placeholder="e.g. : open.wood.decoration@example.com"/>
+ <field name="url" widget="url" string="Website"
+ placeholder=">e.g. : https://www.odoo.com"/>
</group>
<group>
<field name="sponsor_type_id"/>
- <field name="partner_phone" widget="phone" options="{'enable_sms': True}"/>
- <field name="partner_mobile" widget="phone" options="{'enable_sms': True}"/>
+ <field name="phone" widget="phone" string="Phone" options="{'enable_sms': True}"/>
+ <field name="mobile" widget="phone" string="Mobile" options="{'enable_sms': True}"/>
</group>
</group>
</sheet>
@@ -85,9 +96,13 @@
<field name="name">event.sponsor.view.tree</field>
<field name="model">event.sponsor</field>
<field name="arch" type="xml">
- <tree>
+ <tree multi_edit="1">
<field name="sequence" widget="handle"/>
- <field name="partner_id"/>
+ <field name="partner_id" readonly="1"/>
+ <field name="name"/>
+ <field name="email"/>
+ <field name="phone"/>
+ <field name="mobile"/>
<field name="url" string="Website"/>
<field name="sponsor_type_id"/>
</tree>
@@ -133,5 +148,6 @@
</p>
</field>
</record>
+
</data>
</odoo>
diff --git a/addons/website_event_track/views/event_templates.xml b/addons/website_event_track/views/event_templates.xml
new file mode 100644
index 00000000000..891c374de1a
--- /dev/null
+++ b/addons/website_event_track/views/event_templates.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+<!-- Add a shortcut to wishlist / talks after registration -->
+<template id="registration_complete" inherit_id="website_event.registration_complete">
+ <xpath expr="//div[hasclass('row')][last()]" position="after">
+ <div t-if="event.website_track" class="row mt-5">
+ <div class="col-12">
+ <h3>Book your seats to the best talks</h3>
+ <p>Get prepared and
+ <a t-att-href="'/event/%s/track' % (slug(event))">register to your favorites talks now.</a>
+ </p>
+ </div>
+ </div>
+ </xpath>
+</template>
+
+<template id="layout" inherit_id="website_event.layout">
+ <xpath expr='//t[@t-call="website.layout"]' position="inside">
+ <t t-set="pageName" t-value="'event'"/>
+ <t t-set="head">
+ <t t-raw="head"/>
+ <t t-call="website_event_track.pwa_manifest"/>
+ </t>
+ </xpath>
+</template>
+
+<template id="index" inherit_id="website_event.index">
+ <xpath expr='//t[@t-call="website.layout"]' position="inside">
+ <t t-set="pageName" t-value="'event'"/>
+ <t t-set="head">
+ <t t-raw="head"/>
+ <t t-call="website_event_track.pwa_manifest"/>
+ </t>
+ </xpath>
+</template>
+
+<template id="pwa_manifest">
+ <link rel="manifest" href="/event/manifest.webmanifest" crossorigin="use-credentials"/>
+ <link rel="apple-touch-icon" t-att-href="website.image_url(website, 'app_icon', size='192x192')"/>
+ <meta name="theme-color" content="#875A7B"/>
+</template>
+
+</odoo>
diff --git a/addons/website_event_track/views/event_track_stage_views.xml b/addons/website_event_track/views/event_track_stage_views.xml
index 833f3c66472..9ee7adf3c2b 100644
--- a/addons/website_event_track/views/event_track_stage_views.xml
+++ b/addons/website_event_track/views/event_track_stage_views.xml
@@ -26,6 +26,7 @@
</group>
<group>
<field name="fold"/>
+ <field name="is_accepted"/>
<field name="is_done"/>
<field name="is_cancel"/>
<field name="color" widget="color_picker"/>
@@ -43,6 +44,9 @@
<tree string="Track Stage">
<field name="sequence" widget="handle" groups="event.group_event_manager"/>
<field name="name"/>
+ <field name="is_accepted"/>
+ <field name="is_done"/>
+ <field name="is_cancel"/>
<field name="fold"/>
</tree>
</field>
diff --git a/addons/website_event_track/views/event_track_tag_views.xml b/addons/website_event_track/views/event_track_tag_views.xml
index 7885bdea14f..780b9814b8b 100644
--- a/addons/website_event_track/views/event_track_tag_views.xml
+++ b/addons/website_event_track/views/event_track_tag_views.xml
@@ -1,7 +1,44 @@
<?xml version="1.0"?>
<odoo>
+ <record id="event_track_tag_category_view_form" model="ir.ui.view">
+ <field name="name">event.track.tag.category.view.form</field>
+ <field name="model">event.track.tag.category</field>
+ <field name="arch" type="xml">
+ <form string="Track Tag Category">
+ <sheet>
+ <group>
+ <field name="name"/>
+ <field name="tag_ids" context="{'default_category_id': active_id}">
+ <tree string="Tags" editable="bottom">
+ <field name="sequence" widget="handle"/>
+ <field name="name"/>
+ <field name="color" widget="color_picker"/>
+ </tree>
+ </field>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="event_track_tag_category_view_list" model="ir.ui.view">
+ <field name="name">event.track.tag.category.view.list</field>
+ <field name="model">event.track.tag.category</field>
+ <field name="arch" type="xml">
+ <tree string="Track Tags Category">
+ <field name="sequence" widget="handle"/>
+ <field name="name"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="event_track_tag_category_action" model="ir.actions.act_window">
+ <field name="name">Track Tag Categories</field>
+ <field name="res_model">event.track.tag.category</field>
+ <field name="view_mode">tree,form</field>
+ </record>
- <!-- EVENTS/CONFIGURATION/EVENT Tags -->
<record model="ir.ui.view" id="view_event_track_tag_form">
<field name="name">Track Tags</field>
<field name="model">event.track.tag</field>
@@ -10,6 +47,8 @@
<sheet>
<group>
<field name="name"/>
+ <field name="color" widget="color_picker"/>
+ <field name="category_id"/>
</group>
</sheet>
</form>
@@ -21,7 +60,10 @@
<field name="model">event.track.tag</field>
<field name="arch" type="xml">
<tree string="Event Track Tag" editable="bottom">
+ <field name="sequence" widget="handle"/>
<field name="name"/>
+ <field name="color" widget="color_picker"/>
+ <field name="category_id"/>
</tree>
</field>
</record>
diff --git a/addons/website_event_track/views/event_track_templates_agenda.xml b/addons/website_event_track/views/event_track_templates_agenda.xml
index 9864d568900..d7ddf00da0b 100644
--- a/addons/website_event_track/views/event_track_templates_agenda.xml
+++ b/addons/website_event_track/views/event_track_templates_agenda.xml
@@ -1,76 +1,194 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
-<template id="agenda">
+<!-- Revamped agenda : Will need to replace agenda_online from website_event_track in master -->
+<template id="agenda_online" name="Track Online: Agenda">
<t t-call="website_event.layout">
- <div class="oe_structure" id="oe_structure_website_event_track_agenda_1"/>
- <section class="container pt48">
- <h1 t-field="event.name"/>
- <div class="form-inline float-right">
- <label class="invisible text-muted" id="search_summary"><span id="search_number">0</span> Found </label>
- <input type="text" class="form-control" placeholder="Filter Tracks..." id="event_track_search"/>
+ <div class="o_wevent_online o_weagenda_index">
+ <!-- Options -->
+ <t t-set="option_track_wishlist" t-value="not event.is_done and request.website.viewref('website_event_track.agenda_topbar_wishlist').active"/>
+ <!-- Topbar -->
+ <t t-call="website_event_track.agenda_topbar"/>
+ <!-- Drag/Drop Area -->
+ <div class="oe_structure" id="oe_structure_website_event_track_agenda_1"/>
+ <!-- Content -->
+ <div class="container">
+ <div class="row mb-5">
+ <t t-call="website_event_track.agenda_main"/>
+ </div>
</div>
- </section>
- <t t-set="dayslist" t-value="list(days.keys())"/>
- <t t-set="dayslist2" t-value="dayslist.sort()"/> <!-- display days in the right order -->
- <section class="container" t-foreach="dayslist" t-as="day">
- <t t-set="locations" t-value="days[day]['locations']"/>
- <t t-set="dates" t-value="days[day]['dates']"/>
- <h3 class="o_page_header mt0">
- <span t-field="tracks_by_days[day][0].date" t-options="{'widget': 'date'}"/>
- <small><t t-esc="len(tracks_by_days[day])"/> talks</small>
- </h3>
- <table id="table_search" class="table table-bordered table-sm">
+ <!-- Drag/Drop Area -->
+ <div class="oe_structure" id="oe_structure_website_event_track_agenda_2"/>
+ </div>
+ </t>
+</template>
+
+<!-- ============================================================ -->
+<!-- TOPBAR: BASE NAVIGATION -->
+<!-- ============================================================ -->
+
+<!-- Main topbar -->
+<template id="agenda_topbar" name="Agenda Tools">
+ <nav class="navbar navbar-light border-top shadow-sm d-print-none">
+ <div class="container">
+ <div class="d-flex flex-column flex-sm-row justify-content-between w-100">
+ <ul class="o_weagenda_topbar_filters o_wevent_index_topbar_filters nav">
+ </ul>
+ <div class="form-inline pl-sm-3 pr-0">
+ <label class="invisible text-muted mr-2" id="search_summary"><span id="search_number" class="mr-1">0</span>Results</label>
+ <input type="text" class="form-control" placeholder="Filter Tracks..." id="event_track_search"/>
+ </div>
+ </div>
+ </div>
+ </nav>
+</template>
+
+<!-- Option: Tracks display: optional wishlist -->
+<template id="agenda_topbar_wishlist"
+ inherit_id="website_event_track.agenda_topbar"
+ name="Allow Wishlists"
+ active="True"
+ customize_show="True">
+ <xpath expr="//ul[hasclass('o_weagenda_topbar_filters')]" position="inside">
+ </xpath>
+</template>
+
+<!-- ============================================================ -->
+<!-- CONTENT: MAIN TEMPLATES -->
+<!-- ============================================================ -->
+
+<!-- Agenda Main Display -->
+<template id="agenda_main" name="Tracks: Main Display">
+ <!-- No tracks -->
+ <div class="col-12" t-if="not tracks_by_days">
+ <div class="h2 mb-3">No track found.</div>
+ <div t-if="search_key" class="alert alert-info text-center">
+ <p class="m-0">We did not find any track matching your <strong t-esc="search_key"/> search.</p>
+ </div>
+ <div t-else="" class="alert alert-info text-center" groups="event.group_event_manager">
+ <p class="m-0">Schedule some tracks to get started !</p>
+ </div>
+ </div>
+
+ <section t-else="" class="col-12" t-foreach="days" t-as="day">
+ <!-- DAY HEADER -->
+ <div class="o_we_track_day_header mt-3 w-100 d-flex justify-content-between align-items-center">
+ <div class="d-flex">
+ <span class="h1 m-0 font-weight-bold" t-esc="day"
+ t-options="{'widget': 'date', 'format': 'EEEE dd'}"/>
+ <div class="d-flex flex-column ml-2">
+ <span class="font-weight-bold" t-esc="day"
+ t-options="{'widget': 'date', 'format': 'MMMM'}"/>
+ <span class="font-weight-bold" t-esc="day"
+ t-options="{'widget': 'date', 'format': 'YYYY'}"/>
+ </div>
+ </div>
+ <small class="float-right text-muted align-self-end"><t t-esc="tracks_by_days[day]"/> tracks</small>
+ </div>
+ <hr class="mt-2 pb-1 mb-1"/>
+
+ <!-- Day Agenda -->
+ <div class="o_we_online_agenda">
+ <table id="table_search" class="table table-sm border-0">
+ <!--Header-->
<tr>
- <th/>
- <t t-foreach="locations.keys()" t-as="location">
- <th t-if="location" class="active">
+ <th class="border-0 bg-white position-sticky"/>
+ <t t-foreach="locations" t-as="location">
+ <th t-if="location" class="active text-center">
<span t-esc="location and location.name or 'Unknown'"/>
</th>
</t>
</tr>
- <tr t-foreach="dates" t-as="dt">
- <td class="active">
- <b t-esc="dt[-1]"/>
- </td>
- <t t-if="dt[2]"> <!-- Not a multi location -->
+
+ <!-- Time Slots -->
+ <t t-set="used_cells" t-value="[]"/>
+ <t t-foreach="time_slots[day]" t-as="time_slot">
+ <t t-set="is_round_hour" t-value="time_slot == time_slot.replace(minute=0)"/>
+ <t t-set="is_half_hour" t-value="time_slot == time_slot.replace(minute=30)"/>
+
+ <tr t-att-class="'%s' % ('active' if is_round_hour else '')">
+ <td class="active">
+ <b t-if="is_round_hour" t-esc="time_slots[day][time_slot]['formatted_time']"/>
+ </td>
+
<t t-foreach="locations" t-as="location">
- <t t-if="location and dt[1].get(location, False)">
- <t t-set="track" t-value="dt[1][location][0]"/>
- <t t-if="track">
- <td t-att-rowspan="dt[1][location][3]" t-attf-class="event_color_#{track.color} #{track and 'event_track' or ''}">
- <t t-if="track" name="track_agenda_track_cell">
- <a t-attf-href="/event/#{ slug(event) }/track/#{ slug(track) }">
- <span t-esc="track and track.name"/>
- <span t-if="not track.website_published" class="badge badge-warning">unpublished</span>
- </a>
- <div class="text-muted">
- <small t-if="track.partner_id" t-esc="track.partner_id.sudo().name"/>
- <small t-if="not track.partner_id" t-esc="track.partner_name"/>
- </div>
- </t>
- </td>
+ <t t-set="tracks" t-value="time_slots[day][time_slot].get(location, {})"/>
+ <t t-if="tracks">
+ <t t-foreach="tracks" t-as="track">
+ <t t-if="track.location_id and track.location_id == location">
+ <td t-att-rowspan="tracks[track]['rowspan']"
+ t-attf-class="text-center event_color_#{track.color} #{track and 'event_track' or ''} o_location_size_#{len(locations)}">
+ <t t-call="website_event_track.agenda_main_track"/>
+ </td>
+ </t>
+ <t t-else="">
+ <td t-att-colspan="len(locations)-1"
+ t-att-rowspan="tracks[track]['rowspan']"
+ t-attf-class="text-center event_color_#{track.color} #{track and 'event_track' or ''} o_location_size_#{len(locations)}">
+ <t t-call="website_event_track.agenda_main_track"/>
+ </td>
+ </t>
+ <t t-set="used_cells" t-value="used_cells + tracks[track]['occupied_cells']"/>
</t>
- <td t-if="not track" t-att-rowspan="dt[1][location][3]" class="event_track"/>
+ </t>
+ <t t-elif="location and (time_slot, location) not in used_cells">
+ <td t-att-rowspan="1"
+ t-att-class="'o_location_size_%s %s' % (len(locations),
+ 'o_we_agenda_time_slot_half' if is_half_hour else
+ 'o_we_agenda_time_slot_main' if is_round_hour else
+ ''
+ )"><div/></td>
</t>
</t>
- </t><t t-if="not dt[2]">
- <t t-set="track" t-value="dt[1][False][0]"/>
- <td t-att-colspan="len(locations)-1" t-attf-class="text-center event_color_#{track.color} #{track and 'event_track' or ''}">
- <a t-attf-href="/event/#{ slug(event) }/track/#{ slug(track) }">
- <span t-esc="track.name"/>
- </a>
- <div class="text-muted">
- <small t-if="track.partner_id" t-esc="track.partner_id.sudo().name"/>
- <small t-if="not track.partner_id" t-esc="track.partner_name"/>
- </div>
- </td>
+ </tr>
+ </t>
+ </table>
+ </div>
+ </section>
+</template>
+
+<template id="agenda_main_track" name="Track Agenda: Track">
+ <div class="d-flex flex-column h-100">
+ <div class="d-flex justify-content-end flex-wrap-reverse o_weagenda_track_badges">
+ <small t-if="not track.website_published and user_event_manager and track.stage_id.is_accepted"
+ title="Unpublished"
+ class="ml-1 badge badge-danger o_wevent_online_badge_unpublished">Unpublished</small>
+ <small t-if="not track.stage_id.is_accepted" title="Not Accepted"
+ class="ml-1 badge badge-danger o_wevent_online_badge_unpublished">Not Accepted</small>
+ <span t-if="option_track_wishlist">
+ <t t-call="website_event_track.track_widget_reminder">
+ <t t-set="reminder_light" t-value="True"/>
+ <t t-set="reminder_small" t-value="True"/>
+ <t t-set="light_theme" t-value="False"/>
</t>
- </tr>
- </table>
- </section>
- <div class="oe_structure" id="oe_structure_website_event_track_agenda_2"/>
- </t>
+ </span>
+ </div>
+
+ <div class="o_we_agenda_card_content d-flex flex-column justify-content-center my-1">
+ <div t-att-class="'text-black' if track.website_published or user_event_manager else 'text-muted'"
+ t-att-onclick="'window.location=\'/event/%s/track/%s\'' % (slug(event), slug(track))
+ if track.website_published or user_event_manager else ''">
+ <span class="o_we_agenda_card_title text-bold" t-esc="track.name"/>
+ </div>
+ <div class="text-muted text-center">
+ <small t-if="track.partner_id or track.partner_name">
+ <t t-esc="track.partner_id.sudo().name or track.partner_name"/></small>
+ </div>
+ <div class="d-flex justify-content-center flex-wrap">
+ <t t-foreach="track.tag_ids" t-as="tag">
+ <span t-if="tag.color" t-att-title="tag.name"
+ t-attf-class="mr-1 mt-1 badge #{'o_tag_color_'+str(tag.color)}" t-esc="tag.name"
+ t-attf-onclick="
+ var value = '#{tag.name}' ;
+ var target = $('#event_track_search');
+ if (target.val() == value) { target.val(''); } else { target.val(value); }
+ target.trigger('input');
+ "
+ />
+ </t>
+ </div>
+ </div>
+ </div>
</template>
</odoo>
diff --git a/addons/website_event_track/views/event_track_templates_list.xml b/addons/website_event_track/views/event_track_templates_list.xml
index 43c9a6efe3d..35cdf9d24ca 100644
--- a/addons/website_event_track/views/event_track_templates_list.xml
+++ b/addons/website_event_track/views/event_track_templates_list.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
-<template id="tracks">
+<template id="tracks" track="True">
<t t-call="website_event.layout">
<div class="oe_structure" id="oe_structure_website_event_track_tracks_1"/>
<section class="pt48 pb48">
@@ -76,4 +76,18 @@
</xpath>
</template>
+<!-- Add wishlist button on tracks / track templates -->
+<template id="tracks_wishlist"
+ name="Wishlist on Tracks"
+ inherit_id="website_event_track.tracks"
+ customize_show="True">
+ <xpath expr="//h3" position="replace">
+ <h3 class="mt0 mb0 d-flex">
+ <a t-attf-href="/event/#{ slug(event) }/track/#{ slug(track) }" class="text-secondary"><span t-field="track.name"> </span></a>
+ <small t-if="not track.website_published" class="badge badge-danger">unpublished</small>
+ <t t-call="website_event_track.track_widget_reminder"/>
+ </h3>
+ </xpath>
+</template>
+
</odoo>
diff --git a/addons/website_event_track/views/event_track_templates_misc.xml b/addons/website_event_track/views/event_track_templates_misc.xml
index b880c7db8c5..398390513fd 100644
--- a/addons/website_event_track/views/event_track_templates_misc.xml
+++ b/addons/website_event_track/views/event_track_templates_misc.xml
@@ -4,26 +4,31 @@
<template name="Sponsors" id="event_sponsor" customize_show="True" inherit_id="website_event.layout">
<xpath expr="//div[@id='wrap']" position="inside">
<div class="container mt32 mb16 d-print-none" t-if="event.sponsor_ids">
- <section>
- <h2 class="text-center mb32">Our Sponsors</h2>
- </section>
- <div class="row">
- <div t-attf-class="col-sm-6 col-lg-#{(len(event.sponsor_ids) > 6) and 2 or (12 // len(event.sponsor_ids))} text-center mb16 oe_sponsor" t-foreach="event.sponsor_ids" t-as="sponsor">
+ <div t-attf-class="d-flex flex-wrap mb-5 #{'' if (len(event.sponsor_ids) > 10) else 'justify-content-md-center'}">
+ <t t-foreach="event.sponsor_ids" t-as="sponsor">
<t t-if="sponsor.url">
- <a t-att-href="sponsor.url" class="bg-white o_wevent_sponsor">
- <span t-field="sponsor.image_128"
- t-options='{"widget": "image", "class": "shadow"}'/>
- <span t-field="sponsor.sponsor_type_id" t-attf-class="o_ribbon o_ribbon_right ribbon_#{sponsor.sponsor_type_id.name}"/>
+ <a class="o_wevent_sponsor_card" target="_blank" t-att-href="sponsor.url">
+ <div class="h-100 shadow-sm p-2">
+ <span t-field="sponsor.image_128"
+ t-options='{"widget": "image", "class": "img img-fluid"}'/>
+ <div t-if="sponsor.sponsor_type_id.display_ribbon_style and sponsor.sponsor_type_id.display_ribbon_style != 'no_ribbon'" class="ribbon-wrapper">
+ <div t-field="sponsor.sponsor_type_id" t-attf-class="ribbon ribbon_#{sponsor.sponsor_type_id.display_ribbon_style}"/>
+ </div>
+ </div>
</a>
</t>
<t t-if="not sponsor.url">
- <span class="bg-white o_wevent_sponsor">
- <span t-field="sponsor.image_128"
- t-options='{"widget": "image", "class": "shadow"}'/>
- <span t-field="sponsor.sponsor_type_id" t-attf-class="o_ribbon o_ribbon_right ribbon_#{sponsor.sponsor_type_id.name}"/>
- </span>
+ <div class="o_wevent_sponsor_card">
+ <div class="h-100 shadow-sm p-2">
+ <span t-field="sponsor.image_128"
+ t-options='{"widget": "image", "class": "img img-fluid"}'/>
+ <div t-if="sponsor.sponsor_type_id.display_ribbon_style and sponsor.sponsor_type_id.display_ribbon_style != 'no_ribbon'" class="ribbon-wrapper">
+ <div t-field="sponsor.sponsor_type_id" t-attf-class="ribbon ribbon_#{sponsor.sponsor_type_id.display_ribbon_style}"/>
+ </div>
+ </div>
+ </div>
</t>
- </div>
+ </t>
</div>
</div>
</xpath>
diff --git a/addons/website_event_track/views/event_track_templates_page.xml b/addons/website_event_track/views/event_track_templates_page.xml
index 593c7086503..5d4b5617b29 100644
--- a/addons/website_event_track/views/event_track_templates_page.xml
+++ b/addons/website_event_track/views/event_track_templates_page.xml
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
-<template id="track_view">
+<template id="track_view" track="True">
<t t-call="website_event.layout">
<section class="pt48">
<div class="container">
@@ -82,4 +82,18 @@
</t>
</template>
+<template id="track_view_wishlist"
+ name="Wishlist on Track"
+ inherit_id="website_event_track.track_view"
+ customize_show="True">
+ <xpath expr="//h1" position="replace">
+ <div class="d-flex">
+ <h1 t-field="track.name"/>
+ <div class="ml-2 pt-1">
+ <t t-call="website_event_track.track_widget_reminder"/>
+ </div>
+ </div>
+ </xpath>
+</template>
+
</odoo>
diff --git a/addons/website_event_track/views/event_track_templates_reminder.xml b/addons/website_event_track/views/event_track_templates_reminder.xml
new file mode 100644
index 00000000000..5731dc81ce7
--- /dev/null
+++ b/addons/website_event_track/views/event_track_templates_reminder.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo>
+
+<!-- Options
+ * reminder_light: no text displayed
+ * reminder_small: text displayed as small
+ * light_theme: bell is white/gold if set; otherwse bell is gray / white
+ -->
+<template id="track_widget_reminder">
+ <div t-att-class="'o_wetrack_js_reminder btn %s %s' % (
+ 'btn-link' if reminder_light else 'btn-outline-primary',
+ 'btn-sm' if reminder_small else '')">
+ <i t-att-class="'fa fa-bell%s inactive_color_%s' % ('' if track.is_reminder_on else '-o', 'dark' if reminder_light and not light_theme else 'light')"
+ t-att-data-track-id="track.id"
+ t-att-title="'Reminder On' if track.is_reminder_on else 'Set a Reminder'"
+ t-att-data-reminder-on="bool(track.is_reminder_on)"></i>
+ <span t-if="not reminder_light" class="o_wetrack_js_reminder_text">
+ <t t-if="not track.is_reminder_on">
+ Set Reminder
+ </t><t t-else="">
+ Reminder On
+ </t>
+ </span>
+ </div>
+</template>
+
+</odoo>
diff --git a/addons/website_event_track/views/event_track_views.xml b/addons/website_event_track/views/event_track_views.xml
index e100a56b554..3ff98ac1132 100644
--- a/addons/website_event_track/views/event_track_views.xml
+++ b/addons/website_event_track/views/event_track_views.xml
@@ -1,234 +1,268 @@
<?xml version="1.0"?>
<odoo>
+ <record id="website_visitor_action_from_track" model="ir.actions.act_window">
+ <field name="name">Visitors Wishlist</field>
+ <field name="res_model">website.visitor</field>
+ <field name="view_mode">kanban,tree,form,graph</field>
+ <field name="domain">[('event_track_wishlisted_ids', 'in', [active_id])]</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Wait for visitors to wishlist your tracks
+ </p>
+ </field>
+ </record>
- <!-- Event Tracks -->
- <record model="ir.ui.view" id="view_event_track_kanban">
- <field name="name">event.track.kanban</field>
- <field name="model">event.track</field>
- <field name="arch" type="xml">
- <kanban default_group_by="stage_id">
- <templates>
- <field name="color"/>
- <field name="partner_id"/>
- <field name="stage_id"/>
- <field name="website_url"/>
- <field name="activity_ids"/>
- <field name="activity_state"/>
- <progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger"}'/>
- <t t-name="kanban-box">
- <div t-attf-class="{{!selection_mode ? 'oe_kanban_color_' + kanban_getcolor(record.color.raw_value) : ''}} oe_kanban_card oe_kanban_global_click">
- <div class="o_dropdown_kanban dropdown" groups="base.group_user">
+ <record model="ir.ui.view" id="view_event_track_kanban">
+ <field name="name">event.track.kanban</field>
+ <field name="model">event.track</field>
+ <field name="arch" type="xml">
+ <kanban default_group_by="stage_id">
+ <templates>
+ <field name="color"/>
+ <field name="partner_id"/>
+ <field name="stage_id"/>
+ <field name="website_url"/>
+ <field name="activity_ids"/>
+ <field name="activity_state"/>
+ <progressbar field="kanban_state" colors='{"done": "success", "blocked": "danger"}'/>
+ <t t-name="kanban-box">
+ <div t-attf-class="{{!selection_mode ? 'oe_kanban_color_' + kanban_getcolor(record.color.raw_value) : ''}} oe_kanban_card oe_kanban_global_click">
+ <div class="o_dropdown_kanban dropdown" groups="base.group_user">
- <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
- <span class="fa fa-ellipsis-v"/>
- </a>
- <div class="dropdown-menu" role="menu">
- <a role="menuitem" t-att-href="record.website_url.value" class="dropdown-item">View Track</a>
- <t t-if="widget.editable"><a role="menuitem" type="edit" class="dropdown-item">Edit Track</a></t>
- <t t-if="widget.deletable"><a role="menuitem" type="delete" class="dropdown-item">Delete</a></t>
- <ul class="oe_kanban_colorpicker" data-field="color"/>
- </div>
+ <a role="button" class="dropdown-toggle o-no-caret btn" data-toggle="dropdown" href="#" aria-label="Dropdown menu" title="Dropdown menu">
+ <span class="fa fa-ellipsis-v"/>
+ </a>
+ <div class="dropdown-menu" role="menu">
+ <a role="menuitem" t-att-href="record.website_url.value" class="dropdown-item">View Track</a>
+ <t t-if="widget.editable"><a role="menuitem" type="edit" class="dropdown-item">Edit Track</a></t>
+ <t t-if="widget.deletable"><a role="menuitem" type="delete" class="dropdown-item">Delete</a></t>
+ <ul class="oe_kanban_colorpicker" data-field="color"/>
</div>
- <div class="oe_kanban_content">
- <div class="o_kanban_record_top">
- <h4 class="o_kanban_record_title"><field name="name"/></h4>
- </div>
- <div class="o_kanban_record_body">
- <t t-if="duration"><field name="duration" widget="float_time"/> hours</t>
- <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ </div>
+ <div class="oe_kanban_content">
+ <div class="o_kanban_record_top">
+ <h4 class="o_kanban_record_title"><field name="name"/></h4>
+ </div>
+ <div class="o_kanban_record_body">
+ <t t-if="duration"><field name="duration" widget="float_time"/> hours</t>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ </div>
+ <div class="o_kanban_record_bottom">
+ <div class="oe_kanban_bottom_left">
+ <field name="priority" widget="priority"/>
+ <field name="activity_ids" widget="kanban_activity"/>
</div>
- <div class="o_kanban_record_bottom">
- <div class="oe_kanban_bottom_left">
- <field name="priority" widget="priority"/>
- <field name="activity_ids" widget="kanban_activity"/>
- </div>
- <div class="oe_kanban_bottom_right">
- <field name="kanban_state" widget="state_selection" groups="base.group_user"/>
- <img t-att-src="kanban_image('res.partner', 'image_128', record.partner_id.raw_value)"
- t-att-title="record.partner_id.value" t-att-alt="record.partner_id.value"
- class="oe_kanban_avatar"/>
- </div>
+ <div class="oe_kanban_bottom_right">
+ <field name="kanban_state" widget="state_selection" groups="base.group_user"/>
+ <img t-att-src="kanban_image('res.partner', 'image_128', record.partner_id.raw_value)"
+ t-att-title="record.partner_id.value" t-att-alt="record.partner_id.value"
+ class="oe_kanban_avatar"/>
</div>
</div>
</div>
- </t>
- </templates>
- </kanban>
- </field>
- </record>
+ </div>
+ </t>
+ </templates>
+ </kanban>
+ </field>
+ </record>
- <record id="view_event_track_calendar" model="ir.ui.view">
- <field name="name">event.track.calendar</field>
- <field name="model">event.track</field>
- <field eval="2" name="priority"/>
- <field name="arch" type="xml">
- <calendar date_start="date" date_delay="duration" string="Event Tracks" color="location_id" event_limit="5">
- <field name="location_id" filters="1"/>
- <field name="event_id"/>
- <field name="partner_id" avatar_field="image_128"/>
- <field name="user_id" avatar_field="image_128"/>
- </calendar>
- </field>
- </record>
+ <record id="view_event_track_calendar" model="ir.ui.view">
+ <field name="name">event.track.calendar</field>
+ <field name="model">event.track</field>
+ <field eval="2" name="priority"/>
+ <field name="arch" type="xml">
+ <calendar date_start="date" date_delay="duration" string="Event Tracks" color="location_id" event_limit="5">
+ <field name="location_id" filters="1"/>
+ <field name="event_id"/>
+ <field name="partner_id" avatar_field="image_128"/>
+ <field name="user_id" avatar_field="image_128"/>
+ </calendar>
+ </field>
+ </record>
- <record model="ir.ui.view" id="view_event_track_search">
- <field name="name">event.track.search</field>
- <field name="model">event.track</field>
- <field name="arch" type="xml">
- <search string="Event Tracks">
- <field name="name"/>
- <field name="event_id"/>
- <field name="location_id"/>
- <field name="stage_id"/>
- <field name="partner_id"/>
- <filter string="My Tracks" name="my_tracks" domain="[('user_id', '=', uid)]"/>
- <separator/>
- <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
- <separator/>
- <filter name="filter_date" date="date"/>
- <separator/>
- <filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
- <separator/>
- <filter invisible="1" string="Late Activities" name="activities_overdue"
- domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
- help="Show all records which has next action date is before today"/>
- <filter invisible="1" string="Today Activities" name="activities_today"
- domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
- <filter invisible="1" string="Future Activities" name="activities_upcoming_all"
- domain="[('activity_ids.date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
- <group expand="0" string="Group By">
- <filter string="Responsible" name="responsible" context="{'group_by': 'user_id'}"/>
- <filter string="Stage" name="stage" context="{'group_by': 'stage_id'}"/>
- <filter string="Date" name="date" context="{'group_by': 'date'}"/>
- <filter string="Event" name="event" context="{'group_by': 'event_id'}"/>
- <filter string="Location" name="location" context="{'group_by': 'location_id'}"/>
- </group>
- </search>
- </field>
- </record>
+ <record model="ir.ui.view" id="view_event_track_search">
+ <field name="name">event.track.search</field>
+ <field name="model">event.track</field>
+ <field name="arch" type="xml">
+ <search string="Event Tracks">
+ <field name="name"/>
+ <field name="event_id"/>
+ <field name="location_id"/>
+ <field name="stage_id"/>
+ <field name="partner_id"/>
+ <filter string="My Tracks" name="my_tracks" domain="[('user_id', '=', uid)]"/>
+ <separator/>
+ <filter string="Unread Messages" name="message_needaction" domain="[('message_needaction', '=', True)]"/>
+ <separator/>
+ <filter string="Always Wishlisted" name="filter_wishlisted_by_default" domain="[('wishlisted_by_default', '=', True)]"/>
+ <separator/>
+ <filter name="filter_date" date="date"/>
+ <separator/>
+ <filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
+ <separator/>
+ <filter invisible="1" string="Late Activities" name="activities_overdue"
+ domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
+ help="Show all records which has next action date is before today"/>
+ <filter invisible="1" string="Today Activities" name="activities_today"
+ domain="[('activity_ids.date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ <filter invisible="1" string="Future Activities" name="activities_upcoming_all"
+ domain="[('activity_ids.date_deadline', '&gt;', context_today().strftime('%Y-%m-%d'))]"/>
+ <group expand="0" string="Group By">
+ <filter string="Responsible" name="responsible" context="{'group_by': 'user_id'}"/>
+ <filter string="Stage" name="stage" context="{'group_by': 'stage_id'}"/>
+ <filter string="Date" name="date" context="{'group_by': 'date'}"/>
+ <filter string="Event" name="event" context="{'group_by': 'event_id'}"/>
+ <filter string="Location" name="location" context="{'group_by': 'location_id'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
- <record model="ir.ui.view" id="view_event_track_form">
- <field name="name">event.track.form</field>
- <field name="model">event.track</field>
- <field name="arch" type="xml">
- <form string="Event Track">
- <header>
- <field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/>
- </header>
- <sheet string="Track">
- <div class="oe_button_box" name="button_box">
- <field name="website_url" invisible="1"/>
- <field name="is_published" widget="website_redirect_button"/>
- </div>
- <widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
- <field name="kanban_state" widget="state_selection" class="ml-3 float-right"/>
- <field name="image" widget="image" class="oe_avatar"/>
- <div class="oe_title">
- <label for="name" class="oe_edit_only"/>
- <h1>
- <field name="name" placeholder="e.g. Inspiring Business Talk"/>
- </h1>
- </div>
+ <record model="ir.ui.view" id="view_event_track_form">
+ <field name="name">event.track.form</field>
+ <field name="model">event.track</field>
+ <field name="arch" type="xml">
+ <form string="Event Track">
+ <header>
+ <field name="stage_id" widget="statusbar" options="{'clickable': '1'}"/>
+ </header>
+ <sheet string="Track">
+ <div class="oe_button_box" name="button_box">
+ <field name="website_url" invisible="1"/>
+ <button name="%(website_event_track.website_visitor_action_from_track)d"
+ type="action"
+ class="oe_stat_button"
+ icon="fa-bell">
+ <field name="wishlist_visitor_count" string="Wishlisted By" widget="statinfo"/>
+ </button>
+ <field name="is_published" widget="website_redirect_button"/>
+ </div>
+ <widget name="web_ribbon" text="Archived" bg_color="bg-danger" attrs="{'invisible': [('active', '=', True)]}"/>
+ <field name="kanban_state" widget="state_selection" class="ml-3 float-right"/>
+ <field name="website_image" widget="image" class="oe_avatar"/>
+ <div class="oe_title">
+ <label for="name" class="oe_edit_only"/>
+ <h1>
+ <field name="name" placeholder="e.g. Inspiring Business Talk"/>
+ </h1>
+ </div>
+ <group>
<group>
+ <field name="date"/>
+ <field name="location_id"/>
+ <label for="duration"/>
+ <div class="o_row">
+ <field name="duration" widget="float_time"/>
+ <span>hours</span>
+ </div>
+ <field name="active" invisible="1"/>
+ <field name="wishlisted_by_default"/>
+ </group>
+ <group>
+ <field name="company_id" invisible="1"/>
+ <field name="user_id" domain="[('share', '=', False)]"/>
+ <field name="event_id"/>
+ <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
+ <field name="color" widget="color_picker"/>
+ </group>
+ </group>
+ <notebook>
+ <page string="Speakers" name="speakers">
<group>
- <field name="date"/>
- <field name="location_id"/>
- <label for="duration"/>
- <div class="o_row">
- <field name="duration" widget="float_time"/>
- <span>hours</span>
- </div>
- <field name="active" invisible="1"/>
- </group>
- <group>
- <field name="company_id" invisible="1"/>
- <field name="user_id" domain="[('share', '=', False)]"/>
- <field name="event_id"/>
- <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
- <field name="color" widget="color_picker"/>
+ <field name="partner_id"
+ domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"
+ context="{'default_phone': partner_phone, 'default_email': partner_email}"/>
+ <field name="partner_name"/>
+ <field name="partner_email"/>
+ <field name="partner_phone" class="o_force_ltr"/>
+ <field name="partner_biography"/>
+ <field name="image" widget="image" class="oe_avatar float-left"/>
</group>
- </group>
- <notebook>
- <page string="Speakers" name="speakers">
- <group>
- <field name="partner_id"
- domain="['|', ('company_id', '=', company_id), ('company_id', '=', False)]"
- context="{'default_phone': partner_phone, 'default_email': partner_email}"/>
- <field name="partner_name"/>
- <field name="partner_email"/>
- <field name="partner_phone" class="o_force_ltr"/>
- <field name="partner_biography"/>
- </group>
- </page>
- <page string="Description" name="description">
- <field name="description"/>
- </page>
- </notebook>
- </sheet>
- <div class="oe_chatter">
- <field name="message_follower_ids"/>
- <field name="activity_ids"/>
- <field name="message_ids" options="{'post_refresh': 'recipients'}"/>
- </div>
- </form>
- </field>
- </record>
+ </page>
+ <page string="Description" name="description">
+ <field name="description"/>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids"/>
+ <field name="activity_ids"/>
+ <field name="message_ids" options="{'post_refresh': 'recipients'}"/>
+ </div>
+ </form>
+ </field>
+ </record>
- <record model="ir.ui.view" id="view_event_track_tree">
- <field name="name">event.track.tree</field>
- <field name="model">event.track</field>
- <field name="arch" type="xml">
- <tree string="Event Track">
- <field name="name"/>
- <field name="active" invisible="1"/>
- <field name="partner_name"/>
- <field name="partner_id" optional="hide"/>
- <field name="partner_email"/>
- <field name="partner_phone"/>
- <field name="event_id" invisible="context.get('default_event_id')"/>
- <field name="stage_id"/>
- <field name="color" widget="color_picker" optional="hide"/>
- <field name="activity_exception_decoration" widget="activity_exception"/>
- </tree>
- </field>
- </record>
+ <record model="ir.ui.view" id="view_event_track_tree">
+ <field name="name">event.track.tree</field>
+ <field name="model">event.track</field>
+ <field name="arch" type="xml">
+ <tree string="Event Track">
+ <field name="name"/>
+ <field name="active" invisible="1"/>
+ <field name="partner_name"/>
+ <field name="partner_id" optional="hide"/>
+ <field name="partner_email"/>
+ <field name="partner_phone"/>
+ <field name="event_id" invisible="context.get('default_event_id')"/>
+ <field name="wishlisted_by_default" optional="hide"/>
+ <field name="wishlist_visitor_count" optional="hide"/>
+ <field name="stage_id"/>
+ <field name="color" widget="color_picker" optional="hide"/>
+ <field name="activity_exception_decoration" widget="activity_exception"/>
+ </tree>
+ </field>
+ </record>
+
+ <record model="ir.ui.view" id="view_event_track_graph">
+ <field name="name">event.track.graph</field>
+ <field name="model">event.track</field>
+ <field name="arch" type="xml">
+ <graph string="Tracks" type="bar" orientation="horizontal" sample="1">
+ <field name="location_id"/>
+ <field name="duration" operator="+"/>
+ </graph>
+ </field>
+ </record>
- <record model="ir.ui.view" id="view_event_track_graph">
- <field name="name">event.track.graph</field>
- <field name="model">event.track</field>
- <field name="arch" type="xml">
- <graph string="Tracks" type="bar" orientation="horizontal" sample="1">
- <field name="location_id"/>
- <field name="duration" operator="+"/>
- </graph>
- </field>
- </record>
+ <record model="ir.actions.act_window" id="action_event_track">
+ <field name="name">Event Tracks</field>
+ <field name="res_model">event.track</field>
+ <field name="view_mode">kanban,tree,form,calendar,graph,activity</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Add a new track
+ </p><p>
+ Tracks define the schedule of your event.<br/>These can be a talk, a round table, a meeting, etc.
+ </p>
+ </field>
+ </record>
- <record model="ir.actions.act_window" id="action_event_track">
- <field name="name">Event Tracks</field>
- <field name="res_model">event.track</field>
- <field name="view_mode">kanban,tree,form,calendar,graph,activity</field>
- <field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
- Add a new track
- </p><p>
- Tracks define the schedule of your event.<br/>These can be a talk, a round table, a meeting, etc.
- </p>
- </field>
- </record>
+ <record model="ir.actions.act_window" id="action_event_track_from_event">
+ <field name="res_model">event.track</field>
+ <field name="name">Event Tracks</field>
+ <field name="view_mode">kanban,tree,form,calendar,graph,activity</field>
+ <field name="context">{'search_default_event_id': active_id, 'default_event_id': active_id}</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Add a new track
+ </p><p>
+ Tracks define the schedule of your event.<br/>These can be a talk, a round table, a meeting, etc.
+ </p>
+ </field>
+ </record>
- <record model="ir.actions.act_window" id="action_event_track_from_event">
- <field name="res_model">event.track</field>
- <field name="name">Event Tracks</field>
- <field name="view_mode">kanban,tree,form,calendar,graph,activity</field>
- <field name="context">{'search_default_event_id': active_id, 'default_event_id': active_id}</field>
- <field name="help" type="html">
- <p class="o_view_nocontent_smiling_face">
- Add a new track
- </p><p>
- Tracks define the schedule of your event.<br/>These can be a talk, a round table, a meeting, etc.
- </p>
- </field>
- </record>
+ <record id="event_track_action_from_visitor" model="ir.actions.act_window">
+ <field name="name">Wishlisted Tracks</field>
+ <field name="res_model">event.track</field>
+ <field name="view_mode">kanban,tree,form</field>
+ <field name="domain">[('wishlist_visitor_ids', 'in', [active_id])]</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_empty_folder">
+ No track wishlisted by this visitor
+ </p>
+ </field>
+ </record>
</odoo>
diff --git a/addons/website_event_track/views/event_track_visitor_views.xml b/addons/website_event_track/views/event_track_visitor_views.xml
new file mode 100644
index 00000000000..2a90e7f5d46
--- /dev/null
+++ b/addons/website_event_track/views/event_track_visitor_views.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0"?>
+<odoo><data>
+
+ <record id="event_track_visitor_view_search" model="ir.ui.view" >
+ <field name="name">event.track.visitor.view.search</field>
+ <field name="model">event.track.visitor</field>
+ <field name="arch" type="xml">
+ <search string="Track Visitors">
+ <field name="track_id"/>
+ <field name="visitor_id"/>
+ <field name="partner_id"/>
+ <field name="is_wishlisted"/>
+ <group string="Group By" expand="0">
+ <filter string="Track" name="groupby_track_id" context="{'group_by': 'track_id'}"/>
+ <filter string="Visitor" name="groupby_visitor_id" context="{'group_by': 'visitor_id'}"/>
+ <filter string="Customer" name="groupby_partner_id" context="{'group_by': 'partner_id'}"/>
+ </group>
+ </search>
+ </field>
+ </record>
+
+ <record id="event_track_visitor_view_form" model="ir.ui.view">
+ <field name="name">event.track.visitor.view.form</field>
+ <field name="model">event.track.visitor</field>
+ <field name="arch" type="xml">
+ <form string="Track Visitor">
+ <sheet string="Track Visitor">
+ <group>
+ <group>
+ <field name="track_id"/>
+ <field name="is_wishlisted"/>
+ </group>
+ <group>
+ <field name="visitor_id"/>
+ <field name="partner_id"/>
+ </group>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="event_track_visitor_view_list" model="ir.ui.view">
+ <field name="name">event.track.visitor.view.list</field>
+ <field name="model">event.track.visitor</field>
+ <field name="arch" type="xml">
+ <tree string="Track Visitors">
+ <field name="track_id"/>
+ <field name="visitor_id"/>
+ <field name="partner_id"/>
+ <field name="is_wishlisted"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="event_track_visitor_action" model="ir.actions.act_window">
+ <field name="name">Track Visitors</field>
+ <field name="res_model">event.track.visitor</field>
+ <field name="view_mode">tree,form</field>
+ <field name="help" type="html">
+ <p class="o_view_nocontent_smiling_face">
+ Add a new track visitor
+ </p><p>
+ Track Visitors store statistics on track attendance, including
+ wishlist management.
+ </p>
+ </field>
+ </record>
+
+</data></odoo>
diff --git a/addons/website_event_track/views/website_visitor_views.xml b/addons/website_event_track/views/website_visitor_views.xml
new file mode 100644
index 00000000000..171186d73ca
--- /dev/null
+++ b/addons/website_event_track/views/website_visitor_views.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<odoo><data>
+
+ <record id="website_visitor_view_tree" model="ir.ui.view">
+ <field name="name">website.visitor.view.tree.inherit.event.track</field>
+ <field name="model">website.visitor</field>
+ <field name="inherit_id" ref="website_event.website_visitor_view_tree"/>
+ <field name="arch" type="xml">
+ <xpath expr="//field[@name='event_registration_count']" position="after">
+ <field name="event_track_wishlisted_count"/>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="website_visitor_view_form" model="ir.ui.view">
+ <field name="name">website.visitor.view.form.inherit.event.track</field>
+ <field name="model">website.visitor</field>
+ <field name="inherit_id" ref="website_event.website_visitor_view_form"/>
+ <field name="arch" type="xml">
+ <xpath expr="//button[@id='w_visitor_visit_counter']" position="before">
+ <button name="%(website_event_track.event_track_action_from_visitor)d"
+ type="action"
+ class="oe_stat_button" icon="fa-ticket"
+ attrs="{'invisible': [('event_track_wishlisted_count', '=', 0)]}">
+ <field name="event_track_wishlisted_count" widget="statinfo" string="Tracks"/>
+ </button>
+ </xpath>
+ </field>
+ </record>
+
+ <record id="website_visitor_view_kanban" model="ir.ui.view">
+ <field name="name">website.visitor.view.kanban.inherit.event.track</field>
+ <field name="model">website.visitor</field>
+ <field name="inherit_id" ref="website_event.website_visitor_view_kanban"/>
+ <field name="arch" type="xml">
+ <field name="page_ids" position="after">
+ <field name="event_track_wishlisted_count"/>
+ </field>
+ <xpath expr="//div[@id='o_page_count']" position="after">
+ <div>Wishlisted Tracks<span class="float-right font-weight-bold"><field name="event_track_wishlisted_count"/></span></div>
+ </xpath>
+ <xpath expr="//div[hasclass('w_visitor_kanban_actions_ungrouped')]" position="before">
+ <div class="col">
+ <b><field name="event_track_wishlisted_count"/></b>
+ <div>Wishlisted Tracks</div>
+ </div>
+ </xpath>
+ </field>
+ </record>
+
+</data></odoo>