1# -*- coding: utf-8 -*- 2 3import collections 4import babel.dates 5import re 6import werkzeug 7from werkzeug.datastructures import OrderedMultiDict 8from werkzeug.exceptions import NotFound 9 10from ast import literal_eval 11from collections import defaultdict 12from datetime import datetime, timedelta 13from dateutil.relativedelta import relativedelta 14 15from odoo import fields, http, _ 16from odoo.addons.http_routing.models.ir_http import slug 17from odoo.addons.website.controllers.main import QueryURL 18from odoo.addons.event.controllers.main import EventController 19from odoo.http import request 20from odoo.osv import expression 21from odoo.tools.misc import get_lang, format_date 22 23 24class WebsiteEventController(http.Controller): 25 26 def sitemap_event(env, rule, qs): 27 if not qs or qs.lower() in '/events': 28 yield {'loc': '/events'} 29 30 @http.route(['/event', '/event/page/<int:page>', '/events', '/events/page/<int:page>'], type='http', auth="public", website=True, sitemap=sitemap_event) 31 def events(self, page=1, **searches): 32 Event = request.env['event.event'] 33 SudoEventType = request.env['event.type'].sudo() 34 35 searches.setdefault('search', '') 36 searches.setdefault('date', 'all') 37 searches.setdefault('tags', '') 38 searches.setdefault('type', 'all') 39 searches.setdefault('country', 'all') 40 41 website = request.website 42 today = fields.Datetime.today() 43 44 def sdn(date): 45 return fields.Datetime.to_string(date.replace(hour=23, minute=59, second=59)) 46 47 def sd(date): 48 return fields.Datetime.to_string(date) 49 50 def get_month_filter_domain(filter_name, months_delta): 51 first_day_of_the_month = today.replace(day=1) 52 filter_string = _('This month') if months_delta == 0 \ 53 else format_date(request.env, value=today + relativedelta(months=months_delta), 54 date_format='LLLL', lang_code=get_lang(request.env).code).capitalize() 55 return [filter_name, filter_string, [ 56 ("date_end", ">=", sd(first_day_of_the_month + relativedelta(months=months_delta))), 57 ("date_begin", "<", sd(first_day_of_the_month + relativedelta(months=months_delta+1)))], 58 0] 59 60 dates = [ 61 ['all', _('Upcoming Events'), [("date_end", ">", sd(today))], 0], 62 ['today', _('Today'), [ 63 ("date_end", ">", sd(today)), 64 ("date_begin", "<", sdn(today))], 65 0], 66 get_month_filter_domain('month', 0), 67 ['old', _('Past Events'), [ 68 ("date_end", "<", sd(today))], 69 0], 70 ] 71 72 # search domains 73 domain_search = {'website_specific': website.website_domain()} 74 75 if searches['search']: 76 domain_search['search'] = [('name', 'ilike', searches['search'])] 77 78 search_tags = self._extract_searched_event_tags(searches) 79 if search_tags: 80 # Example: You filter on age: 10-12 and activity: football. 81 # Doing it this way allows to only get events who are tagged "age: 10-12" AND "activity: football". 82 # Add another tag "age: 12-15" to the search and it would fetch the ones who are tagged: 83 # ("age: 10-12" OR "age: 12-15") AND "activity: football 84 grouped_tags = defaultdict(list) 85 for tag in search_tags: 86 grouped_tags[tag.category_id].append(tag) 87 domain_search['tags'] = [] 88 for group in grouped_tags: 89 domain_search['tags'] = expression.AND([domain_search['tags'], [('tag_ids', 'in', [tag.id for tag in grouped_tags[group]])]]) 90 91 current_date = None 92 current_type = None 93 current_country = None 94 for date in dates: 95 if searches["date"] == date[0]: 96 domain_search["date"] = date[2] 97 if date[0] != 'all': 98 current_date = date[1] 99 100 if searches["type"] != 'all': 101 current_type = SudoEventType.browse(int(searches['type'])) 102 domain_search["type"] = [("event_type_id", "=", int(searches["type"]))] 103 104 if searches["country"] != 'all' and searches["country"] != 'online': 105 current_country = request.env['res.country'].browse(int(searches['country'])) 106 domain_search["country"] = ['|', ("country_id", "=", int(searches["country"])), ("country_id", "=", False)] 107 elif searches["country"] == 'online': 108 domain_search["country"] = [("country_id", "=", False)] 109 110 def dom_without(without): 111 domain = [] 112 for key, search in domain_search.items(): 113 if key != without: 114 domain += search 115 return domain 116 117 # count by domains without self search 118 for date in dates: 119 if date[0] != 'old': 120 date[3] = Event.search_count(dom_without('date') + date[2]) 121 122 domain = dom_without('type') 123 124 domain = dom_without('country') 125 countries = Event.read_group(domain, ["id", "country_id"], groupby="country_id", orderby="country_id") 126 countries.insert(0, { 127 'country_id_count': sum([int(country['country_id_count']) for country in countries]), 128 'country_id': ("all", _("All Countries")) 129 }) 130 131 step = 12 # Number of events per page 132 event_count = Event.search_count(dom_without("none")) 133 pager = website.pager( 134 url="/event", 135 url_args=searches, 136 total=event_count, 137 page=page, 138 step=step, 139 scope=5) 140 141 order = 'date_begin' 142 if searches.get('date', 'all') == 'old': 143 order = 'date_begin desc' 144 order = 'is_published desc, ' + order 145 events = Event.search(dom_without("none"), limit=step, offset=pager['offset'], order=order) 146 147 keep = QueryURL('/event', **{key: value for key, value in searches.items() if (key == 'search' or value != 'all')}) 148 149 values = { 150 'current_date': current_date, 151 'current_country': current_country, 152 'current_type': current_type, 153 'event_ids': events, # event_ids used in website_event_track so we keep name as it is 154 'dates': dates, 155 'categories': request.env['event.tag.category'].search([]), 156 'countries': countries, 157 'pager': pager, 158 'searches': searches, 159 'search_tags': search_tags, 160 'keep': keep, 161 } 162 163 if searches['date'] == 'old': 164 # the only way to display this content is to set date=old so it must be canonical 165 values['canonical_params'] = OrderedMultiDict([('date', 'old')]) 166 167 return request.render("website_event.index", values) 168 169 @http.route(['''/event/<model("event.event"):event>/page/<path:page>'''], type='http', auth="public", website=True, sitemap=False) 170 def event_page(self, event, page, **post): 171 if not event.can_access_from_current_website(): 172 raise werkzeug.exceptions.NotFound() 173 174 values = { 175 'event': event, 176 } 177 178 if '.' not in page: 179 page = 'website_event.%s' % page 180 181 try: 182 # Every event page view should have its own SEO. 183 values['seo_object'] = request.website.get_template(page) 184 values['main_object'] = event 185 except ValueError: 186 # page not found 187 values['path'] = re.sub(r"^website_event\.", '', page) 188 values['from_template'] = 'website_event.default_page' # .strip('website_event.') 189 page = request.website.is_publisher() and 'website.page_404' or 'http_routing.404' 190 191 return request.render(page, values) 192 193 @http.route(['''/event/<model("event.event"):event>'''], type='http', auth="public", website=True, sitemap=True) 194 def event(self, event, **post): 195 if not event.can_access_from_current_website(): 196 raise werkzeug.exceptions.NotFound() 197 198 if event.menu_id and event.menu_id.child_id: 199 target_url = event.menu_id.child_id[0].url 200 else: 201 target_url = '/event/%s/register' % str(event.id) 202 if post.get('enable_editor') == '1': 203 target_url += '?enable_editor=1' 204 return request.redirect(target_url) 205 206 @http.route(['''/event/<model("event.event"):event>/register'''], type='http', auth="public", website=True, sitemap=False) 207 def event_register(self, event, **post): 208 if not event.can_access_from_current_website(): 209 raise werkzeug.exceptions.NotFound() 210 211 values = self._prepare_event_register_values(event, **post) 212 return request.render("website_event.event_description_full", values) 213 214 def _prepare_event_register_values(self, event, **post): 215 """Return the require values to render the template.""" 216 urls = event._get_event_resource_urls() 217 return { 218 'event': event, 219 'main_object': event, 220 'range': range, 221 'google_url': urls.get('google_url'), 222 'iCal_url': urls.get('iCal_url'), 223 } 224 225 @http.route('/event/add_event', type='json', auth="user", methods=['POST'], website=True) 226 def add_event(self, event_name="New Event", **kwargs): 227 event = self._add_event(event_name, request.context) 228 return "/event/%s/register?enable_editor=1" % slug(event) 229 230 def _add_event(self, event_name=None, context=None, **kwargs): 231 if not event_name: 232 event_name = _("New Event") 233 date_begin = datetime.today() + timedelta(days=(14)) 234 vals = { 235 'name': event_name, 236 'date_begin': fields.Date.to_string(date_begin), 237 'date_end': fields.Date.to_string((date_begin + timedelta(days=(1)))), 238 'seats_available': 1000, 239 'website_id': request.website.id, 240 } 241 return request.env['event.event'].with_context(context or {}).create(vals) 242 243 def get_formated_date(self, event): 244 start_date = fields.Datetime.from_string(event.date_begin).date() 245 end_date = fields.Datetime.from_string(event.date_end).date() 246 month = babel.dates.get_month_names('abbreviated', locale=get_lang(event.env).code)[start_date.month] 247 return ('%s %s%s') % (month, start_date.strftime("%e"), (end_date != start_date and ("-" + end_date.strftime("%e")) or "")) 248 249 @http.route('/event/get_country_event_list', type='json', auth='public', website=True) 250 def get_country_events(self, **post): 251 Event = request.env['event.event'] 252 country_code = request.session['geoip'].get('country_code') 253 result = {'events': [], 'country': False} 254 events = None 255 domain = request.website.website_domain() 256 if country_code: 257 country = request.env['res.country'].search([('code', '=', country_code)], limit=1) 258 events = Event.search(domain + ['|', ('address_id', '=', None), ('country_id.code', '=', country_code), ('date_begin', '>=', '%s 00:00:00' % fields.Date.today())], order="date_begin") 259 if not events: 260 events = Event.search(domain + [('date_begin', '>=', '%s 00:00:00' % fields.Date.today())], order="date_begin") 261 for event in events: 262 if country_code and event.country_id.code == country_code: 263 result['country'] = country 264 result['events'].append({ 265 "date": self.get_formated_date(event), 266 "event": event, 267 "url": event.website_url}) 268 return request.env['ir.ui.view']._render_template("website_event.country_events_list", result) 269 270 def _process_tickets_form(self, event, form_details): 271 """ Process posted data about ticket order. Generic ticket are supported 272 for event without tickets (generic registration). 273 274 :return: list of order per ticket: [{ 275 'id': if of ticket if any (0 if no ticket), 276 'ticket': browse record of ticket if any (None if no ticket), 277 'name': ticket name (or generic 'Registration' name if no ticket), 278 'quantity': number of registrations for that ticket, 279 }, {...}] 280 """ 281 ticket_order = {} 282 for key, value in form_details.items(): 283 registration_items = key.split('nb_register-') 284 if len(registration_items) != 2: 285 continue 286 ticket_order[int(registration_items[1])] = int(value) 287 288 ticket_dict = dict((ticket.id, ticket) for ticket in request.env['event.event.ticket'].search([ 289 ('id', 'in', [tid for tid in ticket_order.keys() if tid]), 290 ('event_id', '=', event.id) 291 ])) 292 293 return [{ 294 'id': tid if ticket_dict.get(tid) else 0, 295 'ticket': ticket_dict.get(tid), 296 'name': ticket_dict[tid]['name'] if ticket_dict.get(tid) else _('Registration'), 297 'quantity': count, 298 } for tid, count in ticket_order.items() if count] 299 300 @http.route(['/event/<model("event.event"):event>/registration/new'], type='json', auth="public", methods=['POST'], website=True) 301 def registration_new(self, event, **post): 302 if not event.can_access_from_current_website(): 303 raise werkzeug.exceptions.NotFound() 304 305 tickets = self._process_tickets_form(event, post) 306 availability_check = True 307 if event.seats_limited: 308 ordered_seats = 0 309 for ticket in tickets: 310 ordered_seats += ticket['quantity'] 311 if event.seats_available < ordered_seats: 312 availability_check = False 313 if not tickets: 314 return False 315 return request.env['ir.ui.view']._render_template("website_event.registration_attendee_details", {'tickets': tickets, 'event': event, 'availability_check': availability_check}) 316 317 def _process_attendees_form(self, event, form_details): 318 """ Process data posted from the attendee details form. 319 320 :param form_details: posted data from frontend registration form, like 321 {'1-name': 'r', '1-email': 'r@r.com', '1-phone': '', '1-event_ticket_id': '1'} 322 """ 323 allowed_fields = request.env['event.registration']._get_website_registration_allowed_fields() 324 registration_fields = {key: v for key, v in request.env['event.registration']._fields.items() if key in allowed_fields} 325 registrations = {} 326 global_values = {} 327 for key, value in form_details.items(): 328 counter, attr_name = key.split('-', 1) 329 field_name = attr_name.split('-')[0] 330 if field_name not in registration_fields: 331 continue 332 elif isinstance(registration_fields[field_name], (fields.Many2one, fields.Integer)): 333 value = int(value) or False # 0 is considered as a void many2one aka False 334 else: 335 value = value 336 337 if counter == '0': 338 global_values[attr_name] = value 339 else: 340 registrations.setdefault(counter, dict())[attr_name] = value 341 for key, value in global_values.items(): 342 for registration in registrations.values(): 343 registration[key] = value 344 345 return list(registrations.values()) 346 347 def _create_attendees_from_registration_post(self, event, registration_data): 348 """ Also try to set a visitor (from request) and 349 a partner (if visitor linked to a user for example). Purpose is to gather 350 as much informations as possible, notably to ease future communications. 351 Also try to update visitor informations based on registration info. """ 352 visitor_sudo = request.env['website.visitor']._get_visitor_from_request(force_create=True) 353 visitor_sudo._update_visitor_last_visit() 354 visitor_values = {} 355 356 registrations_to_create = [] 357 for registration_values in registration_data: 358 registration_values['event_id'] = event.id 359 if not registration_values.get('partner_id') and visitor_sudo.partner_id: 360 registration_values['partner_id'] = visitor_sudo.partner_id.id 361 elif not registration_values.get('partner_id'): 362 registration_values['partner_id'] = request.env.user.partner_id.id 363 364 if visitor_sudo: 365 # registration may give a name to the visitor, yay 366 if registration_values.get('name') and not visitor_sudo.name and not visitor_values.get('name'): 367 visitor_values['name'] = registration_values['name'] 368 # update registration based on visitor 369 registration_values['visitor_id'] = visitor_sudo.id 370 371 registrations_to_create.append(registration_values) 372 373 if visitor_values: 374 visitor_sudo.write(visitor_values) 375 376 return request.env['event.registration'].sudo().create(registrations_to_create) 377 378 @http.route(['''/event/<model("event.event"):event>/registration/confirm'''], type='http', auth="public", methods=['POST'], website=True) 379 def registration_confirm(self, event, **post): 380 if not event.can_access_from_current_website(): 381 raise werkzeug.exceptions.NotFound() 382 383 registrations = self._process_attendees_form(event, post) 384 attendees_sudo = self._create_attendees_from_registration_post(event, registrations) 385 386 return request.render("website_event.registration_complete", 387 self._get_registration_confirm_values(event, attendees_sudo)) 388 389 def _get_registration_confirm_values(self, event, attendees_sudo): 390 urls = event._get_event_resource_urls() 391 return { 392 'attendees': attendees_sudo, 393 'event': event, 394 'google_url': urls.get('google_url'), 395 'iCal_url': urls.get('iCal_url') 396 } 397 398 def _extract_searched_event_tags(self, searches): 399 tags = request.env['event.tag'] 400 if searches.get('tags'): 401 try: 402 tag_ids = literal_eval(searches['tags']) 403 except: 404 pass 405 else: 406 # perform a search to filter on existing / valid tags implicitely + apply rules on color 407 tags = request.env['event.tag'].search([('id', 'in', tag_ids)]) 408 return tags 409