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.
5 from collections import defaultdict
10 from base64 import urlsafe_b64encode
12 from django.http import HttpResponse
13 from django.core.files.uploadedfile import UploadedFile
14 from django.core.files.storage import DefaultStorage
15 from django.utils.encoding import force_unicode
16 from django.conf import settings
17 from os import mkdir, path, unlink
18 from errno import EEXIST, ENOENT
19 from fcntl import flock, LOCK_EX
20 from zipfile import ZipFile
22 from reporting.utils import read_chunks
24 # Use the system (hardware-based) random number generator if it exists.
25 if hasattr(random, 'SystemRandom'):
26 randrange = random.SystemRandom().randrange
28 randrange = random.randrange
29 MAX_SESSION_KEY = 18446744073709551616L # 2 << 63
32 def get_random_hash(seed):
33 sha_digest = hashlib.sha1('%s%s%s%s' % (
34 randrange(0, MAX_SESSION_KEY), time.time(), unicode(seed).encode('utf-8', 'replace'), settings.SECRET_KEY)
36 return urlsafe_b64encode(sha_digest).replace('=', '').replace('_', '-').lower()
39 def split_tags(*tag_lists):
40 if len(tag_lists) == 1:
41 result = defaultdict(list)
42 for tag in tag_lists[0]:
43 result[tag.category].append(tag)
45 result = defaultdict(dict)
46 for tag_list in tag_lists:
49 result[tag.category][tag.pk].count += tag.count
51 result[tag.category][tag.pk] = tag
52 for k, v in result.items():
53 result[k] = sorted(v.values(), key=lambda tag: tag.sort_key)
57 class ExistingFile(UploadedFile):
59 def __init__(self, path, *args, **kwargs):
61 super(ExistingFile, self).__init__(*args, **kwargs)
63 def temporary_file_path(self):
70 class LockFile(object):
72 A file lock monitor class; createas an ${objname}.lock
73 file in directory dir, and locks it exclusively.
74 To be used in 'with' construct.
76 def __init__(self, dir, objname):
77 self.lockname = path.join(dir, objname + ".lock")
80 self.lock = open(self.lockname, 'w')
81 flock(self.lock, LOCK_EX)
83 def __exit__(self, *err):
87 if oe.errno != EEXIST:
93 def create_zip(paths, zip_slug):
95 Creates a zip in MEDIA_ROOT/zip directory containing files from path.
96 Resulting archive filename is ${zip_slug}.zip
97 Returns it's path relative to MEDIA_ROOT (no initial slash)
99 # directory to store zip files
100 zip_path = path.join(settings.MEDIA_ROOT, 'zip')
104 except OSError as oe:
105 if oe.errno != EEXIST:
107 zip_filename = zip_slug + ".zip"
109 with LockFile(zip_path, zip_slug):
110 if not path.exists(path.join(zip_path, zip_filename)):
111 zipf = ZipFile(path.join(zip_path, zip_filename), 'w')
113 for arcname, p in paths:
115 arcname = path.basename(p)
116 zipf.write(p, arcname)
120 return 'zip/' + zip_filename
123 def remove_zip(zip_slug):
125 removes the ${zip_slug}.zip file from zip store.
127 zip_file = path.join(settings.MEDIA_ROOT, 'zip', zip_slug + '.zip')
130 except OSError as oe:
131 if oe.errno != ENOENT:
135 class AttachmentHttpResponse(HttpResponse):
136 """Response serving a file to be downloaded.
138 def __init__(self, file_path, file_name, mimetype):
139 super(AttachmentHttpResponse, self).__init__(mimetype=mimetype)
140 self['Content-Disposition'] = 'attachment; filename=%s' % file_name
141 self.file_path = file_path
142 self.file_name = file_name
144 with open(DefaultStorage().path(self.file_path)) as f:
145 for chunk in read_chunks(f):
149 class MultiQuerySet(object):
150 def __init__(self, *args, **kwargs):
151 self.querysets = args
156 self._count = sum(len(qs) for qs in self.querysets)
162 def __getitem__(self, item):
164 (offset, stop, step) = item.indices(self.count())
165 except AttributeError:
166 # it's not a slice - make it one
167 return self[item:item + 1][0]
169 total_len = stop - offset
170 for qs in self.querysets:
174 items += list(qs[offset:stop])
175 if len(items) >= total_len:
179 stop = total_len - len(items)
183 class SortedMultiQuerySet(MultiQuerySet):
184 def __init__(self, *args, **kwargs):
185 self.order_by = kwargs.pop('order_by', None)
186 self.sortfn = kwargs.pop('sortfn', None)
187 if self.order_by is not None:
188 self.sortfn = lambda a, b: cmp((getattr(a, f) for f in self.order_by),
189 (getattr(b, f) for f in self.order_by))
190 super(SortedMultiQuerySet, self).__init__(*args, **kwargs)
192 def __getitem__(self, item):
193 sort_heads = [0] * len(self.querysets)
195 (offset, stop, step) = item.indices(self.count())
196 except AttributeError:
197 # it's not a slice - make it one
198 return self[item:item + 1][0]
200 total_len = stop - offset
202 i_s = range(len(sort_heads))
204 while len(items) < total_len:
209 return self.querysets[i][sort_heads[i]]
211 if candidate is None:
212 candidate = get_next()
215 competitor = get_next()
216 if self.sortfn(candidate, competitor) > 0:
217 candidate = competitor
220 continue # continue next sort_head
221 # we have no more elements:
222 if candidate is None:
224 sort_heads[candidate_i] += 1
227 continue # continue next item
228 items.append(candidate)
233 def truncate_html_words(s, num, end_text='...'):
234 """Truncates HTML to a certain number of words (not counting tags and
235 comments). Closes opened tags if they were correctly closed in the given
236 html. Takes an optional argument of what should be used to notify that the
237 string has been truncated, defaulting to ellipsis (...).
239 Newlines in the HTML are preserved.
241 This is just a version of django.utils.text.truncate_html_words with no space before the end_text.
247 html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input')
248 # Set up regular expressions
249 re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
250 re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
251 # Count non-HTML words and keep note of open tags
256 while words <= length:
257 m = re_words.search(s, pos)
259 # Checked through whole string
263 # It's an actual non-HTML word
269 tag = re_tag.match(m.group(0))
270 if not tag or end_text_pos:
271 # Don't worry about non tags or tags after our truncate point
273 closing_tag, tagname, self_closing = tag.groups()
274 tagname = tagname.lower() # Element names are always case-insensitive
275 if self_closing or tagname in html4_singlets:
278 # Check for match in open tags list
280 i = open_tags.index(tagname)
284 # SGML: An end tag closes, back to the matching start tag,
285 # all unclosed intervening start tags with omitted end tags
286 open_tags = open_tags[i+1:]
288 # Add it to the start of the open tags list
289 open_tags.insert(0, tagname)
291 # Don't try to close tags if we don't need to truncate
293 out = s[:end_text_pos]
296 # Close any tags still open
297 for tag in open_tags:
303 def customizations_hash(customizations):
304 customizations.sort()
305 return hash(tuple(customizations))
308 def get_customized_pdf_path(book, customizations):
310 Returns a MEDIA_ROOT relative path for a customized pdf. The name will contain a hash of customization options.
312 h = customizations_hash(customizations)
313 return 'book/%s/%s-custom-%s.pdf' % (book.slug, book.slug, h)
316 def clear_custom_pdf(book):
318 Returns a list of paths to generated customized pdf of a book
320 from waiter.utils import clear_cache
321 clear_cache('book/%s' % book.slug)
324 class AppSettings(object):
325 """Allows specyfying custom settings for an app, with default values.
327 Just subclass, set some properties and instantiate with a prefix.
328 Getting a SETTING from an instance will check for prefix_SETTING
329 in project settings if set, else take the default. The value will be
330 then filtered through _more_SETTING method, if there is one.
333 def __init__(self, prefix):
334 self._prefix = prefix
336 def __getattribute__(self, name):
337 if name.startswith('_'):
338 return object.__getattribute__(self, name)
339 value = getattr(settings, "%s_%s" % (self._prefix, name), object.__getattribute__(self, name))
340 more = "_more_%s" % name
341 if hasattr(self, more):
342 value = getattr(self, more)(value)
346 def delete_from_cache_by_language(cache, key_template):
347 cache.delete_many([key_template % lc for lc, ln in settings.LANGUAGES])