413 lines
19 KiB
Python
413 lines
19 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import babel.dates
|
|
import re
|
|
import werkzeug
|
|
|
|
from ast import literal_eval
|
|
from werkzeug.datastructures import OrderedMultiDict
|
|
from werkzeug.exceptions import NotFound
|
|
|
|
from odoo import fields, http, _
|
|
from odoo.addons.website.controllers.main import QueryURL
|
|
from odoo.http import request
|
|
from odoo.osv import expression
|
|
from odoo.tools.misc import get_lang
|
|
from odoo.tools import lazy
|
|
from odoo.exceptions import UserError
|
|
|
|
class WebsiteEventController(http.Controller):
|
|
|
|
def sitemap_event(env, rule, qs):
|
|
if not qs or qs.lower() in '/events':
|
|
yield {'loc': '/events'}
|
|
|
|
# ------------------------------------------------------------
|
|
# EVENT LIST
|
|
# ------------------------------------------------------------
|
|
|
|
def _get_events_search_options(self, **post):
|
|
return {
|
|
'displayDescription': False,
|
|
'displayDetail': False,
|
|
'displayExtraDetail': False,
|
|
'displayExtraLink': False,
|
|
'displayImage': False,
|
|
'allowFuzzy': not post.get('noFuzzy'),
|
|
'date': post.get('date'),
|
|
'tags': post.get('tags'),
|
|
'type': post.get('type'),
|
|
'country': post.get('country'),
|
|
}
|
|
|
|
@http.route(['/event', '/event/page/<int:page>', '/events', '/events/page/<int:page>'], type='http', auth="public", website=True, sitemap=sitemap_event)
|
|
def events(self, page=1, **searches):
|
|
Event = request.env['event.event']
|
|
SudoEventType = request.env['event.type'].sudo()
|
|
|
|
searches.setdefault('search', '')
|
|
searches.setdefault('date', 'upcoming')
|
|
searches.setdefault('tags', '')
|
|
searches.setdefault('type', 'all')
|
|
searches.setdefault('country', 'all')
|
|
|
|
website = request.website
|
|
|
|
step = 12 # Number of events per page
|
|
|
|
options = self._get_events_search_options(**searches)
|
|
order = 'date_begin'
|
|
if searches.get('date', 'upcoming') == 'old':
|
|
order = 'date_begin desc'
|
|
order = 'is_published desc, ' + order
|
|
search = searches.get('search')
|
|
event_count, details, fuzzy_search_term = website._search_with_fuzzy("events", search,
|
|
limit=page * step, order=order, options=options)
|
|
event_details = details[0]
|
|
events = event_details.get('results', Event)
|
|
events = events[(page - 1) * step:page * step]
|
|
|
|
# count by domains without self search
|
|
domain_search = [('name', 'ilike', fuzzy_search_term or searches['search'])] if searches['search'] else []
|
|
|
|
no_date_domain = event_details['no_date_domain']
|
|
dates = event_details['dates']
|
|
for date in dates:
|
|
if date[0] not in ['all', 'old']:
|
|
date[3] = Event.search_count(expression.AND(no_date_domain) + domain_search + date[2])
|
|
|
|
no_country_domain = event_details['no_country_domain']
|
|
countries = Event.read_group(expression.AND(no_country_domain) + domain_search, ["id", "country_id"],
|
|
groupby="country_id", orderby="country_id")
|
|
countries.insert(0, {
|
|
'country_id_count': sum([int(country['country_id_count']) for country in countries]),
|
|
'country_id': ("all", _("All Countries"))
|
|
})
|
|
|
|
search_tags = event_details['search_tags']
|
|
current_date = event_details['current_date']
|
|
current_type = None
|
|
current_country = None
|
|
|
|
if searches["type"] != 'all':
|
|
current_type = SudoEventType.browse(int(searches['type']))
|
|
|
|
if searches["country"] != 'all' and searches["country"] != 'online':
|
|
current_country = request.env['res.country'].browse(int(searches['country']))
|
|
|
|
pager = website.pager(
|
|
url="/event",
|
|
url_args=searches,
|
|
total=event_count,
|
|
page=page,
|
|
step=step,
|
|
scope=5)
|
|
|
|
keep = QueryURL('/event', **{
|
|
key: value for key, value in searches.items() if (
|
|
key == 'search' or
|
|
(value != 'upcoming' if key == 'date' else value != 'all'))
|
|
})
|
|
|
|
searches['search'] = fuzzy_search_term or search
|
|
|
|
values = {
|
|
'current_date': current_date,
|
|
'current_country': current_country,
|
|
'current_type': current_type,
|
|
'event_ids': events, # event_ids used in website_event_track so we keep name as it is
|
|
'dates': dates,
|
|
'categories': request.env['event.tag.category'].search([
|
|
('is_published', '=', True), '|', ('website_id', '=', website.id), ('website_id', '=', False)
|
|
]),
|
|
'countries': countries,
|
|
'pager': pager,
|
|
'searches': searches,
|
|
'search_tags': search_tags,
|
|
'keep': keep,
|
|
'search_count': event_count,
|
|
'original_search': fuzzy_search_term and search,
|
|
'website': website
|
|
}
|
|
|
|
if searches['date'] == 'old':
|
|
# the only way to display this content is to set date=old so it must be canonical
|
|
values['canonical_params'] = OrderedMultiDict([('date', 'old')])
|
|
|
|
return request.render("website_event.index", values)
|
|
|
|
# ------------------------------------------------------------
|
|
# EVENT PAGE
|
|
# ------------------------------------------------------------
|
|
|
|
@http.route(['''/event/<model("event.event"):event>/page/<path:page>'''], type='http', auth="public", website=True, sitemap=False)
|
|
def event_page(self, event, page, **post):
|
|
values = {
|
|
'event': event,
|
|
}
|
|
|
|
if '.' not in page:
|
|
page = 'website_event.%s' % page
|
|
|
|
try:
|
|
# Every event page view should have its own SEO.
|
|
values['seo_object'] = request.website.get_template(page)
|
|
values['main_object'] = event
|
|
except ValueError:
|
|
# page not found
|
|
values['path'] = re.sub(r"^website_event\.", '', page)
|
|
values['from_template'] = 'website_event.default_page' # .strip('website_event.')
|
|
page = request.env.user.has_group('website.group_website_designer') and 'website.page_404' or 'http_routing.404'
|
|
|
|
return request.render(page, values)
|
|
|
|
@http.route(['''/event/<model("event.event"):event>'''], type='http', auth="public", website=True, sitemap=True)
|
|
def event(self, event, **post):
|
|
if event.menu_id and event.menu_id.child_id:
|
|
target_url = event.menu_id.child_id[0].url
|
|
else:
|
|
target_url = '/event/%s/register' % str(event.id)
|
|
if post.get('enable_editor') == '1':
|
|
target_url += '?enable_editor=1'
|
|
return request.redirect(target_url)
|
|
|
|
@http.route(['''/event/<model("event.event"):event>/register'''], type='http', auth="public", website=True, sitemap=False)
|
|
def event_register(self, event, **post):
|
|
values = self._prepare_event_register_values(event, **post)
|
|
return request.render("website_event.event_description_full", values)
|
|
|
|
def _prepare_event_register_values(self, event, **post):
|
|
"""Return the require values to render the template."""
|
|
urls = lazy(event._get_event_resource_urls)
|
|
return {
|
|
'event': event,
|
|
'main_object': event,
|
|
'range': range,
|
|
'google_url': lazy(lambda: urls.get('google_url')),
|
|
'iCal_url': lazy(lambda: urls.get('iCal_url')),
|
|
'registration_error_code': post.get('registration_error_code'),
|
|
}
|
|
|
|
def _process_tickets_form(self, event, form_details):
|
|
""" Process posted data about ticket order. Generic ticket are supported
|
|
for event without tickets (generic registration).
|
|
|
|
:return: list of order per ticket: [{
|
|
'id': if of ticket if any (0 if no ticket),
|
|
'ticket': browse record of ticket if any (None if no ticket),
|
|
'name': ticket name (or generic 'Registration' name if no ticket),
|
|
'quantity': number of registrations for that ticket,
|
|
}, {...}]
|
|
"""
|
|
ticket_order = {}
|
|
for key, value in form_details.items():
|
|
registration_items = key.split('nb_register-')
|
|
if len(registration_items) != 2:
|
|
continue
|
|
ticket_order[int(registration_items[1])] = int(value)
|
|
|
|
ticket_dict = dict((ticket.id, ticket) for ticket in request.env['event.event.ticket'].sudo().search([
|
|
('id', 'in', [tid for tid in ticket_order.keys() if tid]),
|
|
('event_id', '=', event.id)
|
|
]))
|
|
|
|
return [{
|
|
'id': tid if ticket_dict.get(tid) else 0,
|
|
'ticket': ticket_dict.get(tid),
|
|
'name': ticket_dict[tid]['name'] if ticket_dict.get(tid) else _('Registration'),
|
|
'quantity': count,
|
|
} for tid, count in ticket_order.items() if count]
|
|
|
|
@http.route(['/event/<model("event.event"):event>/registration/new'], type='json', auth="public", methods=['POST'], website=True)
|
|
def registration_new(self, event, **post):
|
|
tickets = self._process_tickets_form(event, post)
|
|
availability_check = True
|
|
if event.seats_limited:
|
|
ordered_seats = 0
|
|
for ticket in tickets:
|
|
ordered_seats += ticket['quantity']
|
|
if event.seats_available < ordered_seats:
|
|
availability_check = False
|
|
if not tickets:
|
|
return False
|
|
default_first_attendee = {}
|
|
if not request.env.user._is_public():
|
|
default_first_attendee = {
|
|
"name": request.env.user.name,
|
|
"email": request.env.user.email,
|
|
"phone": request.env.user.mobile or request.env.user.phone,
|
|
}
|
|
else:
|
|
visitor = request.env['website.visitor']._get_visitor_from_request()
|
|
if visitor.email:
|
|
default_first_attendee = {
|
|
"name": visitor.display_name,
|
|
"email": visitor.email,
|
|
"phone": visitor.mobile,
|
|
}
|
|
return request.env['ir.ui.view']._render_template("website_event.registration_attendee_details", {
|
|
'tickets': tickets,
|
|
'event': event,
|
|
'availability_check': availability_check,
|
|
'default_first_attendee': default_first_attendee,
|
|
})
|
|
|
|
def _process_attendees_form(self, event, form_details):
|
|
""" Process data posted from the attendee details form.
|
|
Extracts question answers:
|
|
- For both questions asked 'once_per_order' and questions asked to every attendee
|
|
- For questions of type 'simple_choice', extracting the suggested answer id
|
|
- For questions of type 'text_box', extracting the text answer of the attendee.
|
|
|
|
:param form_details: posted data from frontend registration form, like
|
|
{'1-name': 'r', '1-email': 'r@r.com', '1-phone': '', '1-event_ticket_id': '1'}
|
|
"""
|
|
allowed_fields = request.env['event.registration']._get_website_registration_allowed_fields()
|
|
registration_fields = {key: v for key, v in request.env['event.registration']._fields.items() if key in allowed_fields}
|
|
for ticket_id in list(filter(lambda x: x is not None, [form_details[field] if 'event_ticket_id' in field else None for field in form_details.keys()])):
|
|
if int(ticket_id) not in event.event_ticket_ids.ids and len(event.event_ticket_ids.ids) > 0:
|
|
raise UserError(_("This ticket is not available for sale for this event"))
|
|
registrations = {}
|
|
general_answer_ids = []
|
|
general_identification_answers = {}
|
|
# as we may have several questions populating the same field (e.g: the phone)
|
|
# we use this to hold the fields that have already been handled
|
|
# goal is to use the answer to the first question of every 'type' (aka name / phone / email / company name)
|
|
already_handled_fields_data = {}
|
|
for key, value in form_details.items():
|
|
if not value:
|
|
continue
|
|
|
|
key_values = key.split('-')
|
|
# Special case for handling event_ticket_id data that holds only 2 values
|
|
if len(key_values) == 2:
|
|
registration_index, field_name = key_values
|
|
if field_name not in registration_fields:
|
|
continue
|
|
registrations.setdefault(registration_index, dict())[field_name] = int(value) or False
|
|
continue
|
|
|
|
registration_index, question_type, question_id = key_values
|
|
answer_values = None
|
|
if question_type == 'simple_choice':
|
|
answer_values = {
|
|
'question_id': int(question_id),
|
|
'value_answer_id': int(value)
|
|
}
|
|
else:
|
|
answer_values = {
|
|
'question_id': int(question_id),
|
|
'value_text_box': value
|
|
}
|
|
|
|
if answer_values and not int(registration_index):
|
|
general_answer_ids.append((0, 0, answer_values))
|
|
elif answer_values:
|
|
registrations.setdefault(registration_index, dict())\
|
|
.setdefault('registration_answer_ids', list()).append((0, 0, answer_values))
|
|
|
|
if question_type in ('name', 'email', 'phone', 'company_name')\
|
|
and question_type not in already_handled_fields_data.get(registration_index, []):
|
|
if question_type not in registration_fields:
|
|
continue
|
|
|
|
field_name = question_type
|
|
already_handled_fields_data.setdefault(registration_index, list()).append(field_name)
|
|
|
|
if not int(registration_index):
|
|
general_identification_answers[field_name] = value
|
|
else:
|
|
registrations.setdefault(registration_index, dict())[field_name] = value
|
|
|
|
if general_answer_ids:
|
|
for registration in registrations.values():
|
|
registration.setdefault('registration_answer_ids', list()).extend(general_answer_ids)
|
|
|
|
if general_identification_answers:
|
|
for registration in registrations.values():
|
|
registration.update(general_identification_answers)
|
|
|
|
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)
|
|
|
|
registrations_to_create = []
|
|
for registration_values in registration_data:
|
|
registration_values['event_id'] = event.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'] = False if request.env.user._is_public() else request.env.user.partner_id.id
|
|
|
|
# update registration based on visitor
|
|
registration_values['visitor_id'] = visitor_sudo.id
|
|
|
|
registrations_to_create.append(registration_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)
|
|
def registration_confirm(self, event, **post):
|
|
""" Check before creating and finalize the creation of the registrations
|
|
that we have enough seats for all selected tickets.
|
|
If we don't, the user is instead redirected to page to register with a
|
|
formatted error message. """
|
|
registrations_data = self._process_attendees_form(event, post)
|
|
event_ticket_ids = {registration['event_ticket_id'] for registration in registrations_data}
|
|
event_tickets = request.env['event.event.ticket'].browse(event_ticket_ids)
|
|
if any(event_ticket.seats_limited and event_ticket.seats_available < len(registrations_data) for event_ticket in event_tickets):
|
|
return request.redirect('/event/%s/register?registration_error_code=insufficient_seats' % event.id)
|
|
attendees_sudo = self._create_attendees_from_registration_post(event, registrations_data)
|
|
|
|
return request.redirect(('/event/%s/registration/success?' % event.id) + werkzeug.urls.url_encode({'registration_ids': ",".join([str(id) for id in attendees_sudo.ids])}))
|
|
|
|
@http.route(['/event/<model("event.event"):event>/registration/success'], type='http', auth="public", methods=['GET'], website=True, sitemap=False)
|
|
def event_registration_success(self, event, registration_ids):
|
|
# fetch the related registrations, make sure they belong to the correct visitor / event pair
|
|
visitor = request.env['website.visitor']._get_visitor_from_request()
|
|
if not visitor:
|
|
raise NotFound()
|
|
attendees_sudo = request.env['event.registration'].sudo().search([
|
|
('id', 'in', [str(registration_id) for registration_id in registration_ids.split(',')]),
|
|
('event_id', '=', event.id),
|
|
('visitor_id', '=', visitor.id),
|
|
])
|
|
return request.render("website_event.registration_complete",
|
|
self._get_registration_confirm_values(event, attendees_sudo))
|
|
|
|
def _get_registration_confirm_values(self, event, attendees_sudo):
|
|
urls = event._get_event_resource_urls()
|
|
return {
|
|
'attendees': attendees_sudo,
|
|
'event': event,
|
|
'google_url': urls.get('google_url'),
|
|
'iCal_url': urls.get('iCal_url')
|
|
}
|
|
|
|
# ------------------------------------------------------------
|
|
# TOOLS (HELPERS)
|
|
# ------------------------------------------------------------
|
|
|
|
def get_formated_date(self, event):
|
|
start_date = fields.Datetime.from_string(event.date_begin).date()
|
|
end_date = fields.Datetime.from_string(event.date_end).date()
|
|
month = babel.dates.get_month_names('abbreviated', locale=get_lang(event.env).code)[start_date.month]
|
|
return ('%s %s%s') % (month, start_date.strftime("%e"), (end_date != start_date and ("-" + end_date.strftime("%e")) or ""))
|
|
|
|
def _extract_searched_event_tags(self, searches):
|
|
tags = request.env['event.tag']
|
|
if searches.get('tags'):
|
|
try:
|
|
tag_ids = literal_eval(searches['tags'])
|
|
except:
|
|
pass
|
|
else:
|
|
# perform a search to filter on existing / valid tags implicitely + apply rules on color
|
|
tags = request.env['event.tag'].search([('id', 'in', tag_ids)])
|
|
return tags
|