Mango / utils.py

# -*- coding: utf-8 -*-

from __future__ import with_statement
from contextlib import closing
from datetime import datetime
import htmlentitydefs
import logging
import os
import re
import unicodedata
from urllib2 import Request, urlopen
from urlparse import urlparse

import pytz

from django.conf import settings
from django.core.urlresolvers import reverse
from django.http import HttpResponse
from django.template import loader, RequestContext
from django.template.defaultfilters import stringfilter
from django.utils.encoding import smart_str
from django.utils.http import urlencode
from django.utils.safestring import mark_safe

import mango
import mango.settings
from mango.exceptions import EmptySettingError, InvalidSettingError
from mango.template import Script, StyleSheet

logger = logging.getLogger('mango')
logger.setLevel(logging.DEBUG)

if not logger.handlers:
    _fmt = '%(asctime)s %(name)s %(levelname)-8s : %(message)s'
    _formatter = logging.Formatter(fmt=_fmt, datefmt='%Y-%m-%d %H:%M:%S')

    _console = logging.StreamHandler()
    _console.setLevel(logging.DEBUG)
    _console.setFormatter(_formatter)

    _filename = os.path.join(mango.settings.MANGO_PATH, 'mango.log')
    _logfile = logging.FileHandler(encoding='utf-8', filename=_filename)
    _logfile.setLevel(logging.INFO)
    _logfile.setFormatter(_formatter)

    logger.addHandler(_console)
    logger.addHandler(_logfile)

def absolutize(path):
    """
    Returns the absolute path of `path`. If `path` is not (already) an absolute
    path, it is assumed to be relative to the project directory.
    
    >>> absolutize('mango/examples') == absolutize('mango/../mango/examples/')
    True
    """
    path = path.rstrip(u'/')
    if os.path.isabs(path):
        return path
    fragments = []
    head, tail = os.path.split(path)
    while tail:
        fragments.insert(0, tail)
        head, tail = os.path.split(head)
    return os.path.abspath(os.path.join(mango.settings.PROJECT_PATH,
                                        *fragments))

def akismet_api_key():
    cache = []

    def set(value):
        cache.append(value)
        return value

    def wrapper():
        if cache:
            return cache[0]

        if mango.settings.AKISMET_API_KEY is None:
            return set(None)

        req = Request('http://rest.akismet.com/1.1/verify-key',
                data=urlencode({'blog': mango.settings.BASE_URL,
                                'key': mango.settings.AKISMET_API_KEY}),
                headers={'User-Agent': 'Mango/%s' % mango.VERSION})

        with closing(urlopen(req)) as page:
            if page.read().strip() == 'invalid':
                if settings.DEBUG:
                    raise InvalidSettingError('Akismet rejected the supplied '
                                              'API key',
                                              'Double-check `AKISMET_API_KEY` '
                                              'in mango/settings/custom.py.')
                return set(None)
            else:
                return set(mango.settings.AKISMET_API_KEY)
    return wrapper
akismet_api_key = akismet_api_key()

def akismet_request(kind, dict_):
    return Request('http://%s.rest.akismet.com/1.1/%s'
                   % (akismet_api_key(), kind),
                   data=urlencode(dict_),
                   headers={'User-Agent': 'Mango/%s' % mango.VERSION})

def html_response(template_name, request, context=None, status_code=None):
    if context is None:
        context = {}

    # provide access to the request (without relying upon a context processor)
    context['request'] = request

    # make Mango's settings accessible to templates
    for key, value in mango.settings.__dict__.items():
        if not key.startswith('_'):
            context[key] = value

    # Django settings
    context['settings'] = settings

    # set top-level category
    index = mango.main.Index.get()

    context['archives'] = index.archives()
    context['posts'] = index.descendants()
    context['tags'] = index.tags()

    context['stylesheets'] = stylesheets()
    context['scripts'] = scripts()

    context['VERSION'] = mango.VERSION

    template = loader.select_template((template_name + '.html',
                                       template_name + '.dhtml'))

    response = HttpResponse(template.render(RequestContext(request, context)))
    if status_code is not None:
        response.status_code = status_code

    return response

def lstrip(text, char='0'):
    return text[1:] if text.startswith(char) else text

def nonstringiterable(value):
    """
    Return a non-string iterable for `value`.
    
    >>> nonstringiterable('foo')
    ('foo',)
    >>> nonstringiterable(u'bar')
    (u'bar',)
    >>> nonstringiterable(['baz', 'qux'])
    ['baz', 'qux']
    """
    return (value,) if isinstance(value, basestring) else value

def normalizelinebreaks():
    sub = re.compile(r'\r\n?').sub
    return lambda text: sub('\n', text)
normalizelinebreaks = normalizelinebreaks()

def parsedocdate(date, time, zone):
    """
    Return a UTC `datetime` object derived from `date`, `time`, and `zone`.
    
    For example:
    
    >>> parsedocdate('6 February 2012', '9:00am', 'Pacific/Auckland')
    datetime.datetime(2012, 2, 5, 20, 0, tzinfo=<UTC>)
    
    If `date` and/or `time` are incorrectly formatted, `ValueError` is raised:
    
    >>> parsedocdate('6 Feb', '9AM', 'Pacific/Auckland')
    Traceback (most recent call last):
        ...
    ValueError: time data '6 Feb 9AM' does not match format '%d %B %Y %I:%M%p'
    """
    dt = datetime.strptime('%s %s' % (date, time),
                           '%s %s' % (mango.settings.MARKDOWN_DATE_FORMAT,
                                      mango.settings.MARKDOWN_TIME_FORMAT))
    return pytz.timezone(smart_str(zone)).localize(dt).astimezone(pytz.utc)

def posts_directory():
    """ Creates "posts" directory if it does not already exist. """
    try:
        os.makedirs(mango.settings.DOCUMENTS_PATH)
    except OSError:
        if not os.path.exists(mango.settings.DOCUMENTS_PATH):
            raise OSError('Unable to create %s. Check permissions.'
                          % mango.settings.DOCUMENTS_PATH)

def primary_author_email():
    """
    Returns the primary author's e-mail address as specified in Mango's
    settings file.
    
    >>> mango.settings.PRIMARY_AUTHOR_NAME = u'David Chambers'
    >>> mango.settings.PRIMARY_AUTHOR_EMAIL = u'david@mango.io'
    >>> primary_author_email()
    u'David Chambers <david@mango.io>'
    >>> mango.settings.PRIMARY_AUTHOR_NAME = None
    >>> primary_author_email()
    u'david@mango.io'
    """
    name = mango.settings.PRIMARY_AUTHOR_NAME
    email = mango.settings.PRIMARY_AUTHOR_EMAIL

    if email:
        return '%s <%s>' % (name, email) if name else email

    raise EmptySettingError('PRIMARY_AUTHOR_EMAIL setting is empty',
            'Add PRIMARY_AUTHOR_EMAIL to `mango/settings/custom.py`.')

_fragments = re.compile(r'(<pre>.*?</pre>|'
                        r'<code>.*?</code>|'
                        r'<skip>.*?</skip>)', re.DOTALL).split
_is_snippet = re.compile(r'^<(code|pre|skip)>.*?</\1>$', re.DOTALL).match
_replacements = (
    # ... -> ellipsis
    (re.compile(r'(?<![.])[.]{3}(?![.])').sub, u'\u2026'),
    # [space][hyphen][hyphen][space] -> [thin space][em dash][thin space]
    (re.compile(r' -- ').sub, u'\u2009\u2014\u2009'),
)

def replace(html):
    if not mango.settings.REPLACEMENTS:
        return html

    def replace(text):
        for sub, repl in _replacements:
            text = sub(repl, text)
        return text

    return mark_safe(u''.join(chunk if _is_snippet(chunk) else replace(chunk)
            for chunk in _fragments(html)))

def scripts():
    cache = []
    def wrapper():
        if not cache:
            scripts_root = prefix = mango.settings.JS[0]
            if (prefix and not prefix.startswith('/')
                and not urlparse(prefix).scheme):
                scripts_root = reverse('mango.views.index') + prefix
            cache.append([Script(scripts_root + f)
                          for f in mango.settings.JS[1:]])
        return cache[0]
    return wrapper
scripts = scripts()

def slugify(text):
    """
    Normalizes string, converts to lowercase, removes non-alpha characters
    (except full stops), and converts spaces to hyphens.
    """
    text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore')
    text = '.'.join([re.sub(r'[^\w\s-]', '', t)
                     for t in text.split('.')]).strip().lower()
    return mark_safe(unicode(re.sub(r'[-\s]+', '-', text)))
slugify = stringfilter(slugify)

def stylesheets():
    cache = []
    def wrapper():
        if not cache:
            stylesheets_root = prefix = mango.settings.CSS[0]
            if (prefix and not prefix.startswith('/')
                and not urlparse(prefix).scheme):
                stylesheets_root = reverse('mango.views.index') + prefix
            cache.append([StyleSheet(stylesheets_root + filename, media)
                          for media, filenames in mango.settings.CSS[1:]
                          for filename in nonstringiterable(filenames)])
        return cache[0]
    return wrapper
stylesheets = stylesheets()

def text_response(text, status_code=None):
    response = HttpResponse(text, content_type='text/plain; charset=utf-8')
    if status_code:
        response.status_code = status_code
    return response

# taken from http://effbot.org/zone/re-sub.htm#unescape-html (thanks, Fredrik!)
def unescape(text):
    """
    Removes HTML or XML character references and entities from a text string.
    
    >>> unescape('E = mc&#178;')
    u'E = mc\\xb2'
    >>> unescape('Jack &amp; Jill')
    u'Jack & Jill'
    """
    def fixup(m):
        text = m.group(0)
        if text[:2] == '&#':
            # character reference
            try:
                if text[:3] == '&#x':
                    return unichr(int(text[3:-1], 16))
                else:
                    return unichr(int(text[2:-1]))
            except ValueError:
                pass
        else:
            # named entity
            try:
                text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
            except KeyError:
                pass  # leave as is
        return text
    return re.sub('&#?\w+;', fixup, text)

def validfilenames():
    invalid = re.compile(r'(^_index$|^[.]|[\a\b\s]|~$)').search
    return lambda filenames: [name for name in filenames if not invalid(name)]
validfilenames = validfilenames()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.