[FW][MERGE] website_event_*: support Online Events

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 merge, we add several features allowing to improve event experience
in your website, especially when dealing with fully online events. Those
features are added in several new modules :

  * 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, including visitor wishlist and PWA;
  * 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 quizzes 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;

See sub commits of this merge for more details and complete specifications
of all those new modules.

THANKS TO MIGHTY SM

This merge is a joint work of members of SM Team, namely @awa-odoo @dbeguin
and @tde-banana-odoo for the global followup of the project. Additional workforce
came from @qmo-odoo and @std-odoo , with inputs from @edi-odoo (design)
and @pparidans (PWA). This project was followed by @LNAodoo for specifications.

LINKS

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-2283729 (Live/Youtube Tracks)
Task ID-2283771 (Quizzes on Talks, Leaderboard)
Task ID-2283742 (Community gathering on chat rooms)
Task ID-2291344 (Integrate Twitter Wall)

See also
Task ID-2290016 (Visitor synchronization improvements) 86708c7
Task ID-2289148 (Event Bugprovements 1) 83c8012
Task ID-2300907 (Event Bugprovements 2) 0575092
Task ID-2304817 (Event Bugprovements 3) 3498640
Task ID-2309702 (Event Preparatory Merge 4) 2b5c0b4
Task ID-2310491 (Event Preparatory Merge 5) 69d2513
Task ID-2287372 (Fix Twitter Wall) odoo/enterprise@4843313
Task ID-2309964 (Color on track) ab1c1a9
Also 93aacc5 and 19d028a

Community PR #53540
Enterprise PR odoo/enterprise#11384

closes odoo/odoo#55402

Ps: New Event Yaaay !
Forward-port-of: #53540
Related: odoo/enterprise#12195
Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2020-08-04 18:15:34 +02:00
committed by GitHub
225 changed files with 11368 additions and 15 deletions
-2
View File
@@ -1,4 +1,2 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import tests
+14 -3
View File
@@ -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,
}
@@ -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,
)
);
});
+2 -1
View File
@@ -2,4 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_event_crm
from . import test_event_crm
from . import test_wevent_register
+183 -5
View File
@@ -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()
@@ -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
)
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" ?>
<odoo><data>
<template id="assets_tests" inherit_id="web.assets_tests" name="Full Event Tests Assets">
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/test_event_full/static/src/js/tours/wevent_register_tour.js"/>
</xpath>
</template>
</data></odoo>
+19 -1
View File
@@ -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.")
+1 -1
View File
@@ -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))
+5
View File
@@ -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
+34
View File
@@ -0,0 +1,34 @@
# -*- 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': [
# STD-TODO: remove 'website_event_track_online' we just need it to be
# able to use 'website.event.menu' which is defined in 'website_event_track'
# as well as even form view defined in track_online
'website_event_online',
'website_event_track_online',
'website_jitsi',
],
'demo': ['data/website_event_meet_demo.xml'],
'data': [
'security/ir.model.access.csv',
'security/security.xml',
'views/assets.xml',
'views/event_meet_templates_list.xml',
'views/event_meet_templates_page.xml',
'views/event_meeting_room_views.xml',
'views/event_event_views.xml',
'views/event_type_views.xml',
],
'application': False,
'installable': True,
}
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import community
from . import website_event_main
@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from werkzeug.exceptions import Forbidden, NotFound
from werkzeug.utils import redirect
from odoo import exceptions, http
from odoo.http import request
from odoo.addons.http_routing.models.ir_http import slug
from odoo.addons.website_event_track_online.controllers.community import WebsiteEventCommunityController
from odoo.osv import expression
_logger = logging.getLogger(__name__)
class WebsiteEventMeetController(WebsiteEventCommunityController):
def _get_event_rooms_base_domain(self, event):
search_domain_base = [('event_id', '=', event.id), ('is_published', '=', True)]
return search_domain_base
def _sort_event_rooms(self, room):
return (room.is_pinned, room.room_last_activity, room.id)
# ------------------------------------------------------------
# MAIN PAGE
# ------------------------------------------------------------
@http.route(["/event/<model('event.event'):event>/community"], type="http",
auth="public", website=True, sitemap=True)
def community(self, event, page=1, lang=None, **kwargs):
"""Display the meeting rooms of the event on the frontend side.
:param event: event for which we display the meeting rooms
:param lang: lang id used to perform a search
"""
if not event.can_access_from_current_website():
raise Forbidden()
return request.render(
"website_event_meet.event_meet",
self._event_meeting_rooms_get_values(event, lang=lang)
)
def _event_meeting_rooms_get_values(self, event, lang=None):
search_domain = self._get_event_rooms_base_domain(event)
meeting_rooms_all = request.env['event.meeting.room'].sudo().search(search_domain)
if lang:
search_domain = expression.AND([
search_domain,
[('room_lang_id', '=', int(lang))]
])
meeting_rooms = request.env['event.meeting.room'].sudo().search(search_domain)
meeting_rooms = meeting_rooms.sorted(self._sort_event_rooms, reverse=True)
is_event_manager = request.env.user.has_group("event.group_event_manager")
if not is_event_manager:
meeting_rooms = meeting_rooms.filtered(lambda m: not m.room_is_full)
visitor = request.env['website.visitor']._get_visitor_from_request()
return {
# event information
"event": event.sudo(),
'main_object': event,
# rooms
"meeting_rooms": meeting_rooms,
"current_lang": request.env["res.lang"].browse(int(lang)) if lang else False,
"available_languages": meeting_rooms_all.mapped("room_lang_id"),
"default_lang_code": request.context.get('lang', request.env.user.lang),
"default_username": visitor.display_name if visitor else None,
# environment
"is_event_manager": is_event_manager,
}
@http.route("/event/<model('event.event'):event>/meeting_room_create",
type="http", auth="public", methods=["POST"], website=True)
def create_meeting_room(self, event, **post):
if not event or not event.can_access_from_current_website() or not event.is_published or not event.meeting_room_allow_creation:
raise Forbidden()
name = post.get("name")
summary = post.get("summary")
target_audience = post.get("audience")
lang_code = post.get("lang_code")
max_capacity = post.get("capacity")
# get the record to be sure they really exist
lang = request.env["res.lang"].search([("code", "=", lang_code)], limit=1)
if not lang or max_capacity == "no_limit":
raise Forbidden()
meeting_room = request.env["event.meeting.room"].sudo().create({
"name": name,
"summary": summary,
"target_audience": target_audience,
"is_pinned": False,
"event_id": event.id,
"room_lang_id": lang.id,
"room_max_capacity": max_capacity,
"is_published": True,
})
_logger.info("New meeting room (%s) created by %s (uid %s)" % (name, request.httprequest.remote_addr, request.env.uid))
return redirect(f"/event/{slug(event)}/meeting_room/{slug(meeting_room)}")
@http.route(["/event/active_langs"], type="json", auth="public")
def active_langs(self):
return request.env["res.lang"].sudo().get_installed()
# ------------------------------------------------------------
# ROOM PAGE VIEW
# ------------------------------------------------------------
@http.route("/event/<model('event.event'):event>/meeting_room/<model('event.meeting.room'):meeting_room>",
type="http", auth="public", website=True, sitemap=True)
def event_meeting_room_page(self, event, meeting_room, **post):
"""Display the meeting room frontend view.
:param event: Event for which we display the meeting rooms
:param meeting_room: Meeting Room to display
"""
if not event.can_access_from_current_website() or meeting_room not in event.sudo().meeting_room_ids:
raise NotFound()
try:
meeting_room.check_access_rule('read')
except exceptions.AccessError:
raise Forbidden()
meeting_room = meeting_room.sudo()
return request.render(
"website_event_meet.event_meet_main",
self._event_meeting_room_page_get_values(event, meeting_room),
)
def _event_meeting_room_page_get_values(self, event, meeting_room):
# search for meeting room list
meeting_rooms_other = request.env['event.meeting.room'].sudo().search([
('event_id', '=', event.id), ('id', '!=', meeting_room.id), ('is_published', '=', True),
])
if not request.env.user.has_group("event.group_event_manager"):
# only the event manager can see meeting rooms which are full
meeting_rooms_other = meeting_rooms_other.filtered(lambda m: not m.room_is_full)
meeting_rooms_other = meeting_rooms_other.sorted(self._sort_event_rooms, reverse=True)
return {
# event information
'event': event,
'main_object': meeting_room,
'meeting_room': meeting_room,
# sidebar
'meeting_rooms_other': meeting_rooms_other,
# options
'option_widescreen': True,
'is_event_manager': request.env.user.has_group('event.group_event_manager'),
}
@@ -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_room_id" in post and not event.is_ongoing:
meeting_room = request.env["event.meeting.room"].browse(int(post["from_room_id"])).sudo().exists()
if meeting_room and meeting_room.is_published:
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 chat about "%s" !')
% (event.name, date_begin, event.date_tz, meeting_room.name)
)
return values
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="event_meeting_room_0" model="event.meeting.room">
<field name="name">Best wood for furniture</field>
<field name="summary">Let's talk about wood types for furniture</field>
<field name="target_audience">wood expert(s)</field>
<field name="is_pinned" eval="True"/>
<field name="website_published" eval="True"/>
<field name="event_id" ref="event.event_7"/>
<field name="room_lang_id" ref="base.lang_en"/>
<field name="room_max_capacity">12</field>
<field name="room_participant_count">9</field>
</record>
<record id="event_meeting_room_1" model="event.meeting.room">
<field name="name">Reducing the ecological footprint with wood ?</field>
<field name="summary">Share your tips to reduce your ecological footprint using wood.</field>
<field name="target_audience">ecologist(s)</field>
<field name="is_pinned" eval="True"/>
<field name="website_published" eval="True"/>
<field name="event_id" ref="event.event_7"/>
<field name="room_lang_id" ref="base.lang_en"/>
<field name="room_max_capacity">8</field>
<field name="room_participant_count">8</field>
</record>
<record id="event_meeting_room_2" model="event.meeting.room">
<field name="name">Vos meubles préférés ?</field>
<field name="summary">Venez partager vos meubles préférés et l'utilisation que vous en faites.</field>
<field name="target_audience">client(s)</field>
<field name="is_pinned" eval="True"/>
<field name="website_published" eval="True"/>
<field name="event_id" ref="event.event_7"/>
<field name="room_lang_id" ref="base.lang_fr"/>
<field name="room_max_capacity">8</field>
<field name="room_participant_count">3</field>
</record>
<record id="event.event_7" model="event.event">
<field name="meeting_room_allow_creation" eval="True"/>
<field name="community_menu" eval="True"/>
</record>
</data>
</odoo>
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import event_event
from . import event_type
from . import event_meeting_room
from . import ir_autovacuum
from . import website_event_menu
@@ -0,0 +1,42 @@
# -*- 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 Event(models.Model):
_inherit = "event.event"
meeting_room_ids = fields.One2many("event.meeting.room", "event_id", string="Meeting rooms")
meeting_room_count = fields.Integer("Room count", compute="_compute_meeting_room_count")
meeting_room_allow_creation = fields.Boolean(
"Allow Room Creation", compute="_compute_meeting_room_allow_creation",
readonly=False, store=True,
help="Let Visitors Create Rooms")
@api.depends("meeting_room_ids")
def _compute_meeting_room_count(self):
meeting_room_count = self.env["event.meeting.room"].sudo().read_group(
domain=[("event_id", "in", self.ids)],
fields=["id:count"],
groupby=["event_id"],
)
meeting_room_count = {
result["event_id"][0]: result["event_id_count"]
for result in meeting_room_count
}
for event in self:
event.meeting_room_count = meeting_room_count.get(event.id, 0)
@api.depends("event_type_id", "community_menu", "meeting_room_allow_creation")
def _compute_meeting_room_allow_creation(self):
for event in self:
if event.event_type_id and event.event_type_id != event._origin.event_type_id:
event.meeting_room_allow_creation = event.event_type_id.meeting_room_allow_creation
elif event.community_menu and event.community_menu != event._origin.community_menu:
event.meeting_room_allow_creation = True
elif not event.community_menu or not event.meeting_room_allow_creation:
event.meeting_room_allow_creation = False
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.urls import url_join
from odoo import api, fields, models, _
from odoo.addons.http_routing.models.ir_http import slug
class EventMeetingRoom(models.Model):
_name = "event.meeting.room"
_description = "Event Meeting Room"
_order = "is_pinned DESC, id"
_inherit = [
'chat.room.mixin',
'website.published.mixin',
]
name = fields.Char("Topic", required=True, translate=True)
active = fields.Boolean('Active', default=True)
is_published = fields.Boolean(copy=True) # make the inherited field copyable
event_id = fields.Many2one("event.event", string="Event", required=True, ondelete="cascade")
is_pinned = fields.Boolean("Is Pinned")
summary = fields.Char("Summary")
target_audience = fields.Char("Audience", translate=True)
# TDE FIXME: merge with mixin code
ROOM_CONFIG_FIELDS = {
'room_name': 'name',
'room_lang_id': 'lang_id',
'room_max_capacity': 'max_capacity',
'room_participant_count': 'participant_count',
}
@api.depends('name', 'event_id.name')
def _compute_website_url(self):
super(EventMeetingRoom, self)._compute_website_url()
for meeting_room in self:
if meeting_room.id:
base_url = meeting_room.event_id.get_base_url()
meeting_room.website_url = '%s/event/%s/meeting_room/%s' % (base_url, slug(meeting_room.event_id), slug(meeting_room))
@api.model_create_multi
def create(self, values_list):
# TDE FIXME: merge with mixin code
for values in values_list:
if not values.get("chat_room_id"):
# be sure to always create a `chat.room` for each `event.meeting.room`
chat_room = self.env["chat.room"].create({
self.ROOM_CONFIG_FIELDS[field]: value
for field, value in dict(values).items()
if field in self.ROOM_CONFIG_FIELDS and values.pop(field)
})
values["chat_room_id"] = chat_room.id
values["name"] = values["name"].capitalize()
values["target_audience"] = (values.get("target_audience") or _("Attendee(s)")).capitalize()
return super(EventMeetingRoom, self).create(values_list)
@@ -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"
meeting_room_allow_creation = fields.Boolean(
"Allow Room Creation", compute='_compute_meeting_room_allow_creation',
readonly=False, store=True,
help="Let Visitors Create Rooms")
@api.depends('community_menu')
def _compute_meeting_room_allow_creation(self):
for event_type in self:
event_type.meeting_room_allow_creation = event_type.community_menu
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from odoo import api, models, fields
class AutoVacuum(models.AbstractModel):
_inherit = 'ir.autovacuum'
_DELAY_CLEAN = datetime.timedelta(hours=4)
@api.model
def power_on(self, *args, **kwargs):
"""Archive all non-pinned room with 0 participant if nobody has joined it for a moment."""
self.env["event.meeting.room"].sudo().search([
("is_pinned", "=", False),
("active", "=", True),
("room_participant_count", "=", 0),
("room_last_activity", "<", fields.Datetime.now() - self._DELAY_CLEAN),
]).active = False
return super(AutoVacuum, self).power_on(*args, **kwargs)
@@ -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=[("meeting_room", "Event Meeting Room Menus")])
@@ -0,0 +1,6 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
event_meeting_room_access_all,event.meeting.room.access.all,model_event_meeting_room,,1,0,0,0
event_meeting_room_access_user,event.meeting.room.access.user,model_event_meeting_room,event.group_event_user,1,0,0,0
event_meeting_room_access_manager,event.meeting.room.access.manager,model_event_meeting_room,event.group_event_manager,1,1,1,1
chat_room_access_event_user,chat.room.access.event.user,website_jitsi.model_chat_room,event.group_event_user,1,0,0,0
chat_room_access_event_manager,chat.room.access.event.manager,website_jitsi.model_chat_room,event.group_event_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 event_meeting_room_access_all event.meeting.room.access.all model_event_meeting_room 1 0 0 0
3 event_meeting_room_access_user event.meeting.room.access.user model_event_meeting_room event.group_event_user 1 0 0 0
4 event_meeting_room_access_manager event.meeting.room.access.manager model_event_meeting_room event.group_event_manager 1 1 1 1
5 chat_room_access_event_user chat.room.access.event.user website_jitsi.model_chat_room event.group_event_user 1 0 0 0
6 chat_room_access_event_manager chat.room.access.event.manager website_jitsi.model_chat_room event.group_event_manager 1 1 1 1
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="event_meeting_room_rule_share" model="ir.rule">
<field name="name">Event Meeting Room: public/portal published only</field>
<field name="model_id" ref="website_event_meet.model_event_meeting_room"/>
<field name="domain_force">[('website_published', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
</odoo>
@@ -0,0 +1,69 @@
odoo.define('website_event_meet.set_customize_options', function (require) {
"use strict";
let EventSpecificOptions = require('website_event.set_customize_options').EventSpecificOptions;
EventSpecificOptions.include({
xmlDependencies: (EventSpecificOptions.prototype.xmlDependencies || [])
.concat([
'/website_event_meet/static/src/xml/customize_options.xml',
]),
events: _.extend({}, EventSpecificOptions.prototype.events, {
'change #allow-room-creation': '_onAllowRoomCreationChange',
}),
start: function () {
this.$allowRoomCreationInput = this.$('#allow-room-creation');
this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onAllowRoomCreationChange: function () {
let checkboxValue = this.$allowRoomCreationInput.is(':checked');
this._toggleAllowRoomCreation(checkboxValue);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
_getCheckboxFields: function () {
let fields = this._super();
fields = _.union(fields, ['meeting_room_allow_creation']);
return fields;
},
_getCheckboxFieldMatch: function (checkboxField) {
if (checkboxField === 'meeting_room_allow_creation') {
return this.$allowRoomCreationInput;
}
return this._super(checkboxField);
},
_initCheckboxCallback: function (rpcData) {
this._super(rpcData);
if (rpcData[0]['meeting_room_allow_creation']) {
let submenuInput = this._getCheckboxFieldMatch('meeting_room_allow_creation');
submenuInput.attr('checked', 'checked');
}
},
_toggleAllowRoomCreation: async function (val) {
await this._rpc({
model: this.modelName,
method: 'write',
args: [[this.eventId], {
meeting_room_allow_creation: val
}],
});
this._reloadEventPage();
},
});
});
@@ -0,0 +1,59 @@
odoo.define('website_event_meet.website_event_create_room_button', function (require) {
'use strict';
const publicWidget = require('web.public.widget');
const core = require('web.core');
const QWeb = core.qweb;
publicWidget.registry.websiteEventCreateMeetingRoom = publicWidget.Widget.extend({
selector: '.o_wevent_create_room_button',
xmlDependencies: ['/website_event_meet/static/src/xml/website_event_meeting_room.xml'],
events: {
'click': '_onClickCreate',
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
_onClickCreate: async function () {
if (!this.$createModal) {
const langs = await this._rpc({
route: "/event/active_langs",
});
this.$createModal = $(QWeb.render(
'event_meet_create_room_modal',
{
csrf_token: odoo.csrf_token,
eventId: this.$el.data("eventId"),
defaultLangCode: this.$el.data("defaultLangCode"),
langs: langs,
}
));
this.$createModal.appendTo(this.$el.parentNode);
}
this.$createModal.modal('show');
},
//--------------------------------------------------------------------------
// Override
//--------------------------------------------------------------------------
/**
* Remove the create modal from the DOM, to avoid issue when editing the template
* with the website editor.
*
* @override
*/
destroy: function () {
$('.o_wevent_create_meeting_room_modal').remove();
this._super.apply(this, arguments);
},
});
return publicWidget.registry.websiteEventMeetingRoom;
});
@@ -0,0 +1,111 @@
odoo.define('website_event_meet.website_event_meet_meeting_room', function (require) {
'use strict';
const publicWidget = require('web.public.widget');
const core = require('web.core');
const Dialog = require('web.Dialog');
const _t = core._t;
publicWidget.registry.websiteEventMeetingRoom = publicWidget.Widget.extend({
selector: '.o_wevent_meeting_room_card',
xmlDependencies: ['/website_event_meet/static/src/xml/website_event_meeting_room.xml'],
events: {
'click .o_wevent_meeting_room_delete': '_onDeleteClick',
'click .o_wevent_meeting_room_duplicate': '_onDuplicateClick',
'click .o_wevent_meeting_room_is_pinned': '_onPinClick',
},
start: function () {
this._super.apply(this, arguments);
this.meetingRoomId = parseInt(this.$el.data('meeting-room-id'));
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Delete the meeting room.
*
* @private
*/
_onDeleteClick: async function (event) {
event.preventDefault();
event.stopPropagation();
Dialog.confirm(
this,
_t("Are you sure you want to close this room ?"),
{
confirm_callback: async () => {
await this._rpc({
model: 'event.meeting.room',
method: 'write',
args: [this.meetingRoomId, {is_published: false}],
context: this.context,
});
// remove the element so we do not need to refresh the page
this.$el.remove();
}
},
);
},
/**
* Duplicate the room.
*
* @private
*/
_onDuplicateClick: function (event) {
event.preventDefault();
event.stopPropagation();
Dialog.confirm(
this,
_t("Are you sure you want to duplicate this room ?"),
{
confirm_callback: async () => {
await this._rpc({
model: 'event.meeting.room',
method: 'copy',
args: [this.meetingRoomId],
context: this.context,
});
window.location.reload();
}
},
);
},
/**
* Pin/unpin the room.
*
* @private
*/
_onPinClick: async function (event) {
event.preventDefault();
event.stopPropagation();
const pinnedButtonClass = "o_wevent_meeting_room_pinned";
const isPinned = event.currentTarget.classList.contains(pinnedButtonClass);
await this._rpc({
model: 'event.meeting.room',
method: 'write',
args: [this.meetingRoomId, {is_pinned: !isPinned}],
context: this.context,
});
// TDE FIXME: addclass ?
if (isPinned) {
event.currentTarget.classList.remove(pinnedButtonClass);
} else {
event.currentTarget.classList.add(pinnedButtonClass);
}
}
});
return publicWidget.registry.websiteEventMeetingRoom;
});
@@ -0,0 +1,59 @@
.o_wemeet_index {
/*
* COMMON
*/
/*
* Meet Room PAGE
*/
.o_wevent_online_page_container {
// Jitsi container
#o_wemeet_jitsi_iframe {
height: 85vh;
}
}
}
/*
* MEETING MODAL (JS / XML WIDGET)
*/
.o_wevent_create_meeting_room_modal .modal-dialog {
top: 25%;
max-width: 500px;
height: auto;
}
/* TDE FIXME: reorganize css cleanly */
.o_wevent_meeting_room_is_pinned {
transition: 0.2s;
// the icon must be filled with white when the meeting room is not pinned
color: white;
-webkit-text-stroke: 1px $o-main-text-color;
-webkit-text-fill-color: transparent;
&.o_wevent_meeting_room_pinned {
// the icon must be filled with black when the meeting room is pinned
-webkit-text-fill-color: $o-main-text-color;
}
}
.o_wevent_meeting_room_corner_ribbon {
width: 200px;
background: #e43;
top: 25px;
left: -50px;
text-align: center;
line-height: 50px;
letter-spacing: 1px;
color: #f0f0f0;
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
z-index: 1;
position: absolute;
opacity: 0.6;
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-extend="website_event.customize_options">
<t t-jquery="a[name='display-website-menu']" t-operation="after">
<a t-if="window.document.location.pathname.endsWith('/community')" class="dropdown-item" href="#" name="allow-room-creation" role="menuitem">
<label class="o_switch" for="allow-room-creation">
<input id="allow-room-creation" type="checkbox"/>
<span/>
Allow Room Creation
</label>
</a>
</t>
</t>
</templates>
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" ?>
<templates xml:space="preserve">
<t t-name="event_meet_create_room_modal">
<div class="modal fade o_wevent_create_meeting_room_modal" role="dialog">
<div class="modal-dialog">
<div class="modal-content border-0">
<main class="modal-body p-3">
<form t-attf-action="/event/#{eventId}/meeting_room_create"
id="o_wevent_create_meeting_room_form"
class="text-dark"
method="POST">
<h2>Launch a new topic</h2>
<div class="row m-2">
<label class="col-4 mt-2 text-left">Room Topic</label>
<input class="form-control col-8" maxlength="50" name="name" placeholder="e.g. Finance" required="1"/>
</div>
<div class="row m-2">
<label class="col-4 mt-2 text-left">Short Summary</label>
<input class="form-control col-8" maxlength="200" name="summary" placeholder="e.g. Let's talk about Corporate Finance" required="1"/>
</div>
<div class="row m-2">
<label class="col-4 mt-2 text-left">Target People</label>
<input class="form-control col-8" maxlength="30" name="audience" placeholder="e.g. Accountants"/>
</div>
<div class="row m-2">
<label class="col-4 mt-2 text-left">Language</label>
<select class="o_wevent_create_meeting_room_lang form-control col-8" name="lang_code" required="1">
<option t-foreach="langs" t-as="language" t-att-value="language[0]" t-esc="language[1]"
t-att-selected="language[0] == defaultLangCode and 'selected' or None"/>
</select>
</div>
<div class="row m-2">
<label class="col-4 mt-2 text-left">Capacity</label>
<select class="form-control col-8" name="capacity" required="1">
<option value="4">4</option>
<option selected="selected" value="8">8</option>
<option value="12">12</option>
<option value="16">16</option>
<option value="20">20</option>
</select>
</div>
<input name="event" t-att-value="event" type="hidden"/>
<input name="csrf_token" t-att-value="csrf_token" type="hidden"/>
</form>
</main>
<footer class="modal-footer justify-content-start">
<button class="btn btn-primary" form="o_wevent_create_meeting_room_form" type="submit">Create</button>
<button class="btn bg-white text-primary" role="button" data-dismiss="modal">Discard</button>
</footer>
</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_website_event_meet
@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.event.tests.common import TestEventCommon
from odoo.tests import Form
class TestWebsiteEventMeet(TestEventCommon):
def test_meeting_room_create(self):
"""Test that the field of the mixin are automatically filled."""
new_meeting_room_form = Form(self.env["event.meeting.room"])
new_meeting_room_form.name = "Test name"
new_meeting_room_form.event_id = self.event_0
new_meeting_room_form.target_audience = "dev"
new_meeting_room_form.room_max_capacity = "20"
meeting_room = new_meeting_room_form.save()
self.assertTrue(meeting_room.chat_room_id)
self.assertEqual(meeting_room.chat_room_id.max_capacity, "20")
# Try to create a meeting room without filling the related field
# It must create the `chat.room` with the default value
meeting_room = self.env["event.meeting.room"].create({
"name": "Test 2",
"event_id": self.event_0.id,
"target_audience": "dev"
})
self.assertTrue(meeting_room.chat_room_id)
self.assertTrue(meeting_room.chat_room_id.name)
self.assertEqual(meeting_room.chat_room_id.max_capacity, "8")
def test_meeting_room_copy(self):
"""Test the duplication of the meeting room."""
meeting_room_1 = self.env["event.meeting.room"].create({
"name": "Test meeting room",
"event_id": self.event_0.id,
"target_audience": "dev",
"room_max_capacity": "20",
})
meeting_room_2 = meeting_room_1.copy()
chat_room_1 = meeting_room_1.chat_room_id
chat_room_2 = meeting_room_2.chat_room_id
self.assertTrue(chat_room_1)
self.assertTrue(chat_room_2)
self.assertNotEqual(chat_room_1.id, chat_room_2.id, "Must create a new chat room")
self.assertNotEqual(chat_room_1.name, chat_room_2.name, "Must generate a new chat room name")
self.assertEqual(chat_room_1.max_capacity, "20", "Must set the max capacity on the chat room")
self.assertEqual(chat_room_2.max_capacity, "20", "Must copy the max capacity")
def test_meeting_room_unlink(self):
"""Test the duplication of the meeting room."""
meeting_room = self.env["event.meeting.room"].create({
"name": "Test meeting room",
"event_id": self.event_0.id,
"target_audience": "dev",
"room_max_capacity": "20",
})
self.assertTrue(meeting_room.chat_room_id)
chat_room_id = meeting_room.chat_room_id.id
meeting_room.unlink()
self.assertFalse(self.env["chat.room"].browse(chat_room_id).exists())
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Event Meet Assets">
<xpath expr="." position="inside">
<link href="/website_event_meet/static/src/scss/event_meet_templates.scss" rel="stylesheet" type="text/scss"/>
<script src="/website_event_meet/static/src/js/customize_options.js" type="text/javascript"/>
<script src="/website_event_meet/static/src/js/website_event_meeting_room.js" type="text/javascript"/>
<script src="/website_event_meet/static/src/js/website_event_create_meeting_room_button.js" type="text/javascript"/>
</xpath>
</template>
</odoo>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="event_event_view_form" model="ir.ui.view">
<field name="name">event.event.view.form.inherit.meet</field>
<field name="model">event.event</field>
<field name="inherit_id" ref="website_event_track_online.event_event_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='is_published']" position="before">
<button class="oe_stat_button" context="{'default_event_id': active_id, 'search_default_event_id': active_id}" icon="fa-comments-o" name="%(event_meeting_room_action)d" type="action">
<field name="meeting_room_count" string="Rooms" widget="statinfo"/>
</button>
</xpath>
<xpath expr="//field[@name='community_menu']" position="after">
<field name="meeting_room_allow_creation" invisible="1"/>
</xpath>
</field>
</record>
</data></odoo>
@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="event_meet" name="Meeting Rooms">
<t t-call="website_event.layout">
<div class="o_wevent_online o_wevent_online_bg o_wemeet_index">
<!-- Drag/Drop Area -->
<div id="oe_structure_website_event_location_1" class="oe_structure"/>
<!-- Content -->
<div class="o_wemeet_container container">
<div class="row mb-5 mx-0">
<t t-call="website_event_meet.community_main"/>
<t t-call="website_event_meet.community_aside"/>
</div>
</div>
<!-- Drag/Drop Area -->
<div id="oe_structure_website_event_location_2" class="oe_structure"/>
</div>
</t>
</template>
<!-- ============================================================ -->
<!-- CONTENT: MAIN TEMPLATES -->
<!-- ============================================================ -->
<!-- Meeting Rooms Main Display -->
<template id="community_main" name="Meeting Rooms: Main Display">
<div class="col-12 col-md-8 pl-0 pr-0 pr-md-3 mt-3">
<h1 class="d-flex flex-row justify-content-between">
<span>Join a room</span>
<div class="dropdown">
<a class="dropdown-toggle o-no-caret btn pr-0" title="Languages Menu"
aria-label="Dropdown menu" data-display="static" data-toggle="dropdown" href="#" role="button">
<span t-esc="current_lang.name if current_lang else 'All Languages'"/> ▼</a>
<div class="dropdown-menu" role="menu">
<a class="dropdown-item" role="menuitem" t-attf-href="/event/#{slug(event)}/community">All Languages
</a>
<a class="dropdown-item" role="menuitem" t-as="language" t-attf-href="/event/#{slug(event)}/community?lang=#{language.id}" t-esc="language.name" t-foreach="available_languages"/>
</div>
</div>
</h1>
<hr class="mt-2 mb-3"/>
<p class="mt-">Choose a topic that interests you and start talking with the community. <br/> Don't forget to setup your camera and microphone.</p>
<div class="d-flex flex-column justify-content-start align-items-start">
<t t-as="meeting_room" t-call="website_event_meet.meeting_room_card" t-foreach="meeting_rooms">
<t t-set="meeting_room" t-value="meeting_room"/>
<t t-set="opened" t-value="int(meeting_room.id == open_room_id)"/>
</t>
<div t-if="not meeting_rooms" class="m-auto text-center text-muted">
<h3 class="mt8">No Room Open</h3>
<p>Create one to get conversations going</p>
</div>
</div>
</div>
</template>
<template id="meeting_room_card" name="Meeting Room Card">
<div class="modal o_join_later_modal" t-attf-id="o_join_later_modal_#{meeting_room.id}" tabindex="-1" role="dialog" style="height: 400px; top:calc(50% - 200px);">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="mt-4 col-12 alert alert-warning text-center" role="alert">
<nav class="navbar navbar-default">
<div class="container-fluid">
<div class="navbar-header">
<div class="o_wevent_meeting_room_card_menu"></div>
</div>
</div>
</nav>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&amp;times;</button>
<span>This room is not open right now!</span><br />
Join us here on the
<strong itemprop="startDate" t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'format': 'medium'}"/>
to have a chat with us!
</div>
<div class="modal-body row">
<div class="col-3">
<div class="w-100" t-attf-style="background-image: #{json.loads(event.cover_properties).get('background-image')}; min-height: 5rem; background-size: cover;"/>
</div>
<div class="col">
<h5 t-esc="meeting_room.name"/>
<div class="text-muted mb-2"><i class="fa fa-globe"/> <span t-esc="meeting_room.room_lang_id.name"/></div>
<span t-if="meeting_room.summary" t-raw="meeting_room.summary"/>
</div>
</div>
</div>
</div>
</div>
<!-- Action to do when clicking on the card -->
<t t-if="event.is_ongoing or is_event_manager">
<!--During the event or if event manager-->
<t t-set="meeting_room_href" t-value="'/event/' + slug(event) + '/meeting_room/' + slug(meeting_room)"/>
</t>
<t t-elif="not event.is_ongoing and not event.is_participating">
<!--Pre-event, if not registered yet-->
<t t-set="meeting_room_href" t-value="'/event/' + slug(event) + '/register?from_room_id=%i' % meeting_room.id"/>
</t>
<t t-else="">
<!--Pre-event, if registered but event not stared yet-->
<t t-set="meeting_room_href" t-value="'#'"/>
<t t-set="meeting_room_data_toggle" t-value="'modal'"/>
<t t-set="meeting_room_data_target" t-value="'#o_join_later_modal_%i' % meeting_room.id"/>
</t>
<a t-if="is_event_manager or not meeting_room.room_is_full"
t-att-data-meeting-room-id="meeting_room.id"
t-att-data-open-room="opened"
t-att-data-is-event-manager="int(is_event_manager)"
t-attf-class="card o_wevent_meeting_room_card w-100 my-2 bg-light d-block text-decoration-none"
t-att-href="meeting_room_href"
t-att-data-toggle="meeting_room_data_toggle"
t-att-data-target="meeting_room_data_target">
<div class="text-decoration-none w-100 h-100 p-3">
<div class="o_wevent_meeting_room_corner_ribbon" t-if="meeting_room.room_is_full">Full</div>
<div class="d-flex flex-column">
<div class="d-flex flex-row justify-content-between">
<h4 class="text-break mw-75" t-esc="meeting_room.name"/>
<div t-if="is_event_manager" class="w-25">
<div class="dropdown float-right dropleft">
<button class="btn py-0" data-toggle="dropdown"><h3 class="m-0">&#8942;</h3></button>
<div class="dropdown-menu">
<div class="dropdown-item font-weight-bold disabled">Room Manager</div>
<button class="dropdown-item btn btn-danger o_wevent_meeting_room_duplicate" type="button">Duplicate</button>
<button class="dropdown-item btn btn-danger o_wevent_meeting_room_delete" type="button">Close</button>
</div>
</div>
<button t-attf-class="o_wevent_meeting_room_is_pinned float-right btn #{'o_wevent_meeting_room_pinned' if meeting_room.is_pinned else ''}">
<i class="fa fa-thumb-tack"/>
</button>
</div>
</div>
<span t-esc="meeting_room.summary" class="h5 text-muted"/>
<div class="d-flex flex-row justify-content-between align-items-center">
<span class="h6 m-0 row">
<span class="o_wevent_participant_count">
<t t-esc="meeting_room.room_participant_count"/>&amp;nbsp;</span>
<t t-esc="meeting_room.target_audience or 'participant(s)'"/>
</span>
<div class="d-inline border py-1 px-2 bg-secondary" t-esc="meeting_room.room_lang_id.name"/>
</div>
</div>
</div>
</a>
</template>
<!-- ============================================================ -->
<!-- ASIDE: CREATE A ROOM -->
<!-- ============================================================ -->
<template id="community_aside" name="Community: Aside">
<div class="col-md-4 p-0 mt-3 o_wevent_community_aside">
<div class="d-none d-md-block mb-3" t-if="event.meeting_room_allow_creation">
<h1>Launch a new topic</h1>
<hr class="mt-2 mb-3"/>
<p>Want to create your own discussion room ?</p>
<p>Be sure you are ready to spend at least 10 minutes in the room if you want to initiate a new topic.</p>
<a href="#" role="button" class="btn btn-primary o_wevent_create_room_button"
t-att-data-event-id="event.id"
t-att-data-default-lang-code="default_lang_code">
<span>Create a Room</span>
</a>
</div>
</div>
</template>
</odoo>
@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="event_meet_main" name="Event Meet">
<t t-set="no_header" t-value="option_widescreen"/>
<t t-set="no_footer" t-value="option_widescreen"/>
<t t-call="website_event.layout">
<div class="o_wevent_online o_wevent_online_bg o_wemeet_index">
<!-- Options -->
<t t-set="option_widescreen" t-value="option_widescreen or False"/>
<!-- Drag/Drop Area -->
<div id="oe_structure_wemeet_index_1" class="oe_structure"/>
<!-- Content -->
<div t-att-class="'o_wevent_online_page_container %s' % ('container pb-3' if not option_widescreen else 'pb-3')">
<div class="row mb-5 mx-0">
<t t-call="website_event_meet.meeting_room_aside"/>
<t t-call="website_event_meet.meeting_room_main"/>
</div>
</div>
<!-- Drag/Drop Area -->
<div id="oe_structure_wemeet_index_2" class="oe_structure"/>
</div>
</t>
</template>
<!-- ============================================================ -->
<!-- CONTENT: MAIN TEMPLATES -->
<!-- ============================================================ -->
<template id="meeting_room_main" name="Meeting Room: Main Content">
<div t-att-class="'col-12 o_wevent_online_page_main o_wemeet_room_main bg-white p-0 %s' % ('col-md-9 col-lg-10' if option_widescreen else 'col-md-8 col-lg-9')">
<!-- EVENT NOT STARTED ALERTS -->
<div t-if="not meeting_room.event_id.is_ongoing" class="alert alert-warning text-center">
The event <span t-esc="meeting_room.event_id.name" class="font-weight-bold"/>
<span t-if="meeting_room.event_id.start_today">
starts in
<span t-esc="meeting_room.event_id.start_remaining"
t-options="{'widget': 'duration', 'digital': True, 'unit': 'minute', 'round': 'minute'}"/>.
</span>
<span class="my-0" t-else="meeting_room.event_id.start_today">
starts at
<span t-field="meeting_room.event_id.with_context(tz=meeting_room.event_id.date_tz).date_begin"
t-options="{'format': 'medium'}"/> (<t t-esc="meeting_room.event_id.date_tz"/>).
</span>
<br/>
<span>Join us there to chat about "<b t-esc="meeting_room.name"/> !</span>
</div>
<!-- ROOM CONTENT -->
<div class="d-flex flex-column">
<div t-if="meeting_room.room_is_full and not is_event_manager" class="alert alert-warning text-center">
<span>Oops! This room is full !</span><br />Come back later to have a chat with us!
</div>
<div t-else="" class="d-flex flex-column">
<div id="o_wemeet_jitsi_iframe">
<div class="o_wjitsi_chat_room_loading position-absolute w-100 text-center text-muted">
<i class="fa fa-spin fa-spinner mr-3"/>
<span>Loading your room...</span>
</div>
</div>
<div class="d-flex flex-row-reverse">
<t t-call="website_jitsi.chat_room_join_button">
<t t-set="_classes" t-value="'d-none'"/>
<t t-set="room_name" t-value="meeting_room.room_name"/>
<t t-set="chat_room_id" t-value="meeting_room.chat_room_id.id"/>
<t t-set="auto_open" t-value="1"/>
<t t-set="attach_to" t-value="'#o_wemeet_jitsi_iframe'"/>
<t t-set="check_full" t-value="is_event_manager"/>
<t t-set="default_username" t-value="default_username"/>
</t>
</div>
</div>
</div>
<!-- ROOM DESCRIPTION -->
<div class="mx-3">
<span class="h5 d-inline-block mt-3 mb-1" t-esc="meeting_room.name"/>
<br/>
<span class="text-muted">A chat among <span class="font-weight-bold" t-esc="meeting_room.target_audience"/></span>
</div>
<hr class="mt-2 mb-0"/>
<div t-field="meeting_room.summary" class="my-2 mx-3 oe_no_empty"/>
</div>
</template>
<!-- ============================================================ -->
<!-- ASIDE: CONTROL PANEL -->
<!-- ============================================================ -->
<template id="meeting_room_aside" name="Meeting Room: Aside">
<div t-att-class="'col-12 pl-0 pr-0 pr-md-3 o_wevent_online_page_aside %s' % ('col-md-3 col-lg-2' if option_widescreen else 'col-md-4 col-lg-3')">
<div class="bg-white o_wevent_online_page_aside_content">
<div class="d-flex align-items-center justify-content-between mx-2 py-1">
<span class="h5 mb-0 pt-0 pt-md-3 pb-0 pb-md-2">Other Rooms</span>
<a href="#collapse_meet_room_aside" data-toggle="collapse" class="d-lg-none p-2 text-decoration-none o_wevent_online_page_aside_collapse collapsed">
<i class="fa fa-chevron-down d-lg-none"/>
</a>
</div>
<ul id="collapse_meet_room_aside" class="list-unstyled collapse d-lg-block mb-0">
<li t-foreach="meeting_rooms_other" t-as="meeting_room_other">
<a class="d-block w-100 h-100 px-2 pt-2 pb-1 text-decoration-none"
t-att-href="'/event/%s/meeting_room/%s' % (slug(event), slug(meeting_room_other))">
<div class="flex-grow-1 mw-100">
<div class="text-truncate" t-esc="meeting_room_other.name"/>
<span class="text-muted" t-esc="meeting_room_other.summary"></span>
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted">
<b>&amp;#9900;&amp;nbsp;</b>
<small><t t-esc="meeting_room_other.room_participant_count"/> <t t-esc="meeting_room_other.target_audience"/></small>
</div>
<small t-if="meeting_room_other.room_lang_id" class="text-muted"><i class="fa fa-globe"/> <t t-esc="meeting_room_other.room_lang_id.name"/></small>
</div>
</div>
</a>
</li>
</ul>
</div>
</div>
</template>
</odoo>
@@ -0,0 +1,90 @@
<?xml version="1.0"?>
<odoo>
<record id="website_event_meet.event_meeting_room_action" model="ir.actions.act_window">
<field name="name">Meeting Room</field>
<field name="res_model">event.meeting.room</field>
<field name="view_mode">tree,form</field>
</record>
<record id="event_meeting_room_view_search" model="ir.ui.view">
<field name="name">event.meeting.room.search</field>
<field name="model">event.meeting.room</field>
<field name="arch" type="xml">
<search string="Meeting Room">
<field name="event_id"/>
</search>
</field>
</record>
<record id="event_meeting_room_view_form" model="ir.ui.view">
<field name="name">event.meeting.room.form</field>
<field name="model">event.meeting.room</field>
<field name="arch" type="xml">
<form string="Meeting Room">
<sheet>
<div class="oe_button_box" name="button_box">
<field name="website_url" invisible="1"/>
<field name="is_published" widget="website_redirect_button"/>
</div>
<label for="name"/>
<h1>
<field name="name" placeholder="e.g. Finance"/>
</h1>
<group>
<group>
<field name="event_id"/>
<field name="summary" placeholder="e.g. Let's talk about Corporate Finance"/>
<field name="target_audience" placeholder="e.g. Accountants"/>
<field name="is_pinned"/>
</group>
<group>
<field name="chat_room_id"/>
<field name="room_participant_count" readonly="1"/>
<field name="room_max_capacity" widget="radio" options="{'horizontal': true}"/>
<field name="room_lang_id" options="{'no_create': True}"/>
</group>
</group>
<notebook>
<page name="Reporting" string="Reporting">
<group>
<field name="room_last_activity"/>
<field name="room_max_participant_reached"/>
</group>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="event_meeting_room_view_tree" model="ir.ui.view">
<field name="name">event.meeting.room.tree</field>
<field name="model">event.meeting.room</field>
<field name="arch" type="xml">
<tree string="Meeting Room">
<field name="name"/>
<field name="summary"/>
<field name="target_audience"/>
<field name="is_published"/>
<field name="is_pinned"/>
<field name="room_is_full"/>
<field name="room_participant_count"/>
<field name="room_max_capacity"/>
<field name="room_lang_id"/>
</tree>
</field>
</record>
<record id="event_meeting_room_view_search" model="ir.ui.view">
<field name="name">event.meeting.room.search</field>
<field name="model">event.meeting.room</field>
<field name="arch" type="xml">
<search>
<field name="name" string="Topic"/>
<field name="summary" string="Summary"/>
<field name="target_audience" string="Audience"/>
<field name="event_id" string="Event"/>
<filter domain="[('is_published', '=', False)]" name="filter_unpublished" string="Unpublished"/>
<group expand="0" string="Group By">
<filter string="Event" name="groupby_event" domain="[]" context="{'group_by': 'event_id'}"/>
</group>
</search>
</field>
</record>
</odoo>
@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<odoo><data>
<record id="event_type_view_form" model="ir.ui.view">
<field name="name">event.type.view.form.inherit.meet</field>
<field name="model">event.type</field>
<field name="inherit_id" ref="website_event.event_type_view_form_inherit_website"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='website_menu']" position='after'>
<div class="row mt16" attrs="{'invisible': [('website_menu', '=', False)]}">
<label class="col-lg-4" for="meeting_room_allow_creation"/> <field name="meeting_room_allow_creation"/>
</div>
</xpath>
<xpath expr="//div[@id='community-menu']" position="attributes">
<attribute name="groups"></attribute>
</xpath>
</field>
</record>
</data></odoo>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
@@ -0,0 +1,24 @@
# -*- 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': [
'views/event_meet_templates.xml',
],
'demo': [
],
'application': False,
'auto_install': True,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import community
@@ -0,0 +1,23 @@
# -*- 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.community import WebsiteEventCommunityController
from odoo.http import request
from werkzeug.exceptions import Forbidden
class WebsiteEventTrackQuizMeetController(WebsiteEventCommunityController):
@http.route(['/event/<model("event.event"):event>/community'], type='http', auth="public", website=True, sitemap=False)
def community(self, event, page=1, lang=None, **kwargs):
if not event.can_access_from_current_website():
raise Forbidden()
# website_event_track_quiz
values = self._get_community_leaderboard_render_values(event, kwargs.get('search'), page)
# website_event_meet
values.update(self._event_meeting_rooms_get_values(event, lang=lang))
return request.render('website_event_meet.event_meet', values)
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="community_aside" name="Community: Aside (Leaderboard)"
inherit_id="website_event_meet.community_aside">
<xpath expr="//div[hasclass('o_wevent_community_aside')]" position="inside">
<t t-if="top3_visitors" t-call="website_event_meet_quiz.community_leaderboard_small"/>
</xpath>
</template>
<template id="community_leaderboard_small" name="Small Leaderboard">
<div class="row">
<div class="col">
<div class="w-100 d-flex justify-content-between align-items-end o_page_header">
<h1 class="m-0">Leaderboard</h1>
<a t-attf-href="#{'/event/%s/community/leaderboard' % (slug(event))}">View all</a>
</div>
</div>
</div>
<div class="row">
<div class="col">
<t t-foreach="top3_visitors" t-as="visitor">
<t t-call="website_event_meet_quiz.visitor_quiz_points_card"/>
</t>
</div>
</div>
</template>
<template id='visitor_quiz_points_card' name="User Quick Quiz Points Card">
<div class="d-flex mb-3 align-items-center justify-content-between">
<div class="d-flex align-items-center">
<b class="mr-2 text-muted" t-esc="visitor['position']"/>
<img class="rounded-circle float-left"
style="width: 32px; height: 32px; object-fit: cover;"
t-att-src="image_data_uri(visitor['visitor'].partner_image) if visitor['visitor'].partner_image else '/web/static/src/img/user_placeholder.jpg'"
t-att-alt="visitor['visitor'].display_name"/>
<div style="line-height:1.3">
<span class="font-weight-bold" t-esc="visitor['visitor'].display_name"/>
</div>
</div>
<div class="d-flex align-items-center">
<span><t t-esc="visitor['points']"/> Points</span>
</div>
</div>
</template>
</odoo>
+6
View File
@@ -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
@@ -0,0 +1,34 @@
# -*- 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': [
'security/ir.model.access.csv',
'views/assets.xml',
'views/event_event_views.xml',
'views/event_registration_views.xml',
'views/event_tag_views.xml',
'views/event_type_views.xml',
'views/website_visitor_views.xml',
'views/event_templates_event.xml',
],
'demo': [
'data/website_visitor_demo.xml',
'data/event_demo.xml',
'data/event_registration_demo.xml',
'data/event_tag_demo.xml',
],
'application': False,
'installable': True,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import event_registration
@@ -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)
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event.event_1" model="event.event">
<field name="menu_register_cta" eval="True"/>
</record>
<record id="event.event_7" model="event.event">
<field name="menu_register_cta" eval="True"/>
</record>
</odoo>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<!-- OpenWood Collection Online Reveal: Gemini (all) -->
<record id="event.event_registration_7_0" model="event.registration">
<field name="visitor_id" ref="website_event_online.website_visitor_event_0"/>
</record>
<record id="event.event_registration_7_1" model="event.registration">
<field name="visitor_id" ref="website_event_online.website_visitor_event_0"/>
</record>
<record id="event.event_registration_7_2" model="event.registration">
<field name="visitor_id" ref="website_event_online.website_visitor_event_1"/>
</record>
<record id="event.event_registration_7_3" model="event.registration">
<field name="visitor_id" ref="website_event_online.website_visitor_event_1"/>
</record>
</data></odoo>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event.event_tag_category_1_tag_1" model="event.tag">
<field name="color">1</field>
</record>
<record id="event.event_tag_category_1_tag_2" model="event.tag">
<field name="color">2</field>
</record>
<record id="event.event_tag_category_1_tag_3" model="event.tag">
<field name="color">3</field>
</record>
<record id="event.event_tag_category_1_tag_4" model="event.tag">
<field name="color">4</field>
</record>
<record id="event.event_tag_category_2_tag_1" model="event.tag">
<field name="color">5</field>
</record>
<record id="event.event_tag_category_2_tag_2" model="event.tag">
<field name="color">6</field>
</record>
<record id="event.event_tag_category_2_tag_3" model="event.tag">
<field name="color">7</field>
</record>
<record id="event.event_tag_category_3_tag_1" model="event.tag">
<field name="color">8</field>
</record>
<record id="event.event_tag_category_3_tag_2" model="event.tag">
<field name="color">9</field>
</record>
</odoo>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="website_visitor_event_0" model="website.visitor">
<field name="name">Edwin Hansen</field>
<field name="partner_id" ref="base.res_partner_address_5"/>
<field name="country_id" ref="base.us"/>
</record>
<record id="website_visitor_event_1" model="website.visitor">
<field name="name">Soham Palmer</field>
<field name="partner_id" ref="base.res_partner_address_11"/>
<field name="country_id" ref="base.us"/>
</record>
<record id="website_visitor_event_2" model="website.visitor">
<field name="name">Philipe J. Fry</field>
<field name="partner_id" eval="False"/>
<field name="country_id" ref="base.be"/>
</record>
<record id="website_visitor_event_2_1" model="website.visitor">
<field name="name">Philipe J. Fry (old)</field>
<field name="parent_id" ref="website_event_online.website_visitor_event_2"/>
<field name="partner_id" eval="False"/>
<field name="country_id" ref="base.be"/>
</record>
</odoo>
@@ -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_registration
from . import event_tag
from . import website_visitor
@@ -0,0 +1,132 @@
# -*- 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
class Event(models.Model):
_name = 'event.event'
_inherit = 'event.event'
# frontend options
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
* public, no visitor: not participating as we have no information;
* public and visitor: check visitor is linked to a registration. As
visitors are merged on the top parent, current visitor check is
sufficient even for successive visits;
* logged, no visitor: check partner is linked to a registration. Do
not check the email as it is not really secure;
* logged as visitor: check partner or visitor are linked to a
registration;
"""
current_visitor = self.env['website.visitor']._get_visitor_from_request(force_create=False)
if self.env.user._is_public() and not current_visitor:
events = self.env['event.event']
elif self.env.user._is_public():
events = self.env['event.registration'].sudo().search([
('event_id', 'in', self.ids),
('visitor_id', '=', current_visitor.id),
]).event_id
else:
if 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)]
events = self.env['event.registration'].sudo().search(
expression.AND([
domain,
[('event_id', 'in', self.ids)]
])
).event_id
for event in self:
event.is_participating = event in events
# ------------------------------------------------------------
# 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)
@@ -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')
@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields
class EventTag(models.Model):
_inherit = ['event.tag']
color = fields.Integer(string='Color Index')
@@ -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
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 website_visitor_access_event_user website.visitor.access.event.user model_website_visitor event.group_event_user 1 1 0 0
@@ -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;
});
@@ -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;
});
@@ -0,0 +1,130 @@
// Color tags according to assigned background color.
.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} {
@if $size == 1 {
& {
background-color: white;
color: nth($o-colors, $size);
box-shadow: inset 0 0 0 2px nth($o-colors, $size);
}
&::after {
background-color: nth($o-colors, $size);
}
} @else {
&, &::after {
background-color: nth($o-colors, $size);
color: $white;
}
}
}
}
// Complex colored tags according to assigned background color with hover effect
@for $size from 1 through length($o-colors) {
.o_tag_color_hovered_#{$size - 1} {
$background-color: white;
// no color selected
@if $size == 1 {
& {
color: black;
background-color: $background-color;
box-shadow: inset 0 0 0 1px nth($o-colors, $size);
}
} @else {
$background-color: nth($o-colors, $size);
& {
color: white;
background-color: $background-color;
}
}
@at-root a#{&} {
&:hover {
background-color: darken($background-color, 20%);
}
}
}
}
/*
* 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);
}
}
}
}
}
}
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_visitor_events
@@ -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),
})
@@ -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)])
)
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Event Online Assets Frontend">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/website_event_online/static/src/scss/website_event_templates.scss"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website_event_online/static/src/js/display_timer_widget.js"></script>
<script type="text/javascript" src="/website_event_online/static/src/js/register_toaster_widget.js"></script>
</xpath>
</template>
</odoo>
@@ -0,0 +1,23 @@
<?xml version="1.0"?>
<odoo><data>
<record id="event_event_view_form" model="ir.ui.view">
<field name="name">event.event.view.form.inherit.online</field>
<field name="model">event.event</field>
<field name="inherit_id" ref="event.view_event_form"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('oe_title')]" position="after">
<div name="event_menu_configuration" groups="base.group_no_one">
<label for="website_menu" string="Website Submenu"/>
<field name="website_menu"/>
<label for="menu_register_cta" string="Register Button"/>
<field name="menu_register_cta"/>
</div>
</xpath>
<xpath expr="//field[@name='tag_ids']" position="attributes">
<attribute name="options">{'color_field': 'color', 'no_quick_create': True}</attribute>
</xpath>
</field>
</record>
</data></odoo>
@@ -0,0 +1,36 @@
<?xml version="1.0"?>
<odoo><data>
<record id="event_registration_action_from_visitor" model="ir.actions.act_window">
<field name="name">Registrations</field>
<field name="res_model">event.registration</field>
<field name="view_mode">kanban,tree,form</field>
<field name="domain">[('visitor_id', 'in', [active_id])]</field>
<field name="help" type="html">
<p class="o_view_nocontent_empty_folder">
No registration linked to this visitor
</p>
</field>
</record>
<record id="event_registration_view_form" model="ir.ui.view">
<field name="name">event.registration.view.form.inherit.online</field>
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_event_registration_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="visitor_id" groups="base.group_no_one"/>
</xpath>
</field>
</record>
<record id="event_registration_view_tree" model="ir.ui.view">
<field name="name">event.registration.view.tree.inherit.online</field>
<field name="model">event.registration</field>
<field name="inherit_id" ref="event.view_event_registration_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="visitor_id" optional="hide" groups="base.group_no_one"/>
</xpath>
</field>
</record>
</data></odoo>
@@ -0,0 +1,48 @@
<?xml version="1.0"?>
<odoo><data>
<record id="event_tag_category_view_form" model="ir.ui.view">
<field name="name">event.tag.category.view.form.inherit.online</field>
<field name="model">event.tag.category</field>
<field name="inherit_id" ref="event.event_tag_category_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='tag_ids']//field[@name='name']" position="after">
<field name="color" widget="color_picker"/>
</xpath>
</field>
</record>
<record id="event_tag_category_view_list" model="ir.ui.view">
<field name="name">event.tag.category.view.tree.inherit.online</field>
<field name="model">event.tag.category</field>
<field name="inherit_id" ref="event.event_tag_category_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='tag_ids']" position="attributes">
<attribute name="widget">many2many_tags</attribute>
<attribute name="options">{'color_field': 'color'}</attribute>
</xpath>
</field>
</record>
<record id="event_tag_view_list" model="ir.ui.view">
<field name="name">event.tag.view.list.inherit.online</field>
<field name="model">event.tag</field>
<field name="inherit_id" ref="event.event_tag_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='category_id']" position="after">
<field name="color" widget="color_picker"/>
</xpath>
</field>
</record>
<record id="event_tag_view_form" model="ir.ui.view">
<field name="name">event.tag.view.form.inherit.online</field>
<field name="model">event.tag</field>
<field name="inherit_id" ref="event.event_tag_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='category_id']" position="after">
<field name="color" widget="color_picker"/>
</xpath>
</field>
</record>
</data></odoo>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Add Register additional CTA button, in addition to menus -->
<template id="event_layout_main" inherit_id="website_event.layout">
<xpath expr="//div[@id='o_wevent_event_submenu']/ul" position="after">
<a t-if="event.menu_register_cta and not event.is_participating"
t-att-href="'/event/%s/register' % (slug(event))"
class="btn btn-primary ml-auto">
Register
</a>
</xpath>
</template>
<template id="events_list" inherit_id="website_event.events_list">
<xpath expr="//t[@t-foreach='event.tag_ids']/span" position="replace">
<span t-attf-class="badge mr8 #{'badge-primary' if tag in search_tags else 'badge-light'} #{'o_tag_color_%s' % tag.color if tag.color else ''}">
<span t-esc="tag.name"/>
</span>
</xpath>
</template>
<!-- TDE FIXME: move in a separate file -->
<template id="display_timer_widget" name="Display Timer Widget">
<t t-set="pre_countdown_display" t-value="bool(pre_countdown_text) or pre_countdown_display"/>
<t t-set="pre_countdown_time" t-value="datetime.datetime.now().timestamp() + int(pre_remaining_time)"/>
<div class="o_display_timer"
t-att-data-display-class="display_class"
t-att-data-main-countdown-time="datetime.datetime.now().timestamp() + int(main_remaining_time)"
t-att-data-main-countdown-text="main_countdown_text"
t-att-data-main-countdown-display="main_countdown_display"
t-att-data-pre-countdown-time="pre_countdown_time"
t-att-data-pre-countdown-display="pre_countdown_display"
t-att-data-pre-countdown-text="pre_countdown_text">
<t t-set="remaining_time" t-value="pre_remaining_time if pre_remaining_time else main_remaining_time"/>
<span class="o_display_timer_countdown d-flex justify-content-center">
<span class="o_countdown_text pr-1" t-esc="pre_countdown_text if pre_countdown_text else main_countdown_text if not pre_countdown_display else ''"/>
<div t-if="int(remaining_time) > 86400"
class="o_countdown_metric_container"><span class="o_countdown_remaining o_timer_days pr-1">0</span><span class="o_countdown_metric pr-1">days</span></div>
<div t-if="int(remaining_time) > 3600"
class="o_countdown_metric_container"><span class="o_countdown_remaining o_timer_hours">00</span><span class="o_countdown_metric">:</span></div>
<div class="o_countdown_metric_container"><span class="o_countdown_remaining o_timer_minutes">00</span><span class="o_countdown_metric">:</span></div>
<div class="o_countdown_metric_container"><span class="o_countdown_remaining o_timer_seconds">00</span><span class="o_countdown_metric"></span></div>
</span>
</div>
</template>
<template id="registration_template" inherit_id="website_event.registration_template">
<xpath expr="//div" position="after">
<div t-if="toast_message" class="o_wevent_register_toaster d-none" t-att-data-message="toast_message"/>
</xpath>
</template>
</odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0"?>
<odoo><data>
<record id="event_type_view_form" model="ir.ui.view" >
<field name="name">event.type.view.form.inherit.online</field>
<field name="model">event.type</field>
<field name="inherit_id" ref="event.view_event_type_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='tag_ids']" position="attributes">
<attribute name="options">{'color_field': 'color', 'no_quick_create': True}</attribute>
</xpath>
</field>
</record>
</data></odoo>
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data>
<record id="website_visitor_view_search" model="ir.ui.view">
<field name="name">website.visitor.view.search.inherit.event</field>
<field name="model">website.visitor</field>
<field name="inherit_id" ref="website.website_visitor_view_search"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='group_by_lang']" position="after">
<filter string="Main Contact" name="group_by_parent_id" context="{'group_by': 'parent_id'}"/>
</xpath>
</field>
</record>
<record id="website_visitor_view_tree" model="ir.ui.view">
<field name="name">website.visitor.view.tree.inherit.event</field>
<field name="model">website.visitor</field>
<field name="inherit_id" ref="website.website_visitor_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='page_ids']" position="after">
<field name="event_registration_count"/>
</xpath>
</field>
</record>
<record id="website_visitor_view_form" model="ir.ui.view">
<field name="name">website.visitor.view.form.inherit.event</field>
<field name="model">website.visitor</field>
<field name="inherit_id" ref="website.website_visitor_view_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@id='w_visitor_visit_counter']" position="before">
<button name="%(website_event_online.event_registration_action_from_visitor)d"
type="action"
class="oe_stat_button" icon="fa-ticket"
attrs="{'invisible': [('event_registration_count', '=', 0)]}">
<field name="event_registration_count" widget="statinfo" string="Registrations"/>
</button>
</xpath>
<xpath expr="//field[@name='page_ids']" position="after">
<field name="parent_id"/>
</xpath>
</field>
</record>
<record id="website_visitor_view_kanban" model="ir.ui.view">
<field name="name">website.visitor.view.kanban.inherit.event</field>
<field name="model">website.visitor</field>
<field name="inherit_id" ref="website.website_visitor_view_kanban"/>
<field name="arch" type="xml">
<field name="page_ids" position="after">
<field name="event_registration_count"/>
</field>
<xpath expr="//div[@id='o_page_count']" position="after">
<div>Registrations<span class="float-right font-weight-bold"><field name="event_registration_count"/></span></div>
</xpath>
<xpath expr="//div[hasclass('w_visitor_kanban_actions_ungrouped')]" position="before">
<div class="col">
<b><field name="event_registration_count"/></b>
<div>Registrations</div>
</div>
</xpath>
</field>
</record>
</data></odoo>
@@ -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/<model("event.event"):event>/track''',
'''/event/<model("event.event"):event>/track/tag/<model("event.track.tag"):tag>'''
@@ -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
@@ -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,
}
@@ -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
@@ -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/<model("event.event"):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/<model("event.event"):event>/exhibitor/<model("event.sponsor"):sponsor>'], 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/<int:sponsor_id>/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
@@ -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
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event.event_7" model="event.event">
<field name="exhibitor_menu" eval="True"/>
</record>
</odoo>
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="website_event_track.event_7_sponsor_0" model="event.sponsor">
<field name="is_published" eval="True"/>
<field name="subtitle">Catchy sentence !</field>
<field name="is_exhibitor" eval="False"/>
</record>
<record id="website_event_track.event_7_sponsor_1" model="event.sponsor">
<field name="is_published" eval="True"/>
<field name="subtitle">You won't believe this sponsor</field>
<field name="is_exhibitor" eval="False"/>
</record>
<record id="website_event_track.event_7_sponsor_2" model="event.sponsor">
<field name="is_published" eval="True"/>
<field name="subtitle">Honestly we are good</field>
<field name="is_exhibitor" eval="True"/>
<field name="room_name">gemini-furniture</field>
<field name="room_max_capacity">12</field>
</record>
<record id="website_event_track.event_7_sponsor_3" model="event.sponsor">
<field name="is_published" eval="True"/>
<field name="subtitle">Catchy sentence !</field>
<field name="is_exhibitor" eval="True"/>
<field name="room_name">ready-mat</field>
<field name="room_max_capacity">12</field>
</record>
<record id="website_event_track.event_7_sponsor_4" model="event.sponsor">
<field name="is_published" eval="True"/>
<field name="subtitle">You won't believe this sponsor</field>
<field name="is_exhibitor" eval="True"/>
<field name="room_name">the-jackson-group</field>
<field name="room_max_capacity">12</field>
</record>
<record id="website_event_track.event_7_sponsor_5" model="event.sponsor">
<field name="is_published" eval="True"/>
<field name="subtitle">Honestly we are good</field>
<field name="is_exhibitor" eval="True"/>
<field name="room_name">azure-interior</field>
<field name="room_max_capacity">12</field>
</record>
<record id="website_event_track.event_7_sponsor_6" model="event.sponsor">
<field name="is_published" eval="True"/>
<field name="subtitle">You won't believe this sponsor</field>
<field name="is_exhibitor" eval="True"/>
<field name="room_name">bloem-flower</field>
<field name="room_max_capacity">12</field>
</record>
<record id="website_event_track.event_7_sponsor_7" model="event.sponsor">
<field name="is_published" eval="True"/>
<field name="subtitle">Honestly we are good</field>
<field name="is_exhibitor" eval="True"/>
<field name="room_name">open-wood</field>
<field name="room_max_capacity">12</field>
</record>
<record id="website_event_track.event_7_sponsor_8" model="event.sponsor">
<field name="is_published" eval="True"/>
<field name="subtitle">You won't believe this sponsor</field>
<field name="is_exhibitor" eval="True"/>
<field name="room_name">tree-dealers</field>
<field name="room_max_capacity">12</field>
</record>
<record id="website_event_track.event_7_sponsor_9" model="event.sponsor">
<field name="is_published" eval="True"/>
<field name="subtitle">Honestly we are good</field>
<field name="is_exhibitor" eval="True"/>
<field name="room_name">pterocarpus</field>
<field name="room_max_capacity">12</field>
</record>
</odoo>
@@ -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
@@ -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')]
@@ -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
@@ -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
@@ -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')])
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 chat_room_access_event_manager chat.room.access.event.manager website_jitsi.model_chat_room event.group_event_manager 1 1 1 1
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="event_sponsor_rule_share" model="ir.rule">
<field name="name">Event Sponsor: public/portal published only</field>
<field name="model_id" ref="website_event_track.model_event_sponsor"/>
<field name="domain_force">[('website_published', '=', True)]</field>
<field name="groups" eval="[(4, ref('base.group_public')), (4, ref('base.group_portal'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_unlink" eval="False"/>
</record>
</odoo>
@@ -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,
};
});
@@ -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;
}
}
}
}
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="exhibitor.connect.closed.modal">
<div class="o_wesponsor_js_connect_modal_main container">
<div class="row mt-2">
<div class="col-12 alert alert-warning text-center" role="alert"
t-if="! widget.sponsorData.event_is_ongoing">
<span t-esc="widget.sponsorData.name" class="font-weight-bold"/> is not available right now.<br />
Event <span t-esc="widget.sponsorData.event_name" class="font-weight-bold"/>
<span t-if="widget.sponsorData.event_start_today">
starts in
<span t-esc="widget.sponsorData.event_start_remaining"/> minutes
</span>
<span class="my-0" t-else="">
starts at <span t-esc="widget.sponsorData.event_date_begin_located"/>
</span>
</div>
<div class="col-12 alert alert-warning text-center" role="alert"
t-else="">
<span t-esc="widget.sponsorData.name" class="font-weight-bold"/> is not available right now.<br />
Come back between
<strong>
<t t-esc="widget.sponsorData.hour_from" t-options="{'widget': 'float_time'}"/>
-
<t t-esc="widget.sponsorData.hour_to" t-options="{'widget': 'float_time'}"/>
</strong> (<span t-esc="widget.sponsorData.event_date_tz"/>)
to meet them !
</div>
<div class="col-2">
<img class="img" style="max-width: 100%;"
t-att-src="widget.sponsorData.website_image_url"
t-att-alt="widget.sponsorData.name"/>
</div>
<div class="col-10">
<div class="d-flex align-items-top mb-3">
<div class="d-flex flex-column">
<div class="d-flex align-items-center">
<h4 t-esc="widget.sponsorData.name" class="mb4"/>
<span class="badge badge-primary ml-2"
t-esc="widget.sponsorData.sponsor_type_name"/>
</div>
<span class="text-muted" t-if="widget.sponsorData.subtitle" t-esc="widget.sponsorData.subtitle"/>
<span t-if="widget.sponsorData.url">
<i class="fa fa-home mr-2"/><a t-att-href="widget.sponsorData.url"><span t-esc="widget.sponsorData.url"/></a>
</span>
<span t-if="widget.sponsorData.email">
<i class="fa fa-envelope mr-2"/><a t-att-mailto="widget.sponsorData.email"><span t-esc="widget.sponsorData.email"/></a>
</span>
<span t-if="widget.sponsorData.phone">
<i class="fa fa-phone mr-2"/><span t-esc="widget.sponsorData.phone"/>
</span>
</div>
<img class="img ml-auto"
style="max-height: 36px;"
t-att-src="widget.sponsorData.country_flag_url"
t-att-alt="widget.sponsorData.country_name"/>
</div>
</div>
<span class="col-12" t-if="widget.sponsorData.website_description" t-raw="widget.sponsorData.website_description"/>
</div>
<div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_sponsor_internals
@@ -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)
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Event Exhibitor Assets Frontend">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/website_event_track_exhibitor/static/src/scss/event_exhibitor_templates.scss"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website_event_track_exhibitor/static/src/js/event_exhibitor_connect.js"></script>
</xpath>
</template>
</odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event_event_view_form" model="ir.ui.view">
<field name="name">event.event.view.form.inherit.exhibitor</field>
<field name="inherit_id" ref="website_event_track_online.event_event_view_form"/>
<field name="model">event.event</field>
<field name="priority" eval="3"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='website_track_proposal']" position="after">
<label for="exhibitor_menu"/>
<field name="exhibitor_menu"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,238 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="event_layout" inherit_id="website_event.layout">
<xpath expr="//div[@id='wrap']" position="attributes">
<attribute name="t-attf-class">o_wevent_event js_event #{'o_wevent_hide_sponsors' if hide_sponsors else ''}</attribute>
</xpath>
</template>
<template id="event_exhibitors" name="Event Exhibitors">
<t t-call="website_event.layout">
<div class="o_wevent_online o_wevent_online_bg o_wesponsor_index">
<!-- Topbar -->
<t t-call="website_event_track_exhibitor.exhibitors_topbar"/>
<!-- Drag/Drop Area -->
<div id="oe_structure_wesponsor_index_1" class="oe_structure"/>
<!-- Content -->
<div class="o_wesponsor_container container">
<div class="row">
<t t-call="website_event_track_exhibitor.exhibitors_search"/>
</div>
<div class="row">
<t t-call="website_event_track_exhibitor.exhibitors_main"/>
</div>
</div>
<!-- Drag/Drop Area -->
<div id="oe_structure_wesponsor_index_2" class="oe_structure"/>
</div>
</t>
</template>
<!-- ============================================================ -->
<!-- TOPBAR: BASE NAVIGATION -->
<!-- ============================================================ -->
<!-- TOPBAR: BASE NAVIGATION -->
<!-- Main topbar -->
<template id="exhibitors_topbar"
name="Exhibitor Tools">
<nav class="navbar navbar-light border-top shadow-sm d-print-none">
<div class="container">
<div class="d-flex flex-column flex-sm-row justify-content-between w-100">
<ul class="o_wesponsor_topbar_filters o_wevent_index_topbar_filters nav"/>
<div class="d-flex align-items-center flex-wrap pl-sm-3 pr-0">
<t t-call="website_event.events_search_box">
<t t-set="_searches" t-value="searches"/>
<t t-set="action" t-value="'/event/%s/exhibitors' % (slug(event))"/>
<t t-set="_placeholder" t-value="'Search an exhibitor ...'"/>
</t>
</div>
</div>
</div>
</nav>
</template>
<!-- Topbar: optional country filters -->
<template id="exhibitors_topbar_country"
inherit_id="website_event_track_exhibitor.exhibitors_topbar"
name="Filter by Country"
active="True"
customize_show="True">
<xpath expr="//ul[hasclass('o_wesponsor_topbar_filters')]" position="inside">
<li class="nav-item dropdown mr-2 my-1">
<a href="#" role="button" class="btn dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-folder-open"/>
By Country
</a>
<div class="dropdown-menu">
<a t-att-href="'/event/%s/exhibitors?%s' % (slug(event), keep_query('*', countries=''))"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between #{'active' if not search_countries else ''}">
All Countries
</a>
<t t-foreach="sponsor_countries" t-as="sponsor_country">
<a t-att-href="'/event/%s/exhibitors?%s' % (
slug(event),
keep_query('*', countries=str((search_countries - sponsor_country).ids if sponsor_country in search_countries else (sponsor_country | search_countries).ids))
)"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between #{'active' if sponsor_country in search_countries else ''}">
<t t-esc="sponsor_country.name"/>
</a>
</t>
</div>
</li>
</xpath>
</template>
<!-- Topbar: optional sponsorship filters -->
<template id="exhibitors_topbar_sponsorship"
inherit_id="website_event_track_exhibitor.exhibitors_topbar"
name="Filter by Sponsorship"
active="True"
customize_show="True">
<xpath expr="//ul[hasclass('o_wesponsor_topbar_filters')]" position="inside">
<li class="nav-item dropdown mr-2 my-1">
<a href="#" role="button" class="btn dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-folder-open"/>
By Sponsorship
</a>
<div class="dropdown-menu">
<a t-att-href="'/event/%s/exhibitors?%s' % (slug(event), keep_query('*', sponsorships=''))"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between #{'active' if not search_sponsorships else ''}">
All Countries
</a>
<t t-foreach="sponsor_types" t-as="sponsor_type">
<a t-att-href="'/event/%s/exhibitors?%s' % (
slug(event),
keep_query('*', sponsorships=str((search_sponsorships - sponsor_type).ids if sponsor_type in search_sponsorships else (sponsor_type | search_sponsorships).ids))
)"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between #{'active' if sponsor_type in search_sponsorships else ''}">
<t t-esc="sponsor_type.name"/>
</a>
</t>
</div>
</li>
</xpath>
</template>
<!-- ============================================================ -->
<!-- CONTENT: MAIN TEMPLATES -->
<!-- ============================================================ -->
<!-- Exhibitors Main Display -->
<template id="exhibitors_main" name="Exhibitors: Main Display">
<!-- No exhibitors -->
<t t-if="not sponsor_categories">
<div class="col-12">
<div class="h2 mb-3">No exhibitor found.</div>
<div t-if="search_key" class="alert alert-info text-center">
<p class="m-0">We did not find any exhibitor matching your <strong t-esc="search_key"/> search.</p>
</div>
<div t-else="" class="alert alert-info text-center" groups="event.group_event_manager">
<p class="m-0">Add some exhibitors to get started !</p>
</div>
</div>
</t>
<!-- Cards -->
<div class="col-12" t-call="website_event_track_exhibitor.exhibitors_display_cards"/>
</template>
<!-- Exhibitors: Cards-based display -->
<template id="exhibitors_display_cards" name="Exhibitors Cards">
<div t-foreach="sponsor_categories" t-as="sponsor_category" class="row mb-3">
<div class="col-12">
<h5 class="m-0" t-esc="sponsor_category['sponsorship'].name"/>
<hr class="mt-2 pb-1 mb-1"/>
</div>
<div t-foreach="sponsor_category['sponsors']" t-as="sponsor" class="col-md-6 col-lg-3 mb-4">
<t t-call="website_event_track_exhibitor.exhibitor_card"/>
</div>
</div>
</template>
<!-- ============================================================ -->
<!-- TOOL TEMPLATES -->
<!-- ============================================================ -->
<template id="exhibitor_card" name="Exhibitor Card">
<article t-att-class="'h-100 card border-0 shadow-sm o_wesponsor_card %s' % ('o_wesponsor_card_unpublished' if not sponsor.is_published else '')"
itemscope="itemscope" itemtype="http://schema.org/Event">
<div class="h-100 row no-gutters">
<t t-set="sponsor_image_url" t-value="sponsor.website_image_url"/>
<header t-att-class="'overflow-hidden col-12 rounded-top position-static %s' % ('bg-secondary' if not sponsor_image_url else 'bg-light')">
<small t-if="not sponsor.is_published" class="o_wesponsor_card_header_badge bg-danger">
<i class="fa fa-ban mr-2"/>Unpublished
</small>
<div t-att-data-sponsor-url="sponsor.website_url" class="o_wesponsor_js_connect"
t-attf-data-register-url="/event/#{slug(event)}/register?from_sponsor_id=#{sponsor.id}"
t-att-data-is-participating="event.is_participating"
t-att-data-sponsor-id="sponsor.id"
t-att-data-event-is-ongoing="sponsor.event_id.is_ongoing"
t-att-data-sponsor-is-ongoing="sponsor.is_in_opening_hours"
t-att-data-user-event-manager="user_event_manager">
<div t-if="sponsor_image_url" class="card-img-top position-static o_wesponsor_bg_image"
t-attf-style="padding-top: 50%; background-image: url(#{sponsor_image_url});">
<div class="o_wesponsor_connect_button">
<a href="#" class="btn btn-primary h3">Connect</a>
</div>
<img class="position-absolute mr-2 mt-2"
style="right: 0; top: 0; max-height: 20px;"
t-if="sponsor.partner_id.country_id"
t-att-src="website.image_url(sponsor.partner_id.country_id, 'image')"
t-att-alt="sponsor.partner_id.country_id.name"/>
</div>
<div t-else="" class="o_wesponsor_gradient card-img-top position-relative"
style="padding-top: 50%">
<div class="o_wesponsor_connect_button">
<a href="#" class="btn btn-primary h3">Connect</a>
</div>
</div>
</div>
</header>
<div class="col-12">
<main class="card-body h-100">
<!-- Title -->
<h5 class="card-title mt-0 mb-0 text-truncate d-flex align-items-top">
<span t-field="sponsor.name" itemprop="name"/>
<span t-if="sponsor.is_in_opening_hours and sponsor.chat_room_id"
class="ml-auto badge badge-danger">Live
</span>
</h5>
<!-- Catchy sentence -->
<span class="text-muted" t-esc="sponsor.subtitle"/>
</main>
</div>
</div>
</article>
</template>
<!-- Searched terms -->
<template id="exhibitors_search" name="Exhibitors: search terms">
<div class="d-flex align-items-center mb-3">
<t t-foreach="search_countries" t-as="country">
<span class="align-items-baseline border d-inline-flex pl-2 mt-3 rounded ml16 mb-2 bg-white">
<i class="fa fa-tag mr-2 text-muted"/>
<t t-esc="country.display_name"/>
<a t-att-href="'/event/%s/exhibitors?%s' % (
slug(event),
keep_query('*', countries=str((search_countries - country).ids)))"
class="btn border-0 py-1">&#215;</a>
</span>
</t>
<t t-foreach="search_sponsorships" t-as="sponsorship">
<span class="align-items-baseline border d-inline-flex pl-2 mt-3 rounded ml16 mb-2 bg-white">
<i class="fa fa-tag mr-2 text-muted"/>
<t t-esc="sponsorship.display_name"/>
<a t-att-href="'/event/%s/exhibitors?%s' % (
slug(event),
keep_query('*', sponsorships=str((search_sponsorships - sponsorship).ids)))"
class="btn border-0 py-1">&#215;</a>
</span>
</t>
</div>
</template>
</odoo>
@@ -0,0 +1,175 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="event_exhibitor_main" name="Event Exhibitor">
<t t-set="no_header" t-value="option_widescreen"/>
<t t-set="no_footer" t-value="option_widescreen"/>
<t t-call="website_event.layout">
<div class="o_wevent_online o_wevent_online_bg o_wesponsor_index">
<!-- Options -->
<t t-set="option_widescreen" t-value="option_widescreen or False"/>
<!-- Drag/Drop Area -->
<div id="oe_structure_wesponsor_index_1" class="oe_structure"/>
<!-- Content -->
<div t-att-class="'o_wevent_online_page_container %s' % ('container pb-3' if not option_widescreen else 'pb-3')">
<div class="row mb-5 mx-0">
<t t-call="website_event_track_exhibitor.exhibitor_aside"/>
<t t-call="website_event_track_exhibitor.exhibitor_main"/>
</div>
</div>
<!-- Drag/Drop Area -->
<div id="oe_structure_wesponsor_index_2" class="oe_structure"/>
</div>
</t>
</template>
<!-- ============================================================ -->
<!-- CONTENT: MAIN TEMPLATES -->
<!-- ============================================================ -->
<template id="exhibitor_main" name="Exhibitor: Main Content">
<div t-att-class="'col-12 o_wevent_online_page_main o_wesponsor_exhibitor_main bg-white p-0 %s' % ('col-md-9 col-lg-10' if option_widescreen else 'col-md-8 col-lg-9')">
<!-- EVENT NOT STARTED ALERTS -->
<div t-if="not sponsor.event_id.is_ongoing" class="alert alert-warning rounded-0 text-center" role="alert">
Event <span t-esc="sponsor.event_id.name" class="font-weight-bold"/>
<span t-if="sponsor.event_id.start_today">
starts in
<span t-esc="sponsor.event_id.start_remaining"
t-options="{'widget': 'duration', 'digital': True, 'unit': 'minute', 'round': 'minute'}"/>.
</span>
<span class="my-0" t-else="">
starts at
<span t-field="sponsor.event_id.with_context(tz=sponsor.event_id.date_tz).date_begin"
t-options="{'format': 'medium'}"/> (<t t-esc="sponsor.event_id.date_tz"/>).
</span>
<br/>
<span>Join us there to meet <b t-esc="sponsor.partner_name"/> !</span>
</div>
<!-- SPONSOR JITSI + CLOSED/FULL ALERTS -->
<div t-if="sponsor.event_id.is_ongoing and sponsor.chat_room_id" class="d-flex flex-column">
<t t-if="not sponsor.is_in_opening_hours">
<div class="col-12 alert alert-warning rounded-0 text-center" role="alert">
<span>Oops! This room is currently closed</span><br />
Come back between
<strong>
<t t-esc="sponsor.hour_from" t-options="{'widget': 'float_time'}"/>
-
<t t-esc="sponsor.hour_to" t-options="{'widget': 'float_time'}"/>
</strong> (<span t-esc="sponsor.event_date_tz"/>)
to meet them !
</div>
</t>
<t t-elif="sponsor.room_is_full">
<div class="col-12 alert alert-warning rounded-0 text-center" role="alert">
<span>Oops! This room is full</span><br />Come back later to have a chat with us!
</div>
</t>
<t t-else="">
<div id="o_wsponsor_jitsi_iframe"/>
<div class="d-flex flex-row-reverse">
<t t-call="website_jitsi.chat_room_join_button">
<t t-set="_classes" t-value="'d-none'"/>
<t t-set="room_name" t-value="sponsor.room_name"/>
<t t-set="chat_room_id" t-value="sponsor.chat_room_id.id"/>
<t t-set="auto_open" t-value="1"/>
<t t-set="attach_to" t-value="'#o_wsponsor_jitsi_iframe'"/>
<t t-set="check_full" t-value="option_can_edit"/>
</t>
</div>
</t>
</div>
<!-- SPONSOR DESCRIPTION -->
<span class="h5 d-inline-block mx-3 mt-3 mb-0">About <t t-esc="sponsor.name"/></span>
<hr class="mt-2 mb-0"/>
<div class="ml-3">
<div class="float-left pt-3">
<span t-if="sponsor.image_512" t-field="sponsor.image_512" class="o_wevent_online_page_avatar"
t-options="{'widget': 'image', 'class': 'rounded-circle', 'max-width': '96'}"/>
<span t-elif="sponsor.partner_id.image_512" t-field="sponsor.partner_id.image_512" class="o_wevent_online_page_avatar"
t-options="{'widget': 'image', 'class': 'rounded-circle', 'max-width': '96'}"/>
</div>
<div class="px-3 pt-3 d-flex flex-row justify-content-between position-relative">
<div class="d-flex flex-column">
<div class="d-flex align-items-center">
<span t-field="sponsor.name" class="h4 mb-0"/>
<div t-if="sponsor.sponsor_type_id.display_ribbon_style and sponsor.sponsor_type_id.display_ribbon_style != 'no_ribbon'" class="ribbon-wrapper">
<div t-field="sponsor.sponsor_type_id" t-attf-class="ribbon ribbon_#{sponsor.sponsor_type_id.name}"/>
</div>
</div>
<span t-field="sponsor.subtitle" class="text-muted mb-3"/>
<div t-if="sponsor.url" class="d-flex text-break align-items-baseline">
<i class="fa fa-home mr-2"/><a t-att-href="sponsor.url"><span t-field="sponsor.url"/></a>
</div>
<div t-elif="sponsor.partner_id.website" class="d-flex text-break align-items-baseline">
<i class="fa fa-home mr-2"/><a t-att-href="sponsor.partner_id.website"><span t-field="sponsor.partner_id.website"/></a>
</div>
<div t-if="sponsor.email" class="d-flex text-break align-items-baseline">
<i class="fa fa-envelope mr-2"/><a t-att-mailto="sponsor.email"><span t-field="sponsor.email"/></a>
</div>
<div t-if="sponsor.phone" class="d-flex text-break align-items-baseline">
<i class="fa fa-phone mr-2"/><span t-field="sponsor.phone"/>
</div>
</div>
<a t-if="sponsor.partner_id.country_id"
t-att-href="'/event/%s/exhibitors?countries=%s' % (slug(sponsor.event_id), [sponsor.partner_id.country_id.id])"
t-attf-class="text-right d-none d-md-block #{'mr-5' if sponsor.sponsor_type_id.display_ribbon_style and sponsor.sponsor_type_id.display_ribbon_style != 'no_ribbon' else ''}">
<img class="img"
style="max-height: 36px;"
t-att-src="website.image_url(sponsor.partner_id.country_id, 'image')"
t-att-alt="sponsor.partner_id.country_id.name"/>
</a>
</div>
</div>
<div t-field="sponsor.website_description" class="my-2 mx-3 oe_no_empty"/>
</div>
</template>
<!-- ============================================================ -->
<!-- ASIDE: CONTROL PANEL -->
<!-- ============================================================ -->
<template id="exhibitor_aside" name="Exhibitor: Aside">
<div t-att-class="'col-12 pl-0 pr-0 pr-md-3 o_wevent_online_page_aside o_wesponsor_exhibitor_aside %s' % ('col-md-3 col-lg-2' if option_widescreen else 'col-md-4 col-lg-3')">
<div class="bg-white o_wevent_online_page_aside_content">
<div class="d-flex align-items-center justify-content-between mx-2">
<span class="h5 mb-0 pt-0 pt-md-3 pb-0 pb-md-2">Other exhibitors</span>
<a href="#collapse_exhibitor_aside" data-toggle="collapse"
class="d-lg-none p-2 text-decoration-none o_wevent_online_page_aside_collapse collapsed">
<i class="fa fa-chevron-down d-lg-none"/>
</a>
</div>
<ul id="collapse_exhibitor_aside" class="list-unstyled collapse d-lg-block mb-0">
<li t-foreach="sponsors_other" t-as="sponsor_other">
<a class="d-flex w-100 h-100 px-2 pt-2 pb-1 text-decoration-none"
t-att-href="sponsor_other.website_url">
<img t-if="sponsor_other.partner_id.country_id"
class="mr-2 o_wesponsor_aside_logo"
t-att-src="website.image_url(sponsor_other.partner_id.country_id, 'image')"
t-att-alt="sponsor_other.partner_id.country_id.name"/>
<div class="flex-grow-1 overflow-auto">
<span class="d-flex align-items-baseline o_wesponsor_sponsor_name">
<span class="d-inline-block text-truncate" t-esc="sponsor_other.name"/>
<span class="badge badge-light ml-auto" t-esc="sponsor_other.sponsor_type_id.name"/>
</span>
<small class="text-muted" t-esc="sponsor_other.subtitle"/>
</div>
</a>
</li>
</ul>
</div>
</div>
</template>
<!-- ============================================================ -->
<!-- MISC / MENUS -->
<!-- ============================================================ -->
<!-- User Navbar - Edit Options -->
<template id="event_sponsor_edit_options" inherit_id="website.user_navbar" name="Edit Event Sponsor Options">
<xpath expr="//li[@id='edit-page-menu']" position="after">
<t t-if="main_object._name == 'event.sponsor'"
t-set="action" t-value="'website_event_track_exhibitor.event_sponsor_action'"/>
</xpath>
</template>
</odoo>
@@ -0,0 +1,113 @@
<?xml version="1.0"?>
<odoo>
<record id="event_sponsor_view_search" model="ir.ui.view">
<field name="name">event.sponsor.view.search.inherit.exhibitor</field>
<field name="model">event.sponsor</field>
<field name="inherit_id" ref="website_event_track_online.event_sponsor_view_search"/>
<field name="arch" type="xml">
<xpath expr="//filter[@name='archived']" position="after">
<separator/>
<filter string="Exhibitor" name="filter_is_exhibitor" domain="[('is_exhibitor', '=', True)]"/>
</xpath>
</field>
</record>
<record id="event_sponsor_view_tree" model="ir.ui.view">
<field name="name">event.sponsor.view.tree.inherit.exhibitor</field>
<field name="model">event.sponsor</field>
<field name="inherit_id" ref="website_event_track_online.event_sponsor_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='sponsor_type_id']" position="after">
<field name="is_exhibitor"/>
</xpath>
</field>
</record>
<record id="event_sponsor_view_kanban" model="ir.ui.view">
<field name="name">event.sponsor.view.kanban.inherit.kanban</field>
<field name="model">event.sponsor</field>
<field name="inherit_id" ref="website_event_track.event_sponsor_view_kanban"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="replace">
<field name="name"/>
</xpath>
</field>
</record>
<record id="event_sponsor_view_form" model="ir.ui.view">
<field name="name">event.sponsor.view.form.inherit.exhibitor</field>
<field name="model">event.sponsor</field>
<field name="inherit_id" ref="website_event_track_online.event_sponsor_view_form"/>
<field name="arch" type="xml">
<xpath expr="//widget[@name='web_ribbon']" position="before">
<div class="oe_button_box" name="button_box">
<field name="website_url" invisible="1"/>
<field name="is_published" widget="website_redirect_button"/>
</div>
</xpath>
<xpath expr="//h1" position="after">
<div class="oe_title">
<label for="subtitle" class="oe_edit_only"/>
<field name="subtitle" placeholder="e.g. : Your best choice for your home"/>
</div>
</xpath>
<xpath expr="//field[@name='sponsor_type_id']" position="after">
<field name="is_exhibitor"/>
</xpath>
<xpath expr="//field[@name='mobile']" position="after">
<label for="hour_from" string="Opening Hours"/>
<div class="o_row">
<field name="hour_from" widget="float_time" nolabel="1" class="oe_inline"/>
<i class="fa fa-long-arrow-right mx-2" aria-label="Arrow icon" title="Arrow"/>
<field name="hour_to" widget="float_time" nolabel="1" class="oe_inline"/>
<field name="event_date_tz" nolabel="1" class="oe_inline"/>
</div>
</xpath>
<xpath expr="//sheet/group" position="after">
<notebook>
<page string="Description">
<group>
<field name="website_description" nolabel="1"/>
</group>
</page>
<page string="Exhibitor" attrs="{'invisible': [('is_exhibitor', '=', False)]}">
<group>
<group>
<field name="room_name" attrs="{'required': [('is_exhibitor', '=', True)]}" string="Jitsi Name"/>
<field name="room_lang_id"/>
<field name="room_max_capacity" attrs="{'required': [('is_exhibitor', '=', True)]}"/>
<field name="chat_room_id" groups="base.group_no_one"/>
</group>
</group>
</page>
</notebook>
</xpath>
</field>
</record>
<record id="event_sponsor_action" model="ir.actions.act_window">
<field name="name">Event Sponsors</field>
<field name="res_model">event.sponsor</field>
<field name="view_mode">kanban,tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a Sponsor / Exhibitor
</p><p>
Sponsors might be advertised on you event pages footer.<br />
Exhibitors might have a dedicated page with chat room for people to connect with them.
</p>
</field>
</record>
<record id="website_event_track.event_sponsor_action_from_event" model="ir.actions.act_window">
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a Sponsor / Exhibitor
</p><p>
Sponsors might be advertised on you event pages footer.<br />
Exhibitors might have a dedicated page with chat room for people to connect with them.
</p>
</field>
</record>
</odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event_type_view_form" model="ir.ui.view">
<field name="name">event.type.view.form.inherit.exhibitor</field>
<field name="model">event.type</field>
<field name="inherit_id" ref="website_event.event_type_view_form_inherit_website"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='website_menu']" position='after'>
<div class="row mt16" attrs="{'invisible': [('website_menu', '=', False)]}">
<label class="col-lg-4" for="exhibitor_menu"/> <field name="exhibitor_menu"/>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -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
@@ -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,
}
@@ -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
@@ -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
@@ -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
}
}

Some files were not shown because too many files have changed in this diff Show More