From 8793b9b4251a7aa0e71da1dbc5738cd15d0b5dd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 24 Jun 2020 08:19:26 +0000 Subject: [PATCH 01/28] [ADD] website_event_*: add Online Event support base modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Add base modules for some incoming Online Event features. SPECIFICATIONS This commit simply adds modules to be completed while working on new Online Event feature. This is mainly done to ease future work as Online Event was developed in a stable release, leading to some restrictions for pre-merging and organizing commits. Containing : * website_jitsi: allow to manage jitsi-based chat rooms, with widget to integrate into website pages; * website_event_online: website_event upgrades required to support online event; * website_event_track_online: website_event_track upgrades required to support online tracks; * website_event_meet: chat and discussions rooms online, allowing to gather a community online in an event; * website_event_track_exhibitor: sponsors -> exhibitors upgrade with chat capabilities, new list and page views; * website_event_track_session: new session-based tracks display and management, new list and page views; * website_event_track_live: support youtube and streaming on tracks, as well as improved next video suggestion; * website_event_track_quiz: add gamification tools on tracks. Allow to gain points with quiz on talks; As well as some bridge modules * website_event_meet_quiz: integrate leaderboard in community rooms; * website_event_track_live_quiz: integrate quiz and streaming; Future commits will fill those modules LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow) Task ID-2299857 (Event Design Review) Task ID-2284043 (Visitor-based track wishlist) Task ID-2283869 (Notify attendees by push) Task ID-2283738 (Sponsors -> Exhibitors upgrade) Task ID-2283735 (Talks -> Session upgrade) Task ID-2283771 (Quizzes on Talks, Leaderboard) Task ID-2283742 (Community gathering on chat rooms) Task ID-2291344 (Integrate Twitter Wall) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Quentin Mourier Co-Authored-By: Stéphane Debauche Co-Authored-By: Thibault Delavallée --- addons/gamification_quiz/__init__.py | 2 ++ addons/gamification_quiz/__manifest__.py | 23 +++++++++++++++++ addons/website_event_meet/__init__.py | 2 ++ addons/website_event_meet/__manifest__.py | 22 ++++++++++++++++ addons/website_event_meet_quiz/__init__.py | 2 ++ .../website_event_meet_quiz/__manifest__.py | 23 +++++++++++++++++ addons/website_event_online/__init__.py | 2 ++ addons/website_event_online/__manifest__.py | 22 ++++++++++++++++ .../website_event_track_exhibitor/__init__.py | 2 ++ .../__manifest__.py | 23 +++++++++++++++++ addons/website_event_track_live/__init__.py | 2 ++ .../website_event_track_live/__manifest__.py | 23 +++++++++++++++++ .../website_event_track_live_quiz/__init__.py | 2 ++ .../__manifest__.py | 25 +++++++++++++++++++ addons/website_event_track_online/__init__.py | 2 ++ .../__manifest__.py | 24 ++++++++++++++++++ addons/website_event_track_quiz/__init__.py | 2 ++ .../website_event_track_quiz/__manifest__.py | 23 +++++++++++++++++ .../website_event_track_session/__init__.py | 2 ++ .../__manifest__.py | 22 ++++++++++++++++ addons/website_jitsi/__init__.py | 2 ++ addons/website_jitsi/__manifest__.py | 18 +++++++++++++ 22 files changed, 270 insertions(+) create mode 100644 addons/gamification_quiz/__init__.py create mode 100644 addons/gamification_quiz/__manifest__.py create mode 100644 addons/website_event_meet/__init__.py create mode 100644 addons/website_event_meet/__manifest__.py create mode 100644 addons/website_event_meet_quiz/__init__.py create mode 100644 addons/website_event_meet_quiz/__manifest__.py create mode 100644 addons/website_event_online/__init__.py create mode 100644 addons/website_event_online/__manifest__.py create mode 100644 addons/website_event_track_exhibitor/__init__.py create mode 100644 addons/website_event_track_exhibitor/__manifest__.py create mode 100644 addons/website_event_track_live/__init__.py create mode 100644 addons/website_event_track_live/__manifest__.py create mode 100644 addons/website_event_track_live_quiz/__init__.py create mode 100644 addons/website_event_track_live_quiz/__manifest__.py create mode 100644 addons/website_event_track_online/__init__.py create mode 100644 addons/website_event_track_online/__manifest__.py create mode 100644 addons/website_event_track_quiz/__init__.py create mode 100644 addons/website_event_track_quiz/__manifest__.py create mode 100644 addons/website_event_track_session/__init__.py create mode 100644 addons/website_event_track_session/__manifest__.py create mode 100644 addons/website_jitsi/__init__.py create mode 100644 addons/website_jitsi/__manifest__.py diff --git a/addons/gamification_quiz/__init__.py b/addons/gamification_quiz/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/gamification_quiz/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/gamification_quiz/__manifest__.py b/addons/gamification_quiz/__manifest__.py new file mode 100644 index 00000000000..c766813717b --- /dev/null +++ b/addons/gamification_quiz/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Gamification Quizzes', + 'category': 'Marketing/Events', + 'sequence': 1000, + 'version': '1.0', + 'summary': 'Gamification quizzes and leaderboard', + 'website': 'https://www.odoo.com/page/events', + 'description': "", + 'depends': [ + 'gamification', + 'website_profile', + ], + 'data': [ + ], + 'demo': [ + ], + 'application': False, + 'installable': True, +} diff --git a/addons/website_event_meet/__init__.py b/addons/website_event_meet/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/website_event_meet/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/website_event_meet/__manifest__.py b/addons/website_event_meet/__manifest__.py new file mode 100644 index 00000000000..2856e3e88f9 --- /dev/null +++ b/addons/website_event_meet/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Event Meeting / Rooms', + 'category': 'Marketing/Events', + 'sequence': 1002, + 'version': '1.0', + 'summary': 'Event: meeting and chat rooms', + 'website': 'https://www.odoo.com/page/events', + 'description': "", + 'depends': [ + 'website_event_online', + ], + 'data': [ + ], + 'demo': [ + ], + 'application': False, + 'installable': True, +} diff --git a/addons/website_event_meet_quiz/__init__.py b/addons/website_event_meet_quiz/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/website_event_meet_quiz/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/website_event_meet_quiz/__manifest__.py b/addons/website_event_meet_quiz/__manifest__.py new file mode 100644 index 00000000000..00e95e9faea --- /dev/null +++ b/addons/website_event_meet_quiz/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Quiz and Meet on community', + 'category': 'Marketing/Events', + 'sequence': 1007, + 'version': '1.0', + 'summary': 'Quiz and Meet on community route', + 'website': 'https://www.odoo.com/page/events', + 'description': "", + 'depends': [ + 'website_event_meet', + 'website_event_track_quiz', + ], + 'data': [ + ], + 'demo': [ + ], + 'application': False, + 'auto_install': True, +} diff --git a/addons/website_event_online/__init__.py b/addons/website_event_online/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/website_event_online/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/website_event_online/__manifest__.py b/addons/website_event_online/__manifest__.py new file mode 100644 index 00000000000..0aef242191d --- /dev/null +++ b/addons/website_event_online/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Events Online', + 'category': 'Marketing/Events', + 'sequence': 1001, + 'version': '1.0', + 'summary': 'Bridge module to support Online Events', + 'website': 'https://www.odoo.com/page/events', + 'description': "", + 'depends': [ + 'website_event' + ], + 'data': [ + ], + 'demo': [ + ], + 'application': False, + 'installable': True, +} diff --git a/addons/website_event_track_exhibitor/__init__.py b/addons/website_event_track_exhibitor/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/website_event_track_exhibitor/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/website_event_track_exhibitor/__manifest__.py b/addons/website_event_track_exhibitor/__manifest__.py new file mode 100644 index 00000000000..0cbcc6f0c7e --- /dev/null +++ b/addons/website_event_track_exhibitor/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Event Exhibitors', + 'category': 'Marketing/Events', + 'sequence': 1004, + 'version': '1.0', + 'summary': 'Event: upgrade sponsors to exhibitors', + 'website': 'https://www.odoo.com/page/events', + 'description': "", + 'depends': [ + 'website_event_track_online', + 'website_jitsi', + ], + 'data': [ + ], + 'demo': [ + ], + 'application': False, + 'installable': True, +} diff --git a/addons/website_event_track_live/__init__.py b/addons/website_event_track_live/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/website_event_track_live/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/website_event_track_live/__manifest__.py b/addons/website_event_track_live/__manifest__.py new file mode 100644 index 00000000000..98e2c69bc9a --- /dev/null +++ b/addons/website_event_track_live/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Live Event Tracks', + 'category': 'Marketing/Events', + 'sequence': 1006, + 'version': '1.0', + 'summary': 'Support live tracks: streaming, participation, youtube', + 'website': 'https://www.odoo.com/page/events', + 'description': "", + 'depends': [ + 'website_event_track_online', + 'website_event_track_session', + ], + 'data': [ + ], + 'demo': [ + ], + 'application': False, + 'installable': True, +} diff --git a/addons/website_event_track_live_quiz/__init__.py b/addons/website_event_track_live_quiz/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/website_event_track_live_quiz/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/website_event_track_live_quiz/__manifest__.py b/addons/website_event_track_live_quiz/__manifest__.py new file mode 100644 index 00000000000..ec6a1d8c9d5 --- /dev/null +++ b/addons/website_event_track_live_quiz/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Quiz on Live Event Tracks', + 'category': 'Hidden', + 'version': '1.0', + 'summary': 'Bridge module to support quiz features during "live" tracks. ', + 'website': 'https://www.odoo.com/page/events', + 'description': "", + 'depends': [ + 'website_event_track_live', + 'website_event_track_quiz', + ], + 'data': [ + ], + 'demo': [ + ], + 'qweb': [ + ], + 'application': False, + 'installable': True, + 'auto_install': True, +} diff --git a/addons/website_event_track_online/__init__.py b/addons/website_event_track_online/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/website_event_track_online/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/website_event_track_online/__manifest__.py b/addons/website_event_track_online/__manifest__.py new file mode 100644 index 00000000000..95db4436abf --- /dev/null +++ b/addons/website_event_track_online/__manifest__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Tracks Online', + 'category': 'Marketing/Events', + 'sequence': 1003, + 'version': '1.0', + 'summary': 'Bridge module to support Online Events Tracks', + 'website': 'https://www.odoo.com/page/events', + 'description': "", + 'depends': [ + 'sms', + 'website_event_online', + 'website_event_track', + ], + 'data': [ + ], + 'demo': [ + ], + 'application': False, + 'installable': True, +} diff --git a/addons/website_event_track_quiz/__init__.py b/addons/website_event_track_quiz/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/website_event_track_quiz/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/website_event_track_quiz/__manifest__.py b/addons/website_event_track_quiz/__manifest__.py new file mode 100644 index 00000000000..54aa74573c7 --- /dev/null +++ b/addons/website_event_track_quiz/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Quizzes on Tracks', + 'category': 'Marketing/Events', + 'sequence': 1007, + 'version': '1.0', + 'summary': 'Quizzes on tracks', + 'website': 'https://www.odoo.com/page/events', + 'description': "", + 'depends': [ + 'gamification_quiz', + 'website_event_track_online', + ], + 'data': [ + ], + 'demo': [ + ], + 'application': True, + 'installable': True, # set to False +} diff --git a/addons/website_event_track_session/__init__.py b/addons/website_event_track_session/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/website_event_track_session/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/website_event_track_session/__manifest__.py b/addons/website_event_track_session/__manifest__.py new file mode 100644 index 00000000000..4738c7ea203 --- /dev/null +++ b/addons/website_event_track_session/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + 'name': 'Event Live Sessions', + 'category': 'Marketing/Events', + 'sequence': 1005, + 'version': '1.0', + 'summary': 'Event: live session on tracks', + 'website': 'https://www.odoo.com/page/events', + 'description': "", + 'depends': [ + 'website_event_track_online', + ], + 'data': [ + ], + 'demo': [ + ], + 'application': False, + 'installable': True, +} diff --git a/addons/website_jitsi/__init__.py b/addons/website_jitsi/__init__.py new file mode 100644 index 00000000000..67dee8c60db --- /dev/null +++ b/addons/website_jitsi/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. diff --git a/addons/website_jitsi/__manifest__.py b/addons/website_jitsi/__manifest__.py new file mode 100644 index 00000000000..cec3158a81a --- /dev/null +++ b/addons/website_jitsi/__manifest__.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + + +{ + "name": "Website Jitsi", + 'category': 'Hidden', + 'version': '1.0', + "summary": "Create Jitsi room on website.", + 'website': 'https://www.odoo.com/page/events', + "description": "Create Jitsi room on website.", + "depends": [ + "website" + ], + "data": [ + ], + 'application': False, +} From 506832302d15e747bfed83ad585931587ea58c6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 6 Jul 2020 12:09:47 +0000 Subject: [PATCH 02/28] [IMP] website_event_online: add base for tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE In this commit we add base for tests linked to online event and all its sub modules. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- addons/website_event/controllers/main.py | 2 +- addons/website_event_online/__init__.py | 2 + addons/website_event_online/tests/__init__.py | 4 ++ addons/website_event_online/tests/common.py | 68 +++++++++++++++++++ addons/website_event_track_online/__init__.py | 2 + .../tests/__init__.py | 4 ++ .../tests/common.py | 28 ++++++++ 7 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 addons/website_event_online/tests/__init__.py create mode 100644 addons/website_event_online/tests/common.py create mode 100644 addons/website_event_track_online/tests/__init__.py create mode 100644 addons/website_event_track_online/tests/common.py diff --git a/addons/website_event/controllers/main.py b/addons/website_event/controllers/main.py index fe953bf8917..e814ea851b5 100644 --- a/addons/website_event/controllers/main.py +++ b/addons/website_event/controllers/main.py @@ -36,7 +36,7 @@ class WebsiteEventController(http.Controller): searches.setdefault('country', 'all') website = request.website - today = datetime.today() + today = fields.Datetime.today() def sdn(date): return fields.Datetime.to_string(date.replace(hour=23, minute=59, second=59)) diff --git a/addons/website_event_online/__init__.py b/addons/website_event_online/__init__.py index 67dee8c60db..66a1383e8f3 100644 --- a/addons/website_event_online/__init__.py +++ b/addons/website_event_online/__init__.py @@ -1,2 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import tests diff --git a/addons/website_event_online/tests/__init__.py b/addons/website_event_online/tests/__init__.py new file mode 100644 index 00000000000..511e649c9e2 --- /dev/null +++ b/addons/website_event_online/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import common diff --git a/addons/website_event_online/tests/common.py b/addons/website_event_online/tests/common.py new file mode 100644 index 00000000000..58f1779a6af --- /dev/null +++ b/addons/website_event_online/tests/common.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta, time +from unittest.mock import patch + +from odoo.addons.event.tests.common import TestEventCommon +from odoo.fields import Datetime as FieldsDatetime, Date as FieldsDate +from odoo.tests.common import SavepointCase + + +class EventDtPatcher(SavepointCase): + + @classmethod + def setUpClass(cls): + super(EventDtPatcher, cls).setUpClass() + + cls.reference_now = datetime(2020, 7, 6, 10, 0, 0) + cls.reference_today = datetime(2020, 7, 6) + + cls.event_dt = patch( + 'odoo.addons.event.models.event_event.fields.Datetime', + wraps=FieldsDatetime + ) + cls.wevent_main_dt = patch( + 'odoo.addons.website_event.controllers.main.fields.Datetime', + wraps=FieldsDatetime + ) + cls.event_online_dt = patch( + 'odoo.addons.website_event_online.models.event_event.fields.Datetime', + wraps=FieldsDatetime + ) + cls.event_date = patch( + 'odoo.addons.event.models.event_event.fields.Date', + wraps=FieldsDate + ) + cls.wevent_main_date = patch( + 'odoo.addons.website_event.controllers.main.fields.Date', + wraps=FieldsDate + ) + cls.mock_event_dt = cls.event_dt.start() + cls.mock_wevent_main_dt = cls.wevent_main_dt.start() + cls.mock_event_online_dt = cls.event_online_dt.start() + cls.mock_event_date = cls.event_date.start() + cls.mock_wevent_main_date = cls.wevent_main_date.start() + cls.mock_event_dt.now.return_value = cls.reference_now + cls.mock_wevent_main_dt.now.return_value = cls.reference_now + cls.mock_event_online_dt.now.return_value = cls.reference_now + cls.mock_event_date.today.return_value = cls.reference_today + cls.mock_wevent_main_date.today.return_value = cls.reference_today + cls.addClassCleanup(cls.event_dt.stop) + cls.addClassCleanup(cls.wevent_main_dt.stop) + cls.addClassCleanup(cls.event_online_dt.stop) + cls.addClassCleanup(cls.event_date.stop) + cls.addClassCleanup(cls.wevent_main_date.stop) + + +class TestEventOnlineCommon(TestEventCommon, EventDtPatcher): + + @classmethod + def setUpClass(cls): + super(TestEventOnlineCommon, cls).setUpClass() + + # event if 8-18 in Europe/Brussels (DST) (first day: begins at 9, last day: ends at 15) + cls.event_0.write({ + 'date_begin': datetime.combine(cls.reference_now, time(7, 0)) - timedelta(days=1), + 'date_end': datetime.combine(cls.reference_now, time(13, 0)) + timedelta(days=1), + }) diff --git a/addons/website_event_track_online/__init__.py b/addons/website_event_track_online/__init__.py index 67dee8c60db..66a1383e8f3 100644 --- a/addons/website_event_track_online/__init__.py +++ b/addons/website_event_track_online/__init__.py @@ -1,2 +1,4 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import tests diff --git a/addons/website_event_track_online/tests/__init__.py b/addons/website_event_track_online/tests/__init__.py new file mode 100644 index 00000000000..511e649c9e2 --- /dev/null +++ b/addons/website_event_track_online/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import common diff --git a/addons/website_event_track_online/tests/common.py b/addons/website_event_track_online/tests/common.py new file mode 100644 index 00000000000..f3d5e52f53b --- /dev/null +++ b/addons/website_event_track_online/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_online.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, + }) From 0659a500ef67b6b96b923e3ffb0819a61c0b6f46 Mon Sep 17 00:00:00 2001 From: David Beguin Date: Thu, 25 Jun 2020 10:57:54 +0000 Subject: [PATCH 03/28] [IMP] website_event_online: ensure to link visitor at registration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Ensure to have a visitor when people registers, in order to enable wishlist or notifications. Improve visitor model to keep information when people change device. SPECIFICATIONS: REGISTRATION FLOW This commit ensures a visitor is created when a registration is done on an event. Creation is done only if a visitor does not already exists like all visitor-based flows. In case of multiple registrations made by the same visitor, only the first registration is linked to the created/existing visitor. We take the opportunity to update the visitor's information based on the registration's infos. We also add some fields to get all visitors from an event, based on visitor and registration link. This could be used for example to contact them, like sending a mailing or push notification to all attendees of an event. SPECIFICATIONS: VISITOR LOGIN / INFORMATION UPDATE When a user is linked to a visitor (e.g. when customer logs in) its partner is propagated to the registration. Using visitor as middle-model it allows to propagate information through those models, leading to better contact data notably. SPECIFICATIONS: VISITOR IMPROVEMENTS + PUSH TOKEN In this commit we improve visitor behavior. Currently when there are several visitors that may be linked to the same user, only the last one is kept and other one are unlinked. However visitor model holds push tokens, allowing to store approval for push notifications. Unlinking records is therefore a bad idea as we may loose information. In order to solve this an intermediate solution is implemented. A new parent_id field is added on visitors. Instead of unlinking "duplicates" we link them to the parent and de-activate them. This means more visitors are present in database, but this is required to keep push tokens. A better solution would probably be to separate visitor from push tokens but as this merge targets a stable version it was difficult to do in a clean way. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow Task ID-2284043 (Visitor-based track wishlist) Task ID-2283869 (Notify attendees by push) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- addons/website/tests/test_website_visitor.py | 20 ++- addons/website_event_online/__init__.py | 2 + addons/website_event_online/__manifest__.py | 5 + .../controllers/__init__.py | 4 + .../controllers/event_registration.py | 31 +++++ .../data/event_registration_demo.xml | 18 +++ .../data/website_visitor_demo.xml | 26 ++++ .../website_event_online/models/__init__.py | 5 + .../models/event_registration.py | 11 ++ .../models/website_visitor.py | 126 ++++++++++++++++++ .../security/ir.model.access.csv | 2 + addons/website_event_online/tests/__init__.py | 1 + .../tests/test_visitor_events.py | 50 +++++++ .../views/event_registration_views.xml | 36 +++++ .../views/website_visitor_views.xml | 65 +++++++++ 15 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 addons/website_event_online/controllers/__init__.py create mode 100644 addons/website_event_online/controllers/event_registration.py create mode 100644 addons/website_event_online/data/event_registration_demo.xml create mode 100644 addons/website_event_online/data/website_visitor_demo.xml create mode 100644 addons/website_event_online/models/__init__.py create mode 100644 addons/website_event_online/models/event_registration.py create mode 100644 addons/website_event_online/models/website_visitor.py create mode 100644 addons/website_event_online/security/ir.model.access.csv create mode 100644 addons/website_event_online/tests/test_visitor_events.py create mode 100644 addons/website_event_online/views/event_registration_views.xml create mode 100644 addons/website_event_online/views/website_visitor_views.xml diff --git a/addons/website/tests/test_website_visitor.py b/addons/website/tests/test_website_visitor.py index db0af8e5039..374594f955c 100644 --- a/addons/website/tests/test_website_visitor.py +++ b/addons/website/tests/test_website_visitor.py @@ -119,6 +119,23 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo): len(pages) ) + def assertVisitorDeactivated(self, visitor, main_visitor): + """ Temporary method to check that a visitor has been de-activated / merged + with other visitor, notably in case of login (see User.authenticate() as + well as Visitor._link_to_visitor() ). + + As final result depends on installed modules (see overrides) due to stable + improvements linked to EventOnline, this method contains a hack to avoid + doing too much overrides just for that behavior. """ + if 'parent_id' in self.env['website.visitor']: + self.assertTrue(bool(visitor)) + self.assertFalse(visitor.active) + self.assertTrue(main_visitor.active) + self.assertEqual(visitor.parent_id, main_visitor) + else: + self.assertFalse(visitor) + self.assertTrue(bool(main_visitor)) + def test_visitor_creation_on_tracked_page(self): """ Test various flows involving visitor creation and update. """ existing_visitors = self.env['website.visitor'].search([]) @@ -209,7 +226,7 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo): # one visitor is deleted visitor_anonymous = self.env['website.visitor'].with_context(active_test=False).search([('id', '=', visitor_anonymous.id)]) - self.assertFalse(visitor_anonymous) + self.assertVisitorDeactivated(visitor_anonymous, visitor_admin) new_visitors = self.env['website.visitor'].search([('id', 'not in', existing_visitors.ids)]) self.assertEqual(new_visitors, visitor_admin | visitor_portal) visitor_admin = self.env['website.visitor'].search([('partner_id', '=', self.partner_admin.id)]) @@ -300,6 +317,7 @@ class WebsiteVisitorTests(MockVisitor, HttpCaseWithUserDemo): with self.mock_visitor_from_request(force_visitor=new_visitor): self.authenticate('demo', 'demo') (new_visitor | old_visitor).flush() + partner_demo.flush() partner_demo.invalidate_cache(fnames=['visitor_ids']) self.assertEqual(partner_demo.visitor_ids, old_visitor, "The partner visitor should be back to the 'old' visitor.") diff --git a/addons/website_event_online/__init__.py b/addons/website_event_online/__init__.py index 66a1383e8f3..5b9ba23c97a 100644 --- a/addons/website_event_online/__init__.py +++ b/addons/website_event_online/__init__.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import models +from . import controllers from . import tests diff --git a/addons/website_event_online/__manifest__.py b/addons/website_event_online/__manifest__.py index 0aef242191d..227aaa50f93 100644 --- a/addons/website_event_online/__manifest__.py +++ b/addons/website_event_online/__manifest__.py @@ -14,8 +14,13 @@ 'website_event' ], 'data': [ + 'security/ir.model.access.csv', + 'views/event_registration_views.xml', + 'views/website_visitor_views.xml', ], 'demo': [ + 'data/website_visitor_demo.xml', + 'data/event_registration_demo.xml', ], 'application': False, 'installable': True, diff --git a/addons/website_event_online/controllers/__init__.py b/addons/website_event_online/controllers/__init__.py new file mode 100644 index 00000000000..20ce9ac3fb4 --- /dev/null +++ b/addons/website_event_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_registration diff --git a/addons/website_event_online/controllers/event_registration.py b/addons/website_event_online/controllers/event_registration.py new file mode 100644 index 00000000000..a504f1000f1 --- /dev/null +++ b/addons/website_event_online/controllers/event_registration.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.website_event.controllers.main import WebsiteEventController +from odoo.http import request + + +class WebsiteEventOnlineController(WebsiteEventController): + + def _create_attendees_from_registration_post(self, event, registration_data): + """ Override registration data to try to set a visitor (from request) and + a partner (if visitor linked to a user for example). Purpose is to gather + as much informations as possible, notably to ease future communications. + Also try to update visitor informations based on registration info. """ + visitor_sudo = request.env['website.visitor']._get_visitor_from_request(force_create=True) + visitor_sudo._update_visitor_last_visit() + + if visitor_sudo: + visitor_values = {} + for info in registration_data: + # registration may give a name to the visitor, yay + if info.get('name') and not visitor_sudo.name and not visitor_values.get('name'): + visitor_values['name'] = info['name'] + # update registration based on visitor + info['visitor_id'] = visitor_sudo.id + if not info.get('partner_id') and visitor_sudo.partner_id: + info['partner_id'] = visitor_sudo.partner_id.id + if visitor_values: + visitor_sudo.write(visitor_values) + + return super(WebsiteEventOnlineController, self)._create_attendees_from_registration_post(event, registration_data) diff --git a/addons/website_event_online/data/event_registration_demo.xml b/addons/website_event_online/data/event_registration_demo.xml new file mode 100644 index 00000000000..d89b4d1aabf --- /dev/null +++ b/addons/website_event_online/data/event_registration_demo.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/addons/website_event_online/data/website_visitor_demo.xml b/addons/website_event_online/data/website_visitor_demo.xml new file mode 100644 index 00000000000..174b1c17b68 --- /dev/null +++ b/addons/website_event_online/data/website_visitor_demo.xml @@ -0,0 +1,26 @@ + + + + + Edwin Hansen + + + + + Soham Palmer + + + + + Philipe J. Fry + + + + + Philipe J. Fry (old) + + + + + + diff --git a/addons/website_event_online/models/__init__.py b/addons/website_event_online/models/__init__.py new file mode 100644 index 00000000000..ea59b0afad8 --- /dev/null +++ b/addons/website_event_online/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_registration +from . import website_visitor diff --git a/addons/website_event_online/models/event_registration.py b/addons/website_event_online/models/event_registration.py new file mode 100644 index 00000000000..8b56c14f742 --- /dev/null +++ b/addons/website_event_online/models/event_registration.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EventRegistration(models.Model): + _name = 'event.registration' + _inherit = ['event.registration'] + + visitor_id = fields.Many2one('website.visitor', string='Visitor', ondelete='set null') diff --git a/addons/website_event_online/models/website_visitor.py b/addons/website_event_online/models/website_visitor.py new file mode 100644 index 00000000000..bd767cab171 --- /dev/null +++ b/addons/website_event_online/models/website_visitor.py @@ -0,0 +1,126 @@ +# -*- 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'] + + parent_id = fields.Many2one( + 'website.visitor', string="Parent", ondelete='set null', + help="Main identity") + event_registration_ids = fields.One2many( + 'event.registration', 'visitor_id', string='Event Registrations', + groups="event.group_event_user") + event_registration_count = fields.Integer( + '# Registrations', compute='_compute_event_registration_count', + groups="event.group_event_user") + event_registered_ids = fields.Many2many( + 'event.event', string="Registered Events", + compute="_compute_event_registered_ids", compute_sudo=True, + search="_search_event_registered_ids", + groups="event.group_event_user") + + @api.depends('event_registration_ids') + def _compute_event_registration_count(self): + if self.ids: + read_group_res = self.env['event.registration'].read_group( + [('visitor_id', 'in', self.ids)], + ['visitor_id'], ['visitor_id']) + visitor_mapping = dict( + (item['visitor_id'][0], item['visitor_id_count']) + for item in read_group_res) + else: + visitor_mapping = dict() + for visitor in self: + visitor.event_registration_count = visitor_mapping.get(visitor.id) or 0 + + @api.depends('event_registration_ids.email', 'event_registration_ids.mobile', 'event_registration_ids.phone') + def _compute_email_phone(self): + super(WebsiteVisitor, self)._compute_email_phone() + self.flush() + + for visitor in self.filtered(lambda visitor: not visitor.email or not visitor.mobile): + linked_registrations = visitor.event_registration_ids.sorted('create_date', reverse=True) + if not visitor.email: + visitor.email = next((reg.email for reg in linked_registrations if reg.email), False) + if not visitor.mobile: + visitor.mobile = next((reg.mobile or reg.phone for reg in linked_registrations if reg.mobile or reg.phone), False) + + @api.depends('parent_id', 'event_registration_ids') + def _compute_event_registered_ids(self): + # include parent's registrations in a visitor o2m field. We don't add + # child one as child should not have registrations (moved to the parent) + for visitor in self: + all_registrations = visitor.event_registration_ids | visitor.parent_id.event_registration_ids + visitor.event_registered_ids = all_registrations.mapped('event_id') + + def _search_event_registered_ids(self, operator, operand): + """ Search visitors with terms on events within their event registrations. E.g. [('event_registered_ids', + 'in', [1, 2])] should return visitors having a registration on events 1, 2 as + well as their children for notification purpose. """ + if operator == "not in": + raise NotImplementedError("Unsupported 'Not In' operation on visitors registrations") + + all_registrations = self.env['event.registration'].sudo().search([ + ('event_id', operator, operand) + ]) + if all_registrations: + # search children, even archived one, to contact them + visitors = all_registrations.with_context(active_test=False).mapped('visitor_id') + 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 registration records """ + if partner: + registration_wo_partner = self.event_registration_ids.filtered(lambda registration: not registration.partner_id) + if registration_wo_partner: + registration_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 registrations to the final visitor. """ + self.event_registration_ids.write({'visitor_id': target.id}) + + res = super(WebsiteVisitor, self)._link_to_visitor(target, keep_unique=False) + + if keep_unique: + self.partner_id = False + self.parent_id = target.id + self.active = False + + return res + + def _get_visitor_from_request(self, force_create=False): + """ When fetching visitor, now that duplicates are linked to a main visitor + instead of unlinked, you may have more collisions issues with cookie being + set after a de-connection for example. + + In base method, visitor associated to a partner in case of public user is + not taken into account. It is considered as desynchronized cookie. Here + we also discard if the visitor has a main visitor whose partner is set + (aka wrong after logout partner). """ + visitor = super(WebsiteVisitor, self)._get_visitor_from_request(force_create=force_create) + + # also check that visitor parent partner is not different from user's one (indicates duplicate due to invalid or wrong cookie) + if visitor and visitor.parent_id.partner_id: + if self.env.user._is_public(): + visitor = self.env['website.visitor'].sudo() + elif not visitor.partner_id: + visitor = self.env['website.visitor'].sudo().with_context(active_test=False).search( + [('partner_id', '=', self.env.user.partner_id.id)] + ) + + if not visitor and force_create: + visitor = self._create_visitor() + + return visitor diff --git a/addons/website_event_online/security/ir.model.access.csv b/addons/website_event_online/security/ir.model.access.csv new file mode 100644 index 00000000000..20948d77e58 --- /dev/null +++ b/addons/website_event_online/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +website_visitor_access_event_user,website.visitor.access.event.user,model_website_visitor,event.group_event_user,1,1,0,0 diff --git a/addons/website_event_online/tests/__init__.py b/addons/website_event_online/tests/__init__.py index 511e649c9e2..7599117c507 100644 --- a/addons/website_event_online/tests/__init__.py +++ b/addons/website_event_online/tests/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import common +from . import test_visitor_events diff --git a/addons/website_event_online/tests/test_visitor_events.py b/addons/website_event_online/tests/test_visitor_events.py new file mode 100644 index 00000000000..814cd531ac5 --- /dev/null +++ b/addons/website_event_online/tests/test_visitor_events.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta + +from odoo import fields +from odoo.addons.event.tests.common import TestEventCommon + + +class TestVisitorEvents(TestEventCommon): + def test_visitor_events(self): + event_1 = self.env['event.event'].create({ + 'name': 'OtherEvent', + 'auto_confirm': True, + 'date_begin': fields.Datetime.to_string(datetime.today() + timedelta(days=1)), + 'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)), + }) + + [main_visitor, child_visitor] = self.env['website.visitor'].create([{ + 'name': 'Main Visitor', + 'event_registration_ids': [(0, 0, { + 'event_id': self.event_0.id + })] + }, { + 'name': 'Child Visitor', + 'event_registration_ids': [(0, 0, { + 'event_id': event_1.id + })] + }]) + + self.assertEqual(self.event_0, main_visitor.event_registered_ids) + self.assertEqual(event_1, child_visitor.event_registered_ids) + self.assertEqual( + main_visitor, + self.env['website.visitor'].search([('event_registered_ids', 'in', self.event_0.ids)]) + ) + self.assertEqual( + child_visitor, + self.env['website.visitor'].search([('event_registered_ids', 'in', event_1.ids)]) + ) + child_visitor._link_to_visitor(main_visitor) + self.assertEqual(self.event_0 | event_1, main_visitor.event_registered_ids) + self.assertEqual( + main_visitor | child_visitor, + self.env['website.visitor'].with_context(active_test=False).search([('event_registered_ids', 'in', self.event_0.ids)]) + ) + self.assertEqual( + main_visitor | child_visitor, + self.env['website.visitor'].with_context(active_test=False).search([('event_registered_ids', 'in', event_1.ids)]) + ) diff --git a/addons/website_event_online/views/event_registration_views.xml b/addons/website_event_online/views/event_registration_views.xml new file mode 100644 index 00000000000..27e490d6a88 --- /dev/null +++ b/addons/website_event_online/views/event_registration_views.xml @@ -0,0 +1,36 @@ + + + + Registrations + event.registration + kanban,tree,form + [('visitor_id', 'in', [active_id])] + +

+ No registration linked to this visitor +

+
+
+ + + event.registration.view.form.inherit.online + event.registration + + + + + + + + + + event.registration.view.tree.inherit.online + event.registration + + + + + + + +
diff --git a/addons/website_event_online/views/website_visitor_views.xml b/addons/website_event_online/views/website_visitor_views.xml new file mode 100644 index 00000000000..a7b4af81adf --- /dev/null +++ b/addons/website_event_online/views/website_visitor_views.xml @@ -0,0 +1,65 @@ + + + + + website.visitor.view.search.inherit.event + website.visitor + + + + + + + + + + website.visitor.view.tree.inherit.event + website.visitor + + + + + + + + + + website.visitor.view.form.inherit.event + website.visitor + + + + + + + + + + + + + website.visitor.view.kanban.inherit.event + website.visitor + + + + + + +
Registrations
+
+ +
+ +
Registrations
+
+
+
+
+ +
From 8d37126629e7aa2a99242a65d29fbaa8dbd9ea30 Mon Sep 17 00:00:00 2001 From: David Beguin Date: Thu, 25 Jun 2020 08:05:18 +0000 Subject: [PATCH 04/28] [IMP] website_event_track_online : add wishlist mechanism on event tracks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Allow visitors and logged users to wishlist tracks and link tracks to visitors. Purpose is to improve engagement and promote tracks before an event. SPECIFICATIONS: TRACK WISHLIST Add a link between visitors or partners, and tracks. This link is done in a new many2many relationship build on event.track.visitor new model. It is used to store the wishlist flag. That way even public users can wishlist a track using their visitor. At login, information is propagated to their partner. It means that wishlist are saved if customer logs in or registers. We also add a widget for frontend pages. It allows to toggle the wishlist flag on tracks. It is based on visitor and/or current user, leading to a complete flow even for anonymous people. We allow to set some tracks as "keynotes". Those are considered as wishlisted by default. In order to avoid storing too much information, we do not create an entry in the wishlist table for each attendee. We simply consider all event attendees to have wishlisted this track by default. In order to be able to mute a keynote, a blacklist mechanism is implemented. Using the same relationship table between visitors or partners and tracks, a flag is added that blacklists the track. Fields are added on visitor allowing to dynamically build a domain to find visitors that wishlisted a track (or did not blacklist list in case of default wishlist). This could be used for example to automatically contact attendees before a track, like what is done in enterprise counterpart of this merge. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow Task ID-2284043 (Visitor-based track wishlist) Task ID-2283869 (Notify attendees by push) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- addons/website_event_track_online/__init__.py | 2 + .../__manifest__.py | 10 ++ .../controllers/__init__.py | 4 + .../controllers/event_track.py | 68 ++++++++++ .../data/event_track_demo.xml | 15 ++ .../data/event_track_visitor_demo.xml | 15 ++ .../models/__init__.py | 6 + .../models/event_track.py | 128 ++++++++++++++++++ .../models/event_track_visitor.py | 32 +++++ .../models/website_visitor.py | 75 ++++++++++ .../security/ir.model.access.csv | 3 + .../static/src/js/event_track_reminder.js | 86 ++++++++++++ .../src/scss/event_track_templates.scss | 44 ++++++ .../views/assets.xml | 13 ++ .../views/event_menus.xml | 11 ++ .../views/event_track_templates.xml | 31 +++++ .../views/event_track_templates_reminder.xml | 24 ++++ .../views/event_track_views.xml | 69 ++++++++++ .../views/event_track_visitor_views.xml | 70 ++++++++++ .../views/website_visitor_views.xml | 51 +++++++ 20 files changed, 757 insertions(+) create mode 100644 addons/website_event_track_online/controllers/__init__.py create mode 100644 addons/website_event_track_online/controllers/event_track.py create mode 100644 addons/website_event_track_online/data/event_track_demo.xml create mode 100644 addons/website_event_track_online/data/event_track_visitor_demo.xml create mode 100644 addons/website_event_track_online/models/__init__.py create mode 100644 addons/website_event_track_online/models/event_track.py create mode 100644 addons/website_event_track_online/models/event_track_visitor.py create mode 100644 addons/website_event_track_online/models/website_visitor.py create mode 100644 addons/website_event_track_online/security/ir.model.access.csv create mode 100644 addons/website_event_track_online/static/src/js/event_track_reminder.js create mode 100644 addons/website_event_track_online/static/src/scss/event_track_templates.scss create mode 100644 addons/website_event_track_online/views/assets.xml create mode 100644 addons/website_event_track_online/views/event_menus.xml create mode 100644 addons/website_event_track_online/views/event_track_templates.xml create mode 100644 addons/website_event_track_online/views/event_track_templates_reminder.xml create mode 100644 addons/website_event_track_online/views/event_track_views.xml create mode 100644 addons/website_event_track_online/views/event_track_visitor_views.xml create mode 100644 addons/website_event_track_online/views/website_visitor_views.xml 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 @@ + + + + + + + + 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
+
+
+
+
+ +
From 96da5888916c28c8ad530e9ec59a3742f0df200a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 1 Jul 2020 14:04:23 +0000 Subject: [PATCH 05/28] [IMP] website_event(_track)_online: improve pages and menus management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Improve menu generation, linked to website_event and website_event_track. Also remove frontend menu management, getting back backend management. SPECIFICATIONS: MENU GENERATION Add sequence to menus, allowing to ensure order of menus. This is done through an override of some methods, leading to a transparent modification of behavior. SPECIFICATIONS: MENU MANAGEMENT Remove front-end menu management. Keep only the global "website menu" customize option. Indeed it is quite slow and clutters the frontend management UI. Get fields back on event and event type backend views. Put them in group noone to avoid having too much fields on form view while allowing to thune them. JS and code allowing to manage through frontend is simply de-activated in this commit. That way existing use of website_event(_track) is not impacted by this commit. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- addons/website_event_online/__manifest__.py | 1 + .../website_event_online/models/__init__.py | 1 + .../models/event_event.py | 47 +++++++++++++++++++ .../views/event_event_views.xml | 18 +++++++ .../__manifest__.py | 1 + .../models/__init__.py | 1 + .../models/event_event.py | 27 +++++++++++ .../src/js/website_event_customize_options.js | 13 +++++ .../xml/website_event_customize_options.xml | 11 +++++ .../views/assets.xml | 1 + .../views/event_event_views.xml | 18 +++++++ .../views/event_track_templates.xml | 9 ++++ 12 files changed, 148 insertions(+) create mode 100644 addons/website_event_online/models/event_event.py create mode 100644 addons/website_event_online/views/event_event_views.xml create mode 100644 addons/website_event_track_online/models/event_event.py create mode 100644 addons/website_event_track_online/static/src/js/website_event_customize_options.js create mode 100644 addons/website_event_track_online/static/src/xml/website_event_customize_options.xml create mode 100644 addons/website_event_track_online/views/event_event_views.xml diff --git a/addons/website_event_online/__manifest__.py b/addons/website_event_online/__manifest__.py index 227aaa50f93..46ad6a7d2b2 100644 --- a/addons/website_event_online/__manifest__.py +++ b/addons/website_event_online/__manifest__.py @@ -15,6 +15,7 @@ ], 'data': [ 'security/ir.model.access.csv', + 'views/event_event_views.xml', 'views/event_registration_views.xml', 'views/website_visitor_views.xml', ], diff --git a/addons/website_event_online/models/__init__.py b/addons/website_event_online/models/__init__.py index ea59b0afad8..fa937cadda2 100644 --- a/addons/website_event_online/models/__init__.py +++ b/addons/website_event_online/models/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import event_event from . import event_registration from . import website_visitor diff --git a/addons/website_event_online/models/event_event.py b/addons/website_event_online/models/event_event.py new file mode 100644 index 00000000000..9c7d1680f77 --- /dev/null +++ b/addons/website_event_online/models/event_event.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, _ +from odoo.addons.http_routing.models.ir_http import slug + + +class Event(models.Model): + _name = 'event.event' + _inherit = 'event.event' + + # ------------------------------------------------------------ + # WEBSITE MENU MANAGEMENT + # ------------------------------------------------------------ + + def _get_menu_entries(self): + """ Deprecate old method as a new one with more information is used + starting with website_event_online module. """ + return [] + + def _get_menu_entries_online(self): + """ Method returning menu entries to display on the website view of the + event, possibly depending on some options in inheriting modules. + + Each menu entry is a tuple containing : + * name: menu item name + * url: if set, url to a route (do not use xml_id in that case); + * xml_id: template linked to the page (do not use url in that case); + * sequence: specific sequence of menu entry to be set on the menu; + * menu_type: type of menu entry (used in inheriting modules to ease + menu management; not used in this module in 13.3 due to technical + limitations); + """ + self.ensure_one() + return [ + (_('Introduction'), False, 'website_event.template_intro', 1, False), + (_('Location'), False, 'website_event.template_location', 50, False), + (_('Register'), '/event/%s/register' % slug(self), False, 100, False), + ] + + def _update_website_menus(self, menus_update_by_field=None): + """ Synchronize event configuration and its menu entries for frontend. """ + super(Event, self)._update_website_menus(menus_update_by_field=menus_update_by_field) + for event in self: + if event.website_menu and (not menus_update_by_field or event in menus_update_by_field.get('website_menu')): + for name, url, xml_id, menu_sequence, menu_type in event._get_menu_entries_online(): + event._create_menu(menu_sequence, name, url, xml_id, menu_type=menu_type) diff --git a/addons/website_event_online/views/event_event_views.xml b/addons/website_event_online/views/event_event_views.xml new file mode 100644 index 00000000000..e968ea1e903 --- /dev/null +++ b/addons/website_event_online/views/event_event_views.xml @@ -0,0 +1,18 @@ + + + + + event.event.view.form.inherit.online + event.event + + + +
+
+
+
+
+ +
diff --git a/addons/website_event_track_online/__manifest__.py b/addons/website_event_track_online/__manifest__.py index c85e7abeea5..9148c7169a3 100644 --- a/addons/website_event_track_online/__manifest__.py +++ b/addons/website_event_track_online/__manifest__.py @@ -18,6 +18,7 @@ 'data': [ 'security/ir.model.access.csv', 'views/assets.xml', + 'views/event_event_views.xml', 'views/event_track_templates_reminder.xml', 'views/event_track_templates.xml', 'views/event_track_views.xml', diff --git a/addons/website_event_track_online/models/__init__.py b/addons/website_event_track_online/models/__init__.py index 645b184349c..33e6632706b 100644 --- a/addons/website_event_track_online/models/__init__.py +++ b/addons/website_event_track_online/models/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from . import event_event from . import event_track_visitor from . import event_track from . import website_visitor diff --git a/addons/website_event_track_online/models/event_event.py b/addons/website_event_track_online/models/event_event.py new file mode 100644 index 00000000000..1687967c00b --- /dev/null +++ b/addons/website_event_track_online/models/event_event.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, _ +from odoo.addons.http_routing.models.ir_http import slug + + +class EventEvent(models.Model): + _inherit = "event.event" + + # ------------------------------------------------------------ + # WEBSITE MENU MANAGEMENT + # ------------------------------------------------------------ + + # OVERRIDES: ADD SEQUENCE + def _get_track_menu_entries(self): + """ Remove agenda as this is now managed separately """ + self.ensure_one() + return [ + (_('Talks'), '/event/%s/track' % slug(self), False, 10, 'track'), + (_('Agenda'), '/event/%s/agenda' % slug(self), False, 70, 'track') + ] + + def _get_track_proposal_menu_entries(self): + """ See website_event_track._get_track_menu_entries() """ + self.ensure_one() + return [(_('Talk Proposals'), '/event/%s/track_proposal' % slug(self), False, 15, 'track_proposal')] diff --git a/addons/website_event_track_online/static/src/js/website_event_customize_options.js b/addons/website_event_track_online/static/src/js/website_event_customize_options.js new file mode 100644 index 00000000000..d86ef9db4b2 --- /dev/null +++ b/addons/website_event_track_online/static/src/js/website_event_customize_options.js @@ -0,0 +1,13 @@ +odoo.define('website_event_track_online.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_online/static/src/xml/website_event_customize_options.xml', + ]), +}); + +}); diff --git a/addons/website_event_track_online/static/src/xml/website_event_customize_options.xml b/addons/website_event_track_online/static/src/xml/website_event_customize_options.xml new file mode 100644 index 00000000000..22d5e2fd836 --- /dev/null +++ b/addons/website_event_track_online/static/src/xml/website_event_customize_options.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/addons/website_event_track_online/views/assets.xml b/addons/website_event_track_online/views/assets.xml index ca095992bb0..6fc02ebf98c 100644 --- a/addons/website_event_track_online/views/assets.xml +++ b/addons/website_event_track_online/views/assets.xml @@ -7,6 +7,7 @@ + diff --git a/addons/website_event_track_online/views/event_event_views.xml b/addons/website_event_track_online/views/event_event_views.xml new file mode 100644 index 00000000000..0f95fc80b9b --- /dev/null +++ b/addons/website_event_track_online/views/event_event_views.xml @@ -0,0 +1,18 @@ + + + + + event.event.view.from.inherit.track.online + event.event + + + + + + + + diff --git a/addons/website_event_track_online/views/event_track_templates.xml b/addons/website_event_track_online/views/event_track_templates.xml index 6ca0cfbc5de..1d6c198eda0 100644 --- a/addons/website_event_track_online/views/event_track_templates.xml +++ b/addons/website_event_track_online/views/event_track_templates.xml @@ -1,6 +1,15 @@ + + + + + + + + + + + diff --git a/addons/website_event_online/views/event_type_views.xml b/addons/website_event_online/views/event_type_views.xml new file mode 100644 index 00000000000..b87ae0c0107 --- /dev/null +++ b/addons/website_event_online/views/event_type_views.xml @@ -0,0 +1,15 @@ + + + + + event.type.view.form.inherit.online + event.type + + + + {'color_field': 'color', 'no_quick_create': True} + + + + + From b3d2df460fca1395a92f558b5dda3b088e99a113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Mon, 6 Jul 2020 11:49:46 +0000 Subject: [PATCH 08/28] [IMP] website_event_online: add time-related computation on events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Reduce complexity of frontend templates and ease time-related display by adding computed fields on event. SPECIFICATIONS Add following information to be used in future templates * is_ongoing: between start and end of event; * is_done: after end of event; * start_today: will start today, for example to display remaining hours before start; * start_remaining: remaining time before start; Ensure timezone are correctly taken into account. Base heuristic is * datetime are stored in UTC in database; * TZ of event is stored on date_tz of event; * make most computation based on UTC: compare UTC of current visitor to UTC of event datetime; LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- .../models/event_event.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/addons/website_event_online/models/event_event.py b/addons/website_event_online/models/event_event.py index 1419d63f786..05064c3b929 100644 --- a/addons/website_event_online/models/event_event.py +++ b/addons/website_event_online/models/event_event.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime +from pytz import timezone, utc + from odoo import api, fields, models, _ from odoo.addons.http_routing.models.ir_http import slug +from odoo.addons.resource.models.resource import float_to_time from odoo.osv import expression @@ -14,12 +18,42 @@ class Event(models.Model): menu_register_cta = fields.Boolean( 'Add Register Button', compute='_compute_menu_register_cta', readonly=False, store=True) + # live information + is_ongoing = fields.Boolean( + 'Is Ongoing', compute='_compute_time_data', search='_search_is_ongoing', + help="Whether event has begun") + is_done = fields.Boolean( + 'Is Done', compute='_compute_time_data', + help="Whether event is finished") + start_today = fields.Boolean( + 'Start Today', compute='_compute_time_data', + help="Whether event is going to start today if still not ongoing") + start_remaining = fields.Integer( + 'Remaining before start', compute='_compute_time_data', + help="Remaining time before event starts (minutes)") @api.depends('website_menu') def _compute_menu_register_cta(self): for event in self: event.menu_register_cta = event.website_menu + @api.depends('date_begin', 'date_end') + def _compute_time_data(self): + """ Compute start and remaining time. Do everything in UTC as we compute only + time deltas here. """ + now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0)) + for event in self: + date_begin_utc = utc.localize(event.date_begin, is_dst=False) + date_end_utc = utc.localize(event.date_end, is_dst=False) + event.is_ongoing = date_begin_utc <= now_utc <= date_end_utc + event.is_done = now_utc > date_end_utc + event.start_today = date_begin_utc.date() == now_utc.date() + if date_begin_utc >= now_utc: + td = date_begin_utc - now_utc + event.start_remaining = int(td.total_seconds() / 60) + else: + event.start_remaining = 0 + def _compute_is_participating(self): """ Override is_participating to improve heuristic that is now From a0892ad39e27eba2956c7d59b370e396d98d80e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Fri, 17 Jul 2020 16:08:16 +0000 Subject: [PATCH 09/28] [IMP] website_event_online: add some CSS tools / fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... SPECIFICATIONS Add some common CSS to be used in several new modules that will use a "aside panel / main content" like display. This will be used for * exhibitor: display exhibitors list (cards) / page view (main content of exhibitor with jitsi + suggestions on left); * tracks: display track list (cards + list) / page view (main content of track, with youtube and/or quiz + suggestions on left); * community: display room list (cards) / page view (main content of room with jitsi + suggestions on left); Incoming modules will soon use this SCSS. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow) Task ID-2299857 (Event Design Review) Task ID-2284043 (Visitor-based track wishlist) Task ID-2283738 (Sponsors -> Exhibitors upgrade) Task ID-2283735 (Talks -> Session upgrade) Task ID-2283742 (Community gathering on chat rooms) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- .../src/scss/website_event_templates.scss | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/addons/website_event_online/static/src/scss/website_event_templates.scss b/addons/website_event_online/static/src/scss/website_event_templates.scss index 55e714cce79..e0bef5a6c54 100644 --- a/addons/website_event_online/static/src/scss/website_event_templates.scss +++ b/addons/website_event_online/static/src/scss/website_event_templates.scss @@ -2,6 +2,14 @@ .o_wevent_index, .o_wevent_event { + // Text Ellipsis for Event title in mobile mode + nav > div > a.navbar-brand { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + max-width: calc(100% - 80px); + } + // Simple colored tags according to assigned background color @for $size from 1 through length($o-colors) { .o_tag_color_#{$size - 1} { @@ -48,4 +56,75 @@ } } } + + /* + * COMMON TO ALL ONLINE-* PAGES + */ + + .o_wevent_online { + // unpublished badge: put opacity to distinguish form other badges + .o_wevent_online_badge_unpublished{ + opacity: 0.4; + } + + &.o_wevent_online_bg { + @if (color('body') == $o-portal-default-body-bg) { + @extend .bg-100; + } + } + + .o_wevent_online_page_container { + // Widescreen mode + @include media-breakpoint-up(md) { + &:not(.container) { + padding-left: 0.5rem; + padding-right: 0.5rem; + } + } + + // Main panel + .o_wevent_online_page_main { + border: 1px solid $border-color; + + @include media-breakpoint-up(md) { + border-top-width: 0; + } + + .o_wevent_online_page_avatar { + min-width: 64px; + + img { + max-width: 96px; + } + } + } + + // Left panel + .o_wevent_online_page_aside { + + // Left panel: content display + .o_wevent_online_page_aside_content { + border: 1px solid $border-color; + + @include media-breakpoint-up(md) { + border-top-width: 0; + } + + li:not(.nav-item) { + border-bottom: 1px solid $border-color; + &:first-child { + border-top: 1px solid $border-color; + } + &:last-child { + border-bottom: 0px; + } + } + + .o_wevent_online_page_aside_collapse:not(.collapsed) { + transform: rotate(180deg); + } + } + } + } + } } From 6e15f7d113d1023e5b90a5d8a8150d33df3c26fc Mon Sep 17 00:00:00 2001 From: David Beguin Date: Fri, 10 Jul 2020 08:29:45 +0000 Subject: [PATCH 10/28] [IMP] website_event_online: add display-timer widget RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... SPECIFICATIONS Add a timer widget that allows to display a DOM element at the end of a given time. The widget can be configured with two steps * main timer: countdown to display the DOM element; * pre-countdown: countdown to display the main timer; For example, in the context of a event track (talk), the pre-timer can be used to announce when the talk will start. The main timer will then be displayed once the pre-timer is over. At the end of the main timer, the DOM element is finally displayed. [Talk start in 03:45] -> [Survey available in 05:00] -> [Take the Survey!] LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283735 (Talks -> Session upgrade) --- .../static/src/js/display_timer_widget.js | 111 ++++++++++++++++++ addons/website_event_online/views/assets.xml | 3 + .../views/event_templates_event.xml | 26 ++++ 3 files changed, 140 insertions(+) create mode 100644 addons/website_event_online/static/src/js/display_timer_widget.js diff --git a/addons/website_event_online/static/src/js/display_timer_widget.js b/addons/website_event_online/static/src/js/display_timer_widget.js new file mode 100644 index 00000000000..0ada123c3b0 --- /dev/null +++ b/addons/website_event_online/static/src/js/display_timer_widget.js @@ -0,0 +1,111 @@ +odoo.define('website_event_online.display_timer_widget', function (require) { +'use strict'; + +var core = require('web.core'); +var _t = core._t; +var publicWidget = require('web.public.widget'); + +publicWidget.registry.displayTimerWidget = publicWidget.Widget.extend({ + selector: '.o_display_timer', + + /** + * This widget allows to display a dom element at the end of a certain time laps. + * There are 2 timers available: + * - The main-timer: display the DOM element (using the displayClass) at the end of this timer. + * - The pre-timer: additional timer to display the main-timer. This pre-timer can be invisible or visible, + * depending of the startCountdownDisplay option. Once the pre-timer is over, + the main-timer is displayed. + * @override + */ + start: function () { + var self = this; + return this._super.apply(this, arguments).then(function () { + self.options = self.$target.data(); + self.preCountdownDisplay = self.options["preCountdownDisplay"]; + self.preCountdownTime = self.options["preCountdownTime"]; + self.preCountdownText = self.options["preCountdownText"]; + + self.mainCountdownTime = self.options["mainCountdownTime"]; + self.mainCountdownText = self.options["mainCountdownText"]; + self.mainCountdownDisplay = self.options["mainCountdownDisplay"]; + + self.displayClass = self.options["displayClass"]; + + if (self.preCountdownDisplay) { + $(self.$el).parent().removeClass('d-none'); + } + + self._checkTimer(); + self.interval = setInterval(function () { self._checkTimer(); }, 1000); + }); + }, + + /** + * This method removes 1 second to the current timer (pre-timer or main-timer) + * and call the method to update the DOM, unless main-timer is over. In that last case, + * the DOM element to show is displayed. + * + * @private + */ + _checkTimer: function () { + var now = new Date(); + + var remainingPreSeconds = this.preCountdownTime - (now.getTime()/1000); + if (remainingPreSeconds <= 1) { + this.$('.o_countdown_text').text(this.mainCountdownText); + if (this.mainCountdownDisplay) { + $(this.$el).parent().removeClass('d-none'); + } + var remainingMainSeconds = this.mainCountdownTime - (now.getTime()/1000); + if (remainingMainSeconds <= 1) { + clearInterval(this.interval); + $(this.displayClass).removeClass('d-none'); + $(this.$el).parent().addClass('d-none'); + } else { + this._updateCountdown(remainingMainSeconds); + } + } else { + this._updateCountdown(remainingPreSeconds); + } + }, + + /** + * This method update the DOM to display the remaining time. + * from seconds, the method extract the number of days, hours, minutes and seconds and + * override the different DOM elements values. + * + * @private + */ + _updateCountdown: function (remainingTime) { + var remainingSeconds = remainingTime; + var days = Math.floor(remainingSeconds / 86400); + + remainingSeconds = remainingSeconds % 86400; + var hours = Math.floor(remainingSeconds / 3600); + + remainingSeconds = remainingSeconds % 3600; + var minutes = Math.floor(remainingSeconds / 60); + + remainingSeconds = Math.floor(remainingSeconds % 60); + + this.$("span.o_timer_days").text(days); + this.$("span.o_timer_hours").text(this._zeroPad(hours, 2)); + this.$("span.o_timer_minutes").text(this._zeroPad(minutes, 2)); + this.$("span.o_timer_seconds").text(this._zeroPad(remainingSeconds, 2)); + }, + + /** + * Small tool to add leading zéros to the given number, in function of the needed number of leading zéros. + * + * @private + */ + _zeroPad: function (num, places) { + var zero = places - num.toString().length + 1; + return new Array(+(zero > 0 && zero)).join("0") + num; + }, + +}); + +return publicWidget.registry.countdownWidget; + +}); diff --git a/addons/website_event_online/views/assets.xml b/addons/website_event_online/views/assets.xml index d0f9161a9c6..1555eddeb04 100644 --- a/addons/website_event_online/views/assets.xml +++ b/addons/website_event_online/views/assets.xml @@ -5,6 +5,9 @@ + + + diff --git a/addons/website_event_online/views/event_templates_event.xml b/addons/website_event_online/views/event_templates_event.xml index 00665d0fbdd..e2dcefb5c4c 100644 --- a/addons/website_event_online/views/event_templates_event.xml +++ b/addons/website_event_online/views/event_templates_event.xml @@ -20,4 +20,30 @@ + + + From 64304aca2e3e2320af3c02adfc1615e1b908584c Mon Sep 17 00:00:00 2001 From: std-odoo Date: Thu, 30 Jul 2020 13:03:47 +0000 Subject: [PATCH 11/28] [IMP] website_event_online: add a new widget to display toast message RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... SPECIFICATIONS Create a new widget that allows us to display toast message on the event registration frontend page. Its purpose is notably to send a toaster message to customers when they are redirected to the register page. It will be used notably when exhibitors are not yet open before an event, to try to make people register to the event with an explanation. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow) --- .../static/src/js/register_toaster_widget.js | 31 +++++++++++++++++++ addons/website_event_online/views/assets.xml | 1 + .../views/event_templates_event.xml | 6 ++++ 3 files changed, 38 insertions(+) create mode 100644 addons/website_event_online/static/src/js/register_toaster_widget.js diff --git a/addons/website_event_online/static/src/js/register_toaster_widget.js b/addons/website_event_online/static/src/js/register_toaster_widget.js new file mode 100644 index 00000000000..83397e74c40 --- /dev/null +++ b/addons/website_event_online/static/src/js/register_toaster_widget.js @@ -0,0 +1,31 @@ +odoo.define('website_event_online.register_toaster_widget', function (require) { +'use strict'; + +let core = require('web.core'); +let _t = core._t; +let publicWidget = require('web.public.widget'); + +publicWidget.registry.RegisterToasterWidget = publicWidget.Widget.extend({ + selector: '.o_wevent_register_toaster', + + /** + * This widget allows to display a toast message on the page. + * + * @override + */ + start: function () { + const message = this.$el.data('message'); + if (message && message.length) { + this.displayNotification({ + title: _t("Register"), + message: message, + type: 'info' + }); + } + return this._super.apply(this, arguments); + }, +}); + +return publicWidget.registry.RegisterToasterWidget; + +}); diff --git a/addons/website_event_online/views/assets.xml b/addons/website_event_online/views/assets.xml index 1555eddeb04..f90e1d77168 100644 --- a/addons/website_event_online/views/assets.xml +++ b/addons/website_event_online/views/assets.xml @@ -7,6 +7,7 @@ + diff --git a/addons/website_event_online/views/event_templates_event.xml b/addons/website_event_online/views/event_templates_event.xml index e2dcefb5c4c..6889a092ed1 100644 --- a/addons/website_event_online/views/event_templates_event.xml +++ b/addons/website_event_online/views/event_templates_event.xml @@ -46,4 +46,10 @@ + + From 2b10f951f4754de9634bb9305c320c946a6abb36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Wed, 24 Jun 2020 14:06:52 +0000 Subject: [PATCH 12/28] [IMP] website_event_track_online: allow to group track tags into categories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Allow to have categories of tags, used notably to build a dynamic menu when displaying tracks. SPECIFICATIONS Introduce tag category model on event track tags, like already existing in slides (tag and tag group). Link tags to categories in a m2o / o2m fashion. Each tag category will make a menu in the new frontend track list view. Each tag will be a menu entry. Add some demo data to showcase track tags and categories. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow) Task ID-2283735 (Talks -> Session upgrade) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- .../__manifest__.py | 2 + .../data/event_track_demo.xml | 6 +- .../data/event_track_tag_demo.xml | 38 ++++++++++ .../models/__init__.py | 1 + .../models/event_track_tag.py | 22 ++++++ .../security/ir.model.access.csv | 2 + .../views/event_menus.xml | 7 ++ .../views/event_track_tag_views.xml | 69 +++++++++++++++++++ 8 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 addons/website_event_track_online/data/event_track_tag_demo.xml create mode 100644 addons/website_event_track_online/models/event_track_tag.py create mode 100644 addons/website_event_track_online/views/event_track_tag_views.xml diff --git a/addons/website_event_track_online/__manifest__.py b/addons/website_event_track_online/__manifest__.py index e0547b8c030..ba43d258771 100644 --- a/addons/website_event_track_online/__manifest__.py +++ b/addons/website_event_track_online/__manifest__.py @@ -23,12 +23,14 @@ 'views/event_track_templates.xml', 'views/event_track_templates_reminder.xml', 'views/event_track_views.xml', + 'views/event_track_tag_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_tag_demo.xml', 'data/event_track_visitor_demo.xml', ], 'application': False, diff --git a/addons/website_event_track_online/data/event_track_demo.xml b/addons/website_event_track_online/data/event_track_demo.xml index 79bcd16929d..8e1b7dbd3ab 100644 --- a/addons/website_event_track_online/data/event_track_demo.xml +++ b/addons/website_event_track_online/data/event_track_demo.xml @@ -1,5 +1,6 @@ - + + @@ -12,4 +13,5 @@ - + + diff --git a/addons/website_event_track_online/data/event_track_tag_demo.xml b/addons/website_event_track_online/data/event_track_tag_demo.xml new file mode 100644 index 00000000000..35408f450dc --- /dev/null +++ b/addons/website_event_track_online/data/event_track_tag_demo.xml @@ -0,0 +1,38 @@ + + + + + Audience + 10 + + + Format + 20 + + + + + 1 + + + + 2 + + + + 3 + + + + 11 + + + + 12 + + + + 13 + + + diff --git a/addons/website_event_track_online/models/__init__.py b/addons/website_event_track_online/models/__init__.py index 33e6632706b..acdcaefaa1a 100644 --- a/addons/website_event_track_online/models/__init__.py +++ b/addons/website_event_track_online/models/__init__.py @@ -4,4 +4,5 @@ from . import event_event from . import event_track_visitor from . import event_track +from . import event_track_tag from . import website_visitor diff --git a/addons/website_event_track_online/models/event_track_tag.py b/addons/website_event_track_online/models/event_track_tag.py new file mode 100644 index 00000000000..b04fb7bf251 --- /dev/null +++ b/addons/website_event_track_online/models/event_track_tag.py @@ -0,0 +1,22 @@ +# -*- 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") + + +class TrackTag(models.Model): + _inherit = "event.track.tag" + _order = "category_id, sequence, name" + + sequence = fields.Integer('Sequence', default=10) + category_id = fields.Many2one('event.track.tag.category', string="Category", ondelete="set null") diff --git a/addons/website_event_track_online/security/ir.model.access.csv b/addons/website_event_track_online/security/ir.model.access.csv index d756041a499..b0bd4100f47 100644 --- a/addons/website_event_track_online/security/ir.model.access.csv +++ b/addons/website_event_track_online/security/ir.model.access.csv @@ -1,3 +1,5 @@ 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 +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 diff --git a/addons/website_event_track_online/views/event_menus.xml b/addons/website_event_track_online/views/event_menus.xml index a4685ab02d6..f8a69e4d6ef 100644 --- a/addons/website_event_track_online/views/event_menus.xml +++ b/addons/website_event_track_online/views/event_menus.xml @@ -1,6 +1,13 @@ + + + + + event.track.tag.category.view.form + event.track.tag.category + +
+ + + + + + + + + + + + +
+
+
+ + + event.track.tag.category.view.list + event.track.tag.category + + + + + + + + + + + Track Tag Categories + event.track.tag.category + tree,form + + + + event.track.tag.view.form.inherit.online + event.track.tag + + + + + + + + + + + event.track.tag.view.list.inherit.online + event.track.tag + + + + + + + + + + + + +
From e79a68d97db8ae00ecbdae8d48f7d65d46f42c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 7 Jul 2020 08:22:48 +0000 Subject: [PATCH 13/28] [IMP] website_event_track_online: improve sponsor partner synchronization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... SPECIFICATIONS: PARTNER SYNCHRONIZATION Right now, information on the sponsor page in frontend is dependent on partner through related fields. It is not possible to have contact data specific to the sponsor without modifying the related partner. In this commit we make sponsor a model based on partner while allowing to update its contact information. Name, email, mobile and phone are now computed stored based on partner and editable. A new image_512 field is introduced, as base image field is too small for a new list view of sponsors we plan to make. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow) Task ID-2283738 (Sponsors -> Exhibitors upgrade) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- .../__manifest__.py | 3 + .../data/event_sponsor_demo.xml | 35 +++++++++++ .../models/__init__.py | 1 + .../models/event_sponsor.py | 62 +++++++++++++++++++ .../views/event_sponsor_views.xml | 62 +++++++++++++++++++ 5 files changed, 163 insertions(+) create mode 100644 addons/website_event_track_online/data/event_sponsor_demo.xml create mode 100644 addons/website_event_track_online/models/event_sponsor.py create mode 100644 addons/website_event_track_online/views/event_sponsor_views.xml diff --git a/addons/website_event_track_online/__manifest__.py b/addons/website_event_track_online/__manifest__.py index ba43d258771..c6de6027c8e 100644 --- a/addons/website_event_track_online/__manifest__.py +++ b/addons/website_event_track_online/__manifest__.py @@ -19,7 +19,9 @@ 'security/ir.model.access.csv', 'views/assets.xml', 'views/event_event_views.xml', + 'views/event_sponsor_views.xml', 'views/event_templates_registration.xml', + 'views/event_track_templates_reminder.xml', 'views/event_track_templates.xml', 'views/event_track_templates_reminder.xml', 'views/event_track_views.xml', @@ -29,6 +31,7 @@ 'views/event_menus.xml', ], 'demo': [ + 'data/event_sponsor_demo.xml', 'data/event_track_demo.xml', 'data/event_track_tag_demo.xml', 'data/event_track_visitor_demo.xml', diff --git a/addons/website_event_track_online/data/event_sponsor_demo.xml b/addons/website_event_track_online/data/event_sponsor_demo.xml new file mode 100644 index 00000000000..f2576038acf --- /dev/null +++ b/addons/website_event_track_online/data/event_sponsor_demo.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/website_event_track_online/models/__init__.py b/addons/website_event_track_online/models/__init__.py index acdcaefaa1a..7553bc67199 100644 --- a/addons/website_event_track_online/models/__init__.py +++ b/addons/website_event_track_online/models/__init__.py @@ -2,6 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import event_event +from . import event_sponsor from . import event_track_visitor from . import event_track from . import event_track_tag diff --git a/addons/website_event_track_online/models/event_sponsor.py b/addons/website_event_track_online/models/event_sponsor.py new file mode 100644 index 00000000000..16bec7c9fe4 --- /dev/null +++ b/addons/website_event_track_online/models/event_sponsor.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# 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' + _inherit = ['event.sponsor'] + + # contact information + 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_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] diff --git a/addons/website_event_track_online/views/event_sponsor_views.xml b/addons/website_event_track_online/views/event_sponsor_views.xml new file mode 100644 index 00000000000..b229e5266a9 --- /dev/null +++ b/addons/website_event_track_online/views/event_sponsor_views.xml @@ -0,0 +1,62 @@ + + + + event.sponsor.view.search.inherit.online + event.sponsor + + + + + + + + + + + + event.sponsor.view.tree.inherit.online + event.sponsor + + + + + + + + + + + + + event.sponsor.view.form.inherit.online + event.sponsor + + + + + + +
+
+
+ + + + + + + + e.g. : https://www.odoo.com + + + + + + + +
+
+
From 64f08cc7ab123dc53f9b7bf0e8bd0cd530b74d23 Mon Sep 17 00:00:00 2001 From: Krupal Oza Date: Thu, 9 Jul 2020 12:03:41 +0000 Subject: [PATCH 14/28] [IMP] website_event_track_online: improve image and track / speaker synchronization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... SPECIFICATIONS: PARTNER SYNCHRONIZATION Right now, information on the track page in frontend is dependent on partner through related fields. It is not possible to have contact data specific to the sponsor without modifying the related partner. In this commit we make track information fields based on partner while allowing to update this contact information. Name, email, mobile and phone are now computed stored based on partner and editable. SPECIFICATIONS: IMAGE MANAGEMENT Add a website_image field holding image to display in frontend, notably in the upcoming new list and page views for tracks. This image is optional and a default image is computed through a website_image_url field. It allows to have default images without storing the same image on all tracks. SPECIFICATIONS: DEMO UDPATE In order to ensure synchronization, reset demo data partner_id so that demo data is bioutifoul. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow) Task ID-2283735 (Talks -> Session upgrade) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Krupal Oza Co-Authored-By: Thibault Delavallée m> --- .../data/event_track_demo.xml | 77 ++++++++++++++++++- .../models/event_track.py | 42 ++++++++++ .../views/event_track_views.xml | 6 ++ 3 files changed, 123 insertions(+), 2 deletions(-) diff --git a/addons/website_event_track_online/data/event_track_demo.xml b/addons/website_event_track_online/data/event_track_demo.xml index 8e1b7dbd3ab..2b63d8c1c44 100644 --- a/addons/website_event_track_online/data/event_track_demo.xml +++ b/addons/website_event_track_online/data/event_track_demo.xml @@ -3,15 +3,88 @@ + + + + + + + - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/addons/website_event_track_online/models/event_track.py b/addons/website_event_track_online/models/event_track.py index 6413c411506..b6d0efbd852 100644 --- a/addons/website_event_track_online/models/event_track.py +++ b/addons/website_event_track_online/models/event_track.py @@ -3,12 +3,27 @@ from odoo import api, fields, models from odoo.osv import expression +from odoo.tools.mail import is_html_empty +from odoo.modules.module import get_resource_path class Track(models.Model): _name = "event.track" _inherit = ['event.track'] + # speaker + partner_biography = fields.Html( + string='Biography', compute='_compute_partner_biography', + readonly=False, store=True) + image = fields.Image( + string="Speaker Photo", compute="_compute_speaker_image", + readonly=False, store=True, + max_width=256, max_height=256) + # 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", @@ -27,6 +42,33 @@ class Track(models.Model): 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.""") + # SPEAKER + + @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 + + # 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', diff --git a/addons/website_event_track_online/views/event_track_views.xml b/addons/website_event_track_online/views/event_track_views.xml index 6499c869e39..5f2a40903ce 100644 --- a/addons/website_event_track_online/views/event_track_views.xml +++ b/addons/website_event_track_online/views/event_track_views.xml @@ -51,6 +51,12 @@ + + + + + + From 2f47bcf24b5f2535f15937b5ac63b5c617d6a261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 14 Jul 2020 09:41:35 +0000 Subject: [PATCH 15/28] [IMP] website_event_track_online: improve track stage management + agenda based on tage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Purpose of this commit is to be able to showcase tracks by announcing them before publishing them. People may see them before event managers encode all relevant informations and publish it. SPECIFICATIONS: ACCEPTED ON STAGE Add a is_accepted field on tracks stage model. Accepted tracks should be displayed on frontend (agenda, list view) without being available in complete view (no read access). Customers may see the track name and know what it will be about while event managers finish to encode details of the track itself. No field is added on track model for that. Using the stage itself is sufficient for what it does, aka allowing filtering in frontend. No need to add a column for that as it is not as important as published flag. SPECIFICATIONS: DONE STAGES -> PUBLISH When a track reaches a stage which "is_done" field is set, automatically publish it. It allows to manage agenda creation from backend * accepted -> display track on website for teasing purpose (agenda, track list), but not accessible to non event users; * done -> available to everyone, published; SPECIFIATIONS: AGENDA UPDATE Display accepted tracks even for customers; make them not clickable, only displayed. Manually crafting url for those tracks should still raise an error as they are not accessible. An accepted track is simply a track whose stage has is_accepted boolean flag set. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2283796 (Event B2Basics / Registration Flow) Task ID-2299857 (Event Design Review) Task ID-2283735 (Talks -> Session upgrade) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- .../website_event_track/controllers/main.py | 9 ++++-- .../__manifest__.py | 2 ++ .../controllers/event_track.py | 9 +++++- .../data/event_track_stage_data.xml | 11 ++++++++ .../models/__init__.py | 1 + .../models/event_track.py | 25 +++++++++++++++++ .../models/event_track_stage.py | 13 +++++++++ .../views/event_track_stage_views.xml | 28 +++++++++++++++++++ .../views/event_track_templates.xml | 22 +++++++++++++++ 9 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 addons/website_event_track_online/data/event_track_stage_data.xml create mode 100644 addons/website_event_track_online/models/event_track_stage.py create mode 100644 addons/website_event_track_online/views/event_track_stage_views.xml diff --git a/addons/website_event_track/controllers/main.py b/addons/website_event_track/controllers/main.py index 0fb3d3b6989..7618ecb3998 100644 --- a/addons/website_event_track/controllers/main.py +++ b/addons/website_event_track/controllers/main.py @@ -80,7 +80,8 @@ class WebsiteEventTrackController(http.Controller): event = event.with_context(tz=event.date_tz or 'UTC') local_tz = pytz.timezone(event.date_tz or 'UTC') days_tracks = collections.defaultdict(lambda: []) - for track in event.track_ids.sorted(lambda track: (bool(track.date), track.date, bool(track.location_id))): + 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) @@ -97,9 +98,13 @@ class WebsiteEventTrackController(http.Controller): 'main_object': event, 'days': days, 'tracks_by_days': tracks_by_days, - 'tag': tag + '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 + @http.route([ '''/event//track''', '''/event//track/tag/''' diff --git a/addons/website_event_track_online/__manifest__.py b/addons/website_event_track_online/__manifest__.py index c6de6027c8e..7b264ac5f51 100644 --- a/addons/website_event_track_online/__manifest__.py +++ b/addons/website_event_track_online/__manifest__.py @@ -16,11 +16,13 @@ 'website_event_track', ], 'data': [ + 'data/event_track_stage_data.xml', 'security/ir.model.access.csv', 'views/assets.xml', 'views/event_event_views.xml', 'views/event_sponsor_views.xml', 'views/event_templates_registration.xml', + 'views/event_track_stage_views.xml', 'views/event_track_templates_reminder.xml', 'views/event_track_templates.xml', 'views/event_track_templates_reminder.xml', diff --git a/addons/website_event_track_online/controllers/event_track.py b/addons/website_event_track_online/controllers/event_track.py index 5217765f3fc..675908b678f 100644 --- a/addons/website_event_track_online/controllers/event_track.py +++ b/addons/website_event_track_online/controllers/event_track.py @@ -4,10 +4,17 @@ from werkzeug.exceptions import Forbidden, NotFound from odoo import exceptions, http +from odoo.addons.website_event_track.controllers.main import WebsiteEventTrackController from odoo.http import request -class WebsiteEventTrackController(http.Controller): +class EventTrackOnlineController(WebsiteEventTrackController): + + def _event_agenda_get_tracks(self, event): + tracks_sudo = event.sudo().track_ids + 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 _fetch_track(self, track_id, allow_is_accepted=False): track = request.env['event.track'].browse(track_id).exists() diff --git a/addons/website_event_track_online/data/event_track_stage_data.xml b/addons/website_event_track_online/data/event_track_stage_data.xml new file mode 100644 index 00000000000..b9f12bf3b82 --- /dev/null +++ b/addons/website_event_track_online/data/event_track_stage_data.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/addons/website_event_track_online/models/__init__.py b/addons/website_event_track_online/models/__init__.py index 7553bc67199..861f8ef1b00 100644 --- a/addons/website_event_track_online/models/__init__.py +++ b/addons/website_event_track_online/models/__init__.py @@ -5,5 +5,6 @@ from . import event_event from . import event_sponsor from . import event_track_visitor from . import event_track +from . import event_track_stage from . import event_track_tag 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 index b6d0efbd852..131abf0ee2f 100644 --- a/addons/website_event_track_online/models/event_track.py +++ b/addons/website_event_track_online/models/event_track.py @@ -11,6 +11,8 @@ class Track(models.Model): _name = "event.track" _inherit = ['event.track'] + # status management + is_accepted = fields.Boolean('Is Accepted', related='stage_id.is_accepted', readonly=True) # speaker partner_biography = fields.Html( string='Biography', compute='_compute_partner_biography', @@ -133,6 +135,29 @@ class Track(models.Model): ]) 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) + + for track in tracks: + track._synchronize_with_stage(track.stage_id) + + return tracks + + def write(self, vals): + if vals.get('stage_id'): + stage = self.env['event.track.stage'].browse(vals['stage_id']) + self._synchronize_with_stage(stage) + return super(Track, self).write(vals) + + def _synchronize_with_stage(self, stage): + if stage.is_done: + self.is_published = True + # ------------------------------------------------------------ # TOOLS # ------------------------------------------------------------ diff --git a/addons/website_event_track_online/models/event_track_stage.py b/addons/website_event_track_online/models/event_track_stage.py new file mode 100644 index 00000000000..c6e300e5b55 --- /dev/null +++ b/addons/website_event_track_online/models/event_track_stage.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class TrackStage(models.Model): + _inherit = 'event.track.stage' + + is_accepted = fields.Boolean( + string='Accepted Stage', + help='Accepted tracks are displayed in agenda views but not accessible.') + is_done = fields.Boolean(help='Done tracks are automatically published so that they are available in frontend.') \ No newline at end of file diff --git a/addons/website_event_track_online/views/event_track_stage_views.xml b/addons/website_event_track_online/views/event_track_stage_views.xml new file mode 100644 index 00000000000..9840a03cc8d --- /dev/null +++ b/addons/website_event_track_online/views/event_track_stage_views.xml @@ -0,0 +1,28 @@ + + + + + event.track.stage.view.form.inherit.online + event.track.stage + + + + + + + + + + event.track.stage.view.tree.inherit.online + event.track.stage + + + + + + + + + + + diff --git a/addons/website_event_track_online/views/event_track_templates.xml b/addons/website_event_track_online/views/event_track_templates.xml index 1d6c198eda0..4f65344227a 100644 --- a/addons/website_event_track_online/views/event_track_templates.xml +++ b/addons/website_event_track_online/views/event_track_templates.xml @@ -37,4 +37,26 @@ + + + From 19c6f11bf2baeda724a744a2c34b1b7c2404ed47 Mon Sep 17 00:00:00 2001 From: Elisabeth Dickinson Date: Fri, 17 Jul 2020 10:52:14 +0000 Subject: [PATCH 16/28] [IMP] website_event_track_online: improve design of sponsor logos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... SPECIFICATIONS Current 'our sponsors' banner is made for 5/6 sponsors but does not scale well with more sponsors. It takes a lot of space and requires a lot of scrolling in mobile mode. Go for a mozaic style, more compact and better displayed on both mobile and desktop. * current: https://tinyurl.com/ycughxlg * proposal: https://tinyurl.com/y8dzazcy Allow to make ribbon display optional. This is done by adding a field with ribbon color: gold, silver, bronze, or no ribbon. This solution is limited but allows to give flexibility enough for current layouting. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2299857 (Event Design Review) Co-Authored-By: Elisabeth Dickison Co-Authored-By: Thibault Delavallée --- .../__manifest__.py | 2 + .../data/event_sponsor_type_data.xml | 14 +++++ .../models/__init__.py | 1 + .../models/event_sponsor_type.py | 14 +++++ .../src/scss/event_track_templates.scss | 53 +++++++++++++++++++ .../views/event_sponsor_type_views.xml | 24 +++++++++ .../views/event_sponsor_views.xml | 2 +- .../views/event_track_templates.xml | 36 +++++++++++++ 8 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 addons/website_event_track_online/data/event_sponsor_type_data.xml create mode 100644 addons/website_event_track_online/models/event_sponsor_type.py create mode 100644 addons/website_event_track_online/views/event_sponsor_type_views.xml diff --git a/addons/website_event_track_online/__manifest__.py b/addons/website_event_track_online/__manifest__.py index 7b264ac5f51..a846a2942a7 100644 --- a/addons/website_event_track_online/__manifest__.py +++ b/addons/website_event_track_online/__manifest__.py @@ -16,11 +16,13 @@ 'website_event_track', ], 'data': [ + 'data/event_sponsor_type_data.xml', 'data/event_track_stage_data.xml', 'security/ir.model.access.csv', 'views/assets.xml', 'views/event_event_views.xml', 'views/event_sponsor_views.xml', + 'views/event_sponsor_type_views.xml', 'views/event_templates_registration.xml', 'views/event_track_stage_views.xml', 'views/event_track_templates_reminder.xml', diff --git a/addons/website_event_track_online/data/event_sponsor_type_data.xml b/addons/website_event_track_online/data/event_sponsor_type_data.xml new file mode 100644 index 00000000000..da1a8915d8a --- /dev/null +++ b/addons/website_event_track_online/data/event_sponsor_type_data.xml @@ -0,0 +1,14 @@ + + + + + Bronze + + + Silver + + + Gold + + + diff --git a/addons/website_event_track_online/models/__init__.py b/addons/website_event_track_online/models/__init__.py index 861f8ef1b00..45507706c33 100644 --- a/addons/website_event_track_online/models/__init__.py +++ b/addons/website_event_track_online/models/__init__.py @@ -3,6 +3,7 @@ from . import event_event from . import event_sponsor +from . import event_sponsor_type from . import event_track_visitor from . import event_track from . import event_track_stage diff --git a/addons/website_event_track_online/models/event_sponsor_type.py b/addons/website_event_track_online/models/event_sponsor_type.py new file mode 100644 index 00000000000..dc5d0355841 --- /dev/null +++ b/addons/website_event_track_online/models/event_sponsor_type.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 SponsorType(models.Model): + _inherit = "event.sponsor.type" + + 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_online/static/src/scss/event_track_templates.scss b/addons/website_event_track_online/static/src/scss/event_track_templates.scss index 133862c3997..d9fcb3786d8 100644 --- 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 @@ -42,3 +42,56 @@ } } } + +/* + * 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; + } +} diff --git a/addons/website_event_track_online/views/event_sponsor_type_views.xml b/addons/website_event_track_online/views/event_sponsor_type_views.xml new file mode 100644 index 00000000000..31e986fd2f9 --- /dev/null +++ b/addons/website_event_track_online/views/event_sponsor_type_views.xml @@ -0,0 +1,24 @@ + + + + event.sponsor.type.view.tree.inherit.online + event.sponsor.type + + + + + + + + + + event.sponsor.type.view.form.inherit.online + event.sponsor.type + + + + + + + + diff --git a/addons/website_event_track_online/views/event_sponsor_views.xml b/addons/website_event_track_online/views/event_sponsor_views.xml index b229e5266a9..9fd79468f71 100644 --- a/addons/website_event_track_online/views/event_sponsor_views.xml +++ b/addons/website_event_track_online/views/event_sponsor_views.xml @@ -42,7 +42,7 @@ - + + + + From 57da23665778d2a48595ed8a5d55619d5a2451f9 Mon Sep 17 00:00:00 2001 From: David Beguin Date: Fri, 24 Jul 2020 14:27:00 +0000 Subject: [PATCH 17/28] [IMP] website_event_track_online: improve agenda page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Improve Agenda page, to make it more inline with new Online Event design. Make it more usable, with a better UX and mobile-friendly. SPECIFICATIONS: AGENDA Keep a per-day and per-room display, as we think it is a nice design. Hours should be fixed: all hours between first hour and last hour of a day's tracks should be displayed. Otherwise you have some "holes" in agenda, and each line does not hold the same time range. Display tags and reminders on tracks. Support accepted tracks, not clickable but already displayed for people to organize their venue, especially with the wishlist / reminder feature in mind. Try to make it mobile friendly, by allowing horizontal scroll instead of displaying all rooms on a small device. SPECIFICATIONS: SEARCHING Allow to search on tracks, as well as some basic filtering. Use the look from event page to have something more inlined with current website layout. Due to time limitations, currently agenda custom JS-based search is inlined in bar looking like other event search bars. However no menu based on tags or wishlist is available as everything is done in JS. Routes do not support any tag or search, and adding it would take some time we don't have anymore. KNOWN LIMITATIONS Columns still do not have same width, complicated to do with dynamic column number and overlapping tracks going outside of locations columns. Mobile layout could be improved but is already browsable. Search is still custom, and will be improved in master. LINKS Community PR #53540 Enterprise PR odoo/enterprise#11384 Task ID-2252655 (Main Online Event task) Task ID-2299857 (Event Design Review) Co-Authored-By: David Beguin Co-Authored-By: Elisabeth Dickison Co-Authored-By: Thibault Delavallée --- .../__manifest__.py | 1 + .../controllers/event_track.py | 154 +++++++++++++- .../src/scss/event_track_templates.scss | 97 +++++++++ .../views/event_track_templates_agenda.xml | 190 ++++++++++++++++++ 4 files changed, 441 insertions(+), 1 deletion(-) create mode 100644 addons/website_event_track_online/views/event_track_templates_agenda.xml diff --git a/addons/website_event_track_online/__manifest__.py b/addons/website_event_track_online/__manifest__.py index a846a2942a7..d9f2e82d2d6 100644 --- a/addons/website_event_track_online/__manifest__.py +++ b/addons/website_event_track_online/__manifest__.py @@ -27,6 +27,7 @@ 'views/event_track_stage_views.xml', 'views/event_track_templates_reminder.xml', 'views/event_track_templates.xml', + 'views/event_track_templates_agenda.xml', 'views/event_track_templates_reminder.xml', 'views/event_track_views.xml', 'views/event_track_tag_views.xml', diff --git a/addons/website_event_track_online/controllers/event_track.py b/addons/website_event_track_online/controllers/event_track.py index 675908b678f..9e36151e09d 100644 --- a/addons/website_event_track_online/controllers/event_track.py +++ b/addons/website_event_track_online/controllers/event_track.py @@ -2,8 +2,10 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from werkzeug.exceptions import Forbidden, NotFound +from datetime import timedelta +import pytz -from odoo import exceptions, http +from odoo import exceptions, http, fields from odoo.addons.website_event_track.controllers.main import WebsiteEventTrackController from odoo.http import request @@ -16,6 +18,156 @@ class EventTrackOnlineController(WebsiteEventTrackController): tracks_sudo = tracks_sudo.filtered(lambda track: track.is_published or track.stage_id.is_accepted) return tracks_sudo + 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 + + 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 + } + + @http.route(['''/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_online.agenda_online", vals) + def _fetch_track(self, track_id, allow_is_accepted=False): track = request.env['event.track'].browse(track_id).exists() if not track: 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 index d9fcb3786d8..1772d02ea91 100644 --- 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 @@ -43,6 +43,103 @@ } } +/* + * AGENDA + */ +.o_we_online_agenda { + overflow-x: hidden; + @media (max-width: map-get($grid-breakpoints, 'md')) { + 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: #d3d3d36e; + } + th.position-sticky { + left: 0; + } + td { + height: 0px; + border: 0px; + &.active { + position: sticky; + left: 0; + min-width: 100px; + background-color: white; + } + div.o_we_agenda_card_content { + height: 100%; + span { + cursor: pointer; + } + } + .badge { + height: fit-content; + } + &.invisible { + visibility: visible !important; + opacity: 0.3; + } + &.o_we_agenda_time_slot_main { + border-top: 1px solid lightgrey; + } + &.o_we_agenda_time_slot_half { + border-top: 1px dashed lightgrey; + } + &.event_color_0 { + background-color: lightgrey; + } + &.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 */ diff --git a/addons/website_event_track_online/views/event_track_templates_agenda.xml b/addons/website_event_track_online/views/event_track_templates_agenda.xml new file mode 100644 index 00000000000..b6159f55f23 --- /dev/null +++ b/addons/website_event_track_online/views/event_track_templates_agenda.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + From 21e429daaecad8f09810f955e6b58ecf5cafccb8 Mon Sep 17 00:00:00 2001 From: qmo-odoo Date: Thu, 30 Jul 2020 09:00:11 +0000 Subject: [PATCH 18/28] [IMP] website_event_track_online: add a (void) community menu and page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Prepare community addition (both quiz-based and room-based) by adding a community link and page management. SPECIFICATIONS In order to show the leaderboard and the meeting rooms on the same page we have to put a page and a route at the base of two upcoming modules: meet and quiz. The only common ancestor of those two will be website_event_track_online who gains a field for the community menu. It currently does nothing and leads to a void page. Both meet and quiz modules will add their own logic: meeting rooms or leaderboard. Having bothi installed displays rooms with a small leaderboard. See next commits for more details on those modules. LINKS Task ID-2252655 (Main Online Event task) Task ID-2283771 (Quizzes on Talks, Leaderboard) Task ID-2283742 (Community gathering on chat rooms) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Quentin Mourier Co-Authored-By: Stéphane Debauche Co-Authored-By: Thibault Delavallée --- .../__manifest__.py | 1 + .../controllers/__init__.py | 1 + .../controllers/community.py | 13 ++++++ .../models/__init__.py | 2 + .../models/event_event.py | 44 ++++++++++++++++++- .../models/event_type.py | 18 ++++++++ .../models/website_event_menu.py | 12 +++++ .../views/event_event_views.xml | 6 +++ .../views/event_type_views.xml | 16 +++++++ 9 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 addons/website_event_track_online/controllers/community.py create mode 100644 addons/website_event_track_online/models/event_type.py create mode 100644 addons/website_event_track_online/models/website_event_menu.py create mode 100644 addons/website_event_track_online/views/event_type_views.xml diff --git a/addons/website_event_track_online/__manifest__.py b/addons/website_event_track_online/__manifest__.py index d9f2e82d2d6..8ad731cd362 100644 --- a/addons/website_event_track_online/__manifest__.py +++ b/addons/website_event_track_online/__manifest__.py @@ -32,6 +32,7 @@ 'views/event_track_views.xml', 'views/event_track_tag_views.xml', 'views/event_track_visitor_views.xml', + 'views/event_type_views.xml', 'views/website_visitor_views.xml', 'views/event_menus.xml', ], diff --git a/addons/website_event_track_online/controllers/__init__.py b/addons/website_event_track_online/controllers/__init__.py index e3798014924..074cc5bfa61 100644 --- a/addons/website_event_track_online/controllers/__init__.py +++ b/addons/website_event_track_online/controllers/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import event_track +from . import community diff --git a/addons/website_event_track_online/controllers/community.py b/addons/website_event_track_online/controllers/community.py new file mode 100644 index 00000000000..2720a94931f --- /dev/null +++ b/addons/website_event_track_online/controllers/community.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.addons.website_event.controllers.main import WebsiteEventController +from odoo import fields, http, modules, tools +from odoo.http import request + +class WebsiteEventCommunityController(http.Controller): + + @http.route('/event//community', type="http", auth="public", website=True) + def community(self, event, lang=None, **kwargs): + """ This skeleton route will be overriden in website_event_track_quiz, website_event_meet and website_event_meet_quiz. """ + return request.render('website.page_404') diff --git a/addons/website_event_track_online/models/__init__.py b/addons/website_event_track_online/models/__init__.py index 45507706c33..b47218d4ecf 100644 --- a/addons/website_event_track_online/models/__init__.py +++ b/addons/website_event_track_online/models/__init__.py @@ -8,4 +8,6 @@ from . import event_track_visitor from . import event_track from . import event_track_stage from . import event_track_tag +from . import event_type +from . import website_event_menu from . import website_visitor diff --git a/addons/website_event_track_online/models/event_event.py b/addons/website_event_track_online/models/event_event.py index 1687967c00b..36899d5e86b 100644 --- a/addons/website_event_track_online/models/event_event.py +++ b/addons/website_event_track_online/models/event_event.py @@ -1,24 +1,64 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import models, _ +from odoo import models, fields, api, _ from odoo.addons.http_routing.models.ir_http import slug class EventEvent(models.Model): _inherit = "event.event" + community_menu = fields.Boolean( + "Community Menu", compute="_compute_community_menu", + readonly=False, store=True, + help="Display community tab on website") + community_menu_ids = fields.One2many( + "website.event.menu", "event_id", string="Event Community Menus", + domain=[("menu_type", "=", "community")]) + + @api.depends("event_type_id", "website_menu", "community_menu") + def _compute_community_menu(self): + """ At type onchange: synchronize. At website_menu update: synchronize. """ + for event in self: + if event.event_type_id and event.event_type_id != event._origin.event_type_id: + event.community_menu = event.event_type_id.community_menu + elif event.website_menu and event.website_menu != event._origin.website_menu or not event.community_menu: + event.community_menu = True + elif not event.website_menu: + event.community_menu = False + # ------------------------------------------------------------ # WEBSITE MENU MANAGEMENT # ------------------------------------------------------------ # OVERRIDES: ADD SEQUENCE + + def _get_menu_update_fields(self): + update_fields = super(EventEvent, self)._get_menu_update_fields() + update_fields += ['community_menu'] + return update_fields + + def _update_website_menus(self, menus_update_by_field=None): + super(EventEvent, self)._update_website_menus(menus_update_by_field=menus_update_by_field) + for event in self: + if not menus_update_by_field or event in menus_update_by_field.get('community_menu'): + event._update_website_menu_entry('community_menu', 'community_menu_ids', '_get_community_menu_entries') + + def _get_menu_type_field_matching(self): + res = super(EventEvent, self)._get_menu_type_field_matching() + res['community'] = 'community_menu' + return res + + def _get_community_menu_entries(self): + self.ensure_one() + return [(_('Community'), '/event/%s/community' % slug(self), False, 80, 'community')] + def _get_track_menu_entries(self): """ Remove agenda as this is now managed separately """ self.ensure_one() return [ (_('Talks'), '/event/%s/track' % slug(self), False, 10, 'track'), - (_('Agenda'), '/event/%s/agenda' % slug(self), False, 70, 'track') + (_('Agenda'), '/event/%s/agenda' % slug(self), False, 70, False) ] def _get_track_proposal_menu_entries(self): diff --git a/addons/website_event_track_online/models/event_type.py b/addons/website_event_track_online/models/event_type.py new file mode 100644 index 00000000000..9b2efc94af8 --- /dev/null +++ b/addons/website_event_track_online/models/event_type.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class EventType(models.Model): + _inherit = "event.type" + + community_menu = fields.Boolean( + "Community Menu", compute="_compute_community_menu", + readonly=False, store=True, + help="Display community tab on website") + + @api.depends('website_menu') + def _compute_community_menu(self): + for event_type in self: + event_type.community_menu = event_type.website_menu diff --git a/addons/website_event_track_online/models/website_event_menu.py b/addons/website_event_track_online/models/website_event_menu.py new file mode 100644 index 00000000000..97c17aff19a --- /dev/null +++ b/addons/website_event_track_online/models/website_event_menu.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class EventMenu(models.Model): + _inherit = "website.event.menu" + + menu_type = fields.Selection(selection_add=[ + ('community', 'Community Menu') + ]) diff --git a/addons/website_event_track_online/views/event_event_views.xml b/addons/website_event_track_online/views/event_event_views.xml index 0f95fc80b9b..c1aa910cc07 100644 --- a/addons/website_event_track_online/views/event_event_views.xml +++ b/addons/website_event_track_online/views/event_event_views.xml @@ -12,7 +12,13 @@ + + + + diff --git a/addons/website_event_track_online/views/event_type_views.xml b/addons/website_event_track_online/views/event_type_views.xml new file mode 100644 index 00000000000..f3b28139ee7 --- /dev/null +++ b/addons/website_event_track_online/views/event_type_views.xml @@ -0,0 +1,16 @@ + + + + event.type.view.form.inherit.meet + event.type + + + +
+
+
+
+
+ +
From b990f71de0b8f2ad3cda07c1d34b01a6e5f1b3d3 Mon Sep 17 00:00:00 2001 From: std-odoo Date: Mon, 13 Jul 2020 10:58:42 +0000 Subject: [PATCH 19/28] [ADD] website_jitsi: allow to create Jitsi room on the frontend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Build a mixin to create and manage jitsi-based chat rooms on website. SPECIFICATIONS: CHAT.ROOM Add a new model `chat.room` which contains all the configuration of the Jitsi room. A mixin `chat.room.mixin` adds a many2one to any model inheriting from it. That way behavior is optional without adding all fields on the model. We just need to inherit from the mixin and to use the chat room widget to build chat room on website for any model that needs it. SPECIFICATIONS: WIDGET Allow to give a default username when logging to jitsi if customers are logged. That way they gain a step and can directly participate to chat rooms. SPECIFICATIONS: PARTICIPANT COUNT Jitsi do not provide a REST API (or a similar API) to get the number of participant in a room (without hosting our own instance of Jitsi). The only way to get the number of participant is to be in the room and to use the Javascript API. That's why the participant send in RPC the number of participant in the room, to update the counter on the server side. We can not ask the user who is leaving to update the counter, because he can close the tab on his browser (and so the event will not be triggered), so, the first participant in the room send the update in RPC. Update of participant is done using SQL in non blocking mode so that concurrent accesses are not an issue. Even if participant count is not really accurate this is not an issue as managers will monitor rooms. LINKS Task ID-2252655 (Main Online Event task) Task ID-2283742 (Community gathering on chat rooms) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Stéphane Debauche Co-Authored-By: Thibault Delavallée --- addons/website_jitsi/__init__.py | 3 + addons/website_jitsi/__manifest__.py | 3 + addons/website_jitsi/controllers/__init__.py | 4 + addons/website_jitsi/controllers/main.py | 59 ++++ addons/website_jitsi/models/__init__.py | 5 + addons/website_jitsi/models/chat_room.py | 57 ++++ .../website_jitsi/models/chat_room_mixin.py | 57 ++++ .../security/ir.model.access.csv | 4 + .../static/src/css/chat_room.css | 17 ++ .../website_jitsi/static/src/js/chat_room.js | 271 ++++++++++++++++++ .../static/src/xml/chat_room_modal.xml | 20 ++ addons/website_jitsi/views/assets.xml | 11 + .../views/chat_room_templates.xml | 16 ++ .../website_jitsi/views/chat_room_views.xml | 70 +++++ 14 files changed, 597 insertions(+) create mode 100644 addons/website_jitsi/controllers/__init__.py create mode 100644 addons/website_jitsi/controllers/main.py create mode 100644 addons/website_jitsi/models/__init__.py create mode 100644 addons/website_jitsi/models/chat_room.py create mode 100644 addons/website_jitsi/models/chat_room_mixin.py create mode 100644 addons/website_jitsi/security/ir.model.access.csv create mode 100644 addons/website_jitsi/static/src/css/chat_room.css create mode 100644 addons/website_jitsi/static/src/js/chat_room.js create mode 100644 addons/website_jitsi/static/src/xml/chat_room_modal.xml create mode 100644 addons/website_jitsi/views/assets.xml create mode 100644 addons/website_jitsi/views/chat_room_templates.xml create mode 100644 addons/website_jitsi/views/chat_room_views.xml diff --git a/addons/website_jitsi/__init__.py b/addons/website_jitsi/__init__.py index 67dee8c60db..7d34c7c054a 100644 --- a/addons/website_jitsi/__init__.py +++ b/addons/website_jitsi/__init__.py @@ -1,2 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/website_jitsi/__manifest__.py b/addons/website_jitsi/__manifest__.py index cec3158a81a..af6638c3739 100644 --- a/addons/website_jitsi/__manifest__.py +++ b/addons/website_jitsi/__manifest__.py @@ -13,6 +13,9 @@ "website" ], "data": [ + 'views/assets.xml', + 'views/chat_room_templates.xml', + 'views/chat_room_views.xml', ], 'application': False, } diff --git a/addons/website_jitsi/controllers/__init__.py b/addons/website_jitsi/controllers/__init__.py new file mode 100644 index 00000000000..5d4b25db9c0 --- /dev/null +++ b/addons/website_jitsi/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import main diff --git a/addons/website_jitsi/controllers/main.py b/addons/website_jitsi/controllers/main.py new file mode 100644 index 00000000000..b7c91ea7f9f --- /dev/null +++ b/addons/website_jitsi/controllers/main.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from werkzeug.exceptions import NotFound + +from odoo import http +from odoo.http import request + + +class WebsiteJitsiController(http.Controller): + + @http.route(["/jitsi/update_status"], type="json", auth="public") + def jitsi_update_status(self, room_name, participant_count, joined): + """ Update room status: participant count, max reached + + Use the SQL keywords "FOR UPDATE SKIP LOCKED" in order to skip if the row + is locked (instead of raising an exception, wait for a moment and retry). + This endpoint may be called multiple times and we don't care having small + errors in participant count compared to performance issues. + + :raise ValueError: wrong participant count + :raise NotFound: wrong room name + """ + if participant_count < 0: + raise ValueError() + + chat_room = self._chat_room_exists(room_name) + if not chat_room: + raise NotFound() + + request.env.cr.execute( + """ + WITH req AS ( + SELECT id + FROM chat_room + -- Can not update the chat room if we do not have its name + WHERE name = %s + FOR UPDATE SKIP LOCKED + ) + UPDATE chat_room AS wcr + SET participant_count = %s, + last_activity = NOW(), + max_participant_reached = GREATEST(max_participant_reached, %s) + FROM req + WHERE wcr.id = req.id; + """, + [room_name, participant_count, participant_count] + ) + + @http.route(["/jitsi/is_full"], type="json", auth="public") + def jitsi_is_full(self, room_name): + return self._chat_room_exists(room_name).is_full + + # ------------------------------------------------------------ + # TOOLS + # ------------------------------------------------------------ + + def _chat_room_exists(self, room_name): + return request.env["chat.room"].sudo().search([("name", "=", room_name)], limit=1) diff --git a/addons/website_jitsi/models/__init__.py b/addons/website_jitsi/models/__init__.py new file mode 100644 index 00000000000..222bc1df76a --- /dev/null +++ b/addons/website_jitsi/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import chat_room +from . import chat_room_mixin diff --git a/addons/website_jitsi/models/chat_room.py b/addons/website_jitsi/models/chat_room.py new file mode 100644 index 00000000000..8494f01c01d --- /dev/null +++ b/addons/website_jitsi/models/chat_room.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import re + +from uuid import uuid4 + +from odoo import api, fields, models +from odoo.tools import remove_accents + + +class ChatRoom(models.Model): + """ Store all useful information to manage chat room (currently limited + to Jitsi). This model embeds all information about the chat room. We do not + store them in the related mixin (see chat.room.mixin) to avoid to add too + many fields on the models which want to use the chat room mixin as the + behavior can be optional in those models. + + The participant count is automatically updated thanks to the chat room widget + to avoid having a costly computed field with a members model. + """ + _name = "chat.room" + _description = "Chat Room" + + def _default_name(self): + return "odoo-room-%s" % str(uuid4())[:8] + + name = fields.Char( + "Room Name", required=True, copy=False, + default=lambda self: self._default_name()) + is_full = fields.Boolean("Full", compute="_compute_is_full") + lang_id = fields.Many2one( + "res.lang", "Language", + default=lambda self: self.env["res.lang"].search([("code", "=", self.env.user.lang)], limit=1)) + max_capacity = fields.Selection( + [("4", "4"), ("8", "8"), ("12", "12"), ("16", "16"), + ("20", "20"), ("no_limit", "No limit")], string="Max capacity", + default="8", required=True) + participant_count = fields.Integer("Participant count", default=0, copy=False) + # reporting fields + last_activity = fields.Datetime( + "Last Activity", copy=False, readonly=True, + default=lambda self: fields.Datetime.now()) + max_participant_reached = fields.Integer( + "Max participant reached", copy=False, readonly=True, + help="Maximum number of participant reached in the room at the same time") + + @api.depends("max_capacity", "participant_count") + def _compute_is_full(self): + for room in self: + if room.max_capacity == "no_limit": + room.is_full = False + else: + room.is_full = room.participant_count >= int(room.max_capacity) + + def _jitsi_sanitize_name(self, name): + return re.sub(r'[^\w+.]+', '-', remove_accents(name).lower()) diff --git a/addons/website_jitsi/models/chat_room_mixin.py b/addons/website_jitsi/models/chat_room_mixin.py new file mode 100644 index 00000000000..6aedb60bc6f --- /dev/null +++ b/addons/website_jitsi/models/chat_room_mixin.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class ChatRoomMixin(models.AbstractModel): + """Add the chat room configuration (`chat.room`) on the needed models. + + The chat room configuration contains all information about the room. So, we store + all the chat room logic at the same place, for all models. + Embed chat room related fields prefixed with `room_`. + """ + _name = "chat.room.mixin" + _description = "Chat Room Mixin" + ROOM_CONFIG_FIELDS = [ + ('room_name', 'name'), + ('room_lang_id', 'lang_id'), + ('room_max_capacity', 'max_capacity'), + ('room_participant_count', 'participant_count') + ] + + chat_room_id = fields.Many2one("chat.room", "Chat Room", readonly=True, copy=False, ondelete="set null") + # chat room related fields + room_name = fields.Char("Room Name", related="chat_room_id.name") + room_is_full = fields.Boolean("Room Is Full", related="chat_room_id.is_full") + room_lang_id = fields.Many2one("res.lang", "Language", related="chat_room_id.lang_id", readonly=False) + room_max_capacity = fields.Selection(string="Max capacity", related="chat_room_id.max_capacity", readonly=False, required=True) + room_participant_count = fields.Integer("Participant count", related="chat_room_id.participant_count", readonly=False) + room_last_activity = fields.Datetime("Last activity", related="chat_room_id.last_activity") + room_max_participant_reached = fields.Integer("Max participant reached", related="chat_room_id.max_participant_reached") + + @api.model_create_multi + def create(self, values_list): + for values in values_list: + if any(values.get(fmatch[0]) for fmatch in self.ROOM_CONFIG_FIELDS) and not values.get('chat_room_id'): + room_values = dict((fmatch[1], values[fmatch[0]]) for fmatch in self.ROOM_CONFIG_FIELDS if values.get(fmatch[0])) + values['chat_room_id'] = self.env['chat.room'].create(room_values).id + return super(ChatRoomMixin, self).create(values_list) + + def write(self, values): + if any(values.get(fmatch[0]) for fmatch in self.ROOM_CONFIG_FIELDS): + for document in self.filtered(lambda doc: not doc.chat_room_id): + room_values = dict((fmatch[1], values[fmatch[0]]) for fmatch in self.ROOM_CONFIG_FIELDS if values.get(fmatch[0])) + document.chat_room_id = self.env['chat.room'].create(room_values).id + return super(ChatRoomMixin, self).write(values) + + def copy(self, default=None): + if self.chat_room_id: + chat_room_id = self.chat_room_id.copy() + default = dict(default or {}, chat_room_id=chat_room_id.id) + return super().copy(default) + + def unlink(self): + if self.chat_room_id: + self.chat_room_id.unlink() + return super(ChatRoomMixin, self).unlink() diff --git a/addons/website_jitsi/security/ir.model.access.csv b/addons/website_jitsi/security/ir.model.access.csv new file mode 100644 index 00000000000..eacbd51548b --- /dev/null +++ b/addons/website_jitsi/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_chat_room_all,access_chat_room,model_chat_room,,0,0,0,0 +access_chat_room_user,access_chat_room,model_chat_room,base.group_user,1,0,0,0 +access_chat_room_system,access_chat_room,model_chat_room,base.group_system,1,1,1,1 diff --git a/addons/website_jitsi/static/src/css/chat_room.css b/addons/website_jitsi/static/src/css/chat_room.css new file mode 100644 index 00000000000..ad0b2b8b0d5 --- /dev/null +++ b/addons/website_jitsi/static/src/css/chat_room.css @@ -0,0 +1,17 @@ +.o_wjitsi_room_modal .modal-dialog { + top: 10%; + max-width: 100vw; + max-height: 100vh; + width: 80%; + margin: auto; +} + +.o_wjitsi_room_modal .modal-body { + height: 70vh; + overflow: hidden; + margin: 0; +} + +.o_wjitsi_chat_room_loading { + top: 50%; +} diff --git a/addons/website_jitsi/static/src/js/chat_room.js b/addons/website_jitsi/static/src/js/chat_room.js new file mode 100644 index 00000000000..f57a67a0a54 --- /dev/null +++ b/addons/website_jitsi/static/src/js/chat_room.js @@ -0,0 +1,271 @@ +odoo.define('website_jitsi.chat_room', function (require) { +'use strict'; + +const config = require("web.config"); +const core = require('web.core'); +const Dialog = require('web.Dialog'); +const publicWidget = require('web.public.widget'); +const QWeb = core.qweb; +const _t = core._t; + +publicWidget.registry.ChatRoom = publicWidget.Widget.extend({ + selector: '.o_wjitsi_room_widget', + xmlDependencies: ['/website_jitsi/static/src/xml/chat_room_modal.xml'], + events: { + 'click .o_wjitsi_room_link': '_onChatRoomClick', + }, + + start: async function () { + await this._super.apply(this, arguments); + this.roomName = this.$el.data('room-name'); + this.chatRoomId = parseInt(this.$el.data('chat-room-id')); + // automatically open the current room + this.autoOpen = parseInt(this.$el.data('auto-open') || 0); + // before joining, perform a RPC call to verify that the chat room is not full + this.checkFull = parseInt(this.$el.data('check-full') || 0); + // query selector of the element on which we attach the Jitsi iframe + // if not defined, the widget will pop in a modal instead + this.attachTo = this.$el.data('attach-to') || false; + // default username for jitsi + this.defaultUsername = this.$el.data('default-username') || false; + + if (this.autoOpen) { + await this._onChatRoomClick(); + } + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Click on a chat room to join it. + * + * @private + */ + _onChatRoomClick: async function () { + if (this.checkFull) { + // maybe we didn't refresh the page for a while and so we might join a room + // which is full, so we perform a RPC call to verify that we can really join + let isChatRoomFull = await this._rpc({ + route: '/jitsi/is_full', + params: { + room_name: this.roomName, + }, + }); + + if (isChatRoomFull) { + Dialog.alert(this, _t("Sorry, this room is full"), { + title: _t("Warning"), + // reload the page to refresh the participant count + confirm_callback: () => window.location.reload(), + }); + return; + } + } + + if (await this._openMobileApplication(this.roomName)) { + // we opened the mobile application + return; + } + + await this._loadJisti(); + + if (this.attachTo) { + // attach the Jitsi iframe on the given parent node + let $parentNode = $(this.attachTo); + $parentNode.find("iframe").trigger("empty"); + $parentNode.empty(); + + let jitsiRoom = await this._joinJitsiRoom($parentNode); + + $(jitsiRoom._frame).on("empty", async () => { + // we opened an other Jitsi room on the same parent node + await this._updatePartitipantCountIfEmpty(); + }); + + jitsiRoom.addEventListener('videoConferenceLeft', async () => { + await this._updatePartitipantCountIfEmpty(); + }); + } else { + // create a model and append the Jitsi iframe in it + let $jitsiModal = $(QWeb.render('chat_room_modal', {})); + $("body").append($jitsiModal); + $jitsiModal.modal('show'); + + let jitsiRoom = await this._joinJitsiRoom($jitsiModal.find('.modal-body')); + + // close the modal when hanging up + jitsiRoom.addEventListener('videoConferenceLeft', async () => { + $('.o_wjitsi_room_modal').modal('hide'); + }); + + // when the modal is closed, delete the Jitsi room object and clear the DOM + $jitsiModal.on('hidden.bs.modal', async () => { + jitsiRoom.dispose(); + $(".o_wjitsi_room_modal").remove(); + await this._updatePartitipantCountIfEmpty(); + }); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Jitsi do not provide an REST API to get the number of participant in a room. + * The only way to get the number of participant is to be in the room and to use + * the Javascript API. So, to update the participant count on the server side, + * the participant have to send the count in RPC... + * + * When leaving a room, the event "participantLeft" is called for the current user + * once per participant in the room (like if all other participants were leaving the + * room and then the current user himself). + * + * "participantLeft" is called only one time for the other participant who are still + * in the room. + * + * We can not ask the user who is leaving the room to update the participant count + * because user might close their browser tab without hanging up (and so without + * triggering the event "videoConferenceLeft"). So, we wait for a moment (because the + * event "participantLeft" is called many time for the participant who is leaving) + * and the first participant send the new participant count (so we avoid spamming the + * server with HTTP requests). + * + * We use "setTimout" to send maximum one HTTP request per interval, even if multiple + * participants join/leave at the same time in the defined interval. + * + * Update on the 29 June 2020 + * + * @private + * @param {jQuery} $jitsiModal, jQuery modal element in which we add the Jitsi room + * @returns {JitsiRoom} the newly created Jitsi room + */ + _joinJitsiRoom: async function ($parentNode) { + let jitsiRoom = await this._createJitsiRoom(this.roomName, $parentNode); + + if (this.defaultUsername) { + jitsiRoom.executeCommand("displayName", this.defaultUsername); + } + + let timeoutCall = null; + const updateParticipantCount = (joined) => { + // we clear the old timeout to be sure to call it only once each 2 seconds + // (so if 2 participants join/leave in this interval, we will perform only + // one HTTP request for both). + clearTimeout(timeoutCall); + timeoutCall = setTimeout(() => { + this.allParticipantIds = Object.keys(jitsiRoom._participants).sort(); + if (this.participantId === this.allParticipantIds[0]) { + // only the first participant of the room send the new participant + // count so we avoid to send to many HTTP requests + this._updateParticipantCount(this.allParticipantIds.length, joined); + } + }, 2000); + }; + + jitsiRoom.addEventListener('participantJoined', () => updateParticipantCount(true)); + jitsiRoom.addEventListener('participantLeft', () => updateParticipantCount(false)); + + // update the participant count when joining the room + jitsiRoom.addEventListener('videoConferenceJoined', async (event) => { + this.participantId = event.id; + updateParticipantCount(true); + $('.o_wjitsi_chat_room_loading').addClass('d-none'); + }); + + return jitsiRoom; + }, + + /** + * If we are the last participant in the room, we are the only one who can send the + * "zero participant" update to the server. + * + * @private + */ + _updatePartitipantCountIfEmpty: async function () { + if (this.allParticipantIds && this.allParticipantIds.length === 1 && this.allParticipantIds[0] === this.participantId) { + // we are the last participant in the room and we left it + await this._updateParticipantCount(0, false); + } + }, + + /** + * Perform an HTTP request to update the participant count on the server side. + * + * @private + * @param {integer} count, current number of participant in the room + * @param {boolean} joined, true if someone joined the room + */ + _updateParticipantCount: async function (count, joined) { + await this._rpc({ + route: '/jitsi/update_status', + params: { + room_name: this.roomName, + participant_count: count, + joined: joined, + }, + }); + }, + + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Redirect on the Jitsi mobile application if we are on mobile. + * + * @private + * @param {string} roomName + * @returns {boolean} true is we were redirected to the mobile application + */ + _openMobileApplication: async function (roomName) { + if (config.device.isMobile) { + // we are on mobile, open the room in the application + window.location = `intent://meet.jit.si/${roomName}#Intent;scheme=org.jitsi.meet;package=org.jitsi.meet;end`; + return true; + } + return false; + }, + + /** + * Create a Jitsi room on the given DOM element. + * + * @private + * @param {string} roomName + * @param {jQuery} $parentNode + * @returns {JitsiRoom} the newly created Jitsi room + */ + _createJitsiRoom: async function (roomName, $parentNode) { + await this._loadJisti(); + const domain = "meet.jit.si"; + const options = { + roomName: roomName, + width: "100%", + height: "100%", + parentNode: $parentNode[0], + configOverwrite: {disableDeepLinking: true}, + }; + return new window.JitsiMeetExternalAPI(domain, options); + }, + + /** + * Load the Jitsi external library if necessary. + * + * @private + */ + _loadJisti: async function () { + if (!window.JitsiMeetExternalAPI) { + await $.ajax({ + url: "https://meet.jit.si/external_api.js", + dataType: "script", + }); + } + }, +}); + +return publicWidget.registry.ChatRoom; + +}); diff --git a/addons/website_jitsi/static/src/xml/chat_room_modal.xml b/addons/website_jitsi/static/src/xml/chat_room_modal.xml new file mode 100644 index 00000000000..d076a31739f --- /dev/null +++ b/addons/website_jitsi/static/src/xml/chat_room_modal.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/addons/website_jitsi/views/assets.xml b/addons/website_jitsi/views/assets.xml new file mode 100644 index 00000000000..ced7415c40e --- /dev/null +++ b/addons/website_jitsi/views/assets.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/addons/website_event_track_exhibitor/views/event_event_views.xml b/addons/website_event_track_exhibitor/views/event_event_views.xml new file mode 100644 index 00000000000..ff33ca4d6a8 --- /dev/null +++ b/addons/website_event_track_exhibitor/views/event_event_views.xml @@ -0,0 +1,15 @@ + + + + event.event.view.form.inherit.exhibitor + + event.event + + + + + + + diff --git a/addons/website_event_track_exhibitor/views/event_exhibitor_templates_list.xml b/addons/website_event_track_exhibitor/views/event_exhibitor_templates_list.xml new file mode 100644 index 00000000000..373a017c82a --- /dev/null +++ b/addons/website_event_track_exhibitor/views/event_exhibitor_templates_list.xml @@ -0,0 +1,238 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/website_event_track_exhibitor/views/event_exhibitor_templates_page.xml b/addons/website_event_track_exhibitor/views/event_exhibitor_templates_page.xml new file mode 100644 index 00000000000..81745db9186 --- /dev/null +++ b/addons/website_event_track_exhibitor/views/event_exhibitor_templates_page.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/website_event_track_exhibitor/views/event_sponsor_views.xml b/addons/website_event_track_exhibitor/views/event_sponsor_views.xml new file mode 100644 index 00000000000..4228c23527d --- /dev/null +++ b/addons/website_event_track_exhibitor/views/event_sponsor_views.xml @@ -0,0 +1,113 @@ + + + + event.sponsor.view.search.inherit.exhibitor + event.sponsor + + + + + + + + + + + event.sponsor.view.tree.inherit.exhibitor + event.sponsor + + + + + + + + + + event.sponsor.view.kanban.inherit.kanban + event.sponsor + + + + + + + + + + event.sponsor.view.form.inherit.exhibitor + event.sponsor + + + +
+ + +
+
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + Event Sponsors + event.sponsor + kanban,tree,form + +

+ Create a Sponsor / Exhibitor +

+ Sponsors might be advertised on you event pages footer.
+ Exhibitors might have a dedicated page with chat room for people to connect with them. +

+
+
+ + + +

+ Create a Sponsor / Exhibitor +

+ Sponsors might be advertised on you event pages footer.
+ Exhibitors might have a dedicated page with chat room for people to connect with them. +

+
+
+ +
diff --git a/addons/website_event_track_exhibitor/views/event_type_views.xml b/addons/website_event_track_exhibitor/views/event_type_views.xml new file mode 100644 index 00000000000..ab2ffe85c81 --- /dev/null +++ b/addons/website_event_track_exhibitor/views/event_type_views.xml @@ -0,0 +1,15 @@ + + + + event.type.view.form.inherit.exhibitor + event.type + + + +
+
+
+
+
+
From e73ec2ddb90b105bfad72a018726eaf3ac38e21c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Thu, 25 Jun 2020 07:11:27 +0000 Subject: [PATCH 22/28] [ADD] website_event_track_session: new session mode for tracks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Purpose of this commit is to completely redesign the talks pages and frontend flow. Notably better showcase live or upcoming talks, have a usable tracks list, provide reworked page view with tracks suggestions, ... SPECIFICATIONS Mockups are given in https://docs.google.com/presentation/d/1VSxp2luUvp9xLFq9d1Wy-6IC0mP0NA8vLmh44WE5PCs/edit#slide=id.g891af57392_0_84 Main ideas are * new pages: view for all tracks and view for a specific track * list view: display two sections (Live Now, Coming Soon) displaying track cards. After that a list view of tracks by date. This page allows to filter tracks on tags and search on them; * page view: revamped view of a track. Two tabs: main content holds track description and information. Aside tab contains suggestions of tracks based on a given heuristics; SPECIFICATIONS: LIST VIEW: LIVE SECTIONS Have 2 sections: Live Now and Coming Soon. Live Now contains tracks currently running. Coming Soon displays tracks that begins within 30 minutes. In those sections, tracks are listed as cards with basic informations: cover picture, title, speakers, remaining time before start or time since it has started, ... Use the reminder widget to allow people to wishlist tracks. Clicking on a card redirect to the track page view. SPECIFICATIONS: LIST VIEW Display a list view of tracks under those sections. Use a table-like display in desktop, more card-based in mobile to ease reading. Each day is a section of the list view, a bit like the agenda. For each track, display its name, tags, speaker, duration. If it starts today, display remaining time. Otherwise display its start datetime. For done tracks, do not display any time information as it is not useful anymore. Display reminder widget on tracks that are not done. Accepted stages should be displayed text-only to customers. That way they know the track exists but they cannot see the track page. Even managers see links to be able to edit or publish tracks. SPECIFICATIONS: PAGE VIEW Display a main content tab, with track description and speaker information. Display an aside tab, with track suggestions following this heuristics * live first; * then ordered by start date, finished being sent to the end; * wishlisted (manually or by default); * tag matching with current track; * location matching with current track; * finally a random to have an "equivalent wave" randomly given; As with track list, accepted tracks are not clickable for customers. In mobile, main content should take the whole screen and aside should be collapsed. That way we put emphasis on main content. SPECIFICATIONS: CTA BUTTON Allow to configure "Call to action" button on a track. It is now possible to give configuration of a button that appears on the track page view, above the other track suggestions tab. This CTA should be configured using: its name, its URL, and its delay compared to the track beginning. Before this delay it is not displayed. JS widget ensure button is displayed even if the page is not reloaded, to make a super wow effect especially with live session on track. KNOWN LIMITATIONS Tracks do not hold any protection for payment-based tickets. Everything that is published is available using standard ACLs. Content protection based on tickets is currently not supported. LINKS Task ID-2252655 (Main Online Event task) Task ID-2283735 (Talks -> Session upgrade) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- .../website_event_track_session/__init__.py | 4 + .../__manifest__.py | 7 + .../controllers/__init__.py | 4 + .../controllers/session.py | 200 +++++++++ .../data/event_track_demo.xml | 17 + .../models/__init__.py | 4 + .../models/event_track.py | 143 +++++++ .../src/scss/event_track_templates.scss | 93 ++++ .../tests/__init__.py | 4 + .../tests/test_track_internals.py | 123 ++++++ .../views/assets.xml | 10 + .../views/event_track_templates_agenda.xml | 13 + .../views/event_track_templates_list.xml | 398 ++++++++++++++++++ .../views/event_track_templates_misc.xml | 18 + .../views/event_track_templates_page.xml | 209 +++++++++ .../views/event_track_views.xml | 33 ++ 16 files changed, 1280 insertions(+) create mode 100644 addons/website_event_track_session/controllers/__init__.py create mode 100644 addons/website_event_track_session/controllers/session.py create mode 100644 addons/website_event_track_session/data/event_track_demo.xml create mode 100644 addons/website_event_track_session/models/__init__.py create mode 100644 addons/website_event_track_session/models/event_track.py create mode 100644 addons/website_event_track_session/static/src/scss/event_track_templates.scss create mode 100644 addons/website_event_track_session/tests/__init__.py create mode 100644 addons/website_event_track_session/tests/test_track_internals.py create mode 100644 addons/website_event_track_session/views/assets.xml create mode 100644 addons/website_event_track_session/views/event_track_templates_agenda.xml create mode 100644 addons/website_event_track_session/views/event_track_templates_list.xml create mode 100644 addons/website_event_track_session/views/event_track_templates_misc.xml create mode 100644 addons/website_event_track_session/views/event_track_templates_page.xml create mode 100644 addons/website_event_track_session/views/event_track_views.xml diff --git a/addons/website_event_track_session/__init__.py b/addons/website_event_track_session/__init__.py index 67dee8c60db..5a31f331a2a 100644 --- a/addons/website_event_track_session/__init__.py +++ b/addons/website_event_track_session/__init__.py @@ -1,2 +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_session/__manifest__.py b/addons/website_event_track_session/__manifest__.py index 4738c7ea203..f9e3b627e0c 100644 --- a/addons/website_event_track_session/__manifest__.py +++ b/addons/website_event_track_session/__manifest__.py @@ -14,8 +14,15 @@ 'website_event_track_online', ], 'data': [ + 'views/assets.xml', + 'views/event_track_templates_agenda.xml', + 'views/event_track_templates_list.xml', + 'views/event_track_templates_misc.xml', + 'views/event_track_templates_page.xml', + 'views/event_track_views.xml', ], 'demo': [ + 'data/event_track_demo.xml', ], 'application': False, 'installable': True, diff --git a/addons/website_event_track_session/controllers/__init__.py b/addons/website_event_track_session/controllers/__init__.py new file mode 100644 index 00000000000..1c2ee57c54f --- /dev/null +++ b/addons/website_event_track_session/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import session diff --git a/addons/website_event_track_session/controllers/session.py b/addons/website_event_track_session/controllers/session.py new file mode 100644 index 00000000000..8ac27f82eab --- /dev/null +++ b/addons/website_event_track_session/controllers/session.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from ast import literal_eval +from pytz import timezone, utc +from werkzeug.exceptions import Forbidden, NotFound + +from odoo import exceptions, fields, http +from odoo.addons.website_event_track_online.controllers.event_track import EventTrackOnlineController +from odoo.http import request +from odoo.osv import expression +from odoo.tools import is_html_empty + + +class WebsiteEventSessionController(EventTrackOnlineController): + + def _get_event_tracks_base_domain(self, event): + """ Base domain for displaying tracks. Restrict to accepted or published + tracks for people not managing events. Unpublished tracks may be displayed + but not reachable for teasing purpose. """ + search_domain_base = [ + ('event_id', '=', event.id), + ('date', '!=', False) + ] + if not request.env.user.has_group('event.group_event_user'): + search_domain_base = expression.AND([ + search_domain_base, + ['|', ('is_published', '=', True), ('is_accepted', '=', True)] + ]) + return search_domain_base + + # ------------------------------------------------------------ + # MAIN PAGE + # ------------------------------------------------------------ + + @http.route([ + '''/event//track''', + '''/event//track/tag/''' + ], type='http', auth="public", website=True, sitemap=False) + def event_tracks(self, event, tag=None, **searches): + """ Main route + + :param event: event whose tracks are about to be displayed; + :param tag: deprecated: search for a specific tag + :param searches: frontend search dict, containing + + * 'search': search string; + * 'tags': list of tag IDs for filtering; + """ + if not event.can_access_from_current_website(): + raise NotFound() + + return request.render( + "website_event_track.tracks", + self._event_tracks_get_values(event, tag=tag, **searches) + ) + + def _event_tracks_get_values(self, event, tag=None, **searches): + # init and process search terms + searches.setdefault('search', '') + searches.setdefault('search_wishlist', '') + searches.setdefault('tags', '') + search_domain = self._get_event_tracks_base_domain(event) + + # search on content + if searches.get('search'): + search_domain = expression.AND([ + search_domain, + [('name', 'ilike', searches['search'])] + ]) + + # search on tags + search_tags = self._get_search_tags(searches['tags']) + if not search_tags and tag: # backward compatibility + search_tags = tag + if search_tags: + # Example: You filter on age: 10-12 and activity: football. + # Doing it this way allows to only get events who are tagged "age: 10-12" AND "activity: football". + # Add another tag "age: 12-15" to the search and it would fetch the ones who are tagged: + # ("age: 10-12" OR "age: 12-15") AND "activity: football + grouped_tags = dict() + for search_tag in search_tags: + grouped_tags.setdefault(search_tag.category_id, list()).append(search_tag) + search_domain_items = [ + [('tag_ids', 'in', [tag.id for tag in grouped_tags[group]])] + for group in grouped_tags + ] + search_domain = expression.AND([ + search_domain, + *search_domain_items + ]) + + # fetch data to display with TZ set for both event and tracks + now_tz = utc.localize(fields.Datetime.now().replace(microsecond=0), is_dst=False).astimezone(timezone(event.date_tz)) + today_tz = now_tz.date() + event = event.with_context(tz=event.date_tz or 'UTC') + tracks_sudo = event.env['event.track'].sudo().search(search_domain, order='date asc') + tag_categories = request.env['event.track.tag.category'].sudo().search([]) + + # filter on wishlist (as post processing due to costly search on is_reminder_on) + if searches.get('search_wishlist'): + tracks_sudo = tracks_sudo.filtered(lambda track: track.is_reminder_on) + + # organize categories for display: live, soon and day-based + date_begin_tz_all = list(set( + dt.date() + for dt in self._get_dt_in_event_tz(tracks_sudo.mapped('date'), event) + )) + date_begin_tz_all.sort() + tracks_sudo_live = tracks_sudo.filtered(lambda track: track.is_published and track.is_track_live) + tracks_sudo_soon = tracks_sudo.filtered(lambda track: track.is_published and not track.is_track_live and track.is_track_soon) + tracks_by_day = [] + for display_date in date_begin_tz_all: + matching_track = tracks_sudo.filtered(lambda track: self._get_dt_in_event_tz([track.date], event)[0].date() == display_date) + tracks_by_day.append((display_date, matching_track)) + + # return rendering values + return { + # event information + 'event': event, + 'main_object': event, + # tracks display information + 'tracks': tracks_sudo, + 'tracks_by_day': tracks_by_day, + 'tracks_live': tracks_sudo_live, + 'tracks_soon': tracks_sudo_soon, + 'today_tz': today_tz, + # search information + 'searches': searches, + 'search_key': searches['search'], + 'search_wishlist': searches['search_wishlist'], + 'search_tags': search_tags, + 'tag_categories': tag_categories, + # environment + 'is_html_empty': is_html_empty, + 'hostname': request.httprequest.host.split(':')[0], + 'user_event_manager': request.env.user.has_group('event.group_event_manager'), + } + + # ------------------------------------------------------------ + # PAGE VIEW + # ------------------------------------------------------------ + + @http.route('/event//track/', + type='http', auth="public", website=True, sitemap=False) + def event_track_page(self, event, track, **options): + track = self._fetch_track(track.id, allow_is_accepted=False) + + return request.render( + "website_event_track_session.event_track_main", + self._event_track_page_get_values(event, track.sudo(), **options) + ) + + def _event_track_page_get_values(self, event, track, **options): + track = track.sudo() + + option_widescreen = options.get('widescreen', False) + option_widescreen = bool(option_widescreen) if option_widescreen != '0' else False + # search for tracks list + tracks_other = track._get_track_suggestions( + restrict_domain=self._get_event_tracks_base_domain(track.event_id), + limit=10 + ) + + return { + # event information + 'event': event, + 'main_object': track, + 'track': track, + # sidebar + 'tracks_other': tracks_other, + # options + 'option_widescreen': option_widescreen, + # environment + 'is_html_empty': is_html_empty, + 'hostname': request.httprequest.host.split(':')[0], + 'user_event_manager': request.env.user.has_group('event.group_event_manager'), + } + + # ------------------------------------------------------------ + # TOOLS + # ------------------------------------------------------------ + + def _get_search_tags(self, tag_search): + # TDE FIXME: make me generic (slides, event, ...) + try: + tag_ids = literal_eval(tag_search) + except Exception: + tags = request.env['event.track.tag'].sudo() + else: + # perform a search to filter on existing / valid tags implicitly + tags = request.env['event.track.tag'].sudo().search([('id', 'in', tag_ids)]) + return tags + + def _get_dt_in_event_tz(self, datetimes, event): + tz_name = event.date_tz + return [ + utc.localize(dt, is_dst=False).astimezone(timezone(tz_name)) + for dt in datetimes + ] diff --git a/addons/website_event_track_session/data/event_track_demo.xml b/addons/website_event_track_session/data/event_track_demo.xml new file mode 100644 index 00000000000..c9fd08ccd84 --- /dev/null +++ b/addons/website_event_track_session/data/event_track_demo.xml @@ -0,0 +1,17 @@ + + + + + + Try Now + http://www.example.com + 10 + + + + Try Now + http://www.example.com + 10 + + + diff --git a/addons/website_event_track_session/models/__init__.py b/addons/website_event_track_session/models/__init__.py new file mode 100644 index 00000000000..e3798014924 --- /dev/null +++ b/addons/website_event_track_session/models/__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_session/models/event_track.py b/addons/website_event_track_session/models/event_track.py new file mode 100644 index 00000000000..0b20937a7f9 --- /dev/null +++ b/addons/website_event_track_session/models/event_track.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import timedelta +from random import randint +from pytz import utc + +from odoo import api, fields, models +from odoo.osv import expression + + +class EventTrack(models.Model): + _inherit = "event.track" + + # Call to action + website_cta = fields.Boolean('Magic Button') + website_cta_title = fields.Char('Button Title') + website_cta_url = fields.Char('Button Target URL') + website_cta_delay = fields.Integer('Button appears') + # time information for track + is_track_live = fields.Boolean( + 'Is Track Live', compute='_compute_track_time_data', + help="Track has started and is ongoing") + is_track_soon = fields.Boolean( + 'Is Track Soon', compute='_compute_track_time_data', + help="Track begins soon") + is_track_today = fields.Boolean( + 'Is Track Today', compute='_compute_track_time_data', + help="Track begins today") + is_track_upcoming = fields.Boolean( + 'Is Track Upcoming', compute='_compute_track_time_data', + help="Track is not yet started") + is_track_done = fields.Boolean( + 'Is Track Done', compute='_compute_track_time_data', + help="Track is finished") + track_start_remaining = fields.Integer( + 'Minutes before track starts', compute='_compute_track_time_data', + help="Remaining time before track starts (seconds)") + track_start_relative = fields.Integer( + 'Minutes compare to track start', compute='_compute_track_time_data', + help="Relative time compared to track start (seconds)") + # time information for CTA + is_website_cta_live = fields.Boolean( + 'Is CTA Live', compute='_compute_cta_time_data', + help="CTA button is available") + website_cta_start_remaining = fields.Integer( + 'Minutes before CTA starts', compute='_compute_cta_time_data', + help="Remaining time before CTA starts (seconds)") + + @api.depends('date', 'date_end') + def _compute_track_time_data(self): + """ Compute start and remaining time for track itself. Do everything in + UTC as we compute only time deltas here. """ + now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0)) + for track in self: + if not track.date: + track.is_track_live = track.is_track_soon = track.is_track_today = track.is_track_upcoming = track.is_track_done = False + track.track_start_relative = track.track_start_remaining = 0 + continue + date_begin_utc = utc.localize(track.date, is_dst=False) + date_end_utc = utc.localize(track.date_end, is_dst=False) + track.is_track_live = date_begin_utc <= now_utc < date_end_utc + track.is_track_soon = (date_begin_utc - now_utc).total_seconds() < 30*60 if date_begin_utc > now_utc else False + track.is_track_today = date_begin_utc.date() == now_utc.date() + track.is_track_upcoming = date_begin_utc > now_utc + track.is_track_done = date_end_utc <= now_utc + if date_begin_utc >= now_utc: + track.track_start_relative = int((date_begin_utc - now_utc).total_seconds()) + track.track_start_remaining = track.track_start_relative + else: + track.track_start_relative = int((now_utc - date_begin_utc).total_seconds()) + track.track_start_remaining = 0 + + @api.depends('date', 'date_end', 'website_cta', 'website_cta_delay') + def _compute_cta_time_data(self): + """ Compute start and remaining time for track itself. Do everything in + UTC as we compute only time deltas here. """ + now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0)) + for track in self: + if not track.website_cta: + track.is_website_cta_live = track.website_cta_start_remaining = False + continue + + date_begin_utc = utc.localize(track.date, is_dst=False) + timedelta(minutes=track.website_cta_delay or 0) + date_end_utc = utc.localize(track.date_end, is_dst=False) + track.is_website_cta_live = date_begin_utc <= now_utc <= date_end_utc + if date_begin_utc >= now_utc: + td = date_begin_utc - now_utc + track.website_cta_start_remaining = int(td.total_seconds()) + else: + track.website_cta_start_remaining = 0 + + def _get_track_suggestions(self, restrict_domain=None, limit=None): + """ Returns the next tracks suggested after going to the current one + given by self. Tracks always belong to the same event. + + Heuristic is + + * live first; + * then ordered by start date, finished being sent to the end; + * wishlisted (manually or by default); + * tag matching with current track; + * location matching with current track; + * finally a random to have an "equivalent wave" randomly given; + + :param restrict_domain: an additional domain to restrict candidates; + :param limit: number of tracks to return; + """ + self.ensure_one() + + base_domain = [ + '&', + ('event_id', '=', self.event_id.id), + ('id', '!=', self.id), + ] + if restrict_domain: + base_domain = expression.AND([ + base_domain, + restrict_domain + ]) + + track_candidates = self.search(base_domain, limit=None, order='date asc') + if not track_candidates: + return track_candidates + + track_candidates = track_candidates.sorted( + lambda track: + (track.is_published, + track.is_track_live, + track.track_start_remaining > 0, + -1 * track.track_start_remaining, + track.is_reminder_on, + not track.wishlisted_by_default, + len(track.tag_ids & self.tag_ids), + track.location_id == self.location_id, + randint(0, 20), + ), reverse=True + ) + + return track_candidates[:limit] + + def get_backend_menu_id(self): + return self.env.ref('event.event_main_menu').id diff --git a/addons/website_event_track_session/static/src/scss/event_track_templates.scss b/addons/website_event_track_session/static/src/scss/event_track_templates.scss new file mode 100644 index 00000000000..6f2a8352348 --- /dev/null +++ b/addons/website_event_track_session/static/src/scss/event_track_templates.scss @@ -0,0 +1,93 @@ +.o_wesession_index { + + /* + * COMMON + */ + + .o_wesession_gradient { + background-image: linear-gradient(120deg, #875A7B, darken(#875A7B, 10%)); + opacity: 0.8; + } + + /* + * MAIN PAGE: LIST + */ + + // Tracks list + .o_wesession_list_tracks { + li { + border: 1px solid $border-color; + } + + li:not(:first-child) { + border-top: 0; + } + } + + // Track card + .o_wesession_track_card { + .card-body { + padding: 1rem; + } + + .card-footer { + padding: 0.75rem 1rem; + } + + .o_wesession_track_card_header_badge { + position: absolute; + bottom: 0; + width: 100%; + padding: $card-spacer-y $card-spacer-x; + text-align: right; + } + &.o_wesession_track_card_unpublished { + opacity: 0.8; + } + } + + /* + * TRACK PAGE: VIEW + */ + + .o_wevent_online_page_container { + // Main panel: current track + .o_wesession_track_main { + // Force side panel min-width to account for potential Youtube chat + // And adapt main panel max-width accordingly. + @include media-breakpoint-up(xl) { + max-width: calc(100% - 22rem); + } + + @media screen and (min-width: 1400px) { + max-width: none; + } + } + + // Left panel: other tracks + .o_wesession_track_aside { + // Force side panel min-width to account for potential Youtube chat + @include media-breakpoint-up(xl) { + min-width: 22rem; + } + + @media screen and (min-width: 1400px) { + min-width: auto; + } + + // Navigation: keel layout simple + .o_wesession_track_aside_nav { + .nav-link { + background-color: transparent; + border: 0; + color: $gray-600; + + &.active { + color: $gray-800; + font-weight: 500; + } + } + } + } + } +} diff --git a/addons/website_event_track_session/tests/__init__.py b/addons/website_event_track_session/tests/__init__.py new file mode 100644 index 00000000000..c4e08033d49 --- /dev/null +++ b/addons/website_event_track_session/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import test_track_internals diff --git a/addons/website_event_track_session/tests/test_track_internals.py b/addons/website_event_track_session/tests/test_track_internals.py new file mode 100644 index 00000000000..5ebcc48ae8e --- /dev/null +++ b/addons/website_event_track_session/tests/test_track_internals.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import datetime, timedelta +from unittest.mock import patch + +from odoo import fields +from odoo.addons.website.models.website_visitor import WebsiteVisitor +from odoo.addons.website_event_track_online.tests.common import TestEventTrackOnlineCommon + + +class TestTrackSuggestions(TestEventTrackOnlineCommon): + + def test_track_suggestion(self): + [location_1, location_2] = self.env['event.track.location'].create([ + {'name': 'Location 1'}, + {'name': 'Location 2'}, + ]) + + [tag_1, tag_2, tag_3, tag_4] = self.env['event.track.tag'].create([ + {'name': 'Tag 1'}, {'name': 'Tag 2'}, {'name': 'Tag 3'}, {'name': 'Tag 4'} + ]) + + date = fields.Datetime.from_string(datetime.now().strftime('%Y-%m-%d %H:00:00')) + [track_1, track_2, track_3, track_4, track_5, track_6] = self.env['event.track'].create([{ + 'name': 'Track 1', + 'location_id': location_1.id, + 'event_id': self.event_0.id, + 'tag_ids': [(4, tag_1.id), (4, tag_2.id)], + 'date': date + timedelta(hours=-1), + }, { + 'name': 'Track 2', + 'location_id': location_2.id, + 'event_id': self.event_0.id, + 'date': date, + }, { + 'name': 'Track 3', + 'location_id': location_2.id, + 'event_id': self.event_0.id, + 'tag_ids': [(4, tag_1.id), (4, tag_3.id), (4, tag_4.id)], + 'date': date, + }, { + 'name': 'Track 4', + 'event_id': self.event_0.id, + 'tag_ids': [(4, tag_1.id), (4, tag_2.id)], + 'date': date, + }, { + 'name': 'Track 5', + 'event_id': self.event_0.id, + 'tag_ids': [(4, tag_1.id), (4, tag_3.id)], + 'wishlisted_by_default': True, + 'date': date, + }, { + 'name': 'Track 6', + 'location_id': location_1.id, + 'event_id': self.event_0.id, + 'tag_ids': [(4, tag_1.id), (4, tag_3.id)], + 'date': date, + }]) + + emp_visitor = self.env['website.visitor'].create({ + 'name': 'Visitor', + 'partner_id': self.user_employee.partner_id.id + }) + visitor_track = self.env['event.track.visitor'].create({ + 'visitor_id': emp_visitor.id, + 'track_id': track_3.id, + 'is_wishlisted': True, + }) + + with patch.object(WebsiteVisitor, '_get_visitor_from_request', lambda *args, **kwargs: emp_visitor), \ + self.with_user('user_employee'): + current_track = self.env['event.track'].browse(track_1.id) + all_suggestions = current_track._get_track_suggestions() + self.assertEqual( + all_suggestions.ids, + (track_3 + track_5 + track_4 + track_6 + track_2).ids # whlst / wishlst def / tags count / location + ) + + track_suggestion = current_track._get_track_suggestions(limit=1) + self.assertEqual(track_suggestion, track_3, + 'Returned track should be the manually wishlisted one') + + # remove wishlist, keynote should be top + visitor_track.unlink() + track_suggestion = current_track._get_track_suggestions(limit=1) + self.assertEqual( + track_suggestion, track_5, + 'Returned track should be the default wishlisted one') + + # toggle wishlisted by default off through blacklist + track_5_visitor = self.env['event.track.visitor'].sudo().create({ + 'visitor_id': emp_visitor.id, + 'track_id': track_5.id, + 'is_blacklisted': True, + }) + track_suggestion = current_track._get_track_suggestions(limit=1) + self.assertEqual( + track_suggestion, track_4, + 'Returned track should the one with the most common tags as keynote is blacklisted') + track_5_visitor.unlink() + + # remove keynote default, now based on tags + track_5.write({'wishlisted_by_default': False}) + # all_suggestions.invalidate_cache(fnames=['is_reminder_on']) + track_suggestion = current_track._get_track_suggestions(limit=1) + self.assertEqual( + track_suggestion, track_4, + 'Returned track should the one with the most common tags') + + # remove tags, now based on location + all_suggestions.sudo().write({'tag_ids': [(5,)]}) + track_suggestion = current_track._get_track_suggestions(limit=1) + self.assertEqual( + track_suggestion, track_6, + 'Returned track should the one with matching location') + + # remove location, now based o random + all_suggestions.sudo().write({'location_id': False}) + track_suggestion = current_track._get_track_suggestions(limit=1) + self.assertTrue( + track_suggestion in [track_2, track_3, track_4, track_5, track_6], + "Returned track should the a random one (but not the one we're trying to get suggestion for)") diff --git a/addons/website_event_track_session/views/assets.xml b/addons/website_event_track_session/views/assets.xml new file mode 100644 index 00000000000..fd9b752732e --- /dev/null +++ b/addons/website_event_track_session/views/assets.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/addons/website_event_track_session/views/event_track_templates_agenda.xml b/addons/website_event_track_session/views/event_track_templates_agenda.xml new file mode 100644 index 00000000000..1ba3c530ad0 --- /dev/null +++ b/addons/website_event_track_session/views/event_track_templates_agenda.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/addons/website_event_track_session/views/event_track_templates_list.xml b/addons/website_event_track_session/views/event_track_templates_list.xml new file mode 100644 index 00000000000..b1a2f9f31fb --- /dev/null +++ b/addons/website_event_track_session/views/event_track_templates_list.xml @@ -0,0 +1,398 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/website_event_track_session/views/event_track_templates_misc.xml b/addons/website_event_track_session/views/event_track_templates_misc.xml new file mode 100644 index 00000000000..27bcf0b994c --- /dev/null +++ b/addons/website_event_track_session/views/event_track_templates_misc.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/addons/website_event_track_session/views/event_track_templates_page.xml b/addons/website_event_track_session/views/event_track_templates_page.xml new file mode 100644 index 00000000000..a3875fe4cf9 --- /dev/null +++ b/addons/website_event_track_session/views/event_track_templates_page.xml @@ -0,0 +1,209 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/addons/website_event_track_session/views/event_track_views.xml b/addons/website_event_track_session/views/event_track_views.xml new file mode 100644 index 00000000000..a80870faf2d --- /dev/null +++ b/addons/website_event_track_session/views/event_track_views.xml @@ -0,0 +1,33 @@ + + + + + event.track.view.form.inherit.session + event.track + + + + + + + + + + + + + + + + + From fab82c73a5aeb31a3be970dfe31ac754597dbb78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Warnon?= Date: Tue, 30 Jun 2020 12:29:03 +0000 Subject: [PATCH 23/28] [ADD] website_event_track_live: configure Youtube videos / streams on event.tracks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RATIONALE Events are sometimes held online, gathering a community. In this merge we improve Event application to better support full-online events with improved tracks, wishlists, chat rooms, ... PURPOSE Allow to include a youtube video / live stream on event tracks and update the interface to show "online" capabilities. SPECIFICATIONS This commit improves the existing event.tracks to include the possibility to configure a Youtube live stream or video. The frontend view of the track was adapted accordingly to show the stream / video instead of the track description when available. At the end of the video, the track view will automatically suggest a next track to the attendee in order to keep him looping through interesting tracks. Those suggestions are based on an algorithm that tries to select the track that would be the most interesting to this attendee based on his wishlist, similar tags with current track, same room, ... In addition, we automatically fallback to a Youtube thumbnail for tracks website image when available. We support two youtube modes * streaming: start and end of video is detected based on youtube API; * replay: boolean flag on track, telling the video is a static one, allowing to know by advance the end of video, and to pre-compute thumbnail and give early access if necessary; KNOWN LIMITATIONS Tracks do not hold any protection for payment-based tickets. Everything that is published is available using standard ACLs. Content protection based on tickets is currently not supported. LINKS Task ID-2252655 (Main Online Event task) Task ID-2283735 (Talks -> Session upgrade) Task ID-2283729 (Live/Youtube Tracks) Co-Authored-By: Aurélien Warnon Co-Authored-By: David Beguin Co-Authored-By: Thibault Delavallée --- addons/website_event_track_live/__init__.py | 3 + .../website_event_track_live/__manifest__.py | 8 ++ .../controllers/__init__.py | 5 + .../controllers/session.py | 21 +++ .../controllers/track_live.py | 35 +++++ .../data/event_track_demo.xml | 46 +++++++ .../models/__init__.py | 4 + .../models/event_track.py | 42 ++++++ .../static/src/js/website_event_track_live.js | 121 ++++++++++++++++++ .../src/js/website_event_track_suggestion.js | 81 ++++++++++++ .../src/scss/website_event_track_live.scss | 42 ++++++ .../website_event_track_live_templates.xml | 33 +++++ .../website_event_track_live/views/assets.xml | 14 ++ .../views/event_track_templates_list.xml | 22 ++++ .../views/event_track_templates_page.xml | 53 ++++++++ .../views/event_track_views.xml | 16 +++ 16 files changed, 546 insertions(+) create mode 100644 addons/website_event_track_live/controllers/__init__.py create mode 100644 addons/website_event_track_live/controllers/session.py create mode 100644 addons/website_event_track_live/controllers/track_live.py create mode 100644 addons/website_event_track_live/data/event_track_demo.xml create mode 100644 addons/website_event_track_live/models/__init__.py create mode 100644 addons/website_event_track_live/models/event_track.py create mode 100644 addons/website_event_track_live/static/src/js/website_event_track_live.js create mode 100644 addons/website_event_track_live/static/src/js/website_event_track_suggestion.js create mode 100644 addons/website_event_track_live/static/src/scss/website_event_track_live.scss create mode 100644 addons/website_event_track_live/static/src/xml/website_event_track_live_templates.xml create mode 100644 addons/website_event_track_live/views/assets.xml create mode 100644 addons/website_event_track_live/views/event_track_templates_list.xml create mode 100644 addons/website_event_track_live/views/event_track_templates_page.xml create mode 100644 addons/website_event_track_live/views/event_track_views.xml diff --git a/addons/website_event_track_live/__init__.py b/addons/website_event_track_live/__init__.py index 67dee8c60db..7d34c7c054a 100644 --- a/addons/website_event_track_live/__init__.py +++ b/addons/website_event_track_live/__init__.py @@ -1,2 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/website_event_track_live/__manifest__.py b/addons/website_event_track_live/__manifest__.py index 98e2c69bc9a..8f137e270e2 100644 --- a/addons/website_event_track_live/__manifest__.py +++ b/addons/website_event_track_live/__manifest__.py @@ -15,8 +15,16 @@ 'website_event_track_session', ], 'data': [ + 'views/assets.xml', + 'views/event_track_templates_list.xml', + 'views/event_track_templates_page.xml', + 'views/event_track_views.xml', ], 'demo': [ + 'data/event_track_demo.xml' + ], + 'qweb': [ + 'static/src/xml/website_event_track_live_templates.xml' ], 'application': False, 'installable': True, diff --git a/addons/website_event_track_live/controllers/__init__.py b/addons/website_event_track_live/controllers/__init__.py new file mode 100644 index 00000000000..df58fe9572a --- /dev/null +++ b/addons/website_event_track_live/controllers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import track_live +from . import session diff --git a/addons/website_event_track_live/controllers/session.py b/addons/website_event_track_live/controllers/session.py new file mode 100644 index 00000000000..9d6381e6300 --- /dev/null +++ b/addons/website_event_track_live/controllers/session.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import re + +from odoo.addons.website_event_track_session.controllers.session import WebsiteEventSessionController +from odoo.http import request + + +class WebsiteEventSessionLiveController(WebsiteEventSessionController): + + def _event_track_page_get_values(self, event, track, **options): + if 'widescreen' not in options: + options['widescreen'] = bool(track.youtube_video_url) + values = super(WebsiteEventSessionLiveController, self)._event_track_page_get_values(event, track, **options) + # Youtube disables the chat embed on all mobile devices + # This regex is a naive attempt at matching their behavior (should work for most cases) + values['is_mobile_chat_disabled'] = bool(re.match( + r'^.*(Android|iPad|iPhone).*', + request.httprequest.headers.get('User-Agent', request.httprequest.headers.get('user-agent', '')))) + return values diff --git a/addons/website_event_track_live/controllers/track_live.py b/addons/website_event_track_live/controllers/track_live.py new file mode 100644 index 00000000000..d85d3d13c79 --- /dev/null +++ b/addons/website_event_track_live/controllers/track_live.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http + +from odoo.addons.website_event_track_online.controllers.event_track import EventTrackOnlineController + + +class EventTrackLiveController(EventTrackOnlineController): + + @http.route('/event_track/get_track_suggestion', type='json', auth='public', website=True) + def get_next_track_suggestion(self, track_id): + track = self._fetch_track(track_id) + track_suggestion = track._get_track_suggestions( + restrict_domain=[('youtube_video_url', '!=', False), ('is_published', '=', True)], + limit=1) + if not track_suggestion: + return False + track_suggestion_sudo = track_suggestion.sudo() + track_sudo = track.sudo() + return self._prepare_track_suggestion_values(track_sudo, track_suggestion_sudo) + + def _prepare_track_suggestion_values(self, track, track_suggestion): + return { + 'current_track': { + 'name': track.name, + 'website_image_url': track.website_image_url, + }, + 'suggestion': { + 'id': track_suggestion.id, + 'name': track_suggestion.name, + 'speaker_name': track_suggestion.partner_name, + 'website_url': track_suggestion.website_url + } + } diff --git a/addons/website_event_track_live/data/event_track_demo.xml b/addons/website_event_track_live/data/event_track_demo.xml new file mode 100644 index 00000000000..79e01e8c5f0 --- /dev/null +++ b/addons/website_event_track_live/data/event_track_demo.xml @@ -0,0 +1,46 @@ + + + + + https://www.youtube.com/watch?v=66AIsiezq5g + + + + https://www.youtube.com/watch?v=IvUU8joBb1Q + + + + https://www.youtube.com/watch?v=qRrA8al77a0 + + + + https://www.youtube.com/watch?v=bZ-queeu-lk + + + + https://www.youtube.com/watch?v=gV3g5FzmQiM + + + + https://www.youtube.com/watch?v=g3IoL9hxABg + + + + + https://www.youtube.com/watch?v=AVZtyXJB-rI + + + + https://www.youtube.com/watch?v=LHztthKy4RE + + + + https://www.youtube.com/watch?v=B-nEYsyRlYo + + + + https://www.youtube.com/watch?v=7X8xuipr4_A + + + + diff --git a/addons/website_event_track_live/models/__init__.py b/addons/website_event_track_live/models/__init__.py new file mode 100644 index 00000000000..e3798014924 --- /dev/null +++ b/addons/website_event_track_live/models/__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_live/models/event_track.py b/addons/website_event_track_live/models/event_track.py new file mode 100644 index 00000000000..7765bfa455b --- /dev/null +++ b/addons/website_event_track_live/models/event_track.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import re + +from odoo import api, fields, models + + +class Track(models.Model): + _inherit = 'event.track' + + youtube_video_url = fields.Char('Youtube Video URL', + help="Configure this URL so that event attendees can see your Track in video!") + youtube_video_id = fields.Char('Youtube video ID', compute='_compute_youtube_video_id', + help="Extracted from the video URL and used to infer various links (embed/thumbnail/...)") + is_youtube_replay = fields.Boolean('Is Youtube Replay', + help="Check this option if the video is already available on Youtube to avoid showing 'Direct' options (Chat, ...)") + is_youtube_chat_available = fields.Boolean('Is Chat Available', compute='_compute_is_youtube_chat_available') + + @api.depends('youtube_video_url') + def _compute_youtube_video_id(self): + for track in self: + if track.youtube_video_url: + regex = r'^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*' + match = re.match(regex, track.youtube_video_url) + if match and len(match.groups()) == 2 and len(match.group(2)) == 11: + track.youtube_video_id = match.group(2) + + if not track.youtube_video_id: + track.youtube_video_id = False + + @api.depends('youtube_video_id', 'is_youtube_replay', 'date_end', 'is_track_done') + def _compute_website_image_url(self): + youtube_thumbnail_tracks = self.filtered(lambda track: not track.website_image and track.youtube_video_id) + super(Track, self - youtube_thumbnail_tracks)._compute_website_image_url() + for track in youtube_thumbnail_tracks: + track.website_image_url = f'https://img.youtube.com/vi/{track.youtube_video_id}/maxresdefault.jpg' + + @api.depends('youtube_video_url', 'is_youtube_replay', 'date', 'date_end', 'is_track_upcoming', 'is_track_live') + def _compute_is_youtube_chat_available(self): + for track in self: + track.is_youtube_chat_available = track.youtube_video_url and not track.is_youtube_replay and (track.is_track_soon or track.is_track_live) diff --git a/addons/website_event_track_live/static/src/js/website_event_track_live.js b/addons/website_event_track_live/static/src/js/website_event_track_live.js new file mode 100644 index 00000000000..a330308de63 --- /dev/null +++ b/addons/website_event_track_live/static/src/js/website_event_track_live.js @@ -0,0 +1,121 @@ +var onYouTubeIframeAPIReady; + +odoo.define('website_event_track_live.website_event_youtube_embed', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var TrackSuggestionWidget = require('website_event_track_live.website_event_track_suggestion'); + +var YOUTUBE_VIDEO_ENDED = 0; +var YOUTUBE_VIDEO_PLAYING = 1; +var YOUTUBE_VIDEO_PAUSED = 2; + +publicWidget.registry.websiteEventTrackLive = publicWidget.Widget.extend({ + selector: '.o_wevent_event_track_live', + custom_events: _.extend({}, publicWidget.Widget.prototype.custom_events, { + 'video-ended': '_onVideoEnded' + }), + + start: function () { + var self = this; + return this._super(...arguments).then(function () { + self._setupYoutubePlayer(); + self.isFullScreen = !!document.fullscreenElement; + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + _onPlayerReady: function () { + $(window).on('resize', this._onResize.bind(this)); + this.$('.o_wevent_event_track_live_loading').remove(); + }, + + _onPlayerStateChange: function (event) { + if (event.data === YOUTUBE_VIDEO_ENDED) { + this.trigger('video-ended'); + } else if (event.data === YOUTUBE_VIDEO_PLAYING) { + this.trigger('video-playing'); + } else if (event.data === YOUTUBE_VIDEO_PAUSED) { + this.trigger('video-paused'); + } + }, + + _onVideoEnded: function () { + var self = this; + this._rpc({ + route: '/event_track/get_track_suggestion', + params: { + track_id: this.$el.data('trackId'), + } + }).then(function (suggestion) { + self.nextSuggestion = suggestion; + self._showSuggestion(); + }); + }, + + _onReplay: function () { + this.youtubePlayer.seekTo(0); + this.youtubePlayer.playVideo(); + if (this.trackSuggestion) { + delete this.trackSuggestion; + } + }, + + /** + * The 'fullscreenchange' event is probably a better fit but it is unfortunately + * not triggered when Youtube enters fullscreen mode. + * However, the global window 'resize' is. + */ + _onResize: function () { + this.isFullScreen = !!document.fullscreenElement; + this._showSuggestion(); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _setupYoutubePlayer: function () { + var self = this; + + var youtubeId = self.$el.data('youtubeVideoId'); + var $youtubeElement = $(' + + + + + diff --git a/addons/website_event_track_live/views/event_track_templates_list.xml b/addons/website_event_track_live/views/event_track_templates_list.xml new file mode 100644 index 00000000000..b2fc5243af7 --- /dev/null +++ b/addons/website_event_track_live/views/event_track_templates_list.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/addons/website_event_track_live/views/event_track_templates_page.xml b/addons/website_event_track_live/views/event_track_templates_page.xml new file mode 100644 index 00000000000..f72152f6e71 --- /dev/null +++ b/addons/website_event_track_live/views/event_track_templates_page.xml @@ -0,0 +1,53 @@ + + + + + + + + + + +