Purpose of this commit is to remove some complex check embedded in templates and replace them by a computed unstored field. It eases definition and understanding. Its computation has been cleaned, so that sold out appears only when tickets are really sold out, not if their end sales date is reached. Sold out label is displayed in both event list and event specific page views in frontend. Small spacing issues in frontend registration form are also fixed. Task ID 2228189 Community PR odoo/odoo#48652 X-original-commit: c36cf90e7b83d2424b2f52ad8a2fec0bf367e8d8
372 lines
16 KiB
Python
372 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
import babel.dates
|
|
import re
|
|
import werkzeug
|
|
from werkzeug.datastructures import OrderedMultiDict
|
|
|
|
from ast import literal_eval
|
|
from collections import defaultdict
|
|
from datetime import datetime, timedelta
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
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.osv import expression
|
|
from odoo.tools.misc import get_lang, format_date
|
|
|
|
|
|
class WebsiteEventController(http.Controller):
|
|
|
|
def sitemap_event(env, rule, qs):
|
|
if not qs or qs.lower() in '/events':
|
|
yield {'loc': '/events'}
|
|
|
|
@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']
|
|
EventType = request.env['event.type']
|
|
|
|
searches.setdefault('search', '')
|
|
searches.setdefault('date', 'all')
|
|
searches.setdefault('tags', '')
|
|
searches.setdefault('type', 'all')
|
|
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)
|
|
|
|
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', _('Upcoming Events'), [("date_end", ">", sd(today))], 0],
|
|
['today', _('Today'), [
|
|
("date_end", ">", sd(today)),
|
|
("date_begin", "<", sdn(today))],
|
|
0],
|
|
get_month_filter_domain('month', 0),
|
|
get_month_filter_domain('nextmonth1', 1),
|
|
get_month_filter_domain('nextmonth2', 2),
|
|
['old', _('Past Events'), [
|
|
("date_end", "<", sd(today))],
|
|
0],
|
|
]
|
|
|
|
# search domains
|
|
domain_search = {'website_specific': website.website_domain()}
|
|
|
|
if searches['search']:
|
|
domain_search['search'] = [('name', 'ilike', searches['search'])]
|
|
|
|
search_tags = self._extract_searched_event_tags(searches)
|
|
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 = defaultdict(list)
|
|
for tag in search_tags:
|
|
grouped_tags[tag.category_id].append(tag)
|
|
domain_search['tags'] = []
|
|
for group in grouped_tags:
|
|
domain_search['tags'] = expression.AND([domain_search['tags'], [('tag_ids', 'in', [tag.id for tag in grouped_tags[group]])]])
|
|
|
|
current_date = None
|
|
current_type = None
|
|
current_country = None
|
|
for date in dates:
|
|
if searches["date"] == date[0]:
|
|
domain_search["date"] = date[2]
|
|
if date[0] != 'all':
|
|
current_date = date[1]
|
|
|
|
if searches["type"] != 'all':
|
|
current_type = EventType.browse(int(searches['type']))
|
|
domain_search["type"] = [("event_type_id", "=", int(searches["type"]))]
|
|
|
|
if searches["country"] != 'all':
|
|
current_country = request.env['res.country'].browse(int(searches['country']))
|
|
domain_search["country"] = ['|', ("country_id", "=", int(searches["country"])), ("country_id", "=", False)]
|
|
|
|
def dom_without(without):
|
|
domain = []
|
|
for key, search in domain_search.items():
|
|
if key != without:
|
|
domain += search
|
|
return domain
|
|
|
|
# count by domains without self search
|
|
for date in dates:
|
|
if date[0] != 'old':
|
|
date[3] = Event.search_count(dom_without('date') + date[2])
|
|
|
|
domain = dom_without('type')
|
|
|
|
domain = dom_without('country')
|
|
countries = Event.read_group(domain, ["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"))
|
|
})
|
|
|
|
step = 12 # Number of events per page
|
|
event_count = Event.search_count(dom_without("none"))
|
|
pager = website.pager(
|
|
url="/event",
|
|
url_args=searches,
|
|
total=event_count,
|
|
page=page,
|
|
step=step,
|
|
scope=5)
|
|
|
|
order = 'date_begin'
|
|
if searches.get('date', 'all') == 'old':
|
|
order = 'date_begin desc'
|
|
order = 'is_published desc, ' + order
|
|
events = Event.search(dom_without("none"), limit=step, offset=pager['offset'], order=order)
|
|
|
|
keep = QueryURL('/event', **{key: value for key, value in searches.items() if (key == 'search' or value != 'all')})
|
|
|
|
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([]),
|
|
'countries': countries,
|
|
'pager': pager,
|
|
'searches': searches,
|
|
'search_tags': search_tags,
|
|
'keep': keep,
|
|
}
|
|
|
|
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)
|
|
|
|
@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):
|
|
if not event.can_access_from_current_website():
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
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 = 'website.%s' % (request.website.is_publisher() and 'page_404' or '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 not event.can_access_from_current_website():
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
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):
|
|
if not event.can_access_from_current_website():
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
urls = event._get_event_resource_urls()
|
|
values = {
|
|
'event': event,
|
|
'main_object': event,
|
|
'range': range,
|
|
'google_url': urls.get('google_url'),
|
|
'iCal_url': urls.get('iCal_url'),
|
|
}
|
|
return request.render("website_event.event_description_full", values)
|
|
|
|
@http.route('/event/add_event', type='json', auth="user", methods=['POST'], website=True)
|
|
def add_event(self, event_name="New Event", **kwargs):
|
|
event = self._add_event(event_name, request.context)
|
|
return "/event/%s/register?enable_editor=1" % slug(event)
|
|
|
|
def _add_event(self, event_name=None, context=None, **kwargs):
|
|
if not event_name:
|
|
event_name = _("New Event")
|
|
date_begin = datetime.today() + timedelta(days=(14))
|
|
vals = {
|
|
'name': event_name,
|
|
'date_begin': fields.Date.to_string(date_begin),
|
|
'date_end': fields.Date.to_string((date_begin + timedelta(days=(1)))),
|
|
'seats_available': 1000,
|
|
'website_id': request.website.id,
|
|
}
|
|
return request.env['event.event'].with_context(context or {}).create(vals)
|
|
|
|
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 ""))
|
|
|
|
@http.route('/event/get_country_event_list', type='json', auth='public', website=True)
|
|
def get_country_events(self, **post):
|
|
Event = request.env['event.event']
|
|
country_code = request.session['geoip'].get('country_code')
|
|
result = {'events': [], 'country': False}
|
|
events = None
|
|
domain = request.website.website_domain()
|
|
if country_code:
|
|
country = request.env['res.country'].search([('code', '=', country_code)], limit=1)
|
|
events = Event.search(domain + ['|', ('address_id', '=', None), ('country_id.code', '=', country_code), ('date_begin', '>=', '%s 00:00:00' % fields.Date.today())], order="date_begin")
|
|
if not events:
|
|
events = Event.search(domain + [('date_begin', '>=', '%s 00:00:00' % fields.Date.today())], order="date_begin")
|
|
for event in events:
|
|
if country_code and event.country_id.code == country_code:
|
|
result['country'] = country
|
|
result['events'].append({
|
|
"date": self.get_formated_date(event),
|
|
"event": event,
|
|
"url": event.website_url})
|
|
return request.env['ir.ui.view'].render_template("website_event.country_events_list", result)
|
|
|
|
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'].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
|
|
return request.env['ir.ui.view'].render_template("website_event.registration_attendee_details", {'tickets': tickets, 'event': event, 'availability_check': availability_check})
|
|
|
|
def _process_attendees_form(self, event, form_details):
|
|
""" Process data posted from the attendee details form.
|
|
|
|
:param details: posted data from frontend registration form, like
|
|
{'1-name': 'r', '1-email': 'r@r.com', '1-phone': '', '1-event_ticket_id': '1'}
|
|
"""
|
|
registration_fields = request.env['event.registration']._fields
|
|
registrations = {}
|
|
global_values = {}
|
|
for key, value in form_details.items():
|
|
counter, field_name = key.split('-', 1)
|
|
if field_name not in registration_fields:
|
|
continue
|
|
elif isinstance(registration_fields[field_name], (fields.Many2one, fields.Integer)):
|
|
value = int(value) or False # 0 is considered as a void many2one aka False
|
|
else:
|
|
value = value
|
|
|
|
if counter == '0':
|
|
global_values[field_name] = value
|
|
else:
|
|
registrations.setdefault(counter, dict())[field_name] = value
|
|
for key, value in global_values.items():
|
|
for registration in registrations.values():
|
|
registration[key] = value
|
|
return list(registrations.values())
|
|
|
|
def _create_attendees_from_registration_post(self, event, registration_data):
|
|
attendees_sudo = request.env['event.registration'].sudo()
|
|
|
|
for registration_values in registration_data:
|
|
registration_values['event_id'] = event.id
|
|
if not registration_values.get('partner_id'):
|
|
registration_values['partner_id'] = request.env.user.partner_id.id
|
|
attendees_sudo += request.env['event.registration'].sudo().create(registration_values)
|
|
|
|
return attendees_sudo
|
|
|
|
@http.route(['''/event/<model("event.event"):event>/registration/confirm'''], type='http', auth="public", methods=['POST'], website=True)
|
|
def registration_confirm(self, event, **post):
|
|
if not event.can_access_from_current_website():
|
|
raise werkzeug.exceptions.NotFound()
|
|
|
|
registrations = self._process_attendees_form(event, post)
|
|
attendees = self._create_attendees_from_registration_post(event, registrations)
|
|
|
|
urls = event._get_event_resource_urls()
|
|
return request.render("website_event.registration_complete", {
|
|
'attendees': attendees,
|
|
'event': event,
|
|
'google_url': urls.get('google_url'),
|
|
'iCal_url': urls.get('iCal_url')
|
|
})
|
|
|
|
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
|
|
tags = request.env['event.tag'].search([('id', 'in', tag_ids)])
|
|
return tags
|