diff --git a/addons/website_event_track_online/__init__.py b/addons/website_event_track_online/__init__.py index 66a1383e8f3..5a31f331a2a 100644 --- a/addons/website_event_track_online/__init__.py +++ b/addons/website_event_track_online/__init__.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import controllers +from . import models from . import tests diff --git a/addons/website_event_track_online/__manifest__.py b/addons/website_event_track_online/__manifest__.py index 95db4436abf..c85e7abeea5 100644 --- a/addons/website_event_track_online/__manifest__.py +++ b/addons/website_event_track_online/__manifest__.py @@ -16,8 +16,18 @@ 'website_event_track', ], 'data': [ + 'security/ir.model.access.csv', + 'views/assets.xml', + 'views/event_track_templates_reminder.xml', + 'views/event_track_templates.xml', + 'views/event_track_views.xml', + 'views/event_track_visitor_views.xml', + 'views/website_visitor_views.xml', + 'views/event_menus.xml', ], 'demo': [ + 'data/event_track_demo.xml', + 'data/event_track_visitor_demo.xml', ], 'application': False, 'installable': True, diff --git a/addons/website_event_track_online/controllers/__init__.py b/addons/website_event_track_online/controllers/__init__.py new file mode 100644 index 00000000000..e3798014924 --- /dev/null +++ b/addons/website_event_track_online/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_track diff --git a/addons/website_event_track_online/controllers/event_track.py b/addons/website_event_track_online/controllers/event_track.py new file mode 100644 index 00000000000..5217765f3fc --- /dev/null +++ b/addons/website_event_track_online/controllers/event_track.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from werkzeug.exceptions import Forbidden, NotFound + +from odoo import exceptions, http +from odoo.http import request + + +class WebsiteEventTrackController(http.Controller): + + 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 + + @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 diff --git a/addons/website_event_track_online/data/event_track_demo.xml b/addons/website_event_track_online/data/event_track_demo.xml new file mode 100644 index 00000000000..79bcd16929d --- /dev/null +++ b/addons/website_event_track_online/data/event_track_demo.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/addons/website_event_track_online/data/event_track_visitor_demo.xml b/addons/website_event_track_online/data/event_track_visitor_demo.xml new file mode 100644 index 00000000000..3217d3ce709 --- /dev/null +++ b/addons/website_event_track_online/data/event_track_visitor_demo.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/addons/website_event_track_online/models/__init__.py b/addons/website_event_track_online/models/__init__.py new file mode 100644 index 00000000000..645b184349c --- /dev/null +++ b/addons/website_event_track_online/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_track_visitor +from . import event_track +from . import website_visitor diff --git a/addons/website_event_track_online/models/event_track.py b/addons/website_event_track_online/models/event_track.py new file mode 100644 index 00000000000..6413c411506 --- /dev/null +++ b/addons/website_event_track_online/models/event_track.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +from odoo.osv import expression + + +class Track(models.Model): + _name = "event.track" + _inherit = ['event.track'] + + # 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.""") + + # 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)] + + # ------------------------------------------------------------ + # 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_online/models/event_track_visitor.py b/addons/website_event_track_online/models/event_track_visitor.py new file mode 100644 index 00000000000..b84d7c6a4d2 --- /dev/null +++ b/addons/website_event_track_online/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_online/models/website_visitor.py b/addons/website_event_track_online/models/website_visitor.py new file mode 100644 index 00000000000..18072c86d11 --- /dev/null +++ b/addons/website_event_track_online/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_online/security/ir.model.access.csv b/addons/website_event_track_online/security/ir.model.access.csv new file mode 100644 index 00000000000..d756041a499 --- /dev/null +++ b/addons/website_event_track_online/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +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 diff --git a/addons/website_event_track_online/static/src/js/event_track_reminder.js b/addons/website_event_track_online/static/src/js/event_track_reminder.js new file mode 100644 index 00000000000..cd0f59a44ef --- /dev/null +++ b/addons/website_event_track_online/static/src/js/event_track_reminder.js @@ -0,0 +1,86 @@ +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'); + } + + this._rpc({ + route: '/event/track/toggle_reminder', + params: { + track_id: $trackLink.data('trackId'), + set_reminder_on: !this.reminderOn + }, + }).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 = result.reminderOn; + 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_online/static/src/scss/event_track_templates.scss b/addons/website_event_track_online/static/src/scss/event_track_templates.scss new file mode 100644 index 00000000000..133862c3997 --- /dev/null +++ b/addons/website_event_track_online/static/src/scss/event_track_templates.scss @@ -0,0 +1,44 @@ +/* + * EVENT TOOL: REMINDER WIDGET + */ +.o_wevent_event .o_wetrack_js_reminder { + width: fit-content; + height: fit-content; + cursor: pointer; + background: grey; + + i { + &.fa-bell { + color: gold; + } + &.fa-bell-o { + color: white + } + } + + // Icon only + &.o_wetrack_reminder_light { + background: transparent; + padding: 0; + i { + &.inactive_color_dark { + color: grey; + } + } + &:not(.o_wetrack_reminder_small) { + i { + font-size: 1.5em; + } + } + } + + // Text included + &:not(.o_wetrack_reminder_light) { + &.o_wetrack_reminder_small { + padding: 0.25rem 0.50rem; + span { + min-width: 70px; + } + } + } +} diff --git a/addons/website_event_track_online/views/assets.xml b/addons/website_event_track_online/views/assets.xml new file mode 100644 index 00000000000..ca095992bb0 --- /dev/null +++ b/addons/website_event_track_online/views/assets.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/addons/website_event_track_online/views/event_menus.xml b/addons/website_event_track_online/views/event_menus.xml new file mode 100644 index 00000000000..a4685ab02d6 --- /dev/null +++ b/addons/website_event_track_online/views/event_menus.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/addons/website_event_track_online/views/event_track_templates.xml b/addons/website_event_track_online/views/event_track_templates.xml new file mode 100644 index 00000000000..6ca0cfbc5de --- /dev/null +++ b/addons/website_event_track_online/views/event_track_templates.xml @@ -0,0 +1,31 @@ + + + + + + + + unpublished + + + + + + + + + + + + + + + + + diff --git a/addons/website_event_track_online/views/event_track_templates_reminder.xml b/addons/website_event_track_online/views/event_track_templates_reminder.xml new file mode 100644 index 00000000000..2771cda1307 --- /dev/null +++ b/addons/website_event_track_online/views/event_track_templates_reminder.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/addons/website_event_track_online/views/event_track_views.xml b/addons/website_event_track_online/views/event_track_views.xml new file mode 100644 index 00000000000..6499c869e39 --- /dev/null +++ b/addons/website_event_track_online/views/event_track_views.xml @@ -0,0 +1,69 @@ + + + + Visitors Wishlist + website.visitor + kanban,tree,form,graph + [('event_track_wishlisted_ids', 'in', [active_id])] + + + Wait for visitors to wishlist your tracks + + + + + + event.track.view.search.inherit.event.track.online + event.track + + + + + + + + + + event.track.view.tree.inherit.event.track.online + event.track + + + + + + + + + + + event.track.view.form.inherit.event.track.online + event.track + + + + + + + + + + + + + + + Wishlisted Tracks + event.track + kanban,tree,form + [('wishlist_visitor_ids', 'in', [active_id])] + + + No track wishlisted by this visitor + + + + + diff --git a/addons/website_event_track_online/views/event_track_visitor_views.xml b/addons/website_event_track_online/views/event_track_visitor_views.xml new file mode 100644 index 00000000000..2a90e7f5d46 --- /dev/null +++ b/addons/website_event_track_online/views/event_track_visitor_views.xml @@ -0,0 +1,70 @@ + + + + + event.track.visitor.view.search + event.track.visitor + + + + + + + + + + + + + + + + + event.track.visitor.view.form + event.track.visitor + + + + + + + + + + + + + + + + + + + + event.track.visitor.view.list + event.track.visitor + + + + + + + + + + + + Track Visitors + event.track.visitor + tree,form + + + Add a new track visitor + + Track Visitors store statistics on track attendance, including + wishlist management. + + + + + diff --git a/addons/website_event_track_online/views/website_visitor_views.xml b/addons/website_event_track_online/views/website_visitor_views.xml new file mode 100644 index 00000000000..c5ee8456984 --- /dev/null +++ b/addons/website_event_track_online/views/website_visitor_views.xml @@ -0,0 +1,51 @@ + + + + + website.visitor.view.tree.inherit.event.track + website.visitor + + + + + + + + + + website.visitor.view.form.inherit.event.track + website.visitor + + + + + + + + + + + + website.visitor.view.kanban.inherit.event.track + website.visitor + + + + + + + Wishlisted Tracks + + + + + Wishlisted Tracks + + + + + +
+ Wait for visitors to wishlist your tracks +
+ No track wishlisted by this visitor +
+ Add a new track visitor +
+ Track Visitors store statistics on track attendance, including + wishlist management. +