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.conf import settings
18 from os import mkdir, path, unlink
19 from errno import EEXIST, ENOENT
20 from fcntl import flock, LOCK_EX
21 from zipfile import ZipFile
23 from reporting.utils import read_chunks
25 # Use the system (hardware-based) random number generator if it exists.
26 if hasattr(random, 'SystemRandom'):
27 randrange = random.SystemRandom().randrange
29 randrange = random.randrange
30 MAX_SESSION_KEY = 18446744073709551616L # 2 << 63
33 def get_random_hash(seed):
34 sha_digest = hashlib.sha1('%s%s%s%s' %
35 (randrange(0, MAX_SESSION_KEY), time.time(), unicode(seed).encode('utf-8', 'replace'),
36 settings.SECRET_KEY)).digest()
37 return urlsafe_b64encode(sha_digest).replace('=', '').replace('_', '-').lower()
43 result.setdefault(tag.category, []).append(tag)
47 def get_dynamic_path(media, filename, ext=None, maxlen=100):
48 from slughifi import slughifi
50 # how to put related book's slug here?
53 ext = media.formats[media.type].ext
54 if media is None or not media.name:
55 name = slughifi(filename.split(".")[0])
57 name = slughifi(media.name)
58 return 'book/%s/%s.%s' % (ext, name[:maxlen-len('book/%s/.%s' % (ext, ext))-4], ext)
61 # TODO: why is this hard-coded ?
62 def book_upload_path(ext=None, maxlen=100):
63 return lambda *args: get_dynamic_path(*args, ext=ext, maxlen=maxlen)
66 class ExistingFile(UploadedFile):
68 def __init__(self, path, *args, **kwargs):
70 super(ExistingFile, self).__init__(*args, **kwargs)
72 def temporary_file_path(self):
79 class LockFile(object):
81 A file lock monitor class; createas an ${objname}.lock
82 file in directory dir, and locks it exclusively.
83 To be used in 'with' construct.
85 def __init__(self, dir, objname):
86 self.lockname = path.join(dir, objname + ".lock")
89 self.lock = open(self.lockname, 'w')
90 flock(self.lock, LOCK_EX)
92 def __exit__(self, *err):
96 if oe.errno != EEXIST:
102 def create_zip(paths, zip_slug):
104 Creates a zip in MEDIA_ROOT/zip directory containing files from path.
105 Resulting archive filename is ${zip_slug}.zip
106 Returns it's path relative to MEDIA_ROOT (no initial slash)
108 # directory to store zip files
109 zip_path = path.join(settings.MEDIA_ROOT, 'zip')
113 except OSError as oe:
114 if oe.errno != EEXIST:
116 zip_filename = zip_slug + ".zip"
118 with LockFile(zip_path, zip_slug):
119 if not path.exists(path.join(zip_path, zip_filename)):
120 zipf = ZipFile(path.join(zip_path, zip_filename), 'w')
122 for arcname, p in paths:
124 arcname = path.basename(p)
125 zipf.write(p, arcname)
129 return 'zip/' + zip_filename
132 def remove_zip(zip_slug):
134 removes the ${zip_slug}.zip file from zip store.
136 zip_file = path.join(settings.MEDIA_ROOT, 'zip', zip_slug + '.zip')
139 except OSError as oe:
140 if oe.errno != ENOENT:
144 class AttachmentHttpResponse(HttpResponse):
145 """Response serving a file to be downloaded.
147 def __init__ (self, file_path, file_name, mimetype):
148 super(AttachmentHttpResponse, self).__init__(mimetype=mimetype)
149 self['Content-Disposition'] = 'attachment; filename=%s' % file_name
150 self.file_path = file_path
151 self.file_name = file_name
153 with open(DefaultStorage().path(self.file_path)) as f:
154 for chunk in read_chunks(f):
157 class MultiQuerySet(object):
158 def __init__(self, *args, **kwargs):
159 self.querysets = args
164 self._count = sum(len(qs) for qs in self.querysets)
170 def __getitem__(self, item):
172 indices = (offset, stop, step) = item.indices(self.count())
173 except AttributeError:
174 # it's not a slice - make it one
175 return self[item : item + 1][0]
177 total_len = stop - offset
178 for qs in self.querysets:
182 items += list(qs[offset:stop])
183 if len(items) >= total_len:
187 stop = total_len - len(items)
191 def truncate_html_words(s, num, end_text='...'):
192 """Truncates HTML to a certain number of words (not counting tags and
193 comments). Closes opened tags if they were correctly closed in the given
194 html. Takes an optional argument of what should be used to notify that the
195 string has been truncated, defaulting to ellipsis (...).
197 Newlines in the HTML are preserved.
199 This is just a version of django.utils.text.truncate_html_words with no space before the end_text.
205 html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input')
206 # Set up regular expressions
207 re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
208 re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
209 # Count non-HTML words and keep note of open tags
214 while words <= length:
215 m = re_words.search(s, pos)
217 # Checked through whole string
221 # It's an actual non-HTML word
227 tag = re_tag.match(m.group(0))
228 if not tag or end_text_pos:
229 # Don't worry about non tags or tags after our truncate point
231 closing_tag, tagname, self_closing = tag.groups()
232 tagname = tagname.lower() # Element names are always case-insensitive
233 if self_closing or tagname in html4_singlets:
236 # Check for match in open tags list
238 i = open_tags.index(tagname)
242 # SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags
243 open_tags = open_tags[i+1:]
245 # Add it to the start of the open tags list
246 open_tags.insert(0, tagname)
248 # Don't try to close tags if we don't need to truncate
250 out = s[:end_text_pos]
253 # Close any tags still open
254 for tag in open_tags:
260 def customizations_hash(customizations):
261 customizations.sort()
262 return hash(tuple(customizations))
265 def get_customized_pdf_path(book, customizations):
267 Returns a MEDIA_ROOT relative path for a customized pdf. The name will contain a hash of customization options.
269 h = customizations_hash(customizations)
270 return 'book/%s/%s-custom-%s.pdf' % (book.slug, book.slug, h)
273 def clear_custom_pdf(book):
275 Returns a list of paths to generated customized pdf of a book
277 from waiter.utils import clear_cache
278 clear_cache('book/%s' % book.slug)
281 class AppSettings(object):
282 """Allows specyfying custom settings for an app, with default values.
284 Just subclass, set some properties and instantiate with a prefix.
285 Getting a SETTING from an instance will check for prefix_SETTING
286 in project settings if set, else take the default. The value will be
287 then filtered through _more_SETTING method, if there is one.
290 def __init__(self, prefix):
291 self._prefix = prefix
293 def __getattribute__(self, name):
294 if name.startswith('_'):
295 return object.__getattribute__(self, name)
296 value = getattr(settings,
297 "%s_%s" % (self._prefix, name),
298 object.__getattribute__(self, name))
299 more = "_more_%s" % name
300 if hasattr(self, more):
301 value = getattr(self, more)(value)
305 def trim_query_log(trim_to=25):
307 connection.queries includes all SQL statements -- INSERTs, UPDATES, SELECTs, etc. Each time your app hits the database, the query will be recorded.
308 This can sometimes occupy lots of memory, so trim it here a bit.
311 from django.db import connection
312 connection.queries = trim_to > 0 \
313 and connection.queries[-trim_to:] \