[MERGE] website_event(_sale/question): improve event usability and frontend display
Purpose =========== Batch of improvements based on user testing/on site implementation with the focus of improving the flexibility and usability of Event. This way the application should be easier to use for small SMEs that organize events such as local sport clubs, theater company, ... Specifications ================= Deactivate by default some filters in /event page top bar Invert the behavior of is_individual field and rename it accordingly Revamp the event list in front-end Filter by month instead of week on /event page Improve /event/<model: event.event>/register page design Add tracking on name, email, phone fields on event.track Links =========== TaskID 2162438 Community PR #44476 Upgrade PR odoo/upgrade#771 Related: odoo/upgrade#771 Signed-off-by: Thibault Delavallee (tde) <tde@openerp.com> Co-authored-by: David Beguin <dbe@odoo.com> Co-authored-by: Quentin Mourier <qmo@odoo.com>
This commit is contained in:
co-authored by
David Beguin
Quentin Mourier
commit
5676f8d411
@@ -12,7 +12,7 @@ from odoo import fields, http, _
|
||||
from odoo.addons.http_routing.models.ir_http import slug
|
||||
from odoo.addons.website.controllers.main import QueryURL
|
||||
from odoo.http import request
|
||||
from odoo.tools.misc import get_lang
|
||||
from odoo.tools.misc import get_lang, format_date
|
||||
|
||||
|
||||
class WebsiteEventController(http.Controller):
|
||||
@@ -32,37 +32,35 @@ class WebsiteEventController(http.Controller):
|
||||
searches.setdefault('country', 'all')
|
||||
|
||||
website = request.website
|
||||
today = datetime.today()
|
||||
|
||||
def sdn(date):
|
||||
return fields.Datetime.to_string(date.replace(hour=23, minute=59, second=59))
|
||||
|
||||
def sd(date):
|
||||
return fields.Datetime.to_string(date)
|
||||
today = datetime.today()
|
||||
|
||||
def get_month_filter_domain(filter_name, months_delta):
|
||||
first_day_of_the_month = today.replace(day=1)
|
||||
filter_string = _('This month') if months_delta == 0 \
|
||||
else format_date(request.env, value=today + relativedelta(months=months_delta),
|
||||
date_format='LLLL', lang_code=get_lang(request.env).code).capitalize()
|
||||
return [filter_name, filter_string, [
|
||||
("date_end", ">=", sd(first_day_of_the_month + relativedelta(months=months_delta))),
|
||||
("date_begin", "<", sd(first_day_of_the_month + relativedelta(months=months_delta+1)))],
|
||||
0]
|
||||
|
||||
dates = [
|
||||
['all', _('Next Events'), [("date_end", ">", sd(today))], 0],
|
||||
['all', _('Upcoming Events'), [("date_end", ">", sd(today))], 0],
|
||||
['today', _('Today'), [
|
||||
("date_end", ">", sd(today)),
|
||||
("date_begin", "<", sdn(today))],
|
||||
0],
|
||||
['week', _('This Week'), [
|
||||
("date_end", ">=", sd(today + relativedelta(days=-today.weekday()))),
|
||||
("date_begin", "<", sdn(today + relativedelta(days=6-today.weekday())))],
|
||||
0],
|
||||
['nextweek', _('Next Week'), [
|
||||
("date_end", ">=", sd(today + relativedelta(days=7-today.weekday()))),
|
||||
("date_begin", "<", sdn(today + relativedelta(days=13-today.weekday())))],
|
||||
0],
|
||||
['month', _('This month'), [
|
||||
("date_end", ">=", sd(today.replace(day=1))),
|
||||
("date_begin", "<", (today.replace(day=1) + relativedelta(months=1)).strftime('%Y-%m-%d 00:00:00'))],
|
||||
0],
|
||||
['nextmonth', _('Next month'), [
|
||||
("date_end", ">=", sd(today.replace(day=1) + relativedelta(months=1))),
|
||||
("date_begin", "<", (today.replace(day=1) + relativedelta(months=2)).strftime('%Y-%m-%d 00:00:00'))],
|
||||
0],
|
||||
get_month_filter_domain('month', 0),
|
||||
get_month_filter_domain('nextmonth1', 1),
|
||||
get_month_filter_domain('nextmonth2', 2),
|
||||
['old', _('Past Events'), [
|
||||
("date_end", "<", today.strftime('%Y-%m-%d 00:00:00'))],
|
||||
("date_end", "<", sd(today))],
|
||||
0],
|
||||
]
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<!-- Index - OPTION - Sidebar -->
|
||||
<template id="opt_index_sidebar" inherit_id="website_event.index" active="True" customize_show="True" name="Show Sidebar">
|
||||
<template id="opt_index_sidebar" inherit_id="website_event.index" active="False" customize_show="True" name="Show Sidebar">
|
||||
<xpath expr="//div[@id='o_wevent_index_main_col']" position="after">
|
||||
<t t-call="website_event.index_sidebar"/>
|
||||
</xpath>
|
||||
@@ -59,7 +59,7 @@
|
||||
<a href="#" role="button" class="btn dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-calendar"/>
|
||||
<t t-if="current_date" t-esc="current_date"/>
|
||||
<t t-else="">Next Events</t>
|
||||
<t t-else="">Upcoming Events</t>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<t t-foreach="dates" t-as="date">
|
||||
@@ -76,7 +76,7 @@
|
||||
</template>
|
||||
|
||||
<!-- Filter - Category -->
|
||||
<template id="event_category" inherit_id="website_event.index_topbar" active="True" customize_show="True" name="Filter by Category">
|
||||
<template id="event_category" inherit_id="website_event.index_topbar" active="False" customize_show="True" name="Filter by Category">
|
||||
<xpath expr="//ul[hasclass('o_wevent_index_topbar_filters')]" position="inside">
|
||||
<li class="nav-item dropdown mr-2 my-1 my-lg-0">
|
||||
<a href="#" role="button" class="btn dropdown-toggle" data-toggle="dropdown">
|
||||
@@ -99,7 +99,7 @@
|
||||
</template>
|
||||
|
||||
<!-- Filter - Location -->
|
||||
<template id="event_location" inherit_id="website_event.index_topbar" active="True" customize_show="True" name="Filter by Country">
|
||||
<template id="event_location" inherit_id="website_event.index_topbar" active="False" customize_show="True" name="Filter by Country">
|
||||
<xpath expr="//ul[hasclass('o_wevent_index_topbar_filters')]" position="inside">
|
||||
<li class="nav-item dropdown mr-2 my-1 my-lg-0">
|
||||
<a href="#" role="button" class="btn dropdown-toggle" data-toggle="dropdown">
|
||||
@@ -160,8 +160,8 @@
|
||||
</t>
|
||||
<!-- List -->
|
||||
<div t-foreach="event_ids" t-as="event" t-attf-class=" #{opt_event_size} mb-4">
|
||||
<article t-attf-class="#{opt_events_list_cards and 'card border-0 shadow-sm'}" itemscope="itemscope" itemtype="http://schema.org/Event">
|
||||
<div class="row no-gutters">
|
||||
<article t-attf-class="h-100 #{opt_events_list_cards and 'card border-0 shadow-sm'}" itemscope="itemscope" itemtype="http://schema.org/Event">
|
||||
<div class="h-100 row no-gutters">
|
||||
<!-- Header -->
|
||||
<header t-attf-class="overflow-hidden bg-secondary #{opt_events_list_columns and 'col-12 rounded-top' or 'col-sm-4 col-lg-3 rounded-left'} #{(not opt_events_list_cards) and 'rounded shadow'} #{(not opt_events_list_cards and not opt_events_list_columns) and 'rounded-top'}">
|
||||
<!-- Image + Link -->
|
||||
@@ -190,39 +190,34 @@
|
||||
<main t-attf-class="#{opt_events_list_cards and 'card-body' or (opt_events_list_columns and 'py-3' or 'px-4')}">
|
||||
<!-- Title -->
|
||||
<h5 t-attf-class="card-title mt-2 mb-0 text-truncate #{(not event.website_published) and 'text-danger'}">
|
||||
<a t-attf-href="/event/#{ slug(event) }/#{(not event.menu_id) and 'register'}" class="text-reset text-decoration-none" itemprop="url">
|
||||
<a t-attf-href="/event/#{ slug(event) }/#{(not event.menu_id) and 'register'}"
|
||||
class="text-reset text-decoration-none"
|
||||
itemprop="url">
|
||||
<span t-field="event.name" itemprop="name"/>
|
||||
</a>
|
||||
<div class="badge badge-pill badge-info ml-2 mt-2 pt4 pb4" t-if="event.is_online">Online</div>
|
||||
</h5>
|
||||
<!-- Organizer -->
|
||||
<div t-if="event.organizer_id" class="mb-3">
|
||||
<small class="text-muted text-truncate">Organizer: <span t-field="event.organizer_id" itemprop="organizer"/></small>
|
||||
</div>
|
||||
<!-- Location -->
|
||||
<div t-if="event.is_online">Online</div>
|
||||
<div t-else="" itemprop="location" t-field="event.address_id" t-options="{'widget': 'contact', 'fields': ['city'], 'no_marker': 'true'}"/>
|
||||
<!-- Start Date & Time -->
|
||||
<time itemprop="startDate" t-att-datetime="event.date_begin">
|
||||
<span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'date_only': 'true', 'format': 'long'}"/> -
|
||||
<span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'time_only': 'true', 'format': 'short'}"/>
|
||||
</time>
|
||||
<!-- Organizer -->
|
||||
<div t-if="event.organizer_id" class="mb-3">
|
||||
<small class="text-muted text-truncate">Organizer: <span t-field="event.organizer_id" itemprop="organizer"/></small>
|
||||
</div>
|
||||
<!-- Location -->
|
||||
<div t-if="not event.is_online" itemprop="location" t-field="event.address_id" t-options="{'widget': 'contact', 'fields': ['city'], 'no_marker': 'true'}"/>
|
||||
</main>
|
||||
<!-- Footer -->
|
||||
<footer t-attf-class="small #{opt_events_list_cards and 'card-footer' or ((not opt_events_list_columns) and 'border-top mx-4 mt-auto pt-2' or 'border-top py-2')}">
|
||||
<t t-if="event.event_registrations_open">
|
||||
<span class="text-danger"
|
||||
t-if="event.seats_availability == 'limited' and not event.seats_available">
|
||||
Sold Out
|
||||
</span>
|
||||
<span class="text-muted"
|
||||
t-if="event.seats_availability == 'limited' and
|
||||
event.seats_available and
|
||||
(event.seats_available <5 or event.seats_available <= ((event.seats_max or 0) / 4))">
|
||||
<em>Only <t t-esc="event.seats_available"/> Remaining</em>
|
||||
</span>
|
||||
</t>
|
||||
</footer>
|
||||
</div>
|
||||
<!-- Footer -->
|
||||
<footer t-if="event.event_registrations_open"
|
||||
t-attf-class="small align-self-end w-100 #{opt_events_list_cards and 'card-footer' or ((not opt_events_list_columns) and 'border-top mx-4 mt-auto pt-2' or 'border-top py-2')}">
|
||||
<span class="text-danger"
|
||||
t-if="event.seats_availability == 'limited' and not event.seats_available">
|
||||
Sold Out
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -403,7 +398,7 @@
|
||||
<h2 t-field="event.subtitle" class="o_wevent_event_subtitle" placeholder="Event Subtitle"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container mb-5">
|
||||
<div class="container">
|
||||
<t t-call="website_event.registration_template"/>
|
||||
</div>
|
||||
</t>
|
||||
@@ -415,19 +410,21 @@
|
||||
<!-- Event - Description -->
|
||||
<template id="event_description_full" name="Event Description" track="1">
|
||||
<t t-call="website_event.event_details">
|
||||
<section class="bg-200">
|
||||
<div class="container">
|
||||
<div class="row no-gutters mt-n5 pb-5">
|
||||
<section class="bg-200 mt-n5">
|
||||
<div class="container overflow-hidden">
|
||||
<div class="row no-gutters mt-n4">
|
||||
<!-- Description -->
|
||||
<div id="o_wevent_event_main_col" class="col-lg-8 bg-white rounded-left p-5 shadow-sm">
|
||||
<div id="o_wevent_event_main_col" class="col-lg-8 bg-white px-5 pt-4 pb-0 shadow-sm">
|
||||
<span t-field="event.description" itemprop="description"/>
|
||||
</div>
|
||||
<div class="col-lg-4 bg-light rounded-right shadow-sm d-print-none">
|
||||
<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 & Time</h6>
|
||||
<span t-field="event.with_context(tz=event.date_tz).date_begin" t-options="{'date_only': 'true', 'format': 'EEEE'}"/>
|
||||
<h4 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 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">
|
||||
@@ -436,8 +433,10 @@
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-long-arrow-down d-block text-muted mx-3 my-2" style="font-size: 1.5rem"/>
|
||||
<span t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'date_only': 'true', 'format': 'EEEE'}"/>
|
||||
<h4 class="my-1" t-field="event.with_context(tz=event.date_tz).date_end" t-options="{'date_only': 'true', 'format': 'long'}"/>
|
||||
<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>
|
||||
@@ -523,7 +522,7 @@
|
||||
t-attf-action="/event/#{slug(event)}/registration/new" method="post"
|
||||
class="mb-5" itemscope="itemscope" itemprop="offers" itemtype="http://schema.org/AggregateOffer">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div id="o_wevent_tickets" class="bg-white rounded shadow-sm">
|
||||
<div id="o_wevent_tickets" class="bg-white shadow-sm">
|
||||
|
||||
<t t-set="tickets" t-value="event.event_ticket_ids.filtered(lambda ticket: not ticket.is_expired)"/>
|
||||
<!-- If some tickets expired and there is only one type left, we keep the same layout -->
|
||||
@@ -619,7 +618,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div t-elif="request.env.user.has_group('event.group_event_manager')" class="alert alert-info mb-5 o_website_event_configuration" role="status">
|
||||
<div t-elif="request.env.user.has_group('event.group_event_manager')" class="alert alert-info rounded-0 mb-5 o_website_event_configuration o_not_editable" role="status">
|
||||
<a t-attf-href="/web#id=#{event.id}&view_type=form&model=event.event">
|
||||
<i class="fa fa-pencil mr-2" role="img" aria-label="Create" title="Create"/><em>Configure Event Registration</em>
|
||||
</a>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<odoo><data>
|
||||
<record id="event_0_question_0" model="event.question">
|
||||
<field name="title">Meal Type</field>
|
||||
<field name="is_individual" eval="False"/>
|
||||
<field name="once_per_order" eval="False"/>
|
||||
<field name="event_id" ref="event.event_0"/>
|
||||
</record>
|
||||
<record id="event_0_question_0_answer_0" model="event.answer">
|
||||
|
||||
@@ -19,9 +19,9 @@ class EventEvent(models.Model):
|
||||
|
||||
question_ids = fields.One2many('event.question', 'event_id', 'Questions', copy=True)
|
||||
general_question_ids = fields.One2many('event.question', 'event_id', 'General Questions',
|
||||
domain=[('is_individual', '=', False)])
|
||||
domain=[('once_per_order', '=', True)])
|
||||
specific_question_ids = fields.One2many('event.question', 'event_id', 'Specific Questions',
|
||||
domain=[('is_individual', '=', True)])
|
||||
domain=[('once_per_order', '=', False)])
|
||||
|
||||
@api.onchange('event_type_id')
|
||||
def _onchange_type(self):
|
||||
@@ -31,7 +31,7 @@ class EventEvent(models.Model):
|
||||
(0, 0, {
|
||||
'title': question.title,
|
||||
'sequence': question.sequence,
|
||||
'is_individual': question.is_individual,
|
||||
'once_per_order': question.once_per_order,
|
||||
})
|
||||
for question in self.event_type_id.question_ids
|
||||
]
|
||||
|
||||
@@ -16,9 +16,9 @@ class EventQuestion(models.Model):
|
||||
event_id = fields.Many2one('event.event', 'Event', ondelete='cascade')
|
||||
answer_ids = fields.One2many('event.answer', 'question_id', "Answers", required=True, copy=True)
|
||||
sequence = fields.Integer(default=10)
|
||||
is_individual = fields.Boolean('Ask each attendee',
|
||||
help="If True, this question will be asked for every attendee of a reservation. If "
|
||||
"not it will be asked only once and its value propagated to every attendees.")
|
||||
once_per_order = fields.Boolean('Ask only once per order',
|
||||
help="If True, this question will be asked only once and its value will be propagated to every attendees."
|
||||
"If not it will be asked for every attendee of a reservation.")
|
||||
|
||||
@api.constrains('event_type_id', 'event_id')
|
||||
def _constrains_event(self):
|
||||
|
||||
@@ -13,7 +13,7 @@ class TestEventQuestionCommon(TestEventCommon):
|
||||
cls.event_question_1 = cls.env['event.question'].create({
|
||||
'title': 'Question1',
|
||||
'event_type_id': cls.event_type_complex.id,
|
||||
'is_individual': True,
|
||||
'once_per_order': False,
|
||||
'answer_ids': [
|
||||
(0, 0, {'name': 'Q1-Answer1'}),
|
||||
(0, 0, {'name': 'Q1-Answer2'})
|
||||
@@ -22,7 +22,7 @@ class TestEventQuestionCommon(TestEventCommon):
|
||||
cls.event_question_2 = cls.env['event.question'].create({
|
||||
'title': 'Question2',
|
||||
'event_type_id': cls.event_type_complex.id,
|
||||
'is_individual': False,
|
||||
'once_per_order': True,
|
||||
'answer_ids': [
|
||||
(0, 0, {'name': 'Q2-Answer1'}),
|
||||
(0, 0, {'name': 'Q2-Answer2'})
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Question">
|
||||
<h1><field name="title" /></h1>
|
||||
<field name="is_individual"/>
|
||||
<label for="is_individual"/>
|
||||
<field name="once_per_order"/>
|
||||
<label for="once_per_order"/>
|
||||
<field name="answer_ids">
|
||||
<tree editable="bottom">
|
||||
<field name="sequence" widget="handle" />
|
||||
@@ -66,7 +66,7 @@
|
||||
<tree>
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="title"/>
|
||||
<field name="is_individual"/>
|
||||
<field name="once_per_order"/>
|
||||
<field name="answer_ids"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
@@ -43,9 +43,9 @@ class Track(models.Model):
|
||||
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')
|
||||
partner_id = fields.Many2one('res.partner', 'Speaker')
|
||||
partner_name = fields.Char('Name')
|
||||
partner_email = fields.Char('Email')
|
||||
partner_phone = fields.Char('Phone')
|
||||
partner_name = fields.Char('Name', tracking=10)
|
||||
partner_email = fields.Char('Email', tracking=20)
|
||||
partner_phone = fields.Char('Phone', tracking=30)
|
||||
partner_biography = fields.Html('Biography')
|
||||
tag_ids = fields.Many2many('event.track.tag', string='Tags')
|
||||
stage_id = fields.Many2one(
|
||||
|
||||
Reference in New Issue
Block a user