diff --git a/addons/test_event_full/__init__.py b/addons/test_event_full/__init__.py index 66a1383e8f3..67dee8c60db 100644 --- a/addons/test_event_full/__init__.py +++ b/addons/test_event_full/__init__.py @@ -1,4 +1,2 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -from . import tests diff --git a/addons/test_event_full/__manifest__.py b/addons/test_event_full/__manifest__.py index fb1d94f0dc4..4d6116280a9 100644 --- a/addons/test_event_full/__manifest__.py +++ b/addons/test_event_full/__manifest__.py @@ -8,7 +8,7 @@ 'description': """ This module will test the main event flows of Odoo, both frontend and backend. It installs sale capabilities, front-end flow, eCommerce, questions and -automatic lead generation.. +automatic lead generation, full Online support, ... """, 'depends': [ 'event', @@ -16,10 +16,21 @@ automatic lead generation.. 'event_sale', 'website_event_crm_questions', 'website_event_questions', + 'website_event_online', + 'website_event_questions', + 'website_event_meet', 'website_event_sale', 'website_event_track', + 'website_event_track_online', + 'website_event_track_exhibitor', + 'website_event_track_session', + 'website_event_track_live', + 'website_event_track_quiz', + ], + 'data': [ + 'views/assets.xml', + ], + 'demo': [ ], - 'demo': [], - 'data': [], 'installable': True, } diff --git a/addons/test_event_full/static/src/js/tours/wevent_register_tour.js b/addons/test_event_full/static/src/js/tours/wevent_register_tour.js new file mode 100644 index 00000000000..7441ad07340 --- /dev/null +++ b/addons/test_event_full/static/src/js/tours/wevent_register_tour.js @@ -0,0 +1,147 @@ +odoo.define('test_event_full.tour.register', function (require) { +"use strict"; + +var tour = require('web_tour.tour'); + +/** + * TALKS STEPS + */ + +var discoverTalkSteps = function (talkName, fromList, reminderOn, toggleReminder) { + var steps; + if (fromList) { + steps = [{ + content: 'Go on "' + talkName + '" talk in List', + trigger: 'a:contains("' + talkName + '")', + }]; + } + else { + steps = [{ + content: 'Click on Live Track', + trigger: 'article span:contains("' + talkName + '")', + run: 'click', + }]; + } + if (reminderOn) { + steps = steps.concat([{ + content: "Check Reminder is on", + trigger: 'div.o_wetrack_js_reminder i.fa-bell', + extra_trigger: 'small.o_wetrack_js_reminder_text:contains("Reminder On")', + run: function () {}, // it's a check + }]); + } + else { + steps = steps.concat([{ + content: "Check Reminder is Off", + trigger: 'small.o_wetrack_js_reminder_text:contains("Set Reminder")', + run: function () {}, // it's a check + }]); + if (toggleReminder) { + steps = steps.concat([{ + content: "Set Reminder", + trigger: 'small.o_wetrack_js_reminder_text', + run: 'click', + }, { + content: "Check Reminder is On", + trigger: 'div.o_wetrack_js_reminder i.fa-bell', + extra_trigger: 'small.o_wetrack_js_reminder_text:contains("Reminder On")', + run: function () {}, // it's a check + }]); + } + } + return steps; +}; + + +/** + * ROOMS STEPS + */ + +var discoverRoomSteps = function (roomName) { + var steps = [{ + content: 'Go on "' + roomName + '" room in List', + trigger: 'a.o_wevent_meeting_room_card h3:contains("' + roomName + '")', + run: 'click', + }]; + return steps; +}; + + +/** + * REGISTER STEPS + */ + +var registerSteps = [{ + content: 'Go on Register', + trigger: 'li.btn-primary a:contains("Register")', +}, { + content: "Select 2 units of 'Standard' ticket type", + extra_trigger: '#wrap:not(:has(a[href*="/event"]:contains("Conference for Architects")))', + trigger: 'select:eq(0)', + run: 'text 2', +}, { + content: "Click on 'Register' button", + extra_trigger: 'select:eq(0):has(option:contains(2):propSelected)', + trigger: '.btn-primary:contains("Register")', + run: 'click', +}, { + content: "Fill attendees details", + trigger: 'form[id="attendee_registration"] .btn:contains("Continue")', + run: function () { + $("input[name='1-name']").val("Raoulette Poiluchette"); + $("input[name='1-phone']").val("0456112233"); + $("input[name='1-email']").val("raoulette@example.com"); + $("input[name='2-name']").val("Michel Tractopelle"); + $("input[name='2-phone']").val("0456332211"); + $("input[name='2-email']").val("michel@example.com"); + }, +}, { + content: "Validate attendees details", + extra_trigger: "input[name='1-name'], input[name='2-name'], input[name='3-name']", + trigger: 'button:contains("Continue")', + run: 'click', +}]; + +/** + * MAIN STEPS + */ + +var initTourSteps = function (eventName) { + return [{ + content: 'Go on "' + eventName + '" page', + trigger: 'a[href*="/event"]:contains("' + eventName + '"):first', + }]; +}; + +var browseTalksSteps = [{ + content: 'Browse Talks', + trigger: 'a:contains("Talks")', +}]; + +var browseExhibitorsSteps = [{ + content: 'Browse Exhibitors', + trigger: 'a:contains("Exhibitors")', +}]; + +var browseMeetSteps = [{ + content: 'Browse Meet', + trigger: 'a:contains("Community")', +}]; + + +tour.register('wevent_register', { + url: '/event', + test: true +}, [].concat( + initTourSteps('Online Reveal'), + browseTalksSteps, + discoverTalkSteps('What This Event Is All About', true, true), + browseTalksSteps, + discoverTalkSteps('Live Testimonial', false, false, true), + browseMeetSteps, + discoverRoomSteps('Best wood for furniture'), + registerSteps, + ) +); + +}); diff --git a/addons/test_event_full/tests/__init__.py b/addons/test_event_full/tests/__init__.py index b3be9a3b6f4..7011f22b294 100644 --- a/addons/test_event_full/tests/__init__.py +++ b/addons/test_event_full/tests/__init__.py @@ -2,4 +2,5 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import common -from . import test_event_crm \ No newline at end of file +from . import test_event_crm +from . import test_wevent_register diff --git a/addons/test_event_full/tests/common.py b/addons/test_event_full/tests/common.py index baa2a396568..e835704647e 100644 --- a/addons/test_event_full/tests/common.py +++ b/addons/test_event_full/tests/common.py @@ -1,15 +1,26 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +from datetime import datetime, timedelta, time + +from odoo import fields +from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUserPortal from odoo.addons.event_crm.tests.common import TestEventCrmCommon +from odoo.addons.sales_team.tests.common import TestSalesCommon +from odoo.addons.website.tests.test_website_visitor import MockVisitor +from odoo.addons.website_event_online.tests.common import EventDtPatcher -class TestEventFullCommon(TestEventCrmCommon): +class TestEventFullCommon(TestEventCrmCommon, TestSalesCommon, EventDtPatcher, MockVisitor): @classmethod def setUpClass(cls): super(TestEventFullCommon, cls).setUpClass() + # ------------------------------------------------------------ + # TICKET INFORMATIONS + # ------------------------------------------------------------ + cls.event_product = cls.env['product.product'].create({ 'name': 'Test Registration Product', 'description_sale': 'Mighty Description', @@ -19,7 +30,15 @@ class TestEventFullCommon(TestEventCrmCommon): 'type': 'service', }) + cls.website = cls.env['website'].search([ + ('company_id', '=', cls.env.user.company_id.id) + ], limit=1) + cls.event_0.write({ + # event if 8-18 in Europe/Brussels (DST) (first day: begins at 9, last day: ends at 15) + '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), + # ticket informations 'event_ticket_ids': [ (5, 0), (0, 0, { @@ -33,6 +52,15 @@ class TestEventFullCommon(TestEventCrmCommon): ], }) + # make a SO for a customer, selling some tickets + cls.customer_so = cls.env['sale.order'].with_user(cls.user_sales_salesman).create({ + 'partner_id': cls.event_customer.id, + }) + + # ------------------------------------------------------------ + # QUESTIONS + # ------------------------------------------------------------ + cls.event_question_1 = cls.env['event.question'].create({ 'title': 'Question1', 'question_type': 'simple_choice', @@ -60,10 +88,9 @@ class TestEventFullCommon(TestEventCrmCommon): 'once_per_order': True, }) - # make a SO for a customer, selling some tickets - cls.customer_so = cls.env['sale.order'].with_user(cls.user_sales_salesman).create({ - 'partner_id': cls.event_customer.id, - }) + # ------------------------------------------------------------ + # DATA MARSHMALLING + # ------------------------------------------------------------ cls.website_customer_data = [{ 'name': 'My Customer %02d' % x, @@ -100,3 +127,154 @@ class TestEventFullCommon(TestEventCrmCommon): self.assertIn(answer.value_answer_id.name, lead.description) else: self.assertIn(answer.value_text_box, lead.description) # better: check multi line + + +class TestWEventCommon(HttpCaseWithUserDemo, HttpCaseWithUserPortal, EventDtPatcher, MockVisitor): + + def setUp(self): + super(TestWEventCommon, self).setUp() + + self.event_product = self.env['product.product'].create({ + 'name': 'Test Event Registration', + 'default_code': 'EVENT_REG', + 'description_sale': 'Mighty Description', + 'list_price': 10, + 'event_ok': True, + 'standard_price': 30.0, + 'type': 'service', + }) + + self.event_tag_category_1 = self.env['event.tag.category'].create({ + 'name': 'Type', + 'sequence': 2, + }) + self.event_tag_category_1_tag_1 = self.env['event.tag'].create({ + 'name': 'Online', + 'sequence': 10, + 'category_id': self.event_tag_category_1.id, + 'color': 8, + }) + self.env['event.event'].search( + [('name', 'like', '%Online Reveal%')] + ).write( + {'name': 'Do not click on me'} + ) + self.event = self.env['event.event'].create({ + 'name': 'Online Reveal TestEvent', + 'auto_confirm': True, + 'stage_id': self.env.ref('event.event_stage_booked').id, + 'address_id': False, + 'user_id': self.user_demo.id, + 'tag_ids': [(4, self.event_tag_category_1_tag_1.id)], + # event if 8-18 in Europe/Brussels (DST) (first day: begins at 7, last day: ends at 17) + 'date_tz': 'Europe/Brussels', + 'date_begin': datetime.combine(self.reference_now, time(5, 0)) - timedelta(days=1), + 'date_end': datetime.combine(self.reference_now, time(15, 0)) + timedelta(days=1), + # ticket informations + 'event_ticket_ids': [ + (0, 0, { + 'name': 'Standard', + 'product_id': self.event_product.id, + 'price': 0, + }), (0, 0, { + 'name': 'VIP', + 'product_id': self.event_product.id, + 'seats_max': 10, + }) + ], + # activate menus + 'is_published': True, + 'website_menu': True, + 'website_track': True, + 'website_track_proposal': True, + 'exhibitor_menu': True, + 'community_menu': True, + }) + + self.event_customer = self.env['res.partner'].create({ + 'name': 'Constantin Customer', + 'email': 'constantin@test.example.com', + 'country_id': self.env.ref('base.be').id, + 'phone': '0485112233', + 'mobile': False, + }) + self.event_speaker = self.env['res.partner'].create({ + 'name': 'Brandon Freeman', + 'email': 'brandon.freeman55@example.com', + 'phone': '(355)-687-3262', + }) + + # ------------------------------------------------------------ + # QUESTIONS + # ------------------------------------------------------------ + + self.event_question_1 = self.env['event.question'].create({ + 'title': 'Question1', + 'question_type': 'simple_choice', + 'event_id': self.event.id, + 'once_per_order': False, + 'answer_ids': [ + (0, 0, {'name': 'Q1-Answer1'}), + (0, 0, {'name': 'Q1-Answer2'}) + ], + }) + self.event_question_2 = self.env['event.question'].create({ + 'title': 'Question2', + 'question_type': 'simple_choice', + 'event_id': self.event.id, + 'once_per_order': True, + 'answer_ids': [ + (0, 0, {'name': 'Q2-Answer1'}), + (0, 0, {'name': 'Q2-Answer2'}) + ], + }) + self.event_question_3 = self.env['event.question'].create({ + 'title': 'Question3', + 'question_type': 'text_box', + 'event_id': self.event.id, + 'once_per_order': True, + }) + + # ------------------------------------------------------------ + # TRACKS + # ------------------------------------------------------------ + + self.track_0 = self.env['event.track'].create({ + 'name': 'What This Event Is All About', + 'event_id': self.event.id, + 'stage_id': self.env.ref('website_event_track.event_track_stage3').id, + 'date': datetime.combine(self.reference_now, time(5, 0)), + 'duration': 2, + 'is_published': True, + 'wishlisted_by_default': True, + 'user_id': self.user_admin.id, + 'partner_id': self.event_speaker.id, + }) + self.track_1 = self.env['event.track'].create({ + 'name': 'Live Testimonial', + 'event_id': self.event.id, + 'stage_id': self.env.ref('website_event_track.event_track_stage3').id, + 'date': self.reference_now - timedelta(minutes=30), + 'duration': 0.75, + 'is_published': True, + 'user_id': self.user_admin.id, + 'partner_id': self.event_speaker.id, + }) + + # ------------------------------------------------------------ + # MEETING ROOMS + # ---------------------------------------------------------- + + self.env['event.meeting.room'].create({ + 'name': 'Best wood for furniture', + 'summary': 'Let\'s talk about wood types for furniture', + 'target_audience': 'wood expert(s)', + 'is_pinned': True, + 'website_published': True, + 'event_id': self.event.id, + 'room_lang_id': self.env.ref('base.lang_en').id, + 'room_max_capacity': '12', + 'room_participant_count': 9, + }) + + self.event.flush() diff --git a/addons/test_event_full/tests/test_wevent_register.py b/addons/test_event_full/tests/test_wevent_register.py new file mode 100644 index 00000000000..79ec993ab57 --- /dev/null +++ b/addons/test_event_full/tests/test_wevent_register.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import tests +from odoo.addons.test_event_full.tests.common import TestWEventCommon +from odoo.tests.common import HOST + + +@tests.common.tagged('post_install', '-at_install') +class TestWEventRegister(TestWEventCommon): + + def test_register(self): + self.browser_js( + '/event', + 'odoo.__DEBUG__.services["web_tour.tour"].run("wevent_register", 100)', + 'odoo.__DEBUG__.services["web_tour.tour"].tours.wevent_register.ready', + login=None + ) diff --git a/addons/test_event_full/views/assets.xml b/addons/test_event_full/views/assets.xml new file mode 100644 index 00000000000..0b20fc8de0e --- /dev/null +++ b/addons/test_event_full/views/assets.xml @@ -0,0 +1,8 @@ + + + + + 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..4ed754e39b0 --- /dev/null +++ b/addons/website_event_online/views/event_event_views.xml @@ -0,0 +1,23 @@ + + + + + event.event.view.form.inherit.online + event.event + + + +
+
+
+ + {'color_field': 'color', 'no_quick_create': True} + +
+
+ +
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/event_tag_views.xml b/addons/website_event_online/views/event_tag_views.xml new file mode 100644 index 00000000000..ebaffa264c0 --- /dev/null +++ b/addons/website_event_online/views/event_tag_views.xml @@ -0,0 +1,48 @@ + + + + event.tag.category.view.form.inherit.online + event.tag.category + + + + + + + + + + event.tag.category.view.tree.inherit.online + event.tag.category + + + + many2many_tags + {'color_field': 'color'} + + + + + + event.tag.view.list.inherit.online + event.tag + + + + + + + + + + event.tag.view.form.inherit.online + event.tag + + + + + + + + + diff --git a/addons/website_event_online/views/event_templates_event.xml b/addons/website_event_online/views/event_templates_event.xml new file mode 100644 index 00000000000..6889a092ed1 --- /dev/null +++ b/addons/website_event_online/views/event_templates_event.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + 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} + + + + + 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
+
+
+
+
+ +
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_exhibitor/__init__.py b/addons/website_event_track_exhibitor/__init__.py new file mode 100644 index 00000000000..5a31f331a2a --- /dev/null +++ b/addons/website_event_track_exhibitor/__init__.py @@ -0,0 +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_exhibitor/__manifest__.py b/addons/website_event_track_exhibitor/__manifest__.py new file mode 100644 index 00000000000..9fd2503da71 --- /dev/null +++ b/addons/website_event_track_exhibitor/__manifest__.py @@ -0,0 +1,33 @@ +# -*- 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': [ + 'security/ir.model.access.csv', + 'security/security.xml', + 'views/assets.xml', + 'views/event_sponsor_views.xml', + 'views/event_event_views.xml', + 'views/event_exhibitor_templates_list.xml', + 'views/event_exhibitor_templates_page.xml', + 'views/event_type_views.xml', + ], + 'demo': [ + 'data/event_demo.xml', + 'data/event_sponsor_demo.xml', + ], + 'application': False, + 'installable': True, +} diff --git a/addons/website_event_track_exhibitor/controllers/__init__.py b/addons/website_event_track_exhibitor/controllers/__init__.py new file mode 100644 index 00000000000..5a5d5a97f79 --- /dev/null +++ b/addons/website_event_track_exhibitor/controllers/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import exhibitor +from . import website_event_main diff --git a/addons/website_event_track_exhibitor/controllers/exhibitor.py b/addons/website_event_track_exhibitor/controllers/exhibitor.py new file mode 100644 index 00000000000..e07d2b8f5c3 --- /dev/null +++ b/addons/website_event_track_exhibitor/controllers/exhibitor.py @@ -0,0 +1,226 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from ast import literal_eval +from random import randint +from werkzeug.exceptions import NotFound, Forbidden + +from odoo import exceptions, http +from odoo.addons.website_event_track.controllers.main import WebsiteEventTrackController +from odoo.http import request +from odoo.osv import expression + + +class ExhibitorController(WebsiteEventTrackController): + + def _get_event_sponsors_base_domain(self, event): + search_domain_base = [ + ('event_id', '=', event.id), + ('is_exhibitor', '=', True), + ] + if not request.env.user.has_group('event.group_event_user'): + search_domain_base = expression.AND([search_domain_base, [('is_published', '=', True)]]) + return search_domain_base + + # ------------------------------------------------------------ + # MAIN PAGE + # ------------------------------------------------------------ + + @http.route(['/event//exhibitors'], type='http', auth="public", website=True, sitemap=False) + def event_exhibitors(self, event, **searches): + if not event.can_access_from_current_website(): + raise NotFound() + + return request.render( + "website_event_track_exhibitor.event_exhibitors", + self._event_exhibitors_get_values(event, **searches) + ) + + def _event_exhibitors_get_values(self, event, **searches): + # init and process search terms + searches.setdefault('search', '') + searches.setdefault('countries', '') + searches.setdefault('sponsorships', '') + search_domain_base = self._get_event_sponsors_base_domain(event) + search_domain = search_domain_base + + # search on content + if searches.get('search'): + search_domain = expression.AND([ + search_domain, + ['|', ('name', 'ilike', searches['search']), ('website_description', 'ilike', searches['search'])] + ]) + + # search on countries + search_countries = self._get_search_countries(searches['countries']) + if search_countries: + search_domain = expression.AND([ + search_domain, + [('partner_id.country_id', 'in', search_countries.ids)] + ]) + + # search on sponsor types + search_sponsorships = self._get_search_sponsorships(searches['sponsorships']) + if search_sponsorships: + search_domain = expression.AND([ + search_domain, + [('sponsor_type_id', 'in', search_sponsorships.ids)] + ]) + + # fetch data to display; use sudo to allow reading partner info, be sure domain is correct + event = event.with_context(tz=event.date_tz or 'UTC') + sponsors = request.env['event.sponsor'].sudo().search(search_domain) + sponsors_all = request.env['event.sponsor'].sudo().search(search_domain_base) + sponsor_types = sponsors_all.mapped('sponsor_type_id') + sponsor_countries = sponsors_all.mapped('partner_id.country_id') + # organize sponsors into categories to help display + sponsor_categories = dict() + for sponsor in sponsors: + if not sponsor_categories.get(sponsor.sponsor_type_id): + sponsor_categories[sponsor.sponsor_type_id] = request.env['event.sponsor'].sudo() + sponsor_categories[sponsor.sponsor_type_id] |= sponsor + sponsor_categories = [ + dict({ + 'sponsorship': sponsor_category, + 'sponsors': sponsors, + }) for sponsor_category, sponsors in sponsor_categories.items()] + + # return rendering values + return { + # event information + 'event': event, + 'main_object': event, + 'sponsor_categories': sponsor_categories, + 'hide_sponsors': True, + # search information + 'searches': searches, + 'search_key': searches['search'], + 'search_countries': search_countries, + 'search_sponsorships': search_sponsorships, + 'sponsor_types': sponsor_types, + 'sponsor_countries': sponsor_countries, + # environment + 'hostname': request.httprequest.host.split(':')[0], + 'user_event_manager': request.env.user.has_group('event.group_event_manager'), + } + + # ------------------------------------------------------------ + # FRONTEND FORM + # ------------------------------------------------------------ + + @http.route(['/event//exhibitor/'], type='http', auth="public", website=True, sitemap=False) + def event_exhibitor(self, event, sponsor, **options): + if not event.can_access_from_current_website(): + raise NotFound() + + try: + sponsor.check_access_rule('read') + except exceptions.AccessError: + raise Forbidden() + sponsor = sponsor.sudo() + + if 'widescreen' not in options and sponsor.chat_room_id and sponsor.is_in_opening_hours: + options['widescreen'] = True + + return request.render( + "website_event_track_exhibitor.event_exhibitor_main", + self._event_exhibitor_get_values(event, sponsor, **options) + ) + + def _event_exhibitor_get_values(self, event, sponsor, **options): + # search for exhibitor list + search_domain_base = self._get_event_sponsors_base_domain(event) + search_domain_base = expression.AND([ + search_domain_base, + [('id', '!=', sponsor.id)] + ]) + sponsors_other = request.env['event.sponsor'].sudo().search(search_domain_base) + sponsors_other = sponsors_other.filtered(lambda sponsor: sponsor.is_in_opening_hours) + current_country = sponsor.partner_id.country_id + + sponsors_other = sponsors_other.sorted(key=lambda sponsor: ( + sponsor.partner_id.country_id == current_country, + -1 * sponsor.sponsor_type_id.sequence, + randint(0, 20) + ), reverse=True) + + option_widescreen = options.get('widescreen', False) + option_widescreen = bool(option_widescreen) if option_widescreen != '0' else False + + return { + # event information + 'event': event, + 'main_object': sponsor, + 'sponsor': sponsor, + 'hide_sponsors': True, + # sidebar + 'sponsors_other': sponsors_other, + # options + 'option_widescreen': option_widescreen, + 'option_can_edit': request.env.user.has_group('event.group_event_manager'), + # environment + 'hostname': request.httprequest.host.split(':')[0], + 'user_event_manager': request.env.user.has_group('event.group_event_manager'), + } + + # ------------------------------------------------------------ + # BUSINESS / MISC + # ------------------------------------------------------------ + + @http.route('/event_sponsor//read', type='json', auth='public', website=True) + def event_sponsor_read(self, sponsor_id): + """ Marshmalling data for "event not started / sponsor not available" modal """ + sponsor = request.env['event.sponsor'].browse(sponsor_id) + sponsor_data = sponsor.read([ + 'name', 'subtitle', + 'url', 'email', 'phone', + 'website_description', 'website_image_url', + 'hour_from', 'hour_to', 'is_in_opening_hours', + 'event_date_tz', 'country_flag_url', + ])[0] + if sponsor.country_id: + sponsor_data['country_name'] = sponsor.country_id.name + sponsor_data['country_id'] = sponsor.country_id.id + else: + sponsor_data['country_name'] = False + sponsor_data['country_id'] = False + if sponsor.sponsor_type_id: + sponsor_data['sponsor_type_name'] = sponsor.sponsor_type_id.name + sponsor_data['sponsor_type_id'] = sponsor.sponsor_type_id.id + else: + sponsor_data['sponsor_type_name'] = False + sponsor_data['sponsor_type_id'] = False + sponsor_data['event_name'] = sponsor.event_id.name + sponsor_data['event_is_ongoing'] = sponsor.event_id.is_ongoing + sponsor_data['event_start_today'] = sponsor.event_id.start_today + sponsor_data['event_start_remaining'] = sponsor.event_id.start_remaining + sponsor_data['event_date_begin_located'] = sponsor.event_id.date_begin_located + sponsor_data['event_date_end_located'] = sponsor.event_id.date_end_located + + return sponsor_data + + # ------------------------------------------------------------ + # TOOLS + # ------------------------------------------------------------ + + def _get_search_countries(self, country_search): + # TDE FIXME: make me generic (slides, event, ...) + try: + country_ids = literal_eval(country_search) + except Exception: + countries = request.env['res.country'].sudo() + else: + # perform a search to filter on existing / valid tags implicitly + countries = request.env['res.country'].sudo().search([('id', 'in', country_ids)]) + return countries + + def _get_search_sponsorships(self, sponsorship_search): + # TDE FIXME: make me generic (slides, event, ...) + try: + sponsorship_ids = literal_eval(sponsorship_search) + except Exception: + sponsorships = request.env['event.sponsor.type'].sudo() + else: + # perform a search to filter on existing / valid tags implicitly + sponsorships = request.env['event.sponsor.type'].sudo().search([('id', 'in', sponsorship_ids)]) + return sponsorships diff --git a/addons/website_event_track_exhibitor/controllers/website_event_main.py b/addons/website_event_track_exhibitor/controllers/website_event_main.py new file mode 100644 index 00000000000..66c663e7977 --- /dev/null +++ b/addons/website_event_track_exhibitor/controllers/website_event_main.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from babel.dates import format_datetime + +from odoo import _ +from odoo.http import request +from odoo.addons.website_event.controllers.main import WebsiteEventController + + +class WebsiteEventController(WebsiteEventController): + def _prepare_event_register_values(self, event, **post): + values = super(WebsiteEventController, self)._prepare_event_register_values(event, **post) + + if "from_sponsor_id" in post and not event.is_ongoing: + sponsor = request.env["event.sponsor"].browse(int(post["from_sponsor_id"])).exists() + if sponsor: + date_begin = format_datetime(event.with_context(tz=event.date_tz).date_begin, format="medium") + + values["toast_message"] = ( + _('The event %s starts at %s (%s). \nJoin us there to meet %s !') + % (event.name, date_begin, event.date_tz, sponsor.partner_name) + ) + + return values diff --git a/addons/website_event_track_exhibitor/data/event_demo.xml b/addons/website_event_track_exhibitor/data/event_demo.xml new file mode 100644 index 00000000000..fdcc32bdc0e --- /dev/null +++ b/addons/website_event_track_exhibitor/data/event_demo.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/addons/website_event_track_exhibitor/data/event_sponsor_demo.xml b/addons/website_event_track_exhibitor/data/event_sponsor_demo.xml new file mode 100644 index 00000000000..f7e5726452b --- /dev/null +++ b/addons/website_event_track_exhibitor/data/event_sponsor_demo.xml @@ -0,0 +1,71 @@ + + + + + + Catchy sentence ! + + + + + You won't believe this sponsor + + + + + Honestly we are good + + gemini-furniture + 12 + + + + Catchy sentence ! + + ready-mat + 12 + + + + You won't believe this sponsor + + the-jackson-group + 12 + + + + Honestly we are good + + azure-interior + 12 + + + + You won't believe this sponsor + + bloem-flower + 12 + + + + Honestly we are good + + open-wood + 12 + + + + You won't believe this sponsor + + tree-dealers + 12 + + + + Honestly we are good + + pterocarpus + 12 + + + diff --git a/addons/website_event_track_exhibitor/models/__init__.py b/addons/website_event_track_exhibitor/models/__init__.py new file mode 100644 index 00000000000..65b747dd5bb --- /dev/null +++ b/addons/website_event_track_exhibitor/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import event_event +from . import event_sponsor +from . import event_type +from . import website_event_menu diff --git a/addons/website_event_track_exhibitor/models/event_event.py b/addons/website_event_track_exhibitor/models/event_event.py new file mode 100644 index 00000000000..49081272586 --- /dev/null +++ b/addons/website_event_track_exhibitor/models/event_event.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models, _ +from odoo.addons.http_routing.models.ir_http import slug + + +class EventEvent(models.Model): + _inherit = "event.event" + + exhibitor_menu = fields.Boolean( + string='Showcase Exhibitors', compute='_compute_exhibitor_menu', + readonly=False, store=True) + exhibitor_menu_ids = fields.One2many( + 'website.event.menu', 'event_id', string='Exhibitors Menus', + domain=[('menu_type', '=', 'exhibitor')]) + + @api.depends('event_type_id', 'website_menu', 'exhibitor_menu') + def _compute_exhibitor_menu(self): + for event in self: + if event.event_type_id and event.event_type_id != event._origin.event_type_id: + event.exhibitor_menu = event.event_type_id.exhibitor_menu + elif event.website_menu and event.website_menu != event._origin.website_menu or not event.exhibitor_menu: + event.exhibitor_menu = True + elif not event.website_menu: + event.exhibitor_menu = False + + # ------------------------------------------------------------ + # WEBSITE MENU MANAGEMENT + # ------------------------------------------------------------ + + def toggle_exhibitor_menu(self, val): + self.exhibitor_menu = val + + def _get_menu_update_fields(self): + return super(EventEvent, self)._get_menu_update_fields() + ['exhibitor_menu'] + + 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('exhibitor_menu'): + event._update_website_menu_entry('exhibitor_menu', 'exhibitor_menu_ids', '_get_exhibitor_menu_entries') + + def _get_menu_type_field_matching(self): + res = super(EventEvent, self)._get_menu_type_field_matching() + res['exhibitor'] = 'exhibitor_menu' + return res + + def _get_exhibitor_menu_entries(self): + self.ensure_one() + return [(_('Exhibitors'), '/event/%s/exhibitors' % slug(self), False, 60, 'exhibitor')] diff --git a/addons/website_event_track_exhibitor/models/event_sponsor.py b/addons/website_event_track_exhibitor/models/event_sponsor.py new file mode 100644 index 00000000000..1f91250377d --- /dev/null +++ b/addons/website_event_track_exhibitor/models/event_sponsor.py @@ -0,0 +1,117 @@ +# -*- 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.tools import is_html_empty +from odoo.tools.translate import html_translate + + +class EventSponsor(models.Model): + _name = 'event.sponsor' + _inherit = [ + 'event.sponsor', + 'website.published.mixin', + 'chat.room.mixin', + ] + _rec_name = 'name' + _order = 'sponsor_type_id, sequence' + + # description + subtitle = fields.Char('Slogan', help='Catchy marketing sentence for promote') + is_exhibitor = fields.Boolean("Exhibitor's Chat") + website_description = fields.Html( + 'Description', compute='_compute_website_description', + sanitize_attributes=False, sanitize_form=True, translate=html_translate, + readonly=False, store=True) + # live mode + hour_from = fields.Float('Opening hour', default=8.0) + hour_to = fields.Float('End hour', default=18.0) + event_date_tz = fields.Selection(string='Timezone', related='event_id.date_tz', readonly=True) + is_in_opening_hours = fields.Boolean( + 'Within opening hours', compute='_compute_is_in_opening_hours') + # chat room + chat_room_id = fields.Many2one(readonly=False) + room_name = fields.Char(readonly=False) + # country information (related to ease frontend templates) + country_id = fields.Many2one( + 'res.country', string='Country', + related='partner_id.country_id', readonly=True) + country_flag_url = fields.Char( + string='Country Flag', + compute='_compute_country_flag_url', compute_sudo=True) + + @api.onchange('is_exhibitor') + def _onchange_is_exhibitor(self): + """ Keep an explicit onchange to allow configuration of room names, even + if this field is normally a related on chat_room_id.name. It is not a real + computed field, an onchange used in form view is sufficient. """ + for sponsor in self: + if sponsor.is_exhibitor and not sponsor.room_name: + if sponsor.name: + sponsor.room_name = self.env['chat.room']._jitsi_sanitize_name("odoo-exhibitor-%s" % sponsor.name) + else: + sponsor.room_name = self.env['chat.room']._default_name() + + @api.depends('partner_id') + def _compute_website_description(self): + for sponsor in self: + if is_html_empty(sponsor.website_description): + sponsor.website_description = sponsor.partner_id.website_description + + @api.depends('event_id.is_ongoing', 'hour_from', 'hour_to', 'event_id.date_begin', 'event_id.date_end') + def _compute_is_in_opening_hours(self): + """ Opening hours: hour_from and hour_to are given within event TZ or UTC. + Now() must therefore be computed based on that TZ. """ + for sponsor in self: + if not sponsor.event_id.is_ongoing: + sponsor.is_in_opening_hours = False + elif not sponsor.hour_from or not sponsor.hour_to: + sponsor.is_in_opening_hours = True + else: + event_tz = timezone(sponsor.event_id.date_tz) + # localize now, begin and end datetimes in event tz + dt_begin = sponsor.event_id.date_begin.astimezone(event_tz) + dt_end = sponsor.event_id.date_end.astimezone(event_tz) + now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0)) + now_tz = now_utc.astimezone(event_tz) + + # compute opening hours + opening_from_tz = event_tz.localize(datetime.combine(now_tz.date(), float_to_time(sponsor.hour_from))) + opening_to_tz = event_tz.localize(datetime.combine(now_tz.date(), float_to_time(sponsor.hour_to))) + + opening_from = max([dt_begin, opening_from_tz]) + opening_to = min([dt_end, opening_to_tz]) + + sponsor.is_in_opening_hours = opening_from <= now_tz < opening_to + + @api.depends('partner_id.country_id.image') + def _compute_country_flag_url(self): + for sponsor in self: + if sponsor.partner_id.country_id: + sponsor.country_flag_url = self.env['website'].image_url(sponsor.partner_id.country_id, 'image', size=256) + else: + sponsor.country_flag_url = False + + @api.depends('name', 'event_id.name') + def _compute_website_url(self): + super(EventSponsor, self)._compute_website_url() + for sponsor in self: + if sponsor.id: # avoid to perform a slug on a not yet saved record in case of an onchange. + base_url = sponsor.event_id.get_base_url() + sponsor.website_url = '%s/event/%s/exhibitor/%s' % (base_url, slug(sponsor.event_id), slug(sponsor)) + + @api.model_create_multi + def create(self, values_list): + for values in values_list: + if values.get('is_exhibitor') and not values.get('room_name'): + name = values['name'] if values.get('name') else self.env['res.partner'].browse(values['partner_id']).name or 'sponsor' + values['room_name'] = self.env['chat.room']._jitsi_sanitize_name(name) + return super(EventSponsor, self).create(values_list) + + def get_backend_menu_id(self): + return self.env.ref('event.event_main_menu').id diff --git a/addons/website_event_track_exhibitor/models/event_type.py b/addons/website_event_track_exhibitor/models/event_type.py new file mode 100644 index 00000000000..0aaa96600e6 --- /dev/null +++ b/addons/website_event_track_exhibitor/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" + + exhibitor_menu = fields.Boolean( + string='Showcase Exhibitors', compute='_compute_exhibitor_menu', + readonly=False, store=True, + help='Display exhibitors on website') + + @api.depends('website_menu') + def _compute_exhibitor_menu(self): + for event_type in self: + event_type.exhibitor_menu = event_type.website_menu diff --git a/addons/website_event_track_exhibitor/models/website_event_menu.py b/addons/website_event_track_exhibitor/models/website_event_menu.py new file mode 100644 index 00000000000..ce37fadbbf5 --- /dev/null +++ b/addons/website_event_track_exhibitor/models/website_event_menu.py @@ -0,0 +1,10 @@ +# -*- 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=[('exhibitor', 'Exhibitors Menus')]) diff --git a/addons/website_event_track_exhibitor/security/ir.model.access.csv b/addons/website_event_track_exhibitor/security/ir.model.access.csv new file mode 100644 index 00000000000..e897683cd40 --- /dev/null +++ b/addons/website_event_track_exhibitor/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 +chat_room_access_event_manager,chat.room.access.event.manager,website_jitsi.model_chat_room,event.group_event_manager,1,1,1,1 diff --git a/addons/website_event_track_exhibitor/security/security.xml b/addons/website_event_track_exhibitor/security/security.xml new file mode 100644 index 00000000000..d49bf7aa729 --- /dev/null +++ b/addons/website_event_track_exhibitor/security/security.xml @@ -0,0 +1,15 @@ + + + + + Event Sponsor: public/portal published only + + [('website_published', '=', True)] + + + + + + + + diff --git a/addons/website_event_track_exhibitor/static/src/js/event_exhibitor_connect.js b/addons/website_event_track_exhibitor/static/src/js/event_exhibitor_connect.js new file mode 100644 index 00000000000..eb0c12fd9fa --- /dev/null +++ b/addons/website_event_track_exhibitor/static/src/js/event_exhibitor_connect.js @@ -0,0 +1,137 @@ +odoo.define('website_event_track_exhibitor.event_exhibitor_connect', function (require) { +'use strict'; + +var Dialog = require('web.Dialog'); +var publicWidget = require('web.public.widget'); + +var ExhibitorConnectClosedDialog = Dialog.extend({ + events: _.extend({}, Dialog.prototype.events, { + 'click .o_wesponsor_js_connect_modal_contry': '_onClickCountryFlag', + }), + template: 'exhibitor.connect.closed.modal', + + /** + * @override + * @param {Object} parent; + * @param {Object} options holding a sponsorData obj with required values to + * display (see .xml for details); + */ + init: function (parent, options) { + options = _.defaults(options || {}, { + size: 'medium', + renderHeader: false, + renderFooter: false, + backdrop: true, + }); + this.sponsorId = options.sponsorId; + this._super(parent, options); + }, + + /** + * @override + * Wait for fetching sponsor data; + */ + willStart: function () { + return Promise.all([ + this._super(...arguments), + this._fetchSponsor() + ]); + }, + + //--------------------------------------------------------------------- + // Private + //--------------------------------------------------------------------- + + /** + * @private + * @returns {Promise<*>} promise after fetching sponsor data, given its + * sponsorId. Necessary to render template content; + */ + _fetchSponsor: function () { + let self = this; + let rpcPromise = this._rpc({ + route: `/event_sponsor/${this.sponsorId}/read`, + }).then(function (readData) { + self.sponsorData = readData; + return Promise.resolve(); + }); + return rpcPromise; + }, +}); + + +publicWidget.registry.eventExhibitorConnect = publicWidget.Widget.extend({ + selector: '.o_wesponsor_js_connect', + xmlDependencies: ['/website_event_track_exhibitor/static/src/xml/event_exhibitor_connect.xml'], + + /** + * @override + * @public + */ + init: function () { + this._super(...arguments); + this._onConnectClick = _.debounce(this._onConnectClick, 500, true); + }, + + /** + * @override + * @public + */ + start: function () { + var self = this; + return this._super(...arguments).then(function () { + self.eventIsOngoing = self.$el.data('eventIsOngoing') || false; + self.sponsorIsOngoing = self.$el.data('sponsorIsOngoing') || false; + self.isParticipating = self.$el.data('isParticipating') || false; + self.userEventManager = self.$el.data('userEventManager') || false; + self.$el.on('click', self._onConnectClick.bind(self)); + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //------------------------------------------------------------------------- + + /** + * @private + * @param {Event} ev + * On click, if sponsor is not within opening hours, display a modal instead + * of redirecting on the sponsor view; + */ + _onConnectClick: function (ev) { + ev.stopPropagation(); + ev.preventDefault(); + + if (this.userEventManager) { + document.location = this.$el.data('sponsorUrl'); + } else if (!this.eventIsOngoing && !this.isParticipating) { + document.location = this.$el.data('registerUrl'); + } else if (!this.eventIsOngoing || ! this.sponsorIsOngoing) { + return this._openClosedDialog(); + } else { + document.location = this.$el.data('sponsorUrl'); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + _openClosedDialog: function ($element) { + const sponsorId = this.$el.data('sponsorId'); + return new ExhibitorConnectClosedDialog( + this, { + sponsorId: sponsorId, + } + ).open(); + }, + +}); + + +return { + ExhibitorConnectClosedDialog: ExhibitorConnectClosedDialog, + eventExhibitorConnect: publicWidget.registry.eventExhibitorConnect, +}; + +}); diff --git a/addons/website_event_track_exhibitor/static/src/scss/event_exhibitor_templates.scss b/addons/website_event_track_exhibitor/static/src/scss/event_exhibitor_templates.scss new file mode 100644 index 00000000000..3458877876f --- /dev/null +++ b/addons/website_event_track_exhibitor/static/src/scss/event_exhibitor_templates.scss @@ -0,0 +1,103 @@ +.o_wevent_hide_sponsors .container.mt32.mb16.d-print-none { + // Not a very accurate way to target the 'sponsors' block -> improve in master + display: none; +} + +.o_wesponsor_index { + + /* + * COMMON + */ + + .o_wesponsor_gradient { + background-image: linear-gradient(120deg, #875A7B, darken(#875A7B, 10%)); + opacity: 0.8; + } + + /* + * MAIN PAGE: LIST + */ + + // Sponsor card + .o_wesponsor_card { + .card-body { + padding: 1rem; + } + + .card-footer { + padding: 0.75rem 1rem; + } + + .o_wesponsor_card_header_badge { + position: absolute; + bottom: 0; + width: 100%; + padding: $card-spacer-y $card-spacer-x; + text-align: right; + } + &.o_wesponsor_card_unpublished { + opacity: 0.8; + } + + .o_wesponsor_bg_image { + background-size: contain; + background-repeat: no-repeat; + background-position:center; + } + + // Display connect block on hover on large devices only + .o_wesponsor_connect_button { + z-index: 1; + position: absolute !important; + right: 0; + top: 0; + padding-top: 30%; + margin-left: auto; + margin-right: auto; + width: 100%; + height: 100%; + text-align: center; + background-color: rgba(0,0,0,0); + + a { + display: none; + } + } + + @include media-breakpoint-up(md) { + :hover .o_wesponsor_connect_button { + cursor: pointer; + transition: background-color .2s linear; + background-color: rgba(0,0,0,.1); + + a { + display: inline-block; + } + } + } + } + + /* + * EXHIBITOR PAGE + */ + + .o_wevent_online_page_container { + + // Jitsi container + #o_wsponsor_jitsi_iframe { + height: 85vh; + } + + .o_wesponsor_exhibitor_aside { + .o_wesponsor_sponsor_name { + line-height: 1rem; + } + + .o_wesponsor_aside_logo { + max-width: 40px; + object-fit: contain; + object-position: top; + } + } + } +} diff --git a/addons/website_event_track_exhibitor/static/src/xml/event_exhibitor_connect.xml b/addons/website_event_track_exhibitor/static/src/xml/event_exhibitor_connect.xml new file mode 100644 index 00000000000..31979df4de8 --- /dev/null +++ b/addons/website_event_track_exhibitor/static/src/xml/event_exhibitor_connect.xml @@ -0,0 +1,67 @@ + + + + +
+
+ + +
+ +
+
+
+
+
+

+ +

+ + + + + + + + + + +
+ +
+
+ +
+
+
+
+
+ +
diff --git a/addons/website_event_track_exhibitor/tests/__init__.py b/addons/website_event_track_exhibitor/tests/__init__.py new file mode 100644 index 00000000000..9ec226b64d9 --- /dev/null +++ b/addons/website_event_track_exhibitor/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_sponsor_internals diff --git a/addons/website_event_track_exhibitor/tests/test_sponsor_internals.py b/addons/website_event_track_exhibitor/tests/test_sponsor_internals.py new file mode 100644 index 00000000000..7856eb8e091 --- /dev/null +++ b/addons/website_event_track_exhibitor/tests/test_sponsor_internals.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from datetime import date, datetime, timedelta + +from odoo.addons.website_event_track_online.tests.common import TestEventTrackOnlineCommon +from odoo.tests.common import users + + +class TestSponsorData(TestEventTrackOnlineCommon): + + @classmethod + def setUpClass(cls): + super(TestSponsorData, cls).setUpClass() + + cls.sponsor_0.write({ + 'hour_from': 8.0, + 'hour_to': 18.0, + }) + + @users('user_eventmanager') + def test_event_date_computation(self): + """ Test date computation. Pay attention that mocks returns UTC values, meaning + we have to take into account Europe/Brussels offset """ + event = self.env['event.event'].browse(self.event_0.id) + sponsor = self.env['event.sponsor'].browse(self.sponsor_0.id) + event.invalidate_cache(fnames=['is_ongoing']) + self.assertTrue(sponsor.is_in_opening_hours) + self.assertTrue(event.is_ongoing) + + # After hour_from (9 > 8) + self.mock_event_online_dt.now.return_value = datetime(2020, 7, 6, 7, 0, 0) + event.invalidate_cache(fnames=['is_ongoing']) + sponsor.invalidate_cache(fnames=['is_in_opening_hours']) + self.assertTrue(sponsor.is_in_opening_hours) + self.assertTrue(event.is_ongoing) + + # At hour_from (8 = 8) + self.mock_event_online_dt.now.return_value = datetime(2020, 7, 6, 6, 0, 0) + event.invalidate_cache(fnames=['is_ongoing']) + sponsor.invalidate_cache(fnames=['is_in_opening_hours']) + self.assertTrue(sponsor.is_in_opening_hours) + self.assertTrue(event.is_ongoing) + + # Started but not opened (7h59 < 8) + self.mock_event_online_dt.now.return_value = datetime(2020, 7, 6, 5, 59, 59) + event.invalidate_cache(fnames=['is_ongoing']) + sponsor.invalidate_cache(fnames=['is_in_opening_hours']) + self.assertFalse(sponsor.is_in_opening_hours) + self.assertTrue(event.is_ongoing) + + # Evening event is not in opening hours (20 > 18) + self.mock_event_online_dt.now.return_value = datetime(2020, 7, 6, 18, 0, 0) + event.invalidate_cache(fnames=['is_ongoing']) + sponsor.invalidate_cache(fnames=['is_in_opening_hours']) + self.assertFalse(sponsor.is_in_opening_hours) + self.assertTrue(event.is_ongoing) + + # First day begins later + self.mock_event_online_dt.now.return_value = datetime(2020, 7, 5, 6, 30, 0) + event.invalidate_cache(fnames=['is_ongoing']) + sponsor.invalidate_cache(fnames=['is_in_opening_hours']) + self.assertFalse(sponsor.is_in_opening_hours) + self.assertFalse(event.is_ongoing) + + # End day finished sooner + self.mock_event_online_dt.now.return_value = datetime(2020, 7, 7, 13, 0, 1) + event.invalidate_cache(fnames=['is_ongoing']) + sponsor.invalidate_cache(fnames=['is_in_opening_hours']) + self.assertFalse(sponsor.is_in_opening_hours) + self.assertFalse(event.is_ongoing) diff --git a/addons/website_event_track_exhibitor/views/assets.xml b/addons/website_event_track_exhibitor/views/assets.xml new file mode 100644 index 00000000000..fdf90f4446f --- /dev/null +++ b/addons/website_event_track_exhibitor/views/assets.xml @@ -0,0 +1,13 @@ + + + + + + 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 + + + +
+
+
+
+
+
diff --git a/addons/website_event_track_live/__init__.py b/addons/website_event_track_live/__init__.py new file mode 100644 index 00000000000..7d34c7c054a --- /dev/null +++ b/addons/website_event_track_live/__init__.py @@ -0,0 +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 new file mode 100644 index 00000000000..8f137e270e2 --- /dev/null +++ b/addons/website_event_track_live/__manifest__.py @@ -0,0 +1,31 @@ +# -*- 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': [ + '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 @@ + + + + + + + + + + +