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 __future__ import with_statement
11 from base64 import urlsafe_b64encode
13 from django.http import HttpResponse
14 from django.core.files.uploadedfile import UploadedFile
15 from django.core.files.storage import DefaultStorage
16 from django.utils.encoding import force_unicode
17 from django.utils.translation import get_language
18 from django.conf import settings
19 from os import mkdir, path, unlink
20 from errno import EEXIST, ENOENT
21 from fcntl import flock, LOCK_EX
22 from zipfile import ZipFile
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'),
37 settings.SECRET_KEY)).digest()
38 return urlsafe_b64encode(sha_digest).replace('=', '').replace('_', '-').lower()
41 def split_tags(tags, initial=None):
48 result.setdefault(tag.category, []).append(tag)
52 def get_dynamic_path(media, filename, ext=None, maxlen=100):
53 from fnpdjango.utils.text.slughifi import slughifi
55 # how to put related book's slug here?
58 ext = media.formats[media.type].ext
59 if media is None or not media.name:
60 name = slughifi(filename.split(".")[0])
62 name = slughifi(media.name)
63 return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext)
66 # TODO: why is this hard-coded ?
67 def book_upload_path(ext=None, maxlen=100):
68 return lambda *args: get_dynamic_path(*args, ext=ext, maxlen=maxlen)
71 class ExistingFile(UploadedFile):
73 def __init__(self, path, *args, **kwargs):
75 super(ExistingFile, self).__init__(*args, **kwargs)
77 def temporary_file_path(self):
84 class LockFile(object):
86 A file lock monitor class; createas an ${objname}.lock
87 file in directory dir, and locks it exclusively.
88 To be used in 'with' construct.
90 def __init__(self, dir, objname):
91 self.lockname = path.join(dir, objname + ".lock")
94 self.lock = open(self.lockname, 'w')
95 flock(self.lock, LOCK_EX)
97 def __exit__(self, *err):
100 except OSError as oe:
101 if oe.errno != EEXIST:
107 def create_zip(paths, zip_slug):
109 Creates a zip in MEDIA_ROOT/zip directory containing files from path.
110 Resulting archive filename is ${zip_slug}.zip
111 Returns it's path relative to MEDIA_ROOT (no initial slash)
113 # directory to store zip files
114 zip_path = path.join(settings.MEDIA_ROOT, 'zip')
118 except OSError as oe:
119 if oe.errno != EEXIST:
121 zip_filename = zip_slug + ".zip"
123 with LockFile(zip_path, zip_slug):
124 if not path.exists(path.join(zip_path, zip_filename)):
125 zipf = ZipFile(path.join(zip_path, zip_filename), 'w')
127 for arcname, p in paths:
129 arcname = path.basename(p)
130 zipf.write(p, arcname)
134 return 'zip/' + zip_filename
137 def remove_zip(zip_slug):
139 removes the ${zip_slug}.zip file from zip store.
141 zip_file = path.join(settings.MEDIA_ROOT, 'zip', zip_slug + '.zip')
144 except OSError as oe:
145 if oe.errno != ENOENT:
149 class AttachmentHttpResponse(HttpResponse):
150 """Response serving a file to be downloaded.
152 def __init__ (self, file_path, file_name, mimetype):
153 super(AttachmentHttpResponse, self).__init__(mimetype=mimetype)
154 self['Content-Disposition'] = 'attachment; filename=%s' % file_name
155 self.file_path = file_path
156 self.file_name = file_name
158 with open(DefaultStorage().path(self.file_path)) as f:
159 for chunk in read_chunks(f):
162 class MultiQuerySet(object):
163 def __init__(self, *args, **kwargs):
164 self.querysets = args
169 self._count = sum(len(qs) for qs in self.querysets)
175 def __getitem__(self, item):
177 indices = (offset, stop, step) = item.indices(self.count())
178 except AttributeError:
179 # it's not a slice - make it one
180 return self[item : item + 1][0]
182 total_len = stop - offset
183 for qs in self.querysets:
187 items += list(qs[offset:stop])
188 if len(items) >= total_len:
192 stop = total_len - len(items)
195 class SortedMultiQuerySet(MultiQuerySet):
196 def __init__(self, *args, **kwargs):
197 self.order_by = kwargs.pop('order_by', None)
198 self.sortfn = kwargs.pop('sortfn', None)
199 if self.order_by is not None:
200 self.sortfn = lambda a, b: cmp(getattr(a, self.order_by),
201 getattr(b, self.order_by))
202 super(SortedMultiQuerySet, self).__init__(*args, **kwargs)
204 def __getitem__(self, item):
205 sort_heads = [0] * len(self.querysets)
207 indices = (offset, stop, step) = item.indices(self.count())
208 except AttributeError:
209 # it's not a slice - make it one
210 return self[item : item + 1][0]
212 total_len = stop - offset
214 i_s = range(len(sort_heads))
216 while len(items) < total_len:
221 return self.querysets[i][sort_heads[i]]
223 if candidate is None:
224 candidate = get_next()
227 competitor = get_next()
228 if self.sortfn(candidate, competitor) > 0:
229 candidate = competitor
232 continue # continue next sort_head
233 # we have no more elements:
234 if candidate is None:
236 sort_heads[candidate_i] += 1
239 continue # continue next item
240 items.append(candidate)
245 def truncate_html_words(s, num, end_text='...'):
246 """Truncates HTML to a certain number of words (not counting tags and
247 comments). Closes opened tags if they were correctly closed in the given
248 html. Takes an optional argument of what should be used to notify that the
249 string has been truncated, defaulting to ellipsis (...).
251 Newlines in the HTML are preserved.
253 This is just a version of django.utils.text.truncate_html_words with no space before the end_text.
259 html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input')
260 # Set up regular expressions
261 re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
262 re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
263 # Count non-HTML words and keep note of open tags
268 while words <= length:
269 m = re_words.search(s, pos)
271 # Checked through whole string
275 # It's an actual non-HTML word
281 tag = re_tag.match(m.group(0))
282 if not tag or end_text_pos:
283 # Don't worry about non tags or tags after our truncate point
285 closing_tag, tagname, self_closing = tag.groups()
286 tagname = tagname.lower() # Element names are always case-insensitive
287 if self_closing or tagname in html4_singlets:
290 # Check for match in open tags list
292 i = open_tags.index(tagname)
296 # SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags
297 open_tags = open_tags[i+1:]
299 # Add it to the start of the open tags list
300 open_tags.insert(0, tagname)
302 # Don't try to close tags if we don't need to truncate
304 out = s[:end_text_pos]
307 # Close any tags still open
308 for tag in open_tags:
314 def customizations_hash(customizations):
315 customizations.sort()
316 return hash(tuple(customizations))
319 def get_customized_pdf_path(book, customizations):
321 Returns a MEDIA_ROOT relative path for a customized pdf. The name will contain a hash of customization options.
323 h = customizations_hash(customizations)
324 return 'book/%s/%s-custom-%s.pdf' % (book.slug, book.slug, h)
327 def clear_custom_pdf(book):
329 Returns a list of paths to generated customized pdf of a book
331 from waiter.utils import clear_cache
332 clear_cache('book/%s' % book.slug)
335 class AppSettings(object):
336 """Allows specyfying custom settings for an app, with default values.
338 Just subclass, set some properties and instantiate with a prefix.
339 Getting a SETTING from an instance will check for prefix_SETTING
340 in project settings if set, else take the default. The value will be
341 then filtered through _more_SETTING method, if there is one.
344 def __init__(self, prefix):
345 self._prefix = prefix
347 def __getattribute__(self, name):
348 if name.startswith('_'):
349 return object.__getattribute__(self, name)
350 value = getattr(settings,
351 "%s_%s" % (self._prefix, name),
352 object.__getattribute__(self, name))
353 more = "_more_%s" % name
354 if hasattr(self, more):
355 value = getattr(self, more)(value)
359 def trim_query_log(trim_to=25):
361 connection.queries includes all SQL statements -- INSERTs, UPDATES, SELECTs, etc. Each time your app hits the database, the query will be recorded.
362 This can sometimes occupy lots of memory, so trim it here a bit.
365 from django.db import connection
366 connection.queries = trim_to > 0 \
367 and connection.queries[-trim_to:] \
371 def related_tag_name(tag_info, language=None):
372 return tag_info.get("name_%s" % (language or get_language()),
373 tag_info.get("name_%s" % settings.LANGUAGE_CODE, ""))
376 def delete_from_cache_by_language(cache, key_template):
377 cache.delete_many([key_template % lc for lc, ln in settings.LANGUAGES])