[MERGE][REM] website_event_(track)(_online/_session): merge with their base modules

PURPOSE

Clean organization and models linked to Event Online feature introduced in
semi stable saas-13.3 at odoo/odoo@981f95bbf4 and odoo/enterprise@14722028ae

Also clean website event menu not being available in website event but only
in website event track, which implies some extra-code to manage it.

In short: merge website_event_online in website_event, website_event_track
_(online/session) in website_event_track.

RATIONALE

_online modules have been added to extend content of website_event and
website_event_track without having any impact on those module. First step
of cleaning is to move this content directly in base module.

track_session module is mainly a rewrite of track module. Second step of
cleaning is to move its content directly in website_event_track.

SPECIFICATIONS

Move website.event.menu model from website event track to website event
module. It allows to define community menu in website_event and re-use it
in website_event_meet and website_event_track_quiz, removing some
unnecessary dependencies.

Move event.tag color field from website event online to event. This field is
part of standard event models.

Move remaining content of website event online to website event.

Move remaining content of website event track online to website event track.
Agenda notably is completely replaced by the new one developed within
track_online module.

Move all content of website event track session to website event tracK. It
was mainly of rewriting of views and can now be safely merged into the base
track module.

Ease discovering of online features: set bridges as auto install and allow to
configure CTA button on event from event_type, like other frontend related
menus and buttons, and add settings to install meet, exhibitors, live mode
and quizzed.

Update dependencies accordingly.

LINKS

Task ID-2319779
COM odoo/odoo#56067
ENT odoo/enterprise#12520
UPG odoo/upgrade#1654

Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com>
This commit is contained in:
Odoo's Mergebot
2020-08-21 20:08:12 +02:00
committed by GitHub
191 changed files with 2752 additions and 4076 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
{
'name': 'Events Organization',
'version': '1.2',
'version': '1.3',
'website': 'https://www.odoo.com/page/events',
'category': 'Marketing/Events',
'summary': 'Trainings, Conferences, Meetings, Exhibitions, Registrations',
+9
View File
@@ -42,51 +42,60 @@
<field name="name">5-10</field>
<field name="sequence">1</field>
<field name="category_id" ref="event_tag_category_1"/>
<field name="color">1</field>
</record>
<record id="event_tag_category_1_tag_2" model="event.tag">
<field name="name">10-14</field>
<field name="sequence">2</field>
<field name="category_id" ref="event_tag_category_1"/>
<field name="color">2</field>
</record>
<record id="event_tag_category_1_tag_3" model="event.tag">
<field name="name">15-18</field>
<field name="sequence">3</field>
<field name="category_id" ref="event_tag_category_1"/>
<field name="color">3</field>
</record>
<record id="event_tag_category_1_tag_4" model="event.tag">
<field name="name">18+</field>
<field name="sequence">4</field>
<field name="category_id" ref="event_tag_category_1"/>
<field name="color">4</field>
</record>
<record id="event_tag_category_2_tag_1" model="event.tag">
<field name="name">Culture</field>
<field name="sequence">10</field>
<field name="category_id" ref="event_tag_category_2"/>
<field name="color">5</field>
</record>
<record id="event_tag_category_2_tag_2" model="event.tag">
<field name="name">Music</field>
<field name="sequence">11</field>
<field name="category_id" ref="event_tag_category_2"/>
<field name="color">6</field>
</record>
<record id="event_tag_category_2_tag_3" model="event.tag">
<field name="name">Sport</field>
<field name="sequence">12</field>
<field name="category_id" ref="event_tag_category_2"/>
<field name="color">7</field>
</record>
<record id="event_tag_category_3_tag_1" model="event.tag">
<field name="name">Online</field>
<field name="sequence">20</field>
<field name="category_id" ref="event_tag_category_3"/>
<field name="color">8</field>
</record>
<record id="event_tag_category_3_tag_2" model="event.tag">
<field name="name">Conference</field>
<field name="sequence">21</field>
<field name="category_id" ref="event_tag_category_3"/>
<field name="color">9</field>
</record>
</data></odoo>
+9 -1
View File
@@ -1,7 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _, api, fields, models
from random import randint
from odoo import api, fields, models
class EventTagCategory(models.Model):
@@ -18,9 +20,15 @@ class EventTag(models.Model):
_description = "Event Tag"
_order = "sequence"
def _default_color(self):
return randint(1, 11)
name = fields.Char("Name", required=True, translate=True)
sequence = fields.Integer('Sequence', default=0)
category_id = fields.Many2one("event.tag.category", string="Category", required=True, ondelete='cascade')
color = fields.Integer(
string='Color Index', default=lambda self: self._default_color(),
help='Tag color. No color means no display in kanban or front-end, to distinguish internal tags from public categorization tags.')
def name_get(self):
return [(tag.id, "%s: %s" % (tag.category_id.name, tag.name)) for tag in self]
@@ -6,7 +6,11 @@ class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
module_event_sale = fields.Boolean("Tickets")
module_website_event_meet = fields.Boolean("Discussion Rooms")
module_website_event_track = fields.Boolean("Tracks and Agenda")
module_website_event_track_live = fields.Boolean("Live Mode")
module_website_event_track_quiz = fields.Boolean("Quiz on Tracks")
module_website_event_track_exhibitor = fields.Boolean("Advanced Sponsors")
module_website_event_questions = fields.Boolean("Registration Survey")
module_event_barcode = fields.Boolean("Barcode")
module_website_event_sale = fields.Boolean("Online Ticketing")
+4 -1
View File
@@ -9,7 +9,7 @@
<tree string="Event Category">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="tag_ids" widget="many2many_tags" />
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
</tree>
</field>
</record>
@@ -28,6 +28,7 @@
<tree string="Tags" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="color" widget="color_picker"/>
</tree>
</field>
</group>
@@ -51,6 +52,7 @@
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="category_id"/>
<field name="color" widget="color_picker"/>
</tree>
</field>
</record>
@@ -64,6 +66,7 @@
<group>
<field name="name"/>
<field name="category_id" widget="many2one"/>
<field name="color" widget="color_picker"/>
</group>
</sheet>
</form>
+2 -2
View File
@@ -78,7 +78,7 @@
<label for="tag_ids" string="Tags"/>
<div class="row">
<div class="col-12 mt16">
<field name="tag_ids" widget="many2many_tags" options="{'no_quick_create': True}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_quick_create': True}"/>
</div>
</div>
</div>
@@ -232,7 +232,7 @@
</div>
<field name="date_tz"/>
<field name="event_type_id" string="Template" options="{'no_create':True}"/>
<field name="tag_ids" widget="many2many_tags" options="{'no_quick_create': True}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_quick_create': True}"/>
</group>
<group>
<field name="organizer_id"/>
@@ -22,6 +22,44 @@
<div class="text-muted">
Manage &amp; publish a schedule with tracks
</div>
<div class="mt-3 d-flex" attrs="{'invisible': [('module_website_event_track', '=', False)]}">
<field name="module_website_event_track_live" class="w-auto"/>
<div>
<label string="Live Mode" for="module_website_event_track_live"/><br/>
<span class="text-muted">Youtube support, Chat</span>
</div>
</div>
<div class="mt-3 d-flex" attrs="{'invisible': [('module_website_event_track', '=', False)]}">
<field name="module_website_event_track_quiz" class="w-auto"/>
<div>
<label string="Quizzes on tracks" for="module_website_event_track_quiz"/><br/>
<span class="text-muted">Quizzes, leaderboard</span>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box">
<div>
<div class="o_setting_left_pane">
<field name="module_website_event_meet"/>
</div>
<div class="o_setting_right_pane">
<label string="Discussion Rooms" for="module_website_event_meet"/>
<div class="text-muted">
Integrate discussion rooms
</div>
</div>
</div>
<div class="mt-3">
<div class="o_setting_left_pane">
<field name="module_website_event_track_exhibitor"/>
</div>
<div class="o_setting_right_pane">
<label string="Advanced Sponsors" for="module_website_event_track_exhibitor"/>
<div class="text-muted">
Chat, advanced exhibitors
</div>
</div>
</div>
</div>
</div>
@@ -111,7 +111,7 @@ var LivechatButton = Widget.extend({
this.$el.css('color', this.options.button_text_color);
}
// If website_event_track_online installed, put the livechat banner above the PWA banner.
// If website_event_track installed, put the livechat banner above the PWA banner.
var pwaBannerHeight = $('.o_pwa_install_banner').outerHeight(true);
if (pwaBannerHeight) {
this.$el.css('bottom', pwaBannerHeight + 'px');
-3
View File
@@ -16,14 +16,11 @@ automatic lead generation, full Online support, ...
'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',
],
+1 -1
View File
@@ -7,7 +7,7 @@ from odoo.addons.base.tests.common import HttpCaseWithUserDemo, HttpCaseWithUser
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
from odoo.addons.website_event.tests.common import EventDtPatcher
class TestEventFullCommon(TestEventCrmCommon, TestSalesCommon, EventDtPatcher, MockVisitor):
+22 -6
View File
@@ -1,29 +1,45 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Events',
'version': '1.1',
'category': 'Marketing/Events',
'sequence': 140,
'summary': 'Publish events, sell tickets',
'website': 'https://www.odoo.com/page/events',
'description': "",
'depends': ['website', 'website_partner', 'website_mail', 'event'],
'depends': [
'event',
'website',
'website_partner',
'website_mail',
],
'data': [
'data/event_data.xml',
'views/assets.xml',
'views/res_config_settings_views.xml',
'views/event_snippets.xml',
'views/event_templates.xml',
'views/event_templates_misc_pages.xml',
'views/event_templates_registration.xml',
'views/event_templates_list.xml',
'views/event_templates_page.xml',
'views/event_templates_page_registration.xml',
'views/event_templates_page_misc.xml',
'views/event_templates_widgets.xml',
'views/website_templates.xml',
'views/event_views.xml',
'views/event_event_views.xml',
'views/event_registration_views.xml',
'views/event_type_views.xml',
'views/website_event_menu_views.xml',
'views/website_visitor_views.xml',
'views/event_menus.xml',
'security/ir.model.access.csv',
'security/event_security.xml',
],
'demo': [
'data/res_partner_demo.xml',
'data/event_demo.xml'
'data/website_visitor_demo.xml',
'data/event_demo.xml',
'data/event_registration_demo.xml',
],
'application': True,
}
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
from . import main
from . import community
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.website_event.controllers.main import WebsiteEventController
from odoo import fields, http, modules, tools
from odoo import http
from odoo.http import request
class WebsiteEventCommunityController(http.Controller):
class EventCommunityController(http.Controller):
@http.route('/event/<model("event.event"):event>/community', type="http", auth="public", website=True)
def community(self, event, lang=None, **kwargs):
+22 -1
View File
@@ -345,13 +345,34 @@ class WebsiteEventController(http.Controller):
return list(registrations.values())
def _create_attendees_from_registration_post(self, event, registration_data):
""" Also 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()
visitor_values = {}
registrations_to_create = []
for registration_values in registration_data:
registration_values['event_id'] = event.id
if not registration_values.get('partner_id'):
if not registration_values.get('partner_id') and visitor_sudo.partner_id:
registration_values['partner_id'] = visitor_sudo.partner_id.id
elif not registration_values.get('partner_id'):
registration_values['partner_id'] = request.env.user.partner_id.id
if visitor_sudo:
# registration may give a name to the visitor, yay
if registration_values.get('name') and not visitor_sudo.name and not visitor_values.get('name'):
visitor_values['name'] = registration_values['name']
# update registration based on visitor
registration_values['visitor_id'] = visitor_sudo.id
registrations_to_create.append(registration_values)
if visitor_values:
visitor_sudo.write(visitor_values)
return request.env['event.registration'].sudo().create(registrations_to_create)
@http.route(['''/event/<model("event.event"):event>/registration/confirm'''], type='http', auth="public", methods=['POST'], website=True)
+2
View File
@@ -41,6 +41,7 @@
<record id="event.event_1" model="event.event">
<field name="website_menu" eval="True"/>
<field name="website_published" eval="True"/>
<field name="menu_register_cta" eval="True"/>
<field name="subtitle">The Great Reno Balloon Race is the world's largest free hot-air ballooning event.</field>
<field name="cover_properties">{"background-image": "url('/website_event/static/src/img/event_cover_1.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
<field name="description" type="html">
@@ -151,6 +152,7 @@
<record id="event.event_7" model="event.event">
<field name="website_menu" eval="True"/>
<field name="website_published" eval="True"/>
<field name="menu_register_cta" eval="True"/>
<field name="subtitle">Our newest collection will be revealed online! Interact with us on our live streams!</field>
<field name="cover_properties">{"background-image": "url('/website_event/static/src/img/event_cover_7.jpg')", "resize_class": "o_record_has_cover o_half_screen_height", "opacity": "0.4"}</field>
<field name="description" type="html">
@@ -3,16 +3,16 @@
<!-- 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"/>
<field name="visitor_id" ref="website_event.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"/>
<field name="visitor_id" ref="website_event.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"/>
<field name="visitor_id" ref="website_event.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"/>
<field name="visitor_id" ref="website_event.website_visitor_event_1"/>
</record>
</data></odoo>
@@ -18,7 +18,7 @@
</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="parent_id" ref="website_event.website_visitor_event_2"/>
<field name="partner_id" eval="False"/>
<field name="country_id" ref="base.be"/>
</record>
+6
View File
@@ -1,4 +1,10 @@
# -*- 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_type
from . import website
from . import website_event_menu
from . import website_menu
from . import website_visitor
+178 -29
View File
@@ -1,25 +1,26 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import werkzeug.urls
import json
import werkzeug.urls
from pytz import utc
from odoo import api, fields, models, _
from odoo.addons.http_routing.models.ir_http import slug
from odoo.osv import expression
GOOGLE_CALENDAR_URL = 'https://www.google.com/calendar/render?'
class EventType(models.Model):
_name = 'event.type'
_inherit = ['event.type']
website_menu = fields.Boolean('Display a dedicated menu on Website')
class Event(models.Model):
_name = 'event.event'
_inherit = ['event.event', 'website.seo.metadata', 'website.published.multi.mixin', 'website.cover_properties.mixin']
_inherit = [
'event.event',
'website.seo.metadata',
'website.published.multi.mixin',
'website.cover_properties.mixin'
]
def _default_cover_properties(self):
res = super()._default_cover_properties()
@@ -38,23 +39,68 @@ class Event(models.Model):
help="Creates menus Introduction, Location and Register on the page "
"of the event on the website.")
menu_id = fields.Many2one('website.menu', 'Event Menu', copy=False)
menu_register_cta = fields.Boolean(
'Add Register Button', compute='_compute_menu_register_cta',
readonly=False, store=True)
community_menu = fields.Boolean(
"Community Menu", compute="_compute_community_menu",
readonly=False, store=True,
help="Display community tab on website")
community_menu_ids = fields.One2many(
"website.event.menu", "event_id", string="Event Community Menus",
domain=[("menu_type", "=", "community")])
# 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)")
def _compute_is_participating(self):
# we don't allow public user to see participating label
if self.env.user != self.env['website'].get_current_website().user_id:
email = self.env.user.partner_id.email
for event in self:
domain = ['&', '|', ('email', '=', email), ('partner_id', '=', self.env.user.partner_id.id), ('event_id', '=', event.id)]
event.is_participating = self.env['event.registration'].sudo().search_count(domain)
else:
self.is_participating = False
"""Heuristic
* 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
@api.depends('name')
def _compute_website_url(self):
super(Event, self)._compute_website_url()
for event in self:
if event.id: # avoid to perform a slug on a not yet saved record in case of an onchange.
event.website_url = '/event/%s' % slug(event)
event.is_participating = event in events
@api.depends('event_type_id')
def _compute_website_menu(self):
@@ -66,6 +112,56 @@ class Event(models.Model):
elif not event.website_menu:
event.website_menu = False
@api.depends("event_type_id", "website_menu", "community_menu")
def _compute_community_menu(self):
""" At type onchange: synchronize. At website_menu update: synchronize. """
for event in self:
if event.event_type_id and event.event_type_id != event._origin.event_type_id:
event.community_menu = event.event_type_id.community_menu
elif event.website_menu and event.website_menu != event._origin.website_menu or not event.community_menu:
event.community_menu = True
elif not event.website_menu:
event.community_menu = False
@api.depends("event_type_id", "website_menu")
def _compute_menu_register_cta(self):
""" At type onchange: synchronize. At website_menu update: synchronize. """
for event in self:
if event.event_type_id and event.event_type_id != event._origin.event_type_id:
event.menu_register_cta = event.event_type_id.menu_register_cta
elif event.website_menu and event.website_menu != event._origin.website_menu or not event.menu_register_cta:
event.menu_register_cta = True
elif not event.website_menu:
event.menu_register_cta = False
@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
@api.depends('name')
def _compute_website_url(self):
super(Event, self)._compute_website_url()
for event in self:
if event.id: # avoid to perform a slug on a not yet saved record in case of an onchange.
event.website_url = '/event/%s' % slug(event)
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.model
def create(self, vals):
res = super(Event, self).create(vals)
@@ -93,7 +189,10 @@ class Event(models.Model):
:return list: list of fields, each of which triggering a menu update
like website_menu, website_track, ... """
return ['website_menu']
return ['website_menu', 'community_menu']
def _get_menu_type_field_matching(self):
return {'community': 'community_menu'}
def _split_menus_state_by_field(self):
""" For each field linked to a menu, get the set of events having this
@@ -137,7 +236,7 @@ class Event(models.Model):
menus_update_by_field[fname] |= menus_state_by_field[fname]['deactivated'].filtered(lambda event: event[fname])
return menus_update_by_field
def _get_menu_entries(self):
def _get_website_menu_entries(self):
""" Method returning menu entries to display on the website view of the
event, possibly depending on some options in inheriting modules.
@@ -145,14 +244,22 @@ class Event(models.Model):
* 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'),
(_('Location'), False, 'website_event.template_location'),
(_('Register'), '/event/%s/register' % slug(self), False),
(_('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 _get_community_menu_entries(self):
self.ensure_one()
return [(_('Community'), '/event/%s/community' % slug(self), False, 80, 'community')]
def _update_website_menus(self, menus_update_by_field=None):
""" Synchronize event configuration and its menu entries for frontend.
@@ -163,8 +270,44 @@ class Event(models.Model):
elif event.website_menu and not event.menu_id:
root_menu = self.env['website.menu'].sudo().create({'name': event.name, 'website_id': event.website_id.id})
event.menu_id = root_menu
for sequence, (name, url, xml_id) in enumerate(event._get_menu_entries()):
event._create_menu(sequence, name, url, xml_id)
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_website_menu_entries():
event._create_menu(menu_sequence, name, url, xml_id, menu_type=menu_type)
if event.menu_id and (not menus_update_by_field or event in menus_update_by_field.get('community_menu')):
event._update_website_menu_entry('community_menu', 'community_menu_ids', '_get_community_menu_entries')
def _update_website_menu_entry(self, fname_bool, fname_o2m, method_name):
""" Generic method to create menu entries based on a flag on event. This
method is a bit obscure, but is due to preparation of adding new menus
entries and pages for event in a stable version, leading to some constraints
while developing.
:param fname_bool: field name (e.g. website_track)
:param fname_o2m: o2m linking towards website.event.menu matching the
boolean fields (normally an entry ot website.event.menu with type matching
the boolean field name)
:param method_name: method returning menu entries information: url, sequence, ...
"""
self.ensure_one()
new_menu = None
if self[fname_bool] and not self[fname_o2m]:
# menus not found but boolean True: get menus to create
for sequence, menu_data in enumerate(getattr(self, method_name)()):
# some modules have 4 data: name, url, xml_id, menu_type; however we
# plan to support sequence in future modules, so this hackish code is
# necessary to avoid crashing. Not nice, but stable target = meh.
if len(menu_data) == 4:
(name, url, xml_id, menu_type) = menu_data
menu_sequence = sequence
elif len(menu_data) == 5:
(name, url, xml_id, menu_sequence, menu_type) = menu_data
new_menu = self._create_menu(menu_sequence, name, url, xml_id, menu_type=menu_type)
elif not self[fname_bool]:
# will cascade delete to the website.event.menu
self[fname_o2m].mapped('menu_id').sudo().unlink()
return new_menu
def _create_menu(self, sequence, name, url, xml_id, menu_type=False):
""" If url: create a website menu. Menu leads directly to the URL that
@@ -186,6 +329,12 @@ class Event(models.Model):
'sequence': sequence,
'website_id': self.website_id.id,
})
if menu_type:
self.env['website.event.menu'].create({
'menu_id': website_menu.id,
'event_id': self.id,
'menu_type': menu_type,
})
return website_menu
# ------------------------------------------------------------
@@ -5,14 +5,24 @@ from odoo import api, fields, models
class EventType(models.Model):
_inherit = "event.type"
_name = 'event.type'
_inherit = ['event.type']
website_menu = fields.Boolean('Display a dedicated menu on Website')
community_menu = fields.Boolean(
"Community Menu", compute="_compute_community_menu",
readonly=False, store=True,
help="Display community tab on website")
menu_register_cta = fields.Boolean(
'Add Register Button', compute='_compute_menu_register_cta',
readonly=False, store=True)
@api.depends('website_menu')
def _compute_community_menu(self):
for event_type in self:
event_type.community_menu = event_type.website_menu
@api.depends('website_menu')
def _compute_menu_register_cta(self):
for event_type in self:
event_type.menu_register_cta = event_type.website_menu
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class EventMenu(models.Model):
_name = "website.event.menu"
_description = "Website Event Menu"
menu_id = fields.Many2one('website.menu', string='Menu', ondelete='cascade')
event_id = fields.Many2one('event.event', string='Event', ondelete='cascade')
menu_type = fields.Selection([
('community', 'Community Menu')], string="Menu Type",
ondelete={'community': 'cascade'}, required=True)
@@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class WebsiteMenu(models.Model):
_inherit = "website.menu"
def unlink(self):
""" Override to synchronize event configuration fields with menu deletion.
This should be cleaned in upcoming versions. """
event_updates = {}
website_event_menus = self.env['website.event.menu'].search([('menu_id', 'in', self.ids)])
for event_menu in website_event_menus:
to_update = event_updates.setdefault(event_menu.event_id, list())
for menu_type, fname in event_menu.event_id._get_menu_type_field_matching().items():
if event_menu.menu_type == menu_type:
to_update.append(fname)
# call super that resumes the unlink of menus entries (including website event menus)
res = super(WebsiteMenu, self).unlink()
# update events
for event, to_update in event_updates.items():
if to_update:
event.write(dict((fname, False) for fname in to_update))
return res
@@ -12,6 +12,17 @@
<field name="perm_unlink" eval="False"/>
</record>
<record id="ir_rule_event_tag_public" model="ir.rule">
<field name="name">Event Tag: public/portal: color = published</field>
<field name="model_id" ref="event.model_event_tag"/>
<field name="domain_force">['&amp;', ('color', '!=', False), ('color', '!=', 0)]</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>
<record id="ir_rule_event_event_ticket_public" model="ir.rule">
<field name="name">Event Ticket: public/portal: published read</field>
<field name="model_id" ref="event.model_event_event_ticket"/>
@@ -6,3 +6,6 @@ access_event_event_ticket_public,event.event.ticket.public,event.model_event_eve
access_event_event_ticket_portal,event.event.ticket.portal,event.model_event_event_ticket,base.group_portal,1,0,0,0
access_event_category_public,event.tag.category.public,event.model_event_tag_category,,1,0,0,0
access_event_tag_public,event.tag.public,event.model_event_tag,,1,0,0,0
access_website_event_menu_public,website.event.menu,model_website_event_menu,,1,0,0,0
access_website_event_menu_manager,website.event.menu,model_website_event_menu,event.group_event_manager,1,1,1,1
website_visitor_access_event_user,website.visitor.access.event.user,model_website_visitor,event.group_event_user,1,1,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
6 access_event_event_ticket_portal event.event.ticket.portal event.model_event_event_ticket base.group_portal 1 0 0 0
7 access_event_category_public event.tag.category.public event.model_event_tag_category 1 0 0 0
8 access_event_tag_public event.tag.public event.model_event_tag 1 0 0 0
9 access_website_event_menu_public website.event.menu model_website_event_menu 1 0 0 0
10 access_website_event_menu_manager website.event.menu model_website_event_menu event.group_event_manager 1 1 1 1
11 website_visitor_access_event_user website.visitor.access.event.user model_website_visitor event.group_event_user 1 1 0 0
@@ -1,4 +1,4 @@
odoo.define('website_event_online.display_timer_widget', function (require) {
odoo.define('website_event.display_timer_widget', function (require) {
'use strict';
var core = require('web.core');
@@ -1,4 +1,4 @@
odoo.define('website_event_online.register_toaster_widget', function (require) {
odoo.define('website_event.register_toaster_widget', function (require) {
'use strict';
let core = require('web.core');
+1
View File
@@ -4,5 +4,6 @@
from . import common
from . import test_event_internals
from . import test_ui
from . import test_visitor_events
from . import test_website_event
from . import test_website_event_access
+64
View File
@@ -1,8 +1,59 @@
# -*- 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.addons.mail.tests.common import mail_new_test_user
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_dt = patch(
'odoo.addons.website_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_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_dt = cls.wevent_dt.start()
cls.mock_wevent_main_dt = cls.wevent_main_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_dt.now.return_value = cls.reference_now
cls.mock_wevent_main_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_dt.stop)
cls.addClassCleanup(cls.wevent_main_dt.stop)
cls.addClassCleanup(cls.event_date.stop)
cls.addClassCleanup(cls.wevent_main_date.stop)
class TestWebsiteEventCommon(TestEventCommon):
@@ -43,3 +94,16 @@ class TestWebsiteEventCommon(TestEventCommon):
# without website.event.menu model (or crappy code based on name)
# else:
# self.assertFalse(bool(view))
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),
})
@@ -17,6 +17,7 @@ class TestEventWebsite(TestWebsiteEventCommon):
'date_begin': fields.Datetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
'website_menu': True,
'community_menu': False,
})
self._assert_website_menus(event)
@@ -27,6 +28,7 @@ class TestEventWebsite(TestWebsiteEventCommon):
'date_begin': fields.Datetime.to_string(datetime.today() + timedelta(days=1)),
'date_end': fields.Datetime.to_string(datetime.today() + timedelta(days=15)),
'website_menu': True,
'community_menu': False,
})
self.assertTrue(event.website_menu)
self._assert_website_menus(event)
+3
View File
@@ -10,8 +10,11 @@
<template id="assets_frontend" inherit_id="website.assets_frontend" name="Event Assets Frontend">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/website_event/static/src/scss/website_event.scss"/>
<link rel="stylesheet" type="text/scss" href="/website_event/static/src/scss/website_event_templates.scss"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website_event/static/src/js/display_timer_widget.js"></script>
<script type="text/javascript" src="/website_event/static/src/js/register_toaster_widget.js"></script>
<script type="text/javascript" src="/website_event/static/src/js/website_geolocation.js"></script>
<script type="text/javascript" src="/website_event/static/src/js/website_event.js"></script>
<script type="text/javascript" src="/website_event/static/src/js/website_event_ticket_details.js"></script>
@@ -1,31 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event_type_view_form_inherit_website" model="ir.ui.view">
<field name="name">event.type.view.form.inherit.website</field>
<field name="model">event.type</field>
<field name="inherit_id" ref="event.view_event_type_form"/>
<field name="arch" type="xml">
<div name="event_type_visibility_seats" position="after">
<div class="col-12 col-lg-6 o_setting_box" name="event_type_visibility_website">
<div class="o_setting_left_pane">
<field name="website_menu"/>
</div>
<div class="o_setting_right_pane">
<label for="website_menu"/>
<div class="row" name="website_menu">
<div class="col-lg-8 mt16 text-muted">
Check this option to have menus for your event on the
website: registrations, schedule, map, ...
</div>
</div>
</div>
</div>
</div>
</field>
</record>
<record id="event_event_view_form_inherit_website" model="ir.ui.view">
<record id="event_event_view_form" model="ir.ui.view">
<field name="name">event.event.view.form.inherit.website</field>
<field name="model">event.event</field>
<field name="priority" eval="5"/>
@@ -43,10 +19,20 @@
type="object"
string="Preview Badges"/>
</xpath>
<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"/>
<label for="community_menu" string="Community"/>
<field name="community_menu"/>
</div>
</xpath>
</field>
</record>
<record id="event_event_view_list_inherit_website" model="ir.ui.view">
<record id="event_event_view_list" model="ir.ui.view">
<field name="name">event.event.view.list.inherit.website</field>
<field name="model">event.event</field>
<field name="inherit_id" ref="event.view_event_tree"/>
@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<odoo><data>
<menuitem id="menu_website_event_menu"
name="Website Menus"
action="website_event_menu_action"
parent="event.menu_event_configuration"
groups="base.group_no_one"
sequence="99"/>
</data></odoo>
@@ -12,40 +12,6 @@
</div>
</template>
<!-- Snippet - Country Events - List -->
<template id="country_events_list" name="Country Events List">
<div class="country_events_list">
<t t-if="events">
<h6 class="o_wevent_sidebar_title">
<t t-if="country">
<i class="fa fa-flag mr-2"/>Events: <span t-esc="country.name"/>
<img class="img-fluid" t-att-src="website.image_url(country, 'image')" alt=""/>
</t>
<t t-else="">
<i class="fa fa-globe mr-2"/>Upcoming Events
</t>
</h6>
<ul class="list-group mb-3">
<li t-foreach="events[:5]" t-as="event_dict" class="list-group-item d-flex justify-content-between">
<a t-att-href="event_dict['url']">
<i t-if="not event_dict['event'].website_published" class="fa fa-ban text-danger mr-1" role="img" aria-label="Unpublished" title="Unpublished"/>
<span t-esc="event_dict['event'].name" t-attf-class="#{(not event_dict['event'].website_published) and 'text-danger' or ''}"/>
</a>
<span t-esc="event_dict['date']"/>
</li>
</ul>
<div t-if="len(events) &gt; 5">
<t t-if="country">
<a t-attf-href="/event?country=#{country.id}" class="small"><b>See all events from <span t-esc="country.name"/></b></a>
</t>
<t t-else="">
<a href="/event" class="small"><b>View all</b></a>
</t>
</div>
</t>
</div>
</template>
<!-- Snippets and options -->
<template id="snippets" inherit_id="website.snippets">
<xpath expr="//t[@id='event_local_events_hook']" position="replace">
@@ -97,15 +97,16 @@
<template id="event_category_tag" inherit_id="website_event.index_topbar" active="False" customize_show="True" name="Filter by Tags">
<xpath expr="//ul[hasclass('o_wevent_index_topbar_filters')]" position="inside">
<t t-foreach="categories" t-as="category">
<li t-if="category.tag_ids" class="nav-item dropdown mr-2 my-1">
<li t-if="category.tag_ids and any(tag.color for tag in category.tag_ids)" 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"/>
<t t-esc="category.name"/>
</a>
<div class="dropdown-menu">
<t t-foreach="category.tag_ids" t-as="tag">
<a t-att-href="'/event?%s' % keep_query('*', tags=str((search_tags - tag).ids if tag in search_tags else (tag | search_tags).ids))"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between #{'active' if tag in search_tags else ''}">
<a t-if="tag.color"
t-att-href="'/event?%s' % keep_query('*', tags=str((search_tags - tag).ids if tag in search_tags else (tag | search_tags).ids))"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between #{'active' if tag in search_tags else ''}">
<t t-esc="tag.name"/>
</a>
</t>
@@ -144,23 +145,6 @@
</xpath>
</template>
<!-- Search Box -->
<template id="events_search_box" inherit_id="website.website_search_box" primary="True">
<xpath expr="//div[@role='search']" position="replace">
<form t-attf-class="o_wevent_event_searchbar_form o_wait_lazy_js w-100 my-1 my-lg-0 #{_classes}"
t-att-action="action if action else '/event'" method="get">
<t t-set="search" t-value="search or _searches and _searches['search']"/>
<t t-set="placeholder" t-value="placeholder or _placeholder"/>
<t>$0</t>
<t t-foreach="_searches" t-as="search">
<input t-if="search != 'search' and search_value != 'all'" type="hidden"
t-att-name="search" t-att-value="search_value"/>
</t>
<t t-raw="0"/>
</form>
</xpath>
</template>
<!-- Index - Events list -->
<template id="events_list" name="Events list">
<!-- Options -->
@@ -222,7 +206,8 @@
<div itemprop="location" t-field="event.address_id" t-options="{'widget': 'contact', 'fields': ['city'], 'no_marker': 'true'}"/>
<div class="mt8 d-flex align-items-center">
<t t-foreach="event.tag_ids" t-as="tag">
<span t-attf-class="badge mr8 #{'badge-primary' if tag in search_tags else 'badge-light'}">
<span t-if="tag.color"
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>
</t>
@@ -343,153 +328,4 @@
</xpath>
</template>
<!-- Event -->
<template id="layout" name="Event">
<t t-call="website.layout">
<!-- Options -->
<t t-set="opt_events_list_categories" t-value="request.website.viewref('website_event.opt_events_list_categories').active"/>
<div id="wrap" class="o_wevent_event js_event">
<t t-if="not event.menu_id">
<nav class="navbar navbar-light border-top shadow-sm d-print-none">
<div class="container align-items-baseline justify-content-start">
<a href="/event" class="navbar-brand h4 my-0 mr-0 mr-md-4">
<i class="fa fa-long-arrow-left text-primary mr-2"/>
<span>All Events</span>
</a>
<ul class="navbar-nav flex-row ml-md-auto ml-0">
<li t-if="opt_events_list_categories" class="nav-item mr-3">
<a t-attf-href="/event?type=#{event.event_type_id.id}" t-if="event.event_type_id" class="nav-link">
<i class="fa fa-folder-open text-primary mr-2"/><span t-field="event.event_type_id"/>
</a>
</li>
<li class="nav-item mr-3">
<a t-if="event.country_id" t-attf-href="/event?country=#{event.country_id.id}" class="nav-link">
<i class="fa fa-map-marker text-primary mr-2"/><span t-field="event.country_id"/>
</a>
</li>
</ul>
<div class="d-flex align-items-centerflex-wrap pl-sm-3 pr-0">
<t t-call="website_event.events_search_box">
<t t-set="_classes" t-value="'ml-auto'"/>
<t t-set="_searches" t-value="searches"/>
<t t-set="_placeholder">Search an event ...</t>
</t>
</div>
</div>
</nav>
</t>
<t t-else="">
<nav class="navbar navbar-light border-top shadow-sm navbar-expand-md">
<div class="container align-items-baseline">
<a href="#" t-field="event.name" class="navbar-brand h4 my-0 mr-0 mr-md-4"/>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#o_wevent_event_submenu" aria-controls="o_wevent_event_submenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="o_wevent_event_submenu" class="collapse navbar-collapse">
<ul class="navbar-nav w-100" t-att-data-menu_name="editable and 'Event Menu'" t-att-data-content_menu_id="editable and event.menu_id.id">
<t t-foreach="event.menu_id.child_id" t-as="submenu">
<t t-call="website.submenu">
<t t-set="item_class" t-value="'nav-item'"/>
<t t-set="link_class" t-value="'nav-link'"/>
</t>
</t>
</ul>
</div>
</div>
</nav>
</t>
<t t-raw="0"/>
<t t-set="editor_sub_message">Following content will appear on all events.</t>
<div class="oe_structure oe_empty" id="oe_structure_website_event_layout_1" t-att-data-editor-sub-message="editor_sub_message"/>
</div>
</t>
</template>
<!-- Event - Description -->
<template id="event_description_full" name="Event Description" track="1">
<t t-call="website_event.event_details">
<section class="bg-200 mt-n5">
<div class="container overflow-hidden">
<div class="row no-gutters mt-n4 mb-3">
<!-- Description -->
<div id="o_wevent_event_main_col" class="col-lg-8 bg-white px-3 pt-5 pb-0 shadow-sm">
<span t-field="event.description" itemprop="description"/>
</div>
<div class="col-lg-4 bg-light shadow-sm d-print-none">
<!-- Date & Time -->
<div class="o_wevent_sidebar_block">
<h6 class="o_wevent_sidebar_title">Date &amp; Time</h6>
<div class="d-flex">
<h5 t-field="event.with_context(tz=event.date_tz).date_begin" class="my-1 mr-1" t-options="{'date_only': 'true', 'format': 'EEEE'}"/>
<h5 class="my-1" t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'date_only': 'true', 'format': 'long'}" itemprop="startDate" t-att-datetime="event.date_begin"/>
</div>
<t t-if="not event.is_one_day">Start -</t>
<span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'time_only': 'true', 'format': 'short'}"/>
<t t-if="event.is_one_day">
<i class="fa fa-long-arrow-right mx-1"/>
<span t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'time_only': 'true', 'format': 'short'}"/>
</t>
<t t-else="">
<i class="fa fa-long-arrow-down d-block text-muted mx-3 my-2" style="font-size: 1.5rem"/>
<div class="d-flex">
<h5 t-field="event.with_context(tz=event.date_tz).date_end" class="my-1 mr-1" t-options="{'date_only': 'true', 'format': 'EEEE'}"/>
<h5 class="my-1" t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'date_only': 'true', 'format': 'long'}"/>
</div>
<t t-if="not event.is_one_day">End -</t>
<span t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'time_only': 'true', 'format': 'short'}"/>
</t>
<!-- Timezone -->
<small t-esc="event.date_tz" class="d-block my-3 text-muted"/>
<div class="dropdown">
<i class="fa fa-calendar mr-1"/>
<a href="#" role="button" data-toggle="dropdown">Add to Calendar</a>
<div class="dropdown-menu">
<a t-att-href="iCal_url" class="dropdown-item">iCal/Outlook</a>
<a t-att-href="google_url" class="dropdown-item" target="_blank">Google</a>
</div>
</div>
</div>
<!-- Location -->
<div t-if="event.address_id" class="o_wevent_sidebar_block">
<h6 class="o_wevent_sidebar_title">Location</h6>
<h4 t-field="event.address_id" class="" t-options='{
"widget": "contact",
"fields": ["name"]
}'/>
<div itemprop="location" class="mb-2" t-field="event.address_id" t-options='{
"widget": "contact",
"fields": ["address"],
"no_marker": True
}'/>
<div class="mb-3" t-field="event.address_id" t-options='{
"widget": "contact",
"fields": ["phone", "mobile", "email"]
}'/>
<i class="fa fa-map-marker fa-fw" role="img"/>
<a t-att-href="event._google_map_link()" target="_blank">Get the direction</a>
</div>
<!-- Organizer -->
<div t-if="event.organizer_id" class="o_wevent_sidebar_block">
<h6 class="o_wevent_sidebar_title">Organizer</h6>
<h4 t-field="event.organizer_id"/>
<div itemprop="location" t-field="event.organizer_id" t-options="{'widget': 'contact', 'fields': ['phone', 'mobile', 'email']}"/>
</div>
<!-- Social -->
<div class="o_wevent_sidebar_block">
<h6 class="o_wevent_sidebar_title">SHARE</h6>
<p class="mb-2">Find out what people see and say about this event, and join the conversation.</p>
<t t-snippet-call="website.s_share">
<t t-set="_no_title" t-value="True"/>
<t t-set="_classes" t-valuef="o_wevent_sidebar_social mx-n1"/>
<t t-set="_link_classes" t-valuef="o_wevent_social_link"/>
</t>
</div>
</div>
</div>
</div>
</section>
</t>
</template>
</odoo>
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Event -->
<template id="layout" name="Event">
<t t-call="website.layout">
<!-- Options -->
<t t-set="opt_events_list_categories" t-value="request.website.viewref('website_event.opt_events_list_categories').active"/>
<div id="wrap" class="o_wevent_event js_event">
<t t-if="not event.menu_id">
<nav class="navbar navbar-light border-top shadow-sm d-print-none">
<div class="container align-items-baseline justify-content-start">
<a href="/event" class="navbar-brand h4 my-0 mr-0 mr-md-4">
<i class="fa fa-long-arrow-left text-primary mr-2"/>
<span>All Events</span>
</a>
<ul class="navbar-nav flex-row ml-md-auto ml-0">
<li t-if="opt_events_list_categories" class="nav-item mr-3">
<a t-attf-href="/event?type=#{event.event_type_id.id}" t-if="event.event_type_id" class="nav-link">
<i class="fa fa-folder-open text-primary mr-2"/><span t-field="event.event_type_id"/>
</a>
</li>
<li class="nav-item mr-3">
<a t-if="event.country_id" t-attf-href="/event?country=#{event.country_id.id}" class="nav-link">
<i class="fa fa-map-marker text-primary mr-2"/><span t-field="event.country_id"/>
</a>
</li>
</ul>
<div class="d-flex align-items-centerflex-wrap pl-sm-3 pr-0">
<t t-call="website_event.events_search_box">
<t t-set="_classes" t-value="'ml-auto'"/>
<t t-set="_searches" t-value="searches"/>
<t t-set="_placeholder">Search an event ...</t>
</t>
</div>
</div>
</nav>
</t>
<t t-else="">
<nav class="navbar navbar-light border-top shadow-sm navbar-expand-md">
<div class="container align-items-baseline">
<a href="#" t-field="event.name" class="navbar-brand h4 my-0 mr-0 mr-md-4"/>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#o_wevent_event_submenu" aria-controls="o_wevent_event_submenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="o_wevent_event_submenu" class="collapse navbar-collapse">
<ul class="navbar-nav w-100" t-att-data-menu_name="editable and 'Event Menu'" t-att-data-content_menu_id="editable and event.menu_id.id">
<t t-foreach="event.menu_id.child_id" t-as="submenu">
<t t-call="website.submenu">
<t t-set="item_class" t-value="'nav-item'"/>
<t t-set="link_class" t-value="'nav-link'"/>
</t>
</t>
</ul>
<!-- Add Register additional CTA button, in addition to menus -->
<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>
</div>
</div>
</nav>
</t>
<t t-raw="0"/>
<t t-set="editor_sub_message">Following content will appear on all events.</t>
<div class="oe_structure oe_empty" id="oe_structure_website_event_layout_1" t-att-data-editor-sub-message="editor_sub_message"/>
</div>
</t>
</template>
</odoo>
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="event_details" name="Event Header">
<t t-call="website_event.layout">
<div name="event" itemscope="itemscope" itemtype="http://schema.org/Event">
@@ -26,6 +27,93 @@
</t>
</template>
<!-- Event - Description -->
<template id="event_description_full" name="Event Description" track="1">
<t t-call="website_event.event_details">
<section class="bg-200 mt-n5">
<div class="container overflow-hidden">
<div class="row no-gutters mt-n4 mb-3">
<!-- Description -->
<div id="o_wevent_event_main_col" class="col-lg-8 bg-white px-3 pt-5 pb-0 shadow-sm">
<span t-field="event.description" itemprop="description"/>
</div>
<div class="col-lg-4 bg-light shadow-sm d-print-none">
<!-- Date & Time -->
<div class="o_wevent_sidebar_block">
<h6 class="o_wevent_sidebar_title">Date &amp; Time</h6>
<div class="d-flex">
<h5 t-field="event.with_context(tz=event.date_tz).date_begin" class="my-1 mr-1" t-options="{'date_only': 'true', 'format': 'EEEE'}"/>
<h5 class="my-1" t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'date_only': 'true', 'format': 'long'}" itemprop="startDate" t-att-datetime="event.date_begin"/>
</div>
<t t-if="not event.is_one_day">Start -</t>
<span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'time_only': 'true', 'format': 'short'}"/>
<t t-if="event.is_one_day">
<i class="fa fa-long-arrow-right mx-1"/>
<span t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'time_only': 'true', 'format': 'short'}"/>
</t>
<t t-else="">
<i class="fa fa-long-arrow-down d-block text-muted mx-3 my-2" style="font-size: 1.5rem"/>
<div class="d-flex">
<h5 t-field="event.with_context(tz=event.date_tz).date_end" class="my-1 mr-1" t-options="{'date_only': 'true', 'format': 'EEEE'}"/>
<h5 class="my-1" t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'date_only': 'true', 'format': 'long'}"/>
</div>
<t t-if="not event.is_one_day">End -</t>
<span t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'time_only': 'true', 'format': 'short'}"/>
</t>
<!-- Timezone -->
<small t-esc="event.date_tz" class="d-block my-3 text-muted"/>
<div class="dropdown">
<i class="fa fa-calendar mr-1"/>
<a href="#" role="button" data-toggle="dropdown">Add to Calendar</a>
<div class="dropdown-menu">
<a t-att-href="iCal_url" class="dropdown-item">iCal/Outlook</a>
<a t-att-href="google_url" class="dropdown-item" target="_blank">Google</a>
</div>
</div>
</div>
<!-- Location -->
<div t-if="event.address_id" class="o_wevent_sidebar_block">
<h6 class="o_wevent_sidebar_title">Location</h6>
<h4 t-field="event.address_id" class="" t-options='{
"widget": "contact",
"fields": ["name"]
}'/>
<div itemprop="location" class="mb-2" t-field="event.address_id" t-options='{
"widget": "contact",
"fields": ["address"],
"no_marker": True
}'/>
<div class="mb-3" t-field="event.address_id" t-options='{
"widget": "contact",
"fields": ["phone", "mobile", "email"]
}'/>
<i class="fa fa-map-marker fa-fw" role="img"/>
<a t-att-href="event._google_map_link()" target="_blank">Get the direction</a>
</div>
<!-- Organizer -->
<div t-if="event.organizer_id" class="o_wevent_sidebar_block">
<h6 class="o_wevent_sidebar_title">Organizer</h6>
<h4 t-field="event.organizer_id"/>
<div itemprop="location" t-field="event.organizer_id" t-options="{'widget': 'contact', 'fields': ['phone', 'mobile', 'email']}"/>
</div>
<!-- Social -->
<div class="o_wevent_sidebar_block">
<h6 class="o_wevent_sidebar_title">SHARE</h6>
<p class="mb-2">Find out what people see and say about this event, and join the conversation.</p>
<t t-snippet-call="website.s_share">
<t t-set="_no_title" t-value="True"/>
<t t-set="_classes" t-valuef="o_wevent_sidebar_social mx-n1"/>
<t t-set="_link_classes" t-valuef="o_wevent_social_link"/>
</t>
</div>
</div>
</div>
</div>
</section>
</t>
</template>
<!-- Event - Registration -->
<template id="registration_template" name="Registration">
<div t-if="request.env.user.has_group('event.group_event_manager')" class="alert alert-info rounded-0 o_website_event_configuration o_not_editable" role="status">
@@ -33,6 +121,7 @@
<i class="fa fa-pencil mr-2" role="img" aria-label="Edit" title="Edit event registration in backend"/><em>Configure Event Tickets</em>
</a>
</div>
<div t-if="toast_message" class="o_wevent_register_toaster d-none" t-att-data-message="toast_message"/>
<div t-if="not event.event_registrations_open" class="bg-white mb-5">
<div class="alert alert-info mb-0 d-flex justify-content-between align-items-center" role="status">
<t t-if="event.start_sale_date and event.start_sale_date &gt; datetime.date.today()">
@@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Search Box -->
<template id="events_search_box" inherit_id="website.website_search_box" primary="True">
<xpath expr="//div[@role='search']" position="replace">
<form t-attf-class="o_wevent_event_searchbar_form o_wait_lazy_js w-100 my-1 my-lg-0 #{_classes}"
t-att-action="action if action else '/event'" method="get">
<t t-set="search" t-value="search or _searches and _searches['search']"/>
<t t-set="placeholder" t-value="placeholder or _placeholder"/>
<t>$0</t>
<t t-foreach="_searches" t-as="search">
<input t-if="search != 'search' and search_value != 'all'" type="hidden"
t-att-name="search" t-att-value="search_value"/>
</t>
<t t-raw="0"/>
</form>
</xpath>
</template>
<!-- Snippet - Country Events - List -->
<template id="country_events_list" name="Country Events List">
<div class="country_events_list">
<t t-if="events">
<h6 class="o_wevent_sidebar_title">
<t t-if="country">
<i class="fa fa-flag mr-2"/>Events: <span t-esc="country.name"/>
<img class="img-fluid" t-att-src="website.image_url(country, 'image')" alt=""/>
</t>
<t t-else="">
<i class="fa fa-globe mr-2"/>Upcoming Events
</t>
</h6>
<ul class="list-group mb-3">
<li t-foreach="events[:5]" t-as="event_dict" class="list-group-item d-flex justify-content-between">
<a t-att-href="event_dict['url']">
<i t-if="not event_dict['event'].website_published" class="fa fa-ban text-danger mr-1" role="img" aria-label="Unpublished" title="Unpublished"/>
<span t-esc="event_dict['event'].name" t-attf-class="#{(not event_dict['event'].website_published) and 'text-danger' or ''}"/>
</a>
<span t-esc="event_dict['date']"/>
</li>
</ul>
<div t-if="len(events) &gt; 5">
<t t-if="country">
<a t-attf-href="/event?country=#{country.id}" class="small"><b>See all events from <span t-esc="country.name"/></b></a>
</t>
<t t-else="">
<a href="/event" class="small"><b>View all</b></a>
</t>
</div>
</t>
</div>
</template>
<!-- Timer widget -->
<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>
</odoo>
@@ -0,0 +1,39 @@
<?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.website</field>
<field name="model">event.type</field>
<field name="inherit_id" ref="event.view_event_type_form"/>
<field name="arch" type="xml">
<div name="event_type_visibility_seats" position="after">
<div class="col-12 col-lg-6 o_setting_box" name="event_type_visibility_website">
<div class="o_setting_left_pane">
<field name="website_menu"/>
</div>
<div class="o_setting_right_pane">
<label for="website_menu"/>
<div class="row" name="website_menu">
<div class="col-lg-8 mt16 text-muted">
Check this option to have menus for your event on the
website: registrations, schedule, map, ...
</div>
</div>
<div class="row mt16" name="menu_register_cta"
groups="base.group_no_one"
attrs="{'invisible': [('website_menu', '=', False)]}">
<label class="col-lg-4" for="menu_register_cta"/> <field name="menu_register_cta"/>
</div>
<div class="row mt16" name="community_menu"
id="community-menu"
groups="base.group_no_one"
attrs="{'invisible': [('website_menu', '=', False)]}">
<label class="col-lg-4" for="community_menu"/> <field name="community_menu"/>
</div>
</div>
</div>
</div>
</field>
</record>
</odoo>
@@ -29,7 +29,7 @@
<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"
<button name="%(website_event.event_registration_action_from_visitor)d"
type="action"
class="oe_stat_button" icon="fa-ticket"
attrs="{'invisible': [('event_registration_count', '=', 0)]}">
+1 -5
View File
@@ -11,11 +11,7 @@
'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_event',
'website_jitsi',
],
'demo': ['data/website_event_meet_demo.xml'],
@@ -8,13 +8,13 @@ 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.addons.website_event.controllers.community import EventCommunityController
from odoo.osv import expression
_logger = logging.getLogger(__name__)
class WebsiteEventMeetController(WebsiteEventCommunityController):
class WebsiteEventMeetController(EventCommunityController):
def _get_event_rooms_base_domain(self, event):
search_domain_base = [('event_id', '=', event.id), ('is_published', '=', True)]
@@ -7,4 +7,6 @@ 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")])
menu_type = fields.Selection(
selection_add=[("meeting_room", "Event Meeting Room Menus")],
ondelete={'meeting_room': 'cascade'})
@@ -4,7 +4,7 @@
<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="inherit_id" ref="website_event.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">
@@ -3,7 +3,7 @@
<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="inherit_id" ref="website_event.event_type_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='website_menu']" position='after'>
<div class="row mt16" attrs="{'invisible': [('website_menu', '=', False)]}">
@@ -1,14 +1,14 @@
# -*- 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
from odoo import http
from odoo.addons.website_event.controllers.community import EventCommunityController
from odoo.http import request
class WebsiteEventTrackQuizMeetController(WebsiteEventCommunityController):
class WebsiteEventTrackQuizMeetController(EventCommunityController):
@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):
-6
View File
@@ -1,6 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from . import tests
@@ -1,36 +0,0 @@
# -*- 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',
'security/event_security.xml',
'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',
'views/event_templates_widgets.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,
}
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import event_registration
@@ -1,31 +0,0 @@
# -*- 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)
@@ -1,9 +0,0 @@
<?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>
@@ -1,32 +0,0 @@
<?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>
@@ -1,7 +0,0 @@
# -*- 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
@@ -1,132 +0,0 @@
# -*- 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)
@@ -1,17 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from random import randint
from odoo import models, fields
class EventTag(models.Model):
_inherit = ['event.tag']
def _default_color(self):
return randint(1, 11)
color = fields.Integer(
string='Color Index', default=lambda self: self._default_color(),
help='Tag color. No color means no display in kanban or front-end, to distinguish internal tags from public categorization tags.')
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo><data noupdate="1">
<record id="ir_rule_event_tag_public" model="ir.rule">
<field name="name">Event Tag: public/portal: color = published</field>
<field name="model_id" ref="event.model_event_tag"/>
<field name="domain_force">['&amp;', ('color', '!=', False), ('color', '!=', 0)]</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>
</data></odoo>
@@ -1,2 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
website_visitor_access_event_user,website.visitor.access.event.user,model_website_visitor,event.group_event_user,1,1,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 website_visitor_access_event_user website.visitor.access.event.user model_website_visitor event.group_event_user 1 1 0 0
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_visitor_events
@@ -1,68 +0,0 @@
# -*- 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),
})
@@ -1,14 +0,0 @@
<?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>
@@ -1,23 +0,0 @@
<?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>
@@ -1,48 +0,0 @@
<?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>
@@ -1,54 +0,0 @@
<?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>
<!-- Support tag colors -->
<template id="events_list" name="Events List (Online)"
inherit_id="website_event.events_list">
<xpath expr="//t[@t-foreach='event.tag_ids']/span" position="replace">
<span t-if="tag.color" 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>
<template id="event_category_tag" name="Filter by Tags (Online)"
inherit_id="website_event.event_category_tag">
<xpath expr="//t[@t-foreach='categories']" position="replace">
<t t-foreach="categories" t-as="category">
<li t-if="category.tag_ids and any(tag.color for tag in category.tag_ids)" 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"/>
<t t-esc="category.name"/>
</a>
<div class="dropdown-menu">
<t t-foreach="category.tag_ids" t-as="tag">
<a t-if="tag.color"
t-att-href="'/event?%s' % keep_query('*', tags=str((search_tags - tag).ids if tag in search_tags else (tag | search_tags).ids))"
t-attf-class="dropdown-item d-flex align-items-center justify-content-between #{'active' if tag in search_tags else ''}">
<t t-esc="tag.name"/>
</a>
</t>
</div>
</li>
</t>
</xpath>
</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>
@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<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>
</odoo>
@@ -1,15 +0,0 @@
<?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>
@@ -3,7 +3,7 @@
<record id="event_type_view_form_inherit_question" model="ir.ui.view">
<field name="name">event.type.view.form.inherit.question</field>
<field name="model">event.type</field>
<field name="inherit_id" ref="website_event.event_type_view_form_inherit_website"/>
<field name="inherit_id" ref="website_event.event_type_view_form"/>
<field name="arch" type="xml">
<div name="event_type_attendees_auto_confirm" position="after">
<div class="col-12 col-lg-12 o_setting_box">
@@ -28,10 +28,10 @@
</field>
</record>
<record id="event_event_view_form_inherit_question" model="ir.ui.view">
<record id="event_event_view_form" model="ir.ui.view">
<field name="name">event.event.view.form.inherit.question</field>
<field name="model">event.event</field>
<field name="inherit_id" ref="website_event.event_event_view_form_inherit_website"/>
<field name="inherit_id" ref="website_event.event_event_view_form"/>
<field name="arch" type="xml">
<data>
<page name="event_notes" position="before">
+20 -5
View File
@@ -5,7 +5,7 @@
'name': 'Advanced Events',
'category': 'Marketing',
'summary': 'Sponsors, Tracks, Agenda, Event News',
'version': '1.0',
'version': '1.1',
'description': "",
'depends': ['website_event'],
'data': [
@@ -13,18 +13,33 @@
'security/event_track_security.xml',
'data/event_data.xml',
'data/event_track_data.xml',
'views/event_track_templates.xml',
'views/assets.xml',
'views/event_templates.xml',
'views/event_track_templates_agenda.xml',
'views/event_track_templates_list.xml',
'views/event_track_templates_misc.xml',
'views/event_track_templates_reminder.xml',
'views/event_track_templates_page.xml',
'views/event_track_templates_proposal.xml',
'views/website_templates.xml',
'views/event_track_views.xml',
'views/event_track_location_views.xml',
'views/event_track_tag_views.xml',
'views/event_track_stage_views.xml',
'views/event_track_visitor_views.xml',
'views/event_sponsor_views.xml',
'views/event_views.xml',
'views/website_event_menu_views.xml',
'views/event_event_views.xml',
'views/event_type_views.xml',
'views/website_visitor_views.xml',
'views/event_menus.xml',
],
'demo': [
'data/event_demo.xml',
'data/event_sponsor_demo.xml',
'data/event_track_demo_misc.xml',
'data/event_track_location_demo.xml',
'data/event_track_tag_demo.xml',
'data/event_track_demo.xml',
'data/event_track_demo_description.xml',
'data/event_track_visitor_demo.xml',
],
}
@@ -1,4 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
from . import event_track
from . import webmanifest
@@ -0,0 +1,477 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from ast import literal_eval
from datetime import timedelta
from pytz import timezone, utc
from werkzeug.exceptions import Forbidden, NotFound
import babel
import babel.dates
import base64
import pytz
from odoo import exceptions, http, fields, _
from odoo.http import request
from odoo.osv import expression
from odoo.tools import is_html_empty, plaintext2html
class EventTrackController(http.Controller):
def _get_event_tracks_base_domain(self, event):
""" Base domain for displaying tracks. Restrict to accepted or published
tracks for people not managing events. Unpublished tracks may be displayed
but not reachable for teasing purpose. """
search_domain_base = [
('event_id', '=', event.id),
]
if not request.env.user.has_group('event.group_event_user'):
search_domain_base = expression.AND([
search_domain_base,
['|', ('is_published', '=', True), ('is_accepted', '=', True)]
])
return search_domain_base
# ------------------------------------------------------------
# TRACK LIST VIEW
# ------------------------------------------------------------
@http.route([
'''/event/<model("event.event"):event>/track''',
'''/event/<model("event.event"):event>/track/tag/<model("event.track.tag"):tag>'''
], type='http', auth="public", website=True, sitemap=False)
def event_tracks(self, event, tag=None, **searches):
""" Main route
:param event: event whose tracks are about to be displayed;
:param tag: deprecated: search for a specific tag
:param searches: frontend search dict, containing
* 'search': search string;
* 'tags': list of tag IDs for filtering;
"""
if not event.can_access_from_current_website():
raise NotFound()
return request.render(
"website_event_track.tracks_session",
self._event_tracks_get_values(event, tag=tag, **searches)
)
def _event_tracks_get_values(self, event, tag=None, **searches):
# init and process search terms
searches.setdefault('search', '')
searches.setdefault('search_wishlist', '')
searches.setdefault('tags', '')
search_domain = self._get_event_tracks_base_domain(event)
# search on content
if searches.get('search'):
search_domain = expression.AND([
search_domain,
[('name', 'ilike', searches['search'])]
])
# search on tags
search_tags = self._get_search_tags(searches['tags'])
if not search_tags and tag: # backward compatibility
search_tags = tag
if search_tags:
# Example: You filter on age: 10-12 and activity: football.
# Doing it this way allows to only get events who are tagged "age: 10-12" AND "activity: football".
# Add another tag "age: 12-15" to the search and it would fetch the ones who are tagged:
# ("age: 10-12" OR "age: 12-15") AND "activity: football
grouped_tags = dict()
for search_tag in search_tags:
grouped_tags.setdefault(search_tag.category_id, list()).append(search_tag)
search_domain_items = [
[('tag_ids', 'in', [tag.id for tag in grouped_tags[group]])]
for group in grouped_tags
]
search_domain = expression.AND([
search_domain,
*search_domain_items
])
# fetch data to display with TZ set for both event and tracks
now_tz = utc.localize(fields.Datetime.now().replace(microsecond=0), is_dst=False).astimezone(timezone(event.date_tz))
today_tz = now_tz.date()
event = event.with_context(tz=event.date_tz or 'UTC')
tracks_sudo = event.env['event.track'].sudo().search(search_domain, order='date asc')
tag_categories = request.env['event.track.tag.category'].sudo().search([])
# filter on wishlist (as post processing due to costly search on is_reminder_on)
if searches.get('search_wishlist'):
tracks_sudo = tracks_sudo.filtered(lambda track: track.is_reminder_on)
# organize categories for display: announced, live, soon and day-based
tracks_announced = tracks_sudo.filtered(lambda track: not track.date)
tracks_wdate = tracks_sudo - tracks_announced
date_begin_tz_all = list(set(
dt.date()
for dt in self._get_dt_in_event_tz(tracks_wdate.mapped('date'), event)
))
date_begin_tz_all.sort()
tracks_sudo_live = tracks_wdate.filtered(lambda track: track.is_published and track.is_track_live)
tracks_sudo_soon = tracks_wdate.filtered(lambda track: track.is_published and not track.is_track_live and track.is_track_soon)
tracks_by_day = []
for display_date in date_begin_tz_all:
matching_tracks = tracks_wdate.filtered(lambda track: self._get_dt_in_event_tz([track.date], event)[0].date() == display_date)
tracks_by_day.append({'date': display_date, 'name': display_date, 'tracks': matching_tracks})
if tracks_announced:
tracks_announced = tracks_announced.sorted('wishlisted_by_default', reverse=True)
tracks_by_day.append({'date': False, 'name': _('Coming soon'), 'tracks': tracks_announced})
# return rendering values
return {
# event information
'event': event,
'main_object': event,
# tracks display information
'tracks': tracks_sudo,
'tracks_by_day': tracks_by_day,
'tracks_live': tracks_sudo_live,
'tracks_soon': tracks_sudo_soon,
'today_tz': today_tz,
# search information
'searches': searches,
'search_key': searches['search'],
'search_wishlist': searches['search_wishlist'],
'search_tags': search_tags,
'tag_categories': tag_categories,
# environment
'is_html_empty': is_html_empty,
'hostname': request.httprequest.host.split(':')[0],
'user_event_manager': request.env.user.has_group('event.group_event_manager'),
}
# ------------------------------------------------------------
# AGENDA VIEW
# ------------------------------------------------------------
@http.route(['''/event/<model("event.event"):event>/agenda'''], type='http', auth="public", website=True, sitemap=False)
def event_agenda(self, event, tag=None, **post):
if not event.can_access_from_current_website():
raise NotFound()
event = event.with_context(tz=event.date_tz or 'UTC')
vals = {
'event': event,
'main_object': event,
'tag': tag,
'user_event_manager': request.env.user.has_group('event.group_event_manager'),
}
vals.update(self._prepare_calendar_values(event))
return request.render("website_event_track.agenda_online", vals)
def _prepare_calendar_values(self, event):
"""
Override that should completely replace original method in v14.
This methods slit the day (max end time - min start time) into 15 minutes time slots.
For each time slot, we assign the tracks that start at this specific time slot, and we add the number
of time slot that the track covers (track duration / 15 min)
The calendar will be divided into rows of 15 min, and the talks will cover the corresponding number of rows
(15 min slots).
"""
event = event.with_context(tz=event.date_tz or 'UTC')
local_tz = pytz.timezone(event.date_tz or 'UTC')
lang_code = request.env.context.get('lang')
event_track_ids = self._event_agenda_get_tracks(event)
locations = list(set(track.location_id for track in event_track_ids))
locations.sort(key=lambda x: x.id)
# First split day by day (based on start time)
time_slots_by_tracks = {track: self._split_track_by_days(track, local_tz) for track in event_track_ids}
# extract all the tracks time slots
track_time_slots = set().union(*(time_slot.keys() for time_slot in [time_slots for time_slots in time_slots_by_tracks.values()]))
# extract unique days
days = list(set(time_slot.date() for time_slot in track_time_slots))
days.sort()
# Create the dict that contains the tracks at the correct time_slots / locations coordinates
tracks_by_days = dict.fromkeys(days, 0)
time_slots_by_day = dict((day, dict(start=set(), end=set())) for day in days)
tracks_by_rounded_times = dict((time_slot, dict((location, {}) for location in locations)) for time_slot in track_time_slots)
for track, time_slots in time_slots_by_tracks.items():
start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz)
end_date = start_date + timedelta(hours=(track.duration or 0.25))
for time_slot, duration in time_slots.items():
tracks_by_rounded_times[time_slot][track.location_id][track] = {
'rowspan': duration, # rowspan
'start_date': self._get_locale_time(start_date, lang_code),
'end_date': self._get_locale_time(end_date, lang_code),
'occupied_cells': self._get_occupied_cells(track, duration, locations, local_tz)
}
# get all the time slots by day to determine the max duration of a day.
day = time_slot.date()
time_slots_by_day[day]['start'].add(time_slot)
time_slots_by_day[day]['end'].add(time_slot+timedelta(minutes=15*duration))
tracks_by_days[day] += 1
# split days into 15 minutes time slots
global_time_slots_by_day = dict((day, {}) for day in days)
for day, time_slots in time_slots_by_day.items():
start_time_slot = min(time_slots['start'])
end_time_slot = max(time_slots['end'])
time_slots_count = int(((end_time_slot - start_time_slot).total_seconds() / 3600) * 4)
current_time_slot = start_time_slot
for i in range(0, time_slots_count + 1):
global_time_slots_by_day[day][current_time_slot] = tracks_by_rounded_times.get(current_time_slot, {})
global_time_slots_by_day[day][current_time_slot]['formatted_time'] = self._get_locale_time(current_time_slot, lang_code)
current_time_slot = current_time_slot + timedelta(minutes=15)
# count the number of tracks by days
tracks_by_days = dict.fromkeys(days, 0)
for track in event_track_ids:
track_day = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz).date()
tracks_by_days[track_day] += 1
return {
'days': days,
'tracks_by_days': tracks_by_days,
'time_slots': global_time_slots_by_day,
'locations': locations
}
def _event_agenda_get_tracks(self, event):
tracks_sudo = event.sudo().track_ids.filtered(lambda track: track.date)
if not request.env.user.has_group('event.group_event_manager'):
tracks_sudo = tracks_sudo.filtered(lambda track: track.is_published or track.stage_id.is_accepted)
return tracks_sudo
def _get_locale_time(self, dt_time, lang_code):
""" Get locale time from datetime object
:param dt_time: datetime object
:param lang_code: language code (eg. en_US)
"""
locale = babel.Locale.parse(lang_code)
return babel.dates.format_time(dt_time, format='short', locale=locale)
def time_slot_rounder(self, time, rounded_minutes):
""" Rounds to nearest hour by adding a timedelta hour if minute >= rounded_minutes
E.g. : If rounded_minutes = 15 -> 09:26:00 becomes 09:30:00
09:17:00 becomes 09:15:00
"""
return (time.replace(second=0, microsecond=0, minute=0, hour=time.hour)
+ timedelta(minutes=rounded_minutes * (time.minute // rounded_minutes)))
def _split_track_by_days(self, track, local_tz):
"""
Based on the track start_date and the duration,
split the track duration into :
start_time by day : number of time slot (15 minutes) that the track takes on that day.
E.g. : start date = 01-01-2000 10:00 PM and duration = 3 hours
return {
01-01-2000 10:00:00 PM: 8 (2 * 4),
01-02-2000 00:00:00 AM: 4 (1 * 4)
}
Also return a set of all the time slots
"""
start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz)
start_datetime = self.time_slot_rounder(start_date, 15)
end_datetime = self.time_slot_rounder(start_datetime + timedelta(hours=(track.duration or 0.25)), 15)
time_slots_count = int(((end_datetime - start_datetime).total_seconds() / 3600) * 4)
time_slots_by_day_start_time = {start_datetime: 0}
for i in range(0, time_slots_count):
# If the new time slot is still on the current day
next_day = (start_datetime + timedelta(days=1)).date()
if (start_datetime + timedelta(minutes=15*i)).date() <= next_day:
time_slots_by_day_start_time[start_datetime] += 1
else:
start_datetime = next_day.datetime()
time_slots_by_day_start_time[start_datetime] = 0
return time_slots_by_day_start_time
def _get_occupied_cells(self, track, rowspan, locations, local_tz):
"""
In order to use only once the cells that the tracks will occupy, we need to reserve those cells
(time_slot, location) coordinate. Those coordinated will be given to the template to avoid adding
blank cells where already occupied by a track.
"""
occupied_cells = []
start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz)
start_date = self.time_slot_rounder(start_date, 15)
for i in range(0, rowspan):
time_slot = start_date + timedelta(minutes=15*i)
if track.location_id:
occupied_cells.append((time_slot, track.location_id))
# when no location, reserve all locations
else:
occupied_cells += [(time_slot, location) for location in locations if location]
return occupied_cells
# ------------------------------------------------------------
# TRACK PAGE VIEW
# ------------------------------------------------------------
@http.route('''/event/<model("event.event", "[('website_track', '=', True)]"):event>/track/<model("event.track", "[('event_id', '=', event.id)]"):track>''',
type='http', auth="public", website=True, sitemap=True)
def event_track_page(self, event, track, **options):
track = self._fetch_track(track.id, allow_is_accepted=False)
return request.render(
"website_event_track.event_track_main",
self._event_track_page_get_values(event, track.sudo(), **options)
)
def _event_track_page_get_values(self, event, track, **options):
track = track.sudo()
option_widescreen = options.get('widescreen', False)
option_widescreen = bool(option_widescreen) if option_widescreen != '0' else False
# search for tracks list
tracks_other = track._get_track_suggestions(
restrict_domain=self._get_event_tracks_base_domain(track.event_id),
limit=10
)
return {
# event information
'event': event,
'main_object': track,
'track': track,
# sidebar
'tracks_other': tracks_other,
# options
'option_widescreen': option_widescreen,
# environment
'is_html_empty': is_html_empty,
'hostname': request.httprequest.host.split(':')[0],
'user_event_manager': request.env.user.has_group('event.group_event_manager'),
}
@http.route("/event/track/toggle_reminder", type="json", auth="public", website=True)
def track_reminder_toggle(self, track_id, set_reminder_on):
""" Set a reminder a track for current visitor. Track visitor is created or updated
if it already exists. Exception made if un-wishlisting and no track_visitor
record found (should not happen unless manually done).
:param boolean set_reminder_on:
If True, set as a wishlist, otherwise un-wishlist track;
If the track is a Key Track (wishlisted_by_default):
if set_reminder_on = False, blacklist the track_partner
otherwise, un-blacklist the track_partner
"""
track = self._fetch_track(track_id, allow_is_accepted=True)
force_create = set_reminder_on or track.wishlisted_by_default
event_track_partner = track._get_event_track_visitors(force_create=force_create)
visitor_sudo = event_track_partner.visitor_id
if not track.wishlisted_by_default:
if not event_track_partner or event_track_partner.is_wishlisted == set_reminder_on: # ignore if new state = old state
return {'error': 'ignored'}
event_track_partner.is_wishlisted = set_reminder_on
else:
if not event_track_partner or event_track_partner.is_blacklisted != set_reminder_on: # ignore if new state = old state
return {'error': 'ignored'}
event_track_partner.is_blacklisted = not set_reminder_on
result = {'reminderOn': set_reminder_on}
if request.httprequest.cookies.get('visitor_uuid', '') != visitor_sudo.access_token:
result['visitor_uuid'] = visitor_sudo.access_token
return result
# ------------------------------------------------------------
# TRACK PROPOSAL
# ------------------------------------------------------------
@http.route(['''/event/<model("event.event"):event>/track_proposal'''], type='http', auth="public", website=True, sitemap=False)
def event_track_proposal(self, event, **post):
if not event.can_access_from_current_website():
raise NotFound()
return request.render("website_event_track.event_track_proposal", {'event': event, 'main_object': event})
@http.route(['''/event/<model("event.event"):event>/track_proposal/post'''], type='http', auth="public", methods=['POST'], website=True)
def event_track_proposal_post(self, event, **post):
if not event.can_access_from_current_website():
raise NotFound()
tags = []
for tag in event.allowed_track_tag_ids:
if post.get('tag_' + str(tag.id)):
tags.append(tag.id)
track = request.env['event.track'].sudo().create({
'name': post['track_name'],
'partner_name': post['partner_name'],
'partner_email': post['email_from'],
'partner_phone': post['phone'],
'partner_biography': plaintext2html(post['biography']),
'event_id': event.id,
'tag_ids': [(6, 0, tags)],
'user_id': False,
'description': plaintext2html(post['description']),
'image': base64.b64encode(post['image'].read()) if post.get('image') else False
})
if request.env.user != request.website.user_id:
track.sudo().message_subscribe(partner_ids=request.env.user.partner_id.ids)
else:
partner = request.env['res.partner'].sudo().search([('email', '=', post['email_from'])])
if partner:
track.sudo().message_subscribe(partner_ids=partner.ids)
return request.render("website_event_track.event_track_proposal", {'track': track, 'event': event})
# ------------------------------------------------------------
# TOOLS
# ------------------------------------------------------------
def _fetch_track(self, track_id, allow_is_accepted=False):
track = request.env['event.track'].browse(track_id).exists()
if not track:
raise NotFound()
try:
track.check_access_rights('read')
track.check_access_rule('read')
except exceptions.AccessError:
track_sudo = track.sudo()
if allow_is_accepted and track_sudo.is_accepted:
track = track_sudo
else:
raise Forbidden()
event = track.event_id
# JSON RPC have no website in requests
if hasattr(request, 'website_id') and not event.can_access_from_current_website():
raise NotFound()
try:
event.check_access_rights('read')
event.check_access_rule('read')
except exceptions.AccessError:
raise Forbidden()
return track
def _get_search_tags(self, tag_search):
# TDE FIXME: make me generic (slides, event, ...)
try:
tag_ids = literal_eval(tag_search)
except Exception:
tags = request.env['event.track.tag'].sudo()
else:
# perform a search to filter on existing / valid tags implicitly
tags = request.env['event.track.tag'].sudo().search([('id', 'in', tag_ids)])
return tags
def _get_dt_in_event_tz(self, datetimes, event):
tz_name = event.date_tz
return [
utc.localize(dt, is_dst=False).astimezone(timezone(tz_name))
for dt in datetimes
]
@@ -1,169 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import babel
import babel.dates
import collections
import datetime
import pytz
import base64
from werkzeug.exceptions import NotFound
from odoo import fields, http
from odoo.http import request
from odoo.tools import plaintext2html, html2plaintext
class WebsiteEventTrackController(http.Controller):
@http.route(['''/event/<model("event.event"):event>/track/<model("event.track", "[('event_id','=',event.id)]"):track>'''], type='http', auth="public", website=True, sitemap=True)
def event_track_view(self, event, track, **post):
if not event.can_access_from_current_website():
raise NotFound()
track = track.sudo().with_context(tz=event.date_tz or 'UTC')
values = {'track': track, 'event': track.event_id, 'main_object': track}
return request.render("website_event_track.track_view", values)
def _get_locale_time(self, dt_time, lang_code):
""" Get locale time from datetime object
:param dt_time: datetime object
:param lang_code: language code (eg. en_US)
"""
locale = babel.Locale.parse(lang_code)
return babel.dates.format_time(dt_time, format='short', locale=locale)
def _prepare_calendar(self, event, event_track_ids):
local_tz = pytz.timezone(event.date_tz or 'UTC')
lang_code = request.env.context.get('lang')
locations = {} # { location: [track, start_date, end_date, rowspan]}
dates = [] # [ (date, {}) ]
for track in event_track_ids:
locations.setdefault(track.location_id or False, [])
forcetr = True
for track in event_track_ids:
start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz)
end_date = start_date + datetime.timedelta(hours=(track.duration or 0.5))
location = track.location_id or False
locations.setdefault(location, [])
# New TR, align all events
if forcetr or (start_date>dates[-1][0]) or not location:
formatted_time = self._get_locale_time(start_date, lang_code)
dates.append((start_date, {}, bool(location), formatted_time))
for loc in list(locations):
if locations[loc] and (locations[loc][-1][2] > start_date):
locations[loc][-1][3] += 1
elif not locations[loc] or locations[loc][-1][2] <= start_date:
locations[loc].append([False, locations[loc] and locations[loc][-1][2] or dates[0][0], start_date, 1])
dates[-1][1][loc] = locations[loc][-1]
forcetr = not bool(location)
# Add event
if locations[location] and locations[location][-1][1] > start_date:
locations[location][-1][3] -= 1
locations[location].append([track, start_date, end_date, 1])
dates[-1][1][location] = locations[location][-1]
locations = collections.OrderedDict(sorted(locations.items(), key=lambda t: t[0].id if t[0] else 0))
return {
'locations': locations,
'dates': dates
}
@http.route(['''/event/<model("event.event"):event>/agenda'''], type='http', auth="public", website=True, sitemap=False)
def event_agenda(self, event, tag=None, **post):
if not event.can_access_from_current_website():
raise NotFound()
event = event.with_context(tz=event.date_tz or 'UTC')
local_tz = pytz.timezone(event.date_tz or 'UTC')
days_tracks = collections.defaultdict(lambda: [])
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)
days_tracks[str(date)[:10]].append(track)
days = {}
tracks_by_days = {}
for day, tracks in days_tracks.items():
tracks_by_days[day] = tracks
days[day] = self._prepare_calendar(event, tracks)
return request.render("website_event_track.agenda", {
'event': event,
'main_object': event,
'days': days,
'tracks_by_days': tracks_by_days,
'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>'''
], type='http', auth="public", website=True, sitemap=False)
def event_tracks(self, event, tag=None, **post):
if not event.can_access_from_current_website() or (tag and tag.color == 0):
raise NotFound()
event = event.with_context(tz=event.date_tz or 'UTC')
searches = {}
if tag:
searches.update(tag=tag.id)
tracks = event.track_ids.filtered(lambda track: tag in track.tag_ids)
else:
tracks = event.track_ids
values = {
'event': event,
'main_object': event,
'tracks': tracks,
'tags': event.tracks_tag_ids,
'searches': searches,
'html2plaintext': html2plaintext
}
return request.render("website_event_track.tracks", values)
@http.route(['''/event/<model("event.event"):event>/track_proposal'''], type='http', auth="public", website=True, sitemap=False)
def event_track_proposal(self, event, **post):
if not event.can_access_from_current_website():
raise NotFound()
return request.render("website_event_track.event_track_proposal", {'event': event, 'main_object': event})
@http.route(['''/event/<model("event.event"):event>/track_proposal/post'''], type='http', auth="public", methods=['POST'], website=True)
def event_track_proposal_post(self, event, **post):
if not event.can_access_from_current_website():
raise NotFound()
tags = []
for tag in event.allowed_track_tag_ids:
if post.get('tag_' + str(tag.id)):
tags.append(tag.id)
track = request.env['event.track'].sudo().create({
'name': post['track_name'],
'partner_name': post['partner_name'],
'partner_email': post['email_from'],
'partner_phone': post['phone'],
'partner_biography': plaintext2html(post['biography']),
'event_id': event.id,
'tag_ids': [(6, 0, tags)],
'user_id': False,
'description': plaintext2html(post['description']),
'image': base64.b64encode(post['image'].read()) if post.get('image') else False
})
if request.env.user != request.website.user_id:
track.sudo().message_subscribe(partner_ids=request.env.user.partner_id.ids)
else:
partner = request.env['res.partner'].sudo().search([('email', '=', post['email_from'])])
if partner:
track.sudo().message_subscribe(partner_ids=partner.ids)
return request.render("website_event_track.event_track_proposal", {'track': track, 'event': event})
@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import pytz
from odoo import http
from odoo.addons.http_routing.models.ir_http import url_for
from odoo.http import request
from odoo.modules.module import get_module_resource
from odoo.tools import ustr
from odoo.tools.translate import _
class TrackManifest(http.Controller):
@http.route('/event/manifest.webmanifest', type='http', auth='public', methods=['GET'], website=True, sitemap=False)
def webmanifest(self):
""" Returns a WebManifest describing the metadata associated with a web application.
Using this metadata, user agents can provide developers with means to create user
experiences that are more comparable to that of a native application.
"""
company = request.env.company
website = request.website
manifest = {
'name': _('%s Online Events') % company.name,
'short_name': company.name,
'description': _('%s Online Events') % company.name,
'scope': url_for('/event'),
'start_url': url_for('/event'),
'display': 'standalone',
'background_color': '#ffffff',
'theme_color': '#875A7B',
}
icon_sizes = ['192x192', '512x512']
manifest['icons'] = [{
'src': website.image_url(website, 'app_icon', size=size),
'sizes': size,
'type': 'image/png',
} for size in icon_sizes]
body = json.dumps(manifest, default=ustr)
response = request.make_response(body, [
('Content-Type', 'application/manifest+json'),
])
return response
@http.route('/event/service-worker.js', type='http', auth='public', methods=['GET'], website=True, sitemap=False)
def service_worker(self):
""" Returns a ServiceWorker javascript file scoped for website_event
"""
sw_file = get_module_resource('website_event_track', 'static/src/js/service_worker.js')
with open(sw_file, 'rb') as fp:
body = fp.read()
response = request.make_response(body, [
('Content-Type', 'text/javascript'),
('Service-Worker-Allowed', url_for('/event')),
])
return response
@@ -33,14 +33,17 @@
<record id="event_sponsor_type1" model="event.sponsor.type">
<field name="name">Bronze</field>
<field name="sequence">3</field>
<field name="display_ribbon_style">Bronze</field>
</record>
<record id="event_sponsor_type2" model="event.sponsor.type">
<field name="name">Silver</field>
<field name="sequence">2</field>
<field name="display_ribbon_style">Silver</field>
</record>
<record id="event_sponsor_type3" model="event.sponsor.type">
<field name="name">Gold</field>
<field name="sequence">1</field>
<field name="display_ribbon_style">Gold</field>
</record>
<record id="event_track_stage0" model="event.track.stage">
@@ -58,15 +61,19 @@
<field name="name">Announced</field>
<field name="sequence">3</field>
<field name="color">3</field>
<field name="is_accepted" eval="True"/>
</record>
<record id="event_track_stage3" model="event.track.stage">
<field name="name">Published</field>
<field name="sequence">4</field>
<field name="color">4</field>
<field name="is_accepted" eval="True"/>
<field name="is_done" eval="True"/>
</record>
<record id="event_track_stage4" model="event.track.stage">
<field name="name">Refused</field>
<field name="sequence">5</field>
<field name="color">5</field>
<field name="fold" eval="True"/>
</record>
<record id="event_track_stage5" model="event.track.stage">
@@ -333,6 +333,7 @@
<field name="color">1</field>
<field name="event_id" ref="event.event_7"/>
<field name="stage_id" ref="event_track_stage3"/>
<field name="wishlisted_by_default" eval="True"/>
<field name="date" eval="(DateTime.now() - timedelta(days=1)).strftime('%Y-%m-%d 06:00:00')"></field>
<field name="tag_ids" eval="[
(4, ref('website_event_track.event_track_tag1')),
@@ -343,6 +344,10 @@
<field name="duration">2</field>
<field name="user_id" ref="base.user_admin"/>
<field name="partner_id" ref="base.res_partner_address_15"/>
<field name="website_cta" eval="True"/>
<field name="website_cta_title">Try Now</field>
<field name="website_cta_url">http://www.example.com</field>
<field name="website_cta_delay">10</field>
</record>
<record id="event_7_track_2" model="event.track">
<field name="name">First Day Wrapup</field>
@@ -386,6 +391,10 @@
<field name="duration">2</field>
<field name="user_id" ref="base.user_admin"/>
<field name="partner_id" ref="base.res_partner_address_15"/>
<field name="website_cta" eval="True"/>
<field name="website_cta_title">Try Now</field>
<field name="website_cta_url">http://www.example.com</field>
<field name="website_cta_delay">10</field>
</record>
<!-- Location 2 -->
<record id="event_7_track_5" model="event.track">
@@ -474,6 +483,7 @@
<field name="color">1</field>
<field name="event_id" ref="event.event_7"/>
<field name="stage_id" ref="event_track_stage3"/>
<field name="wishlisted_by_default" eval="True"/>
<field name="date" eval="DateTime.now().strftime('%Y-%m-%d 06:00:00')"></field>
<field name="tag_ids" eval="[
(4, ref('website_event_track.event_track_tag1')),
@@ -519,6 +529,7 @@
<field name="color">2</field>
<field name="event_id" ref="event.event_7"/>
<field name="stage_id" ref="event_track_stage3"/>
<field name="wishlisted_by_default" eval="True"/>
<field name="location_id" ref="website_event_track.event_track_location_online_1"/>
<field name="date" eval="DateTime.now().strftime('%Y-%m-%d 12:00:00')"></field>
<field name="tag_ids" eval="[(4, ref('website_event_track.event_track_tag3'))]"/>
@@ -572,6 +583,7 @@
<field name="color">7</field>
<field name="event_id" ref="event.event_7"/>
<field name="stage_id" ref="event_track_stage3"/>
<field name="wishlisted_by_default" eval="True"/>
<field name="location_id" ref="website_event_track.event_track_location_online_2"/>
<field name="date" eval="DateTime.now().strftime('%Y-%m-%d 13:30:00')"></field>
<field name="tag_ids" eval="[(4, ref('website_event_track.event_track_tag1')), (4, ref('website_event_track.event_track_tag12'))]"/>
@@ -1,33 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="website_event_track.event_track_stage2" model="event.track.stage">
<field name="color">0</field>
</record>
<record id="event_track_tag1" model="event.track.tag">
<field name="name">Consumers</field>
<field name="color">1</field>
</record>
<record id="event_track_tag2" model="event.track.tag">
<field name="name">Sales</field>
<field name="color">2</field>
</record>
<record id="event_track_tag3" model="event.track.tag">
<field name="name">Research</field>
<field name="color">3</field>
</record>
<record id="event_track_tag11" model="event.track.tag">
<field name="name">Lightning Talks</field>
<field name="color">4</field>
</record>
<record id="event_track_tag12" model="event.track.tag">
<field name="name">Round Table</field>
<field name="color">5</field>
</record>
<record id="event_track_tag13" model="event.track.tag">
<field name="name">Keynote</field>
<field name="color">6</field>
</record>
<record id="event_track_location5" model="event.track.location">
<field name="name">Le Foyer du lac</field>
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="event_track_tag_category_1" model="event.track.tag.category">
<field name="name">Audience</field>
<field name="sequence">10</field>
</record>
<record id="event_track_tag_category_2" model="event.track.tag.category">
<field name="name">Format</field>
<field name="sequence">20</field>
</record>
<record id="event_track_tag1" model="event.track.tag">
<field name="name">Consumers</field>
<field name="color">1</field>
<field name="category_id" ref="event_track_tag_category_1"/>
<field name="sequence">1</field>
</record>
<record id="event_track_tag2" model="event.track.tag">
<field name="name">Sales</field>
<field name="color">2</field>
<field name="category_id" ref="event_track_tag_category_1"/>
<field name="sequence">2</field>
</record>
<record id="event_track_tag3" model="event.track.tag">
<field name="name">Research</field>
<field name="color">3</field>
<field name="category_id" ref="event_track_tag_category_1"/>
<field name="sequence">3</field>
</record>
<record id="event_track_tag11" model="event.track.tag">
<field name="name">Lightning Talks</field>
<field name="color">4</field>
<field name="category_id" ref="event_track_tag_category_2"/>
<field name="sequence">11</field>
</record>
<record id="event_track_tag12" model="event.track.tag">
<field name="name">Round Table</field>
<field name="color">5</field>
<field name="category_id" ref="event_track_tag_category_2"/>
<field name="sequence">12</field>
</record>
<record id="event_track_tag13" model="event.track.tag">
<field name="name">Keynote</field>
<field name="color">6</field>
<field name="category_id" ref="event_track_tag_category_2"/>
<field name="sequence">13</field>
</record>
</odoo>
@@ -3,12 +3,12 @@
<record id="event_7_track_2_visitor_0" model="event.track.visitor">
<field name="track_id" ref="website_event_track.event_7_track_2"/>
<field name="visitor_id" ref="website_event_online.website_visitor_event_0"/>
<field name="visitor_id" ref="website_event.website_visitor_event_0"/>
<field name="is_wishlisted" eval="True"/>
</record>
<record id="event_7_track_2_visitor_2" model="event.track.visitor">
<field name="track_id" ref="website_event_track.event_7_track_2"/>
<field name="visitor_id" ref="website_event_online.website_visitor_event_2"/>
<field name="visitor_id" ref="website_event.website_visitor_event_2"/>
<field name="is_wishlisted" eval="True"/>
</record>
@@ -3,7 +3,15 @@
from . import event_event
from . import event_sponsor
from . import event_sponsor_type
from . import event_track
from . import event_track_location
from . import event_track_stage
from . import event_track_tag
from . import event_track_tag_category
from . import event_track_visitor
from . import event_type
from . import website
from . import website_event_menu
from . import website_menu
from . import website_visitor
@@ -5,24 +5,6 @@ from odoo import api, fields, models, _
from odoo.addons.http_routing.models.ir_http import slug
class EventType(models.Model):
_inherit = 'event.type'
website_track = fields.Boolean(
string='Tracks on Website', compute='_compute_website_menu_data',
readonly=False, store=True)
website_track_proposal = fields.Boolean(
string='Tracks Proposals on Website', compute='_compute_website_menu_data',
readonly=False, store=True)
@api.depends('website_menu')
def _compute_website_menu_data(self):
""" Simply activate or de-activate all menus at once. """
for event_type in self:
event_type.website_track = event_type.website_menu
event_type.website_track_proposal = event_type.website_menu
class Event(models.Model):
_inherit = "event.event"
@@ -103,53 +85,10 @@ class Event(models.Model):
if (not menus_update_by_field) or (event in menus_update_by_field.get('website_track_proposal')):
event._update_website_menu_entry('website_track_proposal', 'track_proposal_menu_ids', '_get_track_proposal_menu_entries')
def _update_website_menu_entry(self, fname_bool, fname_o2m, method_name):
""" Generic method to create menu entries based on a flag on event. This
method is a bit obscure, but is due to preparation of adding new menus
entries and pages for event in a stable version, leading to some constraints
while developing.
:param fname_bool: field name (e.g. website_track)
:param fname_o2m: o2m linking towards website.event.menu matching the
boolean fields (normally an entry ot website.event.menu with type matching
the boolean field name)
:param method_name: method returning menu entries information: url, sequence, ...
"""
self.ensure_one()
new_menu = None
if self[fname_bool] and not self[fname_o2m]:
# menus not found but boolean True: get menus to create
for sequence, menu_data in enumerate(getattr(self, method_name)()):
# some modules have 4 data: name, url, xml_id, menu_type; however we
# plan to support sequence in future modules, so this hackish code is
# necessary to avoid crashing. Not nice, but stable target = meh.
if len(menu_data) == 4:
(name, url, xml_id, menu_type) = menu_data
menu_sequence = sequence
elif len(menu_data) == 5:
(name, url, xml_id, menu_sequence, menu_type) = menu_data
new_menu = self._create_menu(menu_sequence, name, url, xml_id, menu_type=menu_type)
elif not self[fname_bool]:
# will cascade delete to the website.event.menu
self[fname_o2m].mapped('menu_id').sudo().unlink()
return new_menu
def _create_menu(self, sequence, name, url, xml_id, menu_type=False):
""" Override menu creation from website_event to link a website.event.menu
to the newly create menu (either page and url). """
website_menu = super(Event, self)._create_menu(sequence, name, url, xml_id, menu_type=menu_type)
if menu_type:
self.env['website.event.menu'].create({
'menu_id': website_menu.id,
'event_id': self.id,
'menu_type': menu_type,
})
return website_menu
def _get_menu_type_field_matching(self):
return {'track_proposal': 'website_track_proposal'}
res = super(Event, self)._get_menu_type_field_matching()
res['track_proposal'] = 'website_track_proposal'
return res
def _get_track_menu_entries(self):
""" Method returning menu entries to display on the website view of the
@@ -164,10 +103,11 @@ class Event(models.Model):
"""
self.ensure_one()
return [
(_('Talks'), '/event/%s/track' % slug(self), False, 'track'),
(_('Agenda'), '/event/%s/agenda' % slug(self), False, False)]
(_('Talks'), '/event/%s/track' % slug(self), False, 10, 'track'),
(_('Agenda'), '/event/%s/agenda' % slug(self), False, 70, False)
]
def _get_track_proposal_menu_entries(self):
""" See website_event_track._get_track_menu_entries() """
self.ensure_one()
return [(_('Talk Proposals'), '/event/%s/track_proposal' % slug(self), False, 'track_proposal')]
return [(_('Talk Proposals'), '/event/%s/track_proposal' % slug(self), False, 15, 'track_proposal')]
@@ -2,36 +2,40 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class SponsorType(models.Model):
_name = "event.sponsor.type"
_description = 'Event Sponsor Type'
_order = "sequence"
name = fields.Char('Sponsor Type', required=True, translate=True)
sequence = fields.Integer('Sequence')
from odoo.modules.module import get_resource_path
class Sponsor(models.Model):
_name = "event.sponsor"
_description = 'Event Sponsor'
_order = "sequence, sponsor_type_id"
_rec_name = 'partner_name'
_rec_name = 'name'
_inherit = ['mail.thread', 'mail.activity.mixin']
event_id = fields.Many2one('event.event', 'Event', required=True)
sponsor_type_id = fields.Many2one('event.sponsor.type', 'Sponsoring Type', required=True)
url = fields.Char('Sponsor Website', compute='_compute_url', readonly=False, store=True)
sequence = fields.Integer('Sequence')
active = fields.Boolean(default=True)
# contact information
partner_id = fields.Many2one('res.partner', 'Sponsor/Customer', required=True)
partner_name = fields.Char('Name', related='partner_id.name')
partner_email = fields.Char('Email', related='partner_id.email')
partner_phone = fields.Char('Phone', related='partner_id.phone')
partner_mobile = fields.Char('Mobile', related='partner_id.mobile')
url = fields.Char('Sponsor Website', compute='_compute_url', readonly=False, store=True)
sequence = fields.Integer('Sequence')
image_128 = fields.Image(
string="Logo", related='partner_id.image_128', store=True, readonly=False)
active = fields.Boolean(default=True)
name = fields.Char('Sponsor Name', compute='_compute_name', readonly=False, store=True)
email = fields.Char('Sponsor Email', compute='_compute_email', readonly=False, store=True)
phone = fields.Char('Sponsor Phone', compute='_compute_phone', readonly=False, store=True)
mobile = fields.Char('Sponsor Mobile', compute='_compute_mobile', readonly=False, store=True)
# image
image_512 = fields.Image(
string="Logo", max_width=512, max_height=512,
compute='_compute_image_512', readonly=False, store=True)
image_256 = fields.Image("Image 256", related="image_512", max_width=256, max_height=256, store=False)
image_128 = fields.Image("Image 128", related="image_512", max_width=128, max_height=128, store=False)
website_image_url = fields.Char(
string='Image URL', max_width=256, max_height=256,
compute='_compute_website_image_url', compute_sudo=True, store=False)
@api.depends('partner_id')
def _compute_url(self):
@@ -39,6 +43,47 @@ class Sponsor(models.Model):
if sponsor.partner_id.website or not sponsor.url:
sponsor.url = sponsor.partner_id.website
@api.depends('partner_id')
def _compute_name(self):
self._synchronize_with_partner('name')
@api.depends('partner_id')
def _compute_email(self):
self._synchronize_with_partner('email')
@api.depends('partner_id')
def _compute_phone(self):
self._synchronize_with_partner('phone')
@api.depends('partner_id')
def _compute_mobile(self):
self._synchronize_with_partner('mobile')
@api.depends('partner_id')
def _compute_image_512(self):
self._synchronize_with_partner('image_512')
@api.depends('image_256', 'partner_id.image_256')
def _compute_website_image_url(self):
for sponsor in self:
if sponsor.image_256:
sponsor.website_image_url = self.env['website'].image_url(sponsor, 'image_256', size=256)
elif sponsor.partner_id.image_256:
sponsor.website_image_url = self.env['website'].image_url(sponsor.partner_id, 'image_256', size=256)
else:
sponsor.website_image_url = get_resource_path('website_event_track', 'static/src/img', 'event_sponsor_default_%d.png' % (sponsor.id % 1))
def _synchronize_with_partner(self, fname):
""" Synchronize with partner if not set. Setting a value does not write
on partner as this may be event-specific information. """
for sponsor in self:
if not sponsor[fname]:
sponsor[fname] = sponsor.partner_id[fname]
# ------------------------------------------------------------
# MESSAGING
# ------------------------------------------------------------
def _message_get_suggested_recipients(self):
recipients = super(Sponsor, self)._message_get_suggested_recipients()
for sponsor in self:
@@ -5,8 +5,12 @@ from odoo import fields, models
class SponsorType(models.Model):
_inherit = "event.sponsor.type"
_name = "event.sponsor.type"
_description = 'Event Sponsor Type'
_order = "sequence"
name = fields.Char('Sponsor Type', required=True, translate=True)
sequence = fields.Integer('Sequence')
display_ribbon_style = fields.Selection([
('no_ribbon', 'No Ribbon'),
('Gold', 'Gold'),
+353 -52
View File
@@ -2,37 +2,14 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import timedelta
from pytz import utc
from random import randint
from odoo import api, fields, models
from odoo.tools.translate import _, html_translate
from odoo.addons.http_routing.models.ir_http import slug
class TrackTag(models.Model):
_name = "event.track.tag"
_description = 'Event Track Tag'
_order = 'name'
def _default_color(self):
return randint(1, 11)
name = fields.Char('Tag Name', required=True)
track_ids = fields.Many2many('event.track', string='Tracks')
color = fields.Integer(
string='Color Index', default=lambda self: self._default_color(),
help="Note that colorless tags won't be available on the website.")
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
]
class TrackLocation(models.Model):
_name = "event.track.location"
_description = 'Event Track Location'
name = fields.Char('Location', required=True)
from odoo.osv import expression
from odoo.tools.mail import is_html_empty
from odoo.tools.translate import _, html_translate
class Track(models.Model):
@@ -45,10 +22,36 @@ class Track(models.Model):
def _get_default_stage_id(self):
return self.env['event.track.stage'].search([], limit=1).id
# description
name = fields.Char('Title', required=True, translate=True)
event_id = fields.Many2one('event.event', 'Event', required=True)
active = fields.Boolean(default=True)
user_id = fields.Many2one('res.users', 'Responsible', tracking=True, default=lambda self: self.env.user)
company_id = fields.Many2one('res.company', related='event_id.company_id')
tag_ids = fields.Many2many('event.track.tag', string='Tags')
description = fields.Html(translate=html_translate, sanitize_attributes=False, sanitize_form=False)
color = fields.Integer('Color')
priority = fields.Selection([
('0', 'Low'), ('1', 'Medium'),
('2', 'High'), ('3', 'Highest')],
'Priority', required=True, default='1')
# management
stage_id = fields.Many2one(
'event.track.stage', string='Stage', ondelete='restrict',
index=True, copy=False, default=_get_default_stage_id,
group_expand='_read_group_stage_ids',
required=True, tracking=True)
is_accepted = fields.Boolean('Is Accepted', related='stage_id.is_accepted', readonly=True)
kanban_state = fields.Selection([
('normal', 'Grey'),
('done', 'Green'),
('blocked', 'Red')], string='Kanban State',
copy=False, default='normal', required=True, tracking=True,
help="A track's kanban state indicates special situations affecting it:\n"
" * Grey is the default situation\n"
" * Red indicates something is preventing the progress of this track\n"
" * Green indicates the track is ready to be pulled to the next stage")
# speaker
partner_id = fields.Many2one('res.partner', 'Speaker')
partner_name = fields.Char(
string='Name', compute='_compute_partner_info',
@@ -59,34 +62,79 @@ class Track(models.Model):
partner_phone = fields.Char(
string='Phone', compute='_compute_partner_info',
readonly=False, store=True, tracking=30)
partner_biography = fields.Html(string='Biography')
tag_ids = fields.Many2many('event.track.tag', string='Tags')
stage_id = fields.Many2one(
'event.track.stage', string='Stage', ondelete='restrict',
index=True, copy=False, default=_get_default_stage_id,
group_expand='_read_group_stage_ids',
required=True, tracking=True)
kanban_state = fields.Selection([
('normal', 'Grey'),
('done', 'Green'),
('blocked', 'Red')], string='Kanban State',
copy=False, default='normal', required=True, tracking=True,
help="A track's kanban state indicates special situations affecting it:\n"
" * Grey is the default situation\n"
" * Red indicates something is preventing the progress of this track\n"
" * Green indicates the track is ready to be pulled to the next stage")
description = fields.Html(translate=html_translate, sanitize_attributes=False, sanitize_form=False)
partner_biography = fields.Html(
string='Biography', compute='_compute_partner_biography',
readonly=False, store=True)
partner_function = fields.Char(
'Job Position', related='partner_id.function',
compute_sudo=True, readonly=True)
partner_company_name = fields.Char(
'Company Name', related='partner_id.parent_name',
compute_sudo=True, readonly=True)
image = fields.Image(
string="Speaker Photo", compute="_compute_speaker_image",
readonly=False, store=True,
max_width=256, max_height=256)
location_id = fields.Many2one('event.track.location', 'Location')
# time information
date = fields.Datetime('Track Date')
date_end = fields.Datetime('Track End Date', compute='_compute_end_date', store=True)
duration = fields.Float('Duration', default=1.5, help="Track duration in hours.")
location_id = fields.Many2one('event.track.location', 'Location')
event_id = fields.Many2one('event.event', 'Event', required=True)
color = fields.Integer('Color')
priority = fields.Selection([
('0', 'Low'), ('1', 'Medium'),
('2', 'High'), ('3', 'Highest')],
'Priority', required=True, default='1')
image = fields.Image("Image", max_width=128, max_height=128)
is_track_live = fields.Boolean(
'Is Track Live', compute='_compute_track_time_data',
help="Track has started and is ongoing")
is_track_soon = fields.Boolean(
'Is Track Soon', compute='_compute_track_time_data',
help="Track begins soon")
is_track_today = fields.Boolean(
'Is Track Today', compute='_compute_track_time_data',
help="Track begins today")
is_track_upcoming = fields.Boolean(
'Is Track Upcoming', compute='_compute_track_time_data',
help="Track is not yet started")
is_track_done = fields.Boolean(
'Is Track Done', compute='_compute_track_time_data',
help="Track is finished")
track_start_remaining = fields.Integer(
'Minutes before track starts', compute='_compute_track_time_data',
help="Remaining time before track starts (seconds)")
track_start_relative = fields.Integer(
'Minutes compare to track start', compute='_compute_track_time_data',
help="Relative time compared to track start (seconds)")
# frontend description
website_image = fields.Image(string="Website Image", max_width=1024, max_height=1024)
website_image_url = fields.Char(
string='Image URL', compute='_compute_website_image_url',
compute_sudo=True, store=False)
# wishlist / visitors management
event_track_visitor_ids = fields.One2many(
'event.track.visitor', 'track_id', string="Track Visitors",
groups="event.group_event_user")
is_reminder_on = fields.Boolean('Is Reminder On', compute='_compute_is_reminder_on')
wishlist_visitor_ids = fields.Many2many(
'website.visitor', string="Visitor Wishlist",
compute="_compute_wishlist_visitor_ids", compute_sudo=True,
search="_search_wishlist_visitor_ids",
groups="event.group_event_user")
wishlist_visitor_count = fields.Integer(
string="# Wishlisted",
compute="_compute_wishlist_visitor_ids", compute_sudo=True,
groups="event.group_event_user")
wishlisted_by_default = fields.Boolean(
string='Always Wishlisted',
help="""If set, the talk will be starred for each attendee registered to the event. The attendee won't be able to un-star this talk.""")
# Call to action
website_cta = fields.Boolean('Magic Button')
website_cta_title = fields.Char('Button Title')
website_cta_url = fields.Char('Button Target URL')
website_cta_delay = fields.Integer('Button appears')
# time information for CTA
is_website_cta_live = fields.Boolean(
'Is CTA Live', compute='_compute_cta_time_data',
help="CTA button is available")
website_cta_start_remaining = fields.Integer(
'Minutes before CTA starts', compute='_compute_cta_time_data',
help="Remaining time before CTA starts (seconds)")
@api.depends('name')
def _compute_website_url(self):
@@ -95,6 +143,8 @@ class Track(models.Model):
if track.id:
track.website_url = '/event/%s/track/%s' % (slug(track.event_id), slug(track))
# SPEAKER
@api.depends('partner_id')
def _compute_partner_info(self):
for track in self:
@@ -103,6 +153,23 @@ class Track(models.Model):
track.partner_email = track.partner_id.email
track.partner_phone = track.partner_id.phone
@api.depends('partner_id')
def _compute_partner_biography(self):
for track in self:
if not track.partner_biography:
track.partner_biography = track.partner_id.website_description
elif track.partner_id and is_html_empty(track.partner_biography) and \
not is_html_empty(track.partner_id.website_description):
track.partner_biography = track.partner_id.website_description
@api.depends('partner_id')
def _compute_speaker_image(self):
for track in self:
if not track.image:
track.image = track.partner_id.image_256
# TIME
@api.depends('date', 'duration')
def _compute_end_date(self):
for track in self:
@@ -112,8 +179,136 @@ class Track(models.Model):
else:
track.date_end = False
# FRONTEND DESCRIPTION
@api.depends('image', 'partner_id.image_256')
def _compute_website_image_url(self):
for track in self:
if track.website_image:
track.website_image_url = self.env['website'].image_url(track, 'website_image', size=1024)
else:
track.website_image_url = '/website_event_track/static/src/img/event_track_default_%d.jpeg' % (track.id % 2)
# WISHLIST / VISITOR MANAGEMENT
@api.depends('wishlisted_by_default', 'event_track_visitor_ids.visitor_id',
'event_track_visitor_ids.partner_id', 'event_track_visitor_ids.is_wishlisted',
'event_track_visitor_ids.is_blacklisted')
@api.depends_context('uid')
def _compute_is_reminder_on(self):
current_visitor = self.env['website.visitor']._get_visitor_from_request(force_create=False)
if self.env.user._is_public() and not current_visitor:
for track in self:
track.is_reminder_on = track.wishlisted_by_default
else:
if self.env.user._is_public():
domain = [('visitor_id', '=', current_visitor.id)]
elif current_visitor:
domain = [
'|',
('partner_id', '=', self.env.user.partner_id.id),
('visitor_id', '=', current_visitor.id)
]
else:
domain = [('partner_id', '=', self.env.user.partner_id.id)]
event_track_visitors = self.env['event.track.visitor'].sudo().search_read(
expression.AND([
domain,
[('track_id', 'in', self.ids)]
]), fields=['track_id', 'is_wishlisted', 'is_blacklisted']
)
wishlist_map = {
track_visitor['track_id'][0]: {
'is_wishlisted': track_visitor['is_wishlisted'],
'is_blacklisted': track_visitor['is_blacklisted']
} for track_visitor in event_track_visitors
}
for track in self:
if wishlist_map.get(track.id):
track.is_reminder_on = wishlist_map.get(track.id)['is_wishlisted'] or (track.wishlisted_by_default and not wishlist_map[track.id]['is_blacklisted'])
else:
track.is_reminder_on = track.wishlisted_by_default
@api.depends('event_track_visitor_ids.visitor_id', 'event_track_visitor_ids.is_wishlisted')
def _compute_wishlist_visitor_ids(self):
results = self.env['event.track.visitor'].read_group(
[('track_id', 'in', self.ids), ('is_wishlisted', '=', True)],
['track_id', 'visitor_id:array_agg'],
['track_id']
)
visitor_ids_map = {result['track_id'][0]: result['visitor_id'] for result in results}
for track in self:
track.wishlist_visitor_ids = visitor_ids_map.get(track.id, [])
track.wishlist_visitor_count = len(visitor_ids_map.get(track.id, []))
def _search_wishlist_visitor_ids(self, operator, operand):
if operator == "not in":
raise NotImplementedError("Unsupported 'Not In' operation on track wishlist visitors")
track_visitors = self.env['event.track.visitor'].sudo().search([
('visitor_id', operator, operand),
('is_wishlisted', '=', True)
])
return [('id', 'in', track_visitors.track_id.ids)]
# TIME
@api.depends('date', 'date_end')
def _compute_track_time_data(self):
""" Compute start and remaining time for track itself. Do everything in
UTC as we compute only time deltas here. """
now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0))
for track in self:
if not track.date:
track.is_track_live = track.is_track_soon = track.is_track_today = track.is_track_upcoming = track.is_track_done = False
track.track_start_relative = track.track_start_remaining = 0
continue
date_begin_utc = utc.localize(track.date, is_dst=False)
date_end_utc = utc.localize(track.date_end, is_dst=False)
track.is_track_live = date_begin_utc <= now_utc < date_end_utc
track.is_track_soon = (date_begin_utc - now_utc).total_seconds() < 30*60 if date_begin_utc > now_utc else False
track.is_track_today = date_begin_utc.date() == now_utc.date()
track.is_track_upcoming = date_begin_utc > now_utc
track.is_track_done = date_end_utc <= now_utc
if date_begin_utc >= now_utc:
track.track_start_relative = int((date_begin_utc - now_utc).total_seconds())
track.track_start_remaining = track.track_start_relative
else:
track.track_start_relative = int((now_utc - date_begin_utc).total_seconds())
track.track_start_remaining = 0
@api.depends('date', 'date_end', 'website_cta', 'website_cta_delay')
def _compute_cta_time_data(self):
""" Compute start and remaining time for track itself. Do everything in
UTC as we compute only time deltas here. """
now_utc = utc.localize(fields.Datetime.now().replace(microsecond=0))
for track in self:
if not track.website_cta:
track.is_website_cta_live = track.website_cta_start_remaining = False
continue
date_begin_utc = utc.localize(track.date, is_dst=False) + timedelta(minutes=track.website_cta_delay or 0)
date_end_utc = utc.localize(track.date_end, is_dst=False)
track.is_website_cta_live = date_begin_utc <= now_utc <= date_end_utc
if date_begin_utc >= now_utc:
td = date_begin_utc - now_utc
track.website_cta_start_remaining = int(td.total_seconds())
else:
track.website_cta_start_remaining = 0
# ------------------------------------------------------------
# CRUD
# ------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
for values in vals_list:
if values.get('website_cta_url'):
values['website_cta_url'] = self.env['res.partner']._clean_website(values['website_cta_url'])
tracks = super(Track, self).create(vals_list)
for track in tracks:
@@ -123,12 +318,18 @@ class Track(models.Model):
subject=track.name,
subtype_id=self.env.ref('website_event_track.mt_event_track').id,
)
track._synchronize_with_stage(track.stage_id)
return tracks
def write(self, vals):
if vals.get('website_cta_url'):
vals['website_cta_url'] = self.env['res.partner']._clean_website(vals['website_cta_url'])
if 'stage_id' in vals and 'kanban_state' not in vals:
vals['kanban_state'] = 'normal'
if vals.get('stage_id'):
stage = self.env['event.track.stage'].browse(vals['stage_id'])
self._synchronize_with_stage(stage)
res = super(Track, self).write(vals)
if vals.get('partner_id'):
self.message_subscribe([vals['partner_id']])
@@ -139,6 +340,14 @@ class Track(models.Model):
""" Always display all stages """
return stages.search([], order=order)
def _synchronize_with_stage(self, stage):
if stage.is_done:
self.is_published = True
# ------------------------------------------------------------
# MESSAGING
# ------------------------------------------------------------
def _track_template(self, changes):
res = super(Track, self)._track_template(changes)
track = self[0]
@@ -180,6 +389,10 @@ class Track(models.Model):
]).write({'partner_id': new_partner.id})
return super(Track, self)._message_post_after_hook(message, msg_vals)
# ------------------------------------------------------------
# ACTION
# ------------------------------------------------------------
def open_track_speakers_list(self):
return {
'name': _('Speakers'),
@@ -189,3 +402,91 @@ class Track(models.Model):
'view_id': False,
'type': 'ir.actions.act_window',
}
def get_backend_menu_id(self):
return self.env.ref('event.event_main_menu').id
# ------------------------------------------------------------
# TOOLS
# ------------------------------------------------------------
def _get_event_track_visitors(self, force_create=False):
self.ensure_one()
force_visitor_create = self.env.user._is_public()
visitor_sudo = self.env['website.visitor']._get_visitor_from_request(force_create=force_visitor_create)
if visitor_sudo:
visitor_sudo._update_visitor_last_visit()
if self.env.user._is_public():
domain = [('visitor_id', '=', visitor_sudo.id)]
elif visitor_sudo:
domain = [
'|',
('partner_id', '=', self.env.user.partner_id.id),
('visitor_id', '=', visitor_sudo.id)
]
else:
domain = [('partner_id', '=', self.env.user.partner_id.id)]
track_visitors = self.env['event.track.visitor'].sudo().search(
expression.AND([domain, [('track_id', 'in', self.ids)]])
)
missing = self - track_visitors.track_id
if missing and force_create:
track_visitors += self.env['event.track.visitor'].sudo().create([{
'visitor_id': visitor_sudo.id,
'partner_id': self.env.user.partner_id.id if not self.env.user._is_public() else False,
'track_id': track.id,
} for track in missing])
return track_visitors
def _get_track_suggestions(self, restrict_domain=None, limit=None):
""" Returns the next tracks suggested after going to the current one
given by self. Tracks always belong to the same event.
Heuristic is
* live first;
* then ordered by start date, finished being sent to the end;
* wishlisted (manually or by default);
* tag matching with current track;
* location matching with current track;
* finally a random to have an "equivalent wave" randomly given;
:param restrict_domain: an additional domain to restrict candidates;
:param limit: number of tracks to return;
"""
self.ensure_one()
base_domain = [
'&',
('event_id', '=', self.event_id.id),
('id', '!=', self.id),
]
if restrict_domain:
base_domain = expression.AND([
base_domain,
restrict_domain
])
track_candidates = self.search(base_domain, limit=None, order='date asc')
if not track_candidates:
return track_candidates
track_candidates = track_candidates.sorted(
lambda track:
(track.is_published,
track.is_track_live,
track.track_start_remaining > 0,
-1 * track.track_start_remaining,
track.is_reminder_on,
not track.wishlisted_by_default,
len(track.tag_ids & self.tag_ids),
track.location_id == self.location_id,
randint(0, 20),
), reverse=True
)
return track_candidates[:limit]
@@ -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 TrackLocation(models.Model):
_name = "event.track.location"
_description = 'Event Track Location'
name = fields.Char('Location', required=True)
@@ -18,6 +18,12 @@ class TrackStage(models.Model):
fold = fields.Boolean(
string='Folded in Kanban',
help='This stage is folded in the kanban view when there are no records in that stage to display.')
is_done = fields.Boolean(string='Done Stage')
is_accepted = fields.Boolean(
string='Accepted Stage',
help='Accepted tracks are displayed in agenda views but not accessible.')
is_done = fields.Boolean(
string='Done Stage',
help='Done tracks are automatically published so that they are available in frontend.')
is_cancel = fields.Boolean(string='Canceled Stage')
is_done = fields.Boolean()
color = fields.Integer(string='Color')
@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from random import randint
from odoo import fields, models
class TrackTag(models.Model):
_name = "event.track.tag"
_description = 'Event Track Tag'
_order = "category_id, sequence, name"
def _default_color(self):
return randint(1, 11)
name = fields.Char('Tag Name', required=True)
track_ids = fields.Many2many('event.track', string='Tracks')
color = fields.Integer(
string='Color Index', default=lambda self: self._default_color(),
help="Note that colorless tags won't be available on the website.")
sequence = fields.Integer('Sequence', default=10)
category_id = fields.Many2one('event.track.tag.category', string="Category", ondelete="set null")
_sql_constraints = [
('name_uniq', 'unique (name)', "Tag name already exists !"),
]
@@ -12,11 +12,3 @@ class TrackTagCategory(models.Model):
name = fields.Char("Name", required=True, translate=True)
sequence = fields.Integer('Sequence', default=10)
tag_ids = fields.One2many('event.track.tag', 'category_id', string="Tags")
class TrackTag(models.Model):
_inherit = "event.track.tag"
_order = "category_id, sequence, name"
sequence = fields.Integer('Sequence', default=10)
category_id = fields.Many2one('event.track.tag.category', string="Category", ondelete="set null")
@@ -0,0 +1,22 @@
# -*- 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'
website_track = fields.Boolean(
string='Tracks on Website', compute='_compute_website_menu_data',
readonly=False, store=True)
website_track_proposal = fields.Boolean(
string='Tracks Proposals on Website', compute='_compute_website_menu_data',
readonly=False, store=True)
@api.depends('website_menu')
def _compute_website_menu_data(self):
""" Simply activate or de-activate all menus at once. """
for event_type in self:
event_type.website_track = event_type.website_menu
event_type.website_track_proposal = event_type.website_menu
@@ -5,12 +5,8 @@ from odoo import fields, models
class EventMenu(models.Model):
_name = "website.event.menu"
_description = "Website Event Menu"
_inherit = "website.event.menu"
menu_id = fields.Many2one('website.menu', string='Menu', ondelete='cascade')
event_id = fields.Many2one('event.event', string='Event', ondelete='cascade')
menu_type = fields.Selection([
('track', 'Event Tracks Menus'),
('track_proposal', 'Event Proposals Menus'),
])
menu_type = fields.Selection(
selection_add=[('track', 'Event Tracks Menus'), ('track_proposal', 'Event Proposals Menus')],
ondelete={'track': 'cascade', 'track_proposal': 'cascade'})
@@ -18,9 +18,6 @@ class WebsiteMenu(models.Model):
# agenda page that has also menu_type='track'
if event_menu.menu_type == 'track' and '/track' in event_menu.menu_id.url:
to_update.append('website_track')
for menu_type, fname in event_menu.event_id._get_menu_type_field_matching().items():
if event_menu.menu_type == menu_type:
to_update.append(fname)
# call super that resumes the unlink of menus entries (including website event menus)
res = super(WebsiteMenu, self).unlink()
@@ -12,5 +12,7 @@ access_event_track_sponsor_manager,event.track.sponsor.type.public,model_event_s
access_event_track_sponsor_public,event.track.sponsor.public,model_event_sponsor,,1,0,0,0
access_event_track_stage_all,event.track.stage.all,model_event_track_stage,,1,0,0,0
access_event_track_stage_event_manager,event.track.stage.event.manager,model_event_track_stage,event.group_event_manager,1,1,1,1
access_website_event_menu_public,website.event.menu,model_website_event_menu,,1,0,0,0
access_website_event_menu_manager,website.event.menu,model_website_event_menu,event.group_event_manager,1,1,1,1
event_track_visitor_access_all,event.track.visitor.access.all,model_event_track_visitor,,0,0,0,0
event_track_visitor_access_manager,event.track.visitor.access.manager,model_event_track_visitor,event.group_event_manager,1,1,1,1
event_track_tag_category_access_public,event.track.tag.category.access,model_event_track_tag_category,,1,0,0,0
event_track_tag_category_access_manager,event.track.tag.category.access.manager,model_event_track_tag_category,event.group_event_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
12 access_event_track_sponsor_public event.track.sponsor.public model_event_sponsor 1 0 0 0
13 access_event_track_stage_all event.track.stage.all model_event_track_stage 1 0 0 0
14 access_event_track_stage_event_manager event.track.stage.event.manager model_event_track_stage event.group_event_manager 1 1 1 1
15 access_website_event_menu_public event_track_visitor_access_all website.event.menu event.track.visitor.access.all model_website_event_menu model_event_track_visitor 1 0 0 0 0
16 access_website_event_menu_manager event_track_visitor_access_manager website.event.menu event.track.visitor.access.manager model_website_event_menu model_event_track_visitor event.group_event_manager 1 1 1 1
17 event_track_tag_category_access_public event.track.tag.category.access model_event_track_tag_category 1 0 0 0
18 event_track_tag_category_access_manager event.track.tag.category.access.manager model_event_track_tag_category event.group_event_manager 1 1 1 1

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