Fixes #4023: nicer club schedule filtering.
[wolnelektury.git] / src / wolnelektury / utils.py
1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
3 #
4 import codecs
5 import csv
6 from functools import wraps
7 from inspect import getargspec
8 from io import BytesIO
9 import json
10 import os
11 import pytz
12 import re
13
14 from django.conf import settings
15 from django.contrib import admin
16 from django.core.cache import cache
17 from django.core.mail import send_mail
18 from django.http import HttpResponse
19 from django.template.loader import render_to_string
20 from django.utils import timezone
21 from django.utils.translation import get_language
22 from django.conf import settings
23 from django.utils.safestring import mark_safe
24 from django.utils.translation import gettext as _
25
26
27 tz = pytz.timezone(settings.TIME_ZONE)
28
29
30 def localtime_to_utc(localtime):
31     return timezone.utc.normalize(
32         tz.localize(localtime)
33     )
34
35
36 def utc_for_js(dt):
37     return dt.strftime('%Y/%m/%d %H:%M:%S UTC')
38
39
40 def makedirs(path):
41     if not os.path.isdir(path):
42         os.makedirs(path)
43
44
45 def stringify_keys(dictionary):
46     return dict((keyword.encode('ascii'), value)
47                 for keyword, value in dictionary.items())
48
49
50 def json_encode(obj, sort_keys=True, ensure_ascii=False):
51     return json.dumps(obj, sort_keys=sort_keys, ensure_ascii=ensure_ascii)
52
53
54 def json_decode(obj):
55     return json.loads(obj)
56
57
58 def json_decode_fallback(value):
59     try:
60         return json_decode(value)
61     except ValueError:
62         return value
63
64
65 class AjaxError(Exception):
66     pass
67
68
69 def ajax(login_required=False, method=None, template=None, permission_required=None):
70     def decorator(fun):
71         @wraps(fun)
72         def ajax_view(request):
73             kwargs = {}
74             request_params = None
75             if method == 'post':
76                 request_params = request.POST
77             elif method == 'get':
78                 request_params = request.GET
79             fun_params, xx, fun_kwargs, defaults = getargspec(fun)
80             if defaults:
81                 required_params = fun_params[1:-len(defaults)]
82             else:
83                 required_params = fun_params[1:]
84             missing_params = set(required_params) - set(request_params)
85             if missing_params:
86                 res = {
87                     'result': 'missing params',
88                     'missing': ', '.join(missing_params),
89                 }
90             else:
91                 if request_params:
92                     request_params = dict(
93                         (key, json_decode_fallback(value))
94                         for key, value in request_params.items()
95                         if fun_kwargs or key in fun_params)
96                     kwargs.update(stringify_keys(request_params))
97                 res = None
98                 if login_required and not request.user.is_authenticated:
99                     res = {'result': 'logout'}
100                 if (permission_required and
101                         not request.user.has_perm(permission_required)):
102                     res = {'result': 'access denied'}
103             if not res:
104                 try:
105                     res = fun(request, **kwargs)
106                     if res and template:
107                         res = {'html': render_to_string(template, res, request=request)}
108                 except AjaxError as e:
109                     res = {'result': e.args[0]}
110             if 'result' not in res:
111                 res['result'] = 'ok'
112             return HttpResponse(json_encode(res), content_type='application/json; charset=utf-8',
113                                 status=200 if res['result'] == 'ok' else 400)
114
115         return ajax_view
116
117     return decorator
118
119
120 def send_noreply_mail(subject, message, recipient_list, **kwargs):
121     send_mail(
122         '[WolneLektury] ' + subject,
123         message + "\n\n-- \n" + _('Message sent automatically. Please do not reply.'),
124         'no-reply@wolnelektury.pl', recipient_list, **kwargs)
125
126
127 # source: https://docs.python.org/2/library/csv.html#examples
128 class UnicodeCSVWriter(object):
129     """
130     A CSV writer which will write rows to CSV file "f",
131     which is encoded in the given encoding.
132     """
133
134     def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
135         # Redirect output to a queue
136         self.queue = BytesIO()
137         self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
138         self.stream = f
139         self.encoder = codecs.getincrementalencoder(encoding)()
140
141     def writerow(self, row):
142         self.writer.writerow([s.encode("utf-8") for s in row])
143         # Fetch UTF-8 output from the queue ...
144         data = self.queue.getvalue()
145         data = data.decode("utf-8")
146         # ... and reencode it into the target encoding
147         data = self.encoder.encode(data)
148         # write to the target stream
149         self.stream.write(data)
150         # empty queue
151         self.queue.truncate(0)
152
153     def writerows(self, rows):
154         for row in rows:
155             self.writerow(row)
156
157
158 # the original re.escape messes with unicode
159 def re_escape(s):
160     return re.sub(r"[(){}\[\].*?|^$\\+-]", r"\\\g<0>", s)
161
162
163 def get_cached_render_key(instance, property_name, language=None):
164     if language is None:
165         language = get_language()
166     return 'cached_render:%s.%s:%s:%s' % (
167             type(instance).__name__,
168             property_name,
169             instance.pk,
170             language
171         )
172
173
174 def cached_render(template_name, timeout=24 * 60 * 60):
175     def decorator(method):
176         @wraps(method)
177         def wrapper(self):
178             key = get_cached_render_key(self, method.__name__)
179             content = cache.get(key)
180             if content is None:
181                 context = method(self)
182                 content = render_to_string(template_name, context)
183                 cache.set(key, str(content), timeout=timeout)
184             else:
185                 content = mark_safe(content)
186             return content
187         return wrapper
188     return decorator
189
190
191 def clear_cached_renders(bound_method):
192     for lc, ln in settings.LANGUAGES:
193         cache.delete(
194             get_cached_render_key(
195                 bound_method.__self__,
196                 bound_method.__name__,
197                 lc
198             )
199         )
200
201
202 class YesNoFilter(admin.SimpleListFilter):
203     def lookups(self, request, model_admin):
204         return (
205             ('yes', _('Yes')),
206             ('no', _('No')),
207         )
208
209     def queryset(self, request, queryset):
210         if self.value() == 'yes':
211             return queryset.filter(self.q)
212         elif self.value() == 'no':
213             return queryset.exclude(self.q)