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()
44 result.setdefault(tag.category, []).append(tag)
48 def get_dynamic_path(media, filename, ext=None, maxlen=100):
49 from fnpdjango.utils.text.slughifi import slughifi
51 # how to put related book's slug here?
54 ext = media.formats[media.type].ext
55 if media is None or not media.name:
56 name = slughifi(filename.split(".")[0])
58 name = slughifi(media.name)
59 return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext)
62 # TODO: why is this hard-coded ?
63 def book_upload_path(ext=None, maxlen=100):
64 return lambda *args: get_dynamic_path(*args, ext=ext, maxlen=maxlen)
67 class ExistingFile(UploadedFile):
69 def __init__(self, path, *args, **kwargs):
71 super(ExistingFile, self).__init__(*args, **kwargs)
73 def temporary_file_path(self):
80 class LockFile(object):
82 A file lock monitor class; createas an ${objname}.lock
83 file in directory dir, and locks it exclusively.
84 To be used in 'with' construct.
86 def __init__(self, dir, objname):
87 self.lockname = path.join(dir, objname + ".lock")
90 self.lock = open(self.lockname, 'w')
91 flock(self.lock, LOCK_EX)
93 def __exit__(self, *err):
97 if oe.errno != EEXIST:
103 def create_zip(paths, zip_slug):
105 Creates a zip in MEDIA_ROOT/zip directory containing files from path.
106 Resulting archive filename is ${zip_slug}.zip
107 Returns it's path relative to MEDIA_ROOT (no initial slash)
109 # directory to store zip files
110 zip_path = path.join(settings.MEDIA_ROOT, 'zip')
114 except OSError as oe:
115 if oe.errno != EEXIST:
117 zip_filename = zip_slug + ".zip"
119 with LockFile(zip_path, zip_slug):
120 if not path.exists(path.join(zip_path, zip_filename)):
121 zipf = ZipFile(path.join(zip_path, zip_filename), 'w')
123 for arcname, p in paths:
125 arcname = path.basename(p)
126 zipf.write(p, arcname)
130 return 'zip/' + zip_filename
133 def remove_zip(zip_slug):
135 removes the ${zip_slug}.zip file from zip store.
137 zip_file = path.join(settings.MEDIA_ROOT, 'zip', zip_slug + '.zip')
140 except OSError as oe:
141 if oe.errno != ENOENT:
145 class AttachmentHttpResponse(HttpResponse):
146 """Response serving a file to be downloaded.
148 def __init__ (self, file_path, file_name, mimetype):
149 super(AttachmentHttpResponse, self).__init__(mimetype=mimetype)
150 self['Content-Disposition'] = 'attachment; filename=%s' % file_name
151 self.file_path = file_path
152 self.file_name = file_name
154 with open(DefaultStorage().path(self.file_path)) as f:
155 for chunk in read_chunks(f):
158 class MultiQuerySet(object):
159 def __init__(self, *args, **kwargs):
160 self.querysets = args
165 self._count = sum(len(qs) for qs in self.querysets)
171 def __getitem__(self, item):
173 indices = (offset, stop, step) = item.indices(self.count())
174 except AttributeError:
175 # it's not a slice - make it one
176 return self[item : item + 1][0]
178 total_len = stop - offset
179 for qs in self.querysets:
183 items += list(qs[offset:stop])
184 if len(items) >= total_len:
188 stop = total_len - len(items)
192 def truncate_html_words(s, num, end_text='...'):
193 """Truncates HTML to a certain number of words (not counting tags and
194 comments). Closes opened tags if they were correctly closed in the given
195 html. Takes an optional argument of what should be used to notify that the
196 string has been truncated, defaulting to ellipsis (...).
198 Newlines in the HTML are preserved.
200 This is just a version of django.utils.text.truncate_html_words with no space before the end_text.
206 html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input')
207 # Set up regular expressions
208 re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
209 re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
210 # Count non-HTML words and keep note of open tags
215 while words <= length:
216 m = re_words.search(s, pos)
218 # Checked through whole string
222 # It's an actual non-HTML word
228 tag = re_tag.match(m.group(0))
229 if not tag or end_text_pos:
230 # Don't worry about non tags or tags after our truncate point
232 closing_tag, tagname, self_closing = tag.groups()
233 tagname = tagname.lower() # Element names are always case-insensitive
234 if self_closing or tagname in html4_singlets:
237 # Check for match in open tags list
239 i = open_tags.index(tagname)
243 # SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags
244 open_tags = open_tags[i+1:]
246 # Add it to the start of the open tags list
247 open_tags.insert(0, tagname)
249 # Don't try to close tags if we don't need to truncate
251 out = s[:end_text_pos]
254 # Close any tags still open
255 for tag in open_tags:
261 def customizations_hash(customizations):
262 customizations.sort()
263 return hash(tuple(customizations))
266 def get_customized_pdf_path(book, customizations):
268 Returns a MEDIA_ROOT relative path for a customized pdf. The name will contain a hash of customization options.
270 h = customizations_hash(customizations)
271 return 'book/%s/%s-custom-%s.pdf' % (book.slug, book.slug, h)
274 def clear_custom_pdf(book):
276 Returns a list of paths to generated customized pdf of a book
278 from waiter.utils import clear_cache
279 clear_cache('book/%s' % book.slug)
282 class AppSettings(object):
283 """Allows specyfying custom settings for an app, with default values.
285 Just subclass, set some properties and instantiate with a prefix.
286 Getting a SETTING from an instance will check for prefix_SETTING
287 in project settings if set, else take the default. The value will be
288 then filtered through _more_SETTING method, if there is one.
291 def __init__(self, prefix):
292 self._prefix = prefix
294 def __getattribute__(self, name):
295 if name.startswith('_'):
296 return object.__getattribute__(self, name)
297 value = getattr(settings,
298 "%s_%s" % (self._prefix, name),
299 object.__getattribute__(self, name))
300 more = "_more_%s" % name
301 if hasattr(self, more):
302 value = getattr(self, more)(value)
306 def trim_query_log(trim_to=25):
308 connection.queries includes all SQL statements -- INSERTs, UPDATES, SELECTs, etc. Each time your app hits the database, the query will be recorded.
309 This can sometimes occupy lots of memory, so trim it here a bit.
312 from django.db import connection
313 connection.queries = trim_to > 0 \
314 and connection.queries[-trim_to:] \
318 def related_tag_name(tag_info, language=None):
319 return tag_info.get("name_%s" % (language or get_language()),
320 tag_info.get("name_%s" % settings.LANGUAGE_CODE))
323 def delete_from_cache_by_language(cache, key_template):
324 cache.delete_many([key_template % lc for lc, ln in settings.LANGUAGES])