diff options
| author | Thibault Delavallée <tde@odoo.com> | 2020-08-20 09:16:38 +0000 |
|---|---|---|
| committer | Thibault Delavallée <tde@odoo.com> | 2020-08-21 17:30:13 +0000 |
| commit | 1163013ed6bf191a9e1548866a4df699f72b5bf4 (patch) | |
| tree | 8d54ebf3fe6a76ff69ad163541870eac938c0c7c /addons/website_event_track | |
| parent | b0845c0f2b48d6c7029a34ae50fc95876630e7ed (diff) | |
[REM] website_event_track_online: merge into website_event_track
PURPOSE
Clean organization and models linked to Event Online feature introduced in
semi stable saas-13.3 at odoo/odoo@981f95bbf46658eda9f2d33751b9310cc67d86aa and odoo/enterprise@14722028ae0773c3710ea48ce48ff44703e83b5a
Also clean website event menu not being available in website event but only
in website event track, which implies some extra-code to manage it.
In short: merge website_event_online in website_event, website_event_track
_(online/session) in website_event_track.
RATIONALE
_online modules have been added to extend content of website_event and
website_event_track without having any impact on those module. First step
of cleaning is to move this content directly in base module.
track_session module is mainly a rewrite of track module. Second step of
cleaning is to move its content directly in website_event_track.
SPECIFICATIONS
Move remaining content of website event track online to website event track.
Agenda notably is completely replaced by the new one developed within
track_online module.
Update dependencies accordingly.
LINKS
Task ID-2319779
COM odoo/odoo#56067
ENT odoo/enterprise#12520
UPG odoo/upgrade#1654
Diffstat (limited to 'addons/website_event_track')
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', '<', 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', '>', 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', '<', 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', '>', 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> |
