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