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:
 
 220                     return self.querysets[i][sort_heads[i]]
 
 222                     if candidate is None:
 
 223                         candidate = get_next()
 
 225                         competitor = get_next()
 
 226                         if self.sortfn(candidate, competitor) > 0:
 
 227                             candidate = competitor
 
 229                     continue # continue next sort_head
 
 231             # we have no more elements:
 
 232             if candidate is None:
 
 236                 continue # continue next item
 
 237             items.append(candidate)
 
 242 def truncate_html_words(s, num, end_text='...'):
 
 243     """Truncates HTML to a certain number of words (not counting tags and
 
 244     comments). Closes opened tags if they were correctly closed in the given
 
 245     html. Takes an optional argument of what should be used to notify that the
 
 246     string has been truncated, defaulting to ellipsis (...).
 
 248     Newlines in the HTML are preserved.
 
 250     This is just a version of django.utils.text.truncate_html_words with no space before the end_text.
 
 256     html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input')
 
 257     # Set up regular expressions
 
 258     re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
 
 259     re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
 
 260     # Count non-HTML words and keep note of open tags
 
 265     while words <= length:
 
 266         m = re_words.search(s, pos)
 
 268             # Checked through whole string
 
 272             # It's an actual non-HTML word
 
 278         tag = re_tag.match(m.group(0))
 
 279         if not tag or end_text_pos:
 
 280             # Don't worry about non tags or tags after our truncate point
 
 282         closing_tag, tagname, self_closing = tag.groups()
 
 283         tagname = tagname.lower()  # Element names are always case-insensitive
 
 284         if self_closing or tagname in html4_singlets:
 
 287             # Check for match in open tags list
 
 289                 i = open_tags.index(tagname)
 
 293                 # SGML: An end tag closes, back to the matching start tag, all unclosed intervening start tags with omitted end tags
 
 294                 open_tags = open_tags[i+1:]
 
 296             # Add it to the start of the open tags list
 
 297             open_tags.insert(0, tagname)
 
 299         # Don't try to close tags if we don't need to truncate
 
 301     out = s[:end_text_pos]
 
 304     # Close any tags still open
 
 305     for tag in open_tags:
 
 311 def customizations_hash(customizations):
 
 312     customizations.sort()
 
 313     return hash(tuple(customizations))
 
 316 def get_customized_pdf_path(book, customizations):
 
 318     Returns a MEDIA_ROOT relative path for a customized pdf. The name will contain a hash of customization options.
 
 320     h = customizations_hash(customizations)
 
 321     return 'book/%s/%s-custom-%s.pdf' % (book.slug, book.slug, h)
 
 324 def clear_custom_pdf(book):
 
 326     Returns a list of paths to generated customized pdf of a book
 
 328     from waiter.utils import clear_cache
 
 329     clear_cache('book/%s' % book.slug)
 
 332 class AppSettings(object):
 
 333     """Allows specyfying custom settings for an app, with default values.
 
 335     Just subclass, set some properties and instantiate with a prefix.
 
 336     Getting a SETTING from an instance will check for prefix_SETTING
 
 337     in project settings if set, else take the default. The value will be
 
 338     then filtered through _more_SETTING method, if there is one.
 
 341     def __init__(self, prefix):
 
 342         self._prefix = prefix
 
 344     def __getattribute__(self, name):
 
 345         if name.startswith('_'):
 
 346             return object.__getattribute__(self, name)
 
 347         value = getattr(settings,
 
 348                          "%s_%s" % (self._prefix, name),
 
 349                          object.__getattribute__(self, name))
 
 350         more = "_more_%s" % name
 
 351         if hasattr(self, more):
 
 352             value = getattr(self, more)(value)
 
 356 def trim_query_log(trim_to=25):
 
 358 connection.queries includes all SQL statements -- INSERTs, UPDATES, SELECTs, etc. Each time your app hits the database, the query will be recorded.
 
 359 This can sometimes occupy lots of memory, so trim it here a bit.
 
 362         from django.db import connection
 
 363         connection.queries = trim_to > 0 \
 
 364             and connection.queries[-trim_to:] \
 
 368 def related_tag_name(tag_info, language=None):
 
 369     return tag_info.get("name_%s" % (language or get_language()),
 
 370         tag_info.get("name_%s" % settings.LANGUAGE_CODE, ""))
 
 373 def delete_from_cache_by_language(cache, key_template):
 
 374     cache.delete_many([key_template % lc for lc, ln in settings.LANGUAGES])