[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:
@@ -1,4 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import tests
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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>
|
||||
@@ -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.")
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,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
|
||||
|
@@ -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">&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">⋮</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"/>&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>&#9900;&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>
|
||||
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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">×</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">×</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
Reference in New Issue
Block a user