# -*- 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)]), 'countries': countries, 'pager': pager, 'searches': searches, 'search_tags': search_tags, 'keep': keep, 'search_count': event_count, 'original_search': fuzzy_search_term and search, } 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')), } 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.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) 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): registrations = self._process_attendees_form(event, post) attendees_sudo = self._create_attendees_from_registration_post(event, registrations) 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