1 # This file is part of Wolnelektury, licensed under GNU Affero GPLv3 or later.
 
   2 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 
   9 from base64 import urlsafe_b64encode
 
  10 from collections import defaultdict
 
  11 from errno import EEXIST, ENOENT
 
  12 from fcntl import flock, LOCK_EX
 
  13 from os import mkdir, path, unlink
 
  14 from zipfile import ZipFile
 
  16 from django.conf import settings
 
  17 from django.core.files.storage import DefaultStorage
 
  18 from django.core.files.uploadedfile import UploadedFile
 
  19 from django.http import HttpResponse
 
  20 from django.utils.encoding import force_text
 
  22 from reporting.utils import read_chunks
 
  24 # Use the system (hardware-based) random number generator if it exists.
 
  25 if hasattr(random, 'SystemRandom'):
 
  26     randrange = random.SystemRandom().randrange
 
  28     randrange = random.randrange
 
  29 MAX_SESSION_KEY = 18446744073709551616     # 2 << 63
 
  32 def get_random_hash(seed):
 
  33     sha_digest = hashlib.sha1((
 
  35             randrange(0, MAX_SESSION_KEY),
 
  37             str(seed).encode('utf-8', 'replace'),
 
  40     ).encode('utf-8')).digest()
 
  41     return urlsafe_b64encode(sha_digest).decode('latin1').replace('=', '').replace('_', '-').lower()
 
  44 def split_tags(*tag_lists):
 
  45     if len(tag_lists) == 1:
 
  46         result = defaultdict(list)
 
  47         for tag in tag_lists[0]:
 
  48             result[tag.category].append(tag)
 
  50         result = defaultdict(dict)
 
  51         for tag_list in tag_lists:
 
  54                     result[tag.category][tag.pk].count += tag.count
 
  56                     result[tag.category][tag.pk] = tag
 
  57         for k, v in result.items():
 
  58             result[k] = sorted(v.values(), key=lambda tag: tag.sort_key)
 
  62 class ExistingFile(UploadedFile):
 
  64     def __init__(self, path, *args, **kwargs):
 
  66         super(ExistingFile, self).__init__(*args, **kwargs)
 
  68     def temporary_file_path(self):
 
  75 class LockFile(object):
 
  77     A file lock monitor class; createas an ${objname}.lock
 
  78     file in directory dir, and locks it exclusively.
 
  79     To be used in 'with' construct.
 
  81     def __init__(self, dir, objname):
 
  82         self.lockname = path.join(dir, objname + ".lock")
 
  85         self.lock = open(self.lockname, 'w')
 
  86         flock(self.lock, LOCK_EX)
 
  88     def __exit__(self, *err):
 
  92             if oe.errno != ENOENT:
 
  98 def create_zip(paths, zip_slug):
 
 100     Creates a zip in MEDIA_ROOT/zip directory containing files from path.
 
 101     Resulting archive filename is ${zip_slug}.zip
 
 102     Returns it's path relative to MEDIA_ROOT (no initial slash)
 
 104     # directory to store zip files
 
 105     zip_path = path.join(settings.MEDIA_ROOT, 'zip')
 
 109     except OSError as oe:
 
 110         if oe.errno != EEXIST:
 
 112     zip_filename = zip_slug + ".zip"
 
 114     with LockFile(zip_path, zip_slug):
 
 115         if not path.exists(path.join(zip_path, zip_filename)):
 
 116             zipf = ZipFile(path.join(zip_path, zip_filename), 'w')
 
 118                 for arcname, p in paths:
 
 120                         arcname = path.basename(p)
 
 121                     zipf.write(p, arcname)
 
 125         return 'zip/' + zip_filename
 
 128 def remove_zip(zip_slug):
 
 130     removes the ${zip_slug}.zip file from zip store.
 
 132     zip_file = path.join(settings.MEDIA_ROOT, 'zip', zip_slug + '.zip')
 
 135     except OSError as oe:
 
 136         if oe.errno != ENOENT:
 
 140 class AttachmentHttpResponse(HttpResponse):
 
 141     """Response serving a file to be downloaded.
 
 143     def __init__(self, file_path, file_name, mimetype):
 
 144         super(AttachmentHttpResponse, self).__init__(mimetype=mimetype)
 
 145         self['Content-Disposition'] = 'attachment; filename=%s' % file_name
 
 146         self.file_path = file_path
 
 147         self.file_name = file_name
 
 149         with open(DefaultStorage().path(self.file_path)) as f:
 
 150             for chunk in read_chunks(f):
 
 154 class MultiQuerySet(object):
 
 155     def __init__(self, *args, **kwargs):
 
 156         self.querysets = args
 
 161             self._count = sum(len(qs) for qs in self.querysets)
 
 167     def __getitem__(self, item):
 
 169             (offset, stop, step) = item.indices(self.count())
 
 170         except AttributeError:
 
 171             # it's not a slice - make it one
 
 172             return self[item:item + 1][0]
 
 174         total_len = stop - offset
 
 175         for qs in self.querysets:
 
 179                 items += list(qs[offset:stop])
 
 180                 if len(items) >= total_len:
 
 184                     stop = total_len - len(items)
 
 188 class SortedMultiQuerySet(MultiQuerySet):
 
 189     def __init__(self, *args, **kwargs):
 
 190         self.order_by = kwargs.pop('order_by', None)
 
 191         self.sortfn = kwargs.pop('sortfn', None)
 
 192         if self.order_by is not None:
 
 193             self.sortfn = lambda a, b: cmp((getattr(a, f) for f in self.order_by),
 
 194                                            (getattr(b, f) for f in self.order_by))
 
 195         super(SortedMultiQuerySet, self).__init__(*args, **kwargs)
 
 197     def __getitem__(self, item):
 
 198         sort_heads = [0] * len(self.querysets)
 
 200             (offset, stop, step) = item.indices(self.count())
 
 201         except AttributeError:
 
 202             # it's not a slice - make it one
 
 203             return self[item:item + 1][0]
 
 205         total_len = stop - offset
 
 207         i_s = range(len(sort_heads))
 
 209         while len(items) < total_len:
 
 214                     return self.querysets[i][sort_heads[i]]
 
 216                     if candidate is None:
 
 217                         candidate = get_next()
 
 220                         competitor = get_next()
 
 221                         if self.sortfn(candidate, competitor) > 0:
 
 222                             candidate = competitor
 
 225                     continue  # continue next sort_head
 
 226             # we have no more elements:
 
 227             if candidate is None:
 
 229             sort_heads[candidate_i] += 1
 
 232                 continue  # continue next item
 
 233             items.append(candidate)
 
 238 def truncate_html_words(s, num, end_text='...'):
 
 239     """Truncates HTML to a certain number of words (not counting tags and
 
 240     comments). Closes opened tags if they were correctly closed in the given
 
 241     html. Takes an optional argument of what should be used to notify that the
 
 242     string has been truncated, defaulting to ellipsis (...).
 
 244     Newlines in the HTML are preserved.
 
 246     This is just a version of django.utils.text.truncate_html_words with no space before the end_text.
 
 252     html4_singlets = ('br', 'col', 'link', 'base', 'img', 'param', 'area', 'hr', 'input')
 
 253     # Set up regular expressions
 
 254     re_words = re.compile(r'&.*?;|<.*?>|(\w[\w-]*)', re.U)
 
 255     re_tag = re.compile(r'<(/)?([^ ]+?)(?: (/)| .*?)?>')
 
 256     # Count non-HTML words and keep note of open tags
 
 261     while words <= length:
 
 262         m = re_words.search(s, pos)
 
 264             # Checked through whole string
 
 268             # It's an actual non-HTML word
 
 274         tag = re_tag.match(m.group(0))
 
 275         if not tag or end_text_pos:
 
 276             # Don't worry about non tags or tags after our truncate point
 
 278         closing_tag, tagname, self_closing = tag.groups()
 
 279         tagname = tagname.lower()  # Element names are always case-insensitive
 
 280         if self_closing or tagname in html4_singlets:
 
 283             # Check for match in open tags list
 
 285                 i = open_tags.index(tagname)
 
 289                 # SGML: An end tag closes, back to the matching start tag,
 
 290                 # all unclosed intervening start tags with omitted end tags
 
 291                 open_tags = open_tags[i+1:]
 
 293             # Add it to the start of the open tags list
 
 294             open_tags.insert(0, tagname)
 
 296         # Don't try to close tags if we don't need to truncate
 
 298     out = s[:end_text_pos]
 
 301     # Close any tags still open
 
 302     for tag in open_tags:
 
 308 def customizations_hash(customizations):
 
 309     customizations.sort()
 
 310     return hash(tuple(customizations))
 
 313 def get_customized_pdf_path(book, customizations):
 
 315     Returns a MEDIA_ROOT relative path for a customized pdf. The name will contain a hash of customization options.
 
 317     h = customizations_hash(customizations)
 
 318     return 'book/%s/%s-custom-%s.pdf' % (book.slug, book.slug, h)
 
 321 def clear_custom_pdf(book):
 
 323     Returns a list of paths to generated customized pdf of a book
 
 325     from waiter.utils import clear_cache
 
 326     clear_cache('book/%s' % book.slug)
 
 329 class AppSettings(object):
 
 330     """Allows specyfying custom settings for an app, with default values.
 
 332     Just subclass, set some properties and instantiate with a prefix.
 
 333     Getting a SETTING from an instance will check for prefix_SETTING
 
 334     in project settings if set, else take the default. The value will be
 
 335     then filtered through _more_SETTING method, if there is one.
 
 338     def __init__(self, prefix):
 
 339         self._prefix = prefix
 
 341     def __getattribute__(self, name):
 
 342         if name.startswith('_'):
 
 343             return object.__getattribute__(self, name)
 
 344         value = getattr(settings, "%s_%s" % (self._prefix, name), object.__getattribute__(self, name))
 
 345         more = "_more_%s" % name
 
 346         if hasattr(self, more):
 
 347             value = getattr(self, more)(value)
 
 351 def delete_from_cache_by_language(cache, key_template):
 
 352     cache.delete_many([key_template % lc for lc, ln in settings.LANGUAGES])
 
 355 def gallery_path(slug):
 
 356     return os.path.join(settings.MEDIA_ROOT, settings.IMAGE_DIR, slug)
 
 359 def gallery_url(slug):
 
 360     return '%s%s%s/' % (settings.MEDIA_URL, settings.IMAGE_DIR, slug)
 
 363 def get_mp3_length(path):
 
 364     from mutagen.mp3 import MP3
 
 365     return int(MP3(path).info.length)