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)
196 def truncate_html_words(s, num, end_text='...'):
197 """Truncates HTML to a certain number of words (not counting tags and
198 comments). Closes opened tags if they were correctly closed in the given
199 html. Takes an optional argument of what should be used to notify that the
200 string has been truncated, defaulting to ellipsis (...).
202 Newlines in the HTML are preserved.
204 This is just a version of django.utils.text.truncate_html_words with no space before the end_text.
210 html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input')
211 # Set up regular expressions
212 re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
213 re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
214 # Count non-HTML words and keep note of open tags
219 while words <= length:
220 m = re_words.search(s, pos)
222 # Checked through whole string
226 # It's an actual non-HTML word
232 tag = re_tag.match(m.group(0))
233 if not tag or end_text_pos:
234 # Don't worry about non tags or tags after our truncate point
236 closing_tag, tagname, self_closing = tag.groups()
237 tagname = tagname.lower() # Element names are always case-insensitive
238 if self_closing or tagname in html4_singlets:
241 # Check for match in open tags list
243 i = open_tags.index(tagname)
247 # SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags
248 open_tags = open_tags[i+1:]
250 # Add it to the start of the open tags list
251 open_tags.insert(0, tagname)
253 # Don't try to close tags if we don't need to truncate
255 out = s[:end_text_pos]
258 # Close any tags still open
259 for tag in open_tags:
265 def customizations_hash(customizations):
266 customizations.sort()
267 return hash(tuple(customizations))
270 def get_customized_pdf_path(book, customizations):
272 Returns a MEDIA_ROOT relative path for a customized pdf. The name will contain a hash of customization options.
274 h = customizations_hash(customizations)
275 return 'book/%s/%s-custom-%s.pdf' % (book.slug, book.slug, h)
278 def clear_custom_pdf(book):
280 Returns a list of paths to generated customized pdf of a book
282 from waiter.utils import clear_cache
283 clear_cache('book/%s' % book.slug)
286 class AppSettings(object):
287 """Allows specyfying custom settings for an app, with default values.
289 Just subclass, set some properties and instantiate with a prefix.
290 Getting a SETTING from an instance will check for prefix_SETTING
291 in project settings if set, else take the default. The value will be
292 then filtered through _more_SETTING method, if there is one.
295 def __init__(self, prefix):
296 self._prefix = prefix
298 def __getattribute__(self, name):
299 if name.startswith('_'):
300 return object.__getattribute__(self, name)
301 value = getattr(settings,
302 "%s_%s" % (self._prefix, name),
303 object.__getattribute__(self, name))
304 more = "_more_%s" % name
305 if hasattr(self, more):
306 value = getattr(self, more)(value)
310 def trim_query_log(trim_to=25):
312 connection.queries includes all SQL statements -- INSERTs, UPDATES, SELECTs, etc. Each time your app hits the database, the query will be recorded.
313 This can sometimes occupy lots of memory, so trim it here a bit.
316 from django.db import connection
317 connection.queries = trim_to > 0 \
318 and connection.queries[-trim_to:] \
322 def related_tag_name(tag_info, language=None):
323 return tag_info.get("name_%s" % (language or get_language()),
324 tag_info.get("name_%s" % settings.LANGUAGE_CODE, ""))
327 def delete_from_cache_by_language(cache, key_template):
328 cache.delete_many([key_template % lc for lc, ln in settings.LANGUAGES])