1 # -*- coding: utf-8 -*-
2 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
3 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
10 from base64 import urlsafe_b64encode
11 from collections import defaultdict
12 from errno import EEXIST, ENOENT
13 from fcntl import flock, LOCK_EX
14 from os import mkdir, path, unlink
15 from zipfile import ZipFile
17 from django.conf import settings
18 from django.core.files.storage import DefaultStorage
19 from django.core.files.uploadedfile import UploadedFile
20 from django.http import HttpResponse
21 from django.utils.encoding import force_unicode
23 from paypal.rest import user_is_subscribed
24 from reporting.utils import read_chunks
26 # Use the system (hardware-based) random number generator if it exists.
27 if hasattr(random, 'SystemRandom'):
28 randrange = random.SystemRandom().randrange
30 randrange = random.randrange
31 MAX_SESSION_KEY = 18446744073709551616L # 2 << 63
34 def get_random_hash(seed):
35 sha_digest = hashlib.sha1('%s%s%s%s' % (
36 randrange(0, MAX_SESSION_KEY), time.time(), unicode(seed).encode('utf-8', 'replace'), settings.SECRET_KEY)
38 return urlsafe_b64encode(sha_digest).replace('=', '').replace('_', '-').lower()
41 def split_tags(*tag_lists):
42 if len(tag_lists) == 1:
43 result = defaultdict(list)
44 for tag in tag_lists[0]:
45 result[tag.category].append(tag)
47 result = defaultdict(dict)
48 for tag_list in tag_lists:
51 result[tag.category][tag.pk].count += tag.count
53 result[tag.category][tag.pk] = tag
54 for k, v in result.items():
55 result[k] = sorted(v.values(), key=lambda tag: tag.sort_key)
59 class ExistingFile(UploadedFile):
61 def __init__(self, path, *args, **kwargs):
63 super(ExistingFile, self).__init__(*args, **kwargs)
65 def temporary_file_path(self):
72 class LockFile(object):
74 A file lock monitor class; createas an ${objname}.lock
75 file in directory dir, and locks it exclusively.
76 To be used in 'with' construct.
78 def __init__(self, dir, objname):
79 self.lockname = path.join(dir, objname + ".lock")
82 self.lock = open(self.lockname, 'w')
83 flock(self.lock, LOCK_EX)
85 def __exit__(self, *err):
89 if oe.errno != ENOENT:
95 def create_zip(paths, zip_slug):
97 Creates a zip in MEDIA_ROOT/zip directory containing files from path.
98 Resulting archive filename is ${zip_slug}.zip
99 Returns it's path relative to MEDIA_ROOT (no initial slash)
101 # directory to store zip files
102 zip_path = path.join(settings.MEDIA_ROOT, 'zip')
106 except OSError as oe:
107 if oe.errno != EEXIST:
109 zip_filename = zip_slug + ".zip"
111 with LockFile(zip_path, zip_slug):
112 if not path.exists(path.join(zip_path, zip_filename)):
113 zipf = ZipFile(path.join(zip_path, zip_filename), 'w')
115 for arcname, p in paths:
117 arcname = path.basename(p)
118 zipf.write(p, arcname)
122 return 'zip/' + zip_filename
125 def remove_zip(zip_slug):
127 removes the ${zip_slug}.zip file from zip store.
129 zip_file = path.join(settings.MEDIA_ROOT, 'zip', zip_slug + '.zip')
132 except OSError as oe:
133 if oe.errno != ENOENT:
137 class AttachmentHttpResponse(HttpResponse):
138 """Response serving a file to be downloaded.
140 def __init__(self, file_path, file_name, mimetype):
141 super(AttachmentHttpResponse, self).__init__(mimetype=mimetype)
142 self['Content-Disposition'] = 'attachment; filename=%s' % file_name
143 self.file_path = file_path
144 self.file_name = file_name
146 with open(DefaultStorage().path(self.file_path)) as f:
147 for chunk in read_chunks(f):
151 class MultiQuerySet(object):
152 def __init__(self, *args, **kwargs):
153 self.querysets = args
158 self._count = sum(len(qs) for qs in self.querysets)
164 def __getitem__(self, item):
166 (offset, stop, step) = item.indices(self.count())
167 except AttributeError:
168 # it's not a slice - make it one
169 return self[item:item + 1][0]
171 total_len = stop - offset
172 for qs in self.querysets:
176 items += list(qs[offset:stop])
177 if len(items) >= total_len:
181 stop = total_len - len(items)
185 class SortedMultiQuerySet(MultiQuerySet):
186 def __init__(self, *args, **kwargs):
187 self.order_by = kwargs.pop('order_by', None)
188 self.sortfn = kwargs.pop('sortfn', None)
189 if self.order_by is not None:
190 self.sortfn = lambda a, b: cmp((getattr(a, f) for f in self.order_by),
191 (getattr(b, f) for f in self.order_by))
192 super(SortedMultiQuerySet, self).__init__(*args, **kwargs)
194 def __getitem__(self, item):
195 sort_heads = [0] * len(self.querysets)
197 (offset, stop, step) = item.indices(self.count())
198 except AttributeError:
199 # it's not a slice - make it one
200 return self[item:item + 1][0]
202 total_len = stop - offset
204 i_s = range(len(sort_heads))
206 while len(items) < total_len:
211 return self.querysets[i][sort_heads[i]]
213 if candidate is None:
214 candidate = get_next()
217 competitor = get_next()
218 if self.sortfn(candidate, competitor) > 0:
219 candidate = competitor
222 continue # continue next sort_head
223 # we have no more elements:
224 if candidate is None:
226 sort_heads[candidate_i] += 1
229 continue # continue next item
230 items.append(candidate)
235 def truncate_html_words(s, num, end_text='...'):
236 """Truncates HTML to a certain number of words (not counting tags and
237 comments). Closes opened tags if they were correctly closed in the given
238 html. Takes an optional argument of what should be used to notify that the
239 string has been truncated, defaulting to ellipsis (...).
241 Newlines in the HTML are preserved.
243 This is just a version of django.utils.text.truncate_html_words with no space before the end_text.
249 html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input')
250 # Set up regular expressions
251 re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
252 re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
253 # Count non-HTML words and keep note of open tags
258 while words <= length:
259 m = re_words.search(s, pos)
261 # Checked through whole string
265 # It's an actual non-HTML word
271 tag = re_tag.match(m.group(0))
272 if not tag or end_text_pos:
273 # Don't worry about non tags or tags after our truncate point
275 closing_tag, tagname, self_closing = tag.groups()
276 tagname = tagname.lower() # Element names are always case-insensitive
277 if self_closing or tagname in html4_singlets:
280 # Check for match in open tags list
282 i = open_tags.index(tagname)
286 # SGML: An end tag closes, back to the matching start tag,
287 # all unclosed intervening start tags with omitted end tags
288 open_tags = open_tags[i+1:]
290 # Add it to the start of the open tags list
291 open_tags.insert(0, tagname)
293 # Don't try to close tags if we don't need to truncate
295 out = s[:end_text_pos]
298 # Close any tags still open
299 for tag in open_tags:
305 def customizations_hash(customizations):
306 customizations.sort()
307 return hash(tuple(customizations))
310 def get_customized_pdf_path(book, customizations):
312 Returns a MEDIA_ROOT relative path for a customized pdf. The name will contain a hash of customization options.
314 h = customizations_hash(customizations)
315 return 'book/%s/%s-custom-%s.pdf' % (book.slug, book.slug, h)
318 def clear_custom_pdf(book):
320 Returns a list of paths to generated customized pdf of a book
322 from waiter.utils import clear_cache
323 clear_cache('book/%s' % book.slug)
326 class AppSettings(object):
327 """Allows specyfying custom settings for an app, with default values.
329 Just subclass, set some properties and instantiate with a prefix.
330 Getting a SETTING from an instance will check for prefix_SETTING
331 in project settings if set, else take the default. The value will be
332 then filtered through _more_SETTING method, if there is one.
335 def __init__(self, prefix):
336 self._prefix = prefix
338 def __getattribute__(self, name):
339 if name.startswith('_'):
340 return object.__getattribute__(self, name)
341 value = getattr(settings, "%s_%s" % (self._prefix, name), object.__getattribute__(self, name))
342 more = "_more_%s" % name
343 if hasattr(self, more):
344 value = getattr(self, more)(value)
348 def delete_from_cache_by_language(cache, key_template):
349 cache.delete_many([key_template % lc for lc, ln in settings.LANGUAGES])
352 def gallery_path(slug):
353 return os.path.join(settings.MEDIA_ROOT, settings.IMAGE_DIR, slug)
356 def gallery_url(slug):
357 return '%s%s%s/' % (settings.MEDIA_URL, settings.IMAGE_DIR, slug)
360 def is_subscribed(user):
361 return user_is_subscribed(user)