# -*- 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/', '/events', '/events/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//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/'''], 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//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//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//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//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