1# -*- coding: utf-8 -*-
2from __future__ import absolute_import
3from __future__ import unicode_literals
4
5# python lib:
6from datetime import date, timedelta
7
8# django:
9from django.db.models.functions import ExtractHour
10from django.views.generic import ListView, DetailView
11from django.conf import settings
12from django.shortcuts import get_object_or_404
13from django.utils.dates import MONTHS_ALT
14try:
15    from django.core.urlresolvers import reverse
16except ImportError:
17    from django.urls import reverse
18
19# thirdparties:
20import six
21
22# happenings:
23from .models import Event
24from .utils.displays import month_display, day_display
25from .utils.next_event import get_next_event
26from .utils.mixins import JSONResponseMixin
27from .utils import common as c
28
29
30URLS_NAMESPACE = getattr(settings, "CALENDAR_URLS_NAMESPACE", 'calendar')
31
32
33class GenericEventView(JSONResponseMixin, ListView):
34    model = Event
35
36    def render_to_response(self, context, **kwargs):
37        self.postprocess_context(context)
38        if self.request.is_ajax():
39            return self.render_to_json_response(context, **kwargs)
40        return super(GenericEventView, self).render_to_response(
41            context, **kwargs
42        )
43
44    def get_context_data(self, **kwargs):
45        context = super(GenericEventView, self).get_context_data(**kwargs)
46
47        self.net, self.category, self.tag = c.get_net_category_tag(
48            self.request
49        )
50
51        if self.category is not None:
52            context['cal_category'] = self.category
53        if self.tag is not None:
54            context['cal_tag'] = self.tag
55        return context
56
57    def postprocess_context(self, context, *args, **kwargs):
58        return
59
60
61class EventMonthView(GenericEventView):
62    template_name = 'happenings/event_month_list.html'
63
64    def get_year_and_month(self, net, qs, **kwargs):
65        """
66        Get the year and month. First tries from kwargs, then from
67        querystrings. If none, or if cal_ignore qs is specified,
68        sets year and month to this year and this month.
69        """
70        now = c.get_now()
71        year = now.year
72        month = now.month + net
73        month_orig = None
74
75        if 'cal_ignore=true' not in qs:
76            if 'year' and 'month' in self.kwargs:  # try kwargs
77                year, month_orig = map(
78                    int, (self.kwargs['year'], self.kwargs['month'])
79                )
80                month = month_orig + net
81            else:
82                try:  # try querystring
83                    year = int(self.request.GET['cal_year'])
84                    month_orig = int(self.request.GET['cal_month'])
85                    month = month_orig + net
86                except Exception:
87                    pass
88        # return the year and month, and any errors that may have occurred do
89        # to an invalid month/year being given.
90        return c.clean_year_month(year, month, month_orig)
91
92    def get_month_events(self, *args, **kwargs):
93        return Event.objects.all_month_events(*args, **kwargs)
94
95    def get_context_data(self, **kwargs):
96        context = super(EventMonthView, self).get_context_data(**kwargs)
97
98        qs = self.request.META['QUERY_STRING']
99
100        year, month, error = self.get_year_and_month(self.net, qs)
101
102        # add a dict containing the year, month, and month name to the context
103        current = dict(
104            year=year, month_num=month, month=MONTHS_ALT[month][:3]
105        )
106        context['current'] = current
107
108        display_month = MONTHS_ALT[month]
109
110        if isinstance(display_month, six.binary_type):
111            display_month = display_month.decode('utf-8')
112
113        context['month_and_year'] = u"%(month)s, %(year)d" % (
114            {'month': display_month, 'year': year}
115        )
116
117        if error:  # send any year/month errors
118            context['cal_error'] = error
119
120        all_month_events = list(
121            self.get_month_events(
122                year, month, self.category, self.tag, loc=True, cncl=True
123            ).annotate(
124                start_hour=ExtractHour('start_date')
125            ).order_by('start_hour')
126        )
127
128        context['raw_all_month_events'] = all_month_events
129
130        context['show_events'] = False
131        if getattr(settings, "CALENDAR_SHOW_LIST", False):
132            context['show_events'] = True
133            context['events'] = c.order_events(all_month_events, d=True) \
134                if self.request.is_ajax() else c.order_events(all_month_events)
135
136        return context
137
138    def postprocess_context(self, context, *args, **kwargs):
139        qs = self.request.META['QUERY_STRING']
140
141        mini = True if 'cal_mini=true' in qs else False
142
143        start_day = getattr(settings, "CALENDAR_START_DAY", 0)
144
145        # get any querystrings that are not next/prev/year/month
146        if qs:
147            qs = c.get_qs(qs)
148
149        if getattr(settings, "CALENDAR_PASS_VIEW_CONTEXT_TO_DISPLAY_METHOD", False):
150            month_display_base_context = dict(context)
151            month_display_base_context.pop('events', None)
152        else:
153            month_display_base_context = None
154
155        all_month_events = context['raw_all_month_events']
156
157        context['calendar'] = month_display(
158            context['current']['year'],
159            context['current']['month_num'],
160            all_month_events,
161            start_day,
162            self.net,
163            qs,
164            mini,
165            request=self.request,
166            base_context=month_display_base_context,
167        )
168
169
170class EventDayView(GenericEventView):
171    template_name = 'happenings/event_day_list.html'
172
173    def get_calendar_back_url(self, year, month_num):
174        self.request.current_app = self.request.resolver_match.namespace
175        if URLS_NAMESPACE:
176            view_name = URLS_NAMESPACE + ':list'
177        else:
178            view_name = 'list'
179        return reverse(view_name, args=(year, month_num), current_app=self.request.current_app)
180
181    def check_for_cancelled_events(self, d):
182        """Check if any events are cancelled on the given date 'd'."""
183        for event in self.events:
184            for cn in event.cancellations.all():
185                if cn.date == d:
186                    event.title += ' (CANCELLED)'
187
188    def get_month_events(self, *args, **kwargs):
189        return Event.objects.all_month_events(*args, **kwargs)
190
191    def get_context_data(self, **kwargs):
192        context = super(EventDayView, self).get_context_data(**kwargs)
193
194        kw = self.kwargs
195        y, m, d = map(int, (kw['year'], kw['month'], kw['day']))
196        year, month, day, error = c.clean_year_month_day(y, m, d, self.net)
197
198        if error:
199            context['cal_error'] = error
200
201        # Note that we don't prefetch 'cancellations' because they will be
202        # prefetched later (in day_display in displays.py)
203        all_month_events = self.get_month_events(
204            year, month, self.category, self.tag
205        )
206
207        self.events = day_display(
208            year, month, all_month_events, day
209        )
210
211        self.check_for_cancelled_events(d=date(year, month, day))
212        context['events'] = self.events
213
214        display_month = MONTHS_ALT[month]
215        if isinstance(display_month, six.binary_type):
216            display_month = display_month.decode('utf-8')
217
218        context['month'] = display_month
219        context['month_num'] = month
220        context['year'] = year
221        context['day'] = day
222        context['month_day_year'] = u"%(month)s %(day)d, %(year)d" % (
223            {'month': display_month, 'day': day, 'year': year}
224        )
225        context['calendar_back_url'] = self.get_calendar_back_url(year, month)
226
227        # for use in the template to build next & prev querystrings
228        context['next'], context['prev'] = c.get_next_and_prev(self.net)
229        return context
230
231
232class EventDetailView(DetailView):
233    model = Event
234    context_object_name = 'event'
235
236    def get_object(self):
237        return get_object_or_404(
238            Event.objects.prefetch_related(
239                'location', 'categories', 'tags', 'cancellations'
240            ),
241            pk=self.kwargs['pk']
242        )
243
244    def get_cncl_days(self):
245        now = c.get_now()
246        cncl = self.object.cancellations.all()
247        return [(x.date, x.reason) for x in cncl if x.date >= now.date()]
248
249    def check_cncl(self, d):
250        cncl = self.object.cancellations.all()
251        return True if [x for x in cncl if x.date == d] else False
252
253    def get_context_data(self, **kwargs):
254        now = c.get_now()
255        context = super(EventDetailView, self).get_context_data(**kwargs)
256        e = self.object
257
258        for choice in Event.REPEAT_CHOICES:
259            if choice[0] == e.repeat:
260                context['repeat'] = choice[1]
261
262        context['cncl_days'] = self.get_cncl_days()
263
264        event = [e]  # event needs to be an iterable, see get_next_event()
265        if not e.repeats('NEVER'):  # event is ongoing; get next occurrence
266            if e.will_occur(now):
267                year, month, day = get_next_event(event, now)
268                next_event = date(year, month, day)
269                context['next_event'] = date(year, month, day)
270                context['next_or_prev_cncl'] = self.check_cncl(next_event)
271            else:  # event is finished repeating; get last occurrence
272                end = e.end_repeat
273                last_event = end
274                if e.repeats('WEEKDAY'):
275                    year, month, day = c.check_weekday(
276                        end.year, end.month, end.day, reverse=True
277                    )
278                    last_event = date(year, month, day)
279                context['last_event'] = last_event
280                context['next_or_prev_cncl'] = self.check_cncl(last_event)
281        else:
282            if e.is_chunk():
283                # list of days for single-day event chunk
284                context['event_days'] = (  # list comp
285                    (e.l_start_date + timedelta(days=x))
286                    for x in range(e.start_end_diff + 1)
287                )
288            else:
289                # let template know if this single-day, non-repeating event is
290                # cancelled
291                context['this_cncl'] = self.check_cncl(e.l_start_date.date())
292        return context
293