preferences.py
author Tero Marttila <terom@fixme.fi>
Mon, 16 Feb 2009 00:54:25 +0200
changeset 132 0e857c4a67de
parent 131 67f5d2fdca1d
permissions -rw-r--r--
cache version across calls to version_mercurial, so as to avoid opening the repo every time
"""
    Handling user preferences
"""

import functools
import Cookie

from qmsk.web import urltree
import utils

class Preference (urltree.URLType) :
    """
        A specific preference
    """

    # the name to use
    name = None

    # the default value, as from parse()
    default = None
    
    def is_default (self, value) :
        """
            Returns True if the given post-value is the default value for this preference.

            Defaults to just compare against self.default
        """

        return (value == self.default)
        
    def process (self, preferences, value) :
        """
            Post-process this preference value. This can access the post-processed values of all other preferences that
            were defined before this one in the list given to Preferences. 

            Defaults to just return value.
        """

        return value

class RequestPreferences (object) :
    """
        Represents the specific preferences for some request
    """

    def __init__ (self, preferences, request, value_map=None) :
        """
            Initialize with the given Preferences object, http Request, and { key: value } mapping of raw preference values.

            This will build a mapping of { name: pre-value } using Preference.parse/Preference.default, and then
            post-process them into the final { name: value } mapping using Preference.process, in strict pref_list
            order. Note that the process() method will only have access to those preferences processed before it was.
        """
        
        # store
        self.preferences = preferences
        self.request = request

        # initialize
        self.values = {}
        self.set_cookies = {}

        # initial value map
        pre_values = {}

        # load preferences
        for pref in preferences.pref_list :
            # got a value for it?
            if value_map and pref.name in value_map :
                # get value
                value = value_map[pref.name]

                # parse it
                value = pref.parse(value)

            else :
                # use default value
                value = pref.default
                
            # add
            pre_values[pref.name] = value
        
        # then post-process using Preferences.process(), in strict pref_list order
        for pref in preferences.pref_list :
            # store into self.values, so that pref.get(...) will be able to access the still-incomplete self.values
            # dict
            self.values[pref.name] = pref.process(self, pre_values[pref.name])
    
    def _get_name (self, pref) :
        """
            Look up a Preference's name, either by class, object or name.
        """

        # Preference -> name
        if isinstance(pref, Preference) :
            pref = pref.name

        return pref
    
    def pref (self, name) :
        """
            Look up a Preference by object, name
        """

        # Preference
        if isinstance(name, Preference) :
            return name
        
        # Preference.name
        elif isinstance(name, basestring) :
            return self.preferences.pref_map[name]
        
        # XXX: class?
        else :
            assert False

    def get (self, pref) :
        """
            Return the value for the given Preference, or preference name
        """
        
        # look up
        return self.values[self._get_name(pref)]

    # support dict-access
    __getitem__ = get
    
    def is_default (self, pref) :
        """
            Returns True if the given preference is at its default value
        """
        
        # determine using Preference.is_default
        return self.pref(pref).is_default(self.get(pref))

    def build (self, pref) :
        """
            Like 'get', but return the raw cookie value
        """
        
        # the Preference
        pref = self.pref(pref)
        
        # build
        return pref.build(self.get(pref))
    
    def parse (self, pref, value=None) :
        """
            Parse+process the raw value for some pref into a value object.

            Is the given raw value is None, this uses Preference.default
        """

        # lookup pref
        pref = self.pref(pref)
        
        # build value
        if value is not None :
            # parse
            value = pref.parse(value)
        
        else :
            # default
            value = pref.default
        
        # post-process
        value = pref.process(self, value)

        # return
        return value

    def set (self, name, value_obj=None) :
        """
            Set a new value for the given preference (by str name).

            If value_obj is None, then the preference cookie is unset
        """

        # sanity-check to make sure we're not setting it twice...
        assert name not in self.set_cookies
        
        # None?
        if value_obj is not None :
            # encode using the Preference object
            value_str = self.preferences.pref_map[name].build(value_obj)
        
        else :
            # unset as None
            value_str = None

        # update in our dict
        self.values[name] = value_obj

        # add to set_cookies
        self.set_cookies[name] = value_str

class Preferences (object) :
    """
        Handle user preferences using cookies
    """

    def __init__ (self, pref_list) :
        """
            Use the given list of Preference objects.

            The ordering of the given pref_list is significant for the process() implementation, as the
            Preferences are process()'d in order.
        """

        # store
        self.pref_list = pref_list

        # translate to mapping as well
        self.pref_map = dict((pref.name, pref) for pref in pref_list)

    def load (self, request, ) :
        """
            Load the set of preferences for the given request, and return as a { name -> value } dict
        """

        # the dict of values
        values = {}

        # load the cookies
        cookie_data = request.env.get('HTTP_COOKIE')

        # got any?
        if cookie_data :
            # parse into a SimpleCookie
            cookies = Cookie.SimpleCookie(cookie_data)

            # update the the values
            values.update((morsel.key, morsel.value) for morsel in cookies.itervalues())
        
        else :
            cookies = None

        # apply any query parameters
        for pref in self.pref_list :
            # look for a query param
            value = request.get_arg(pref.name)

            if value :
                # override
                values[pref.name] = value

        # build the RequestPreferences object
        return cookies, RequestPreferences(self, request, values)

    def handler (self, *pref_list) :
        """
            Intended to be used as a decorator for a request handler, this will load the give Preferences and pass
            them to the wrapped handler as keyword arguments, in addition to any others given.
        """

        def _decorator (func) :
            @functools.wraps(func)
            def _handler (request, **args) :
                # load preferences
                cookies, prefs = self.load(request)

                # bind to request.prefs
                # XXX: better way to do this? :/
                request.prefs = prefs

                # update args with new ones
                args.update(((pref.name, prefs.get(pref)) for pref in pref_list))

                # handle to get response
                response = func(request, **args)

                # set cookies?
                if prefs.set_cookies :
                    # default, empty, cookiejar
                    if not cookies :
                        cookies = Cookie.SimpleCookie('')

                    # update cookies
                    for key, value in prefs.set_cookies.iteritems() :
                        if value is None :
                            assert False, "Not implemented yet..."

                        else :
                            # set
                            cookies[key] = value
                            cookies[key]["path"] = config.PREF_COOKIE_PATH
                            cookies[key]["expires"] = config.PREF_COOKIE_EXPIRE_SECONDS

                    # add headers
                    for morsel in cookies.itervalues() :
                        response.add_header('Set-cookie', morsel.OutputString())

                return response
            
            # return wrapped handler
            return _handler
        
        # return decorator...
        return _decorator

# now for our defined preferences....
import pytz
import config

class TimeFormat (urltree.URLStringType, Preference) :
    """
        Time format
    """

    # set name
    name = 'time_format'

    # default value
    default = config.PREF_TIME_FMT_DEFAULT

class DateFormat (urltree.URLStringType, Preference) :
    """
        Date format
    """

    # set name
    name = 'date_format'

    # default value
    default = config.PREF_DATE_FMT_DEFAULT

class TimezoneOffset (Preference) :
    """
        If the DST-aware 'timezone' is missing, we can fallback to a fixed-offset timezone as detected by
        Javascript.

        This is read-only, and None by default
    """

    name = 'timezone_offset'
    default = None

    def parse (self, offset) :
        """
            Offset in minutes -> said minutes
        """

        return int(offset)

class Timezone (Preference) :
    """
        Timezone
    """
    
    # set name
    name = 'timezone'

    # default is handled via process()
    default = 'auto'

    # the list of available (value, name) options for use with helpers.select_options
    OPTIONS = [('auto', "Autodetect")] + [(None, tz_name) for tz_name in pytz.common_timezones]

    def parse (self, name) :
        """
            default -> default
            tz_name -> pytz.timezone
        """
        
        # special-case for 'auto'
        if name == self.default :
            return self.default

        else :
            return pytz.timezone(name)

    def is_default (self, tz) :
        """
            True if it's a FixedOffsetTimezone or PREF_TIMEZONE_FALLBACK
        """

        return (isinstance(tz, utils.FixedOffsetTimezone) or tz == config.PREF_TIMEZONE_FALLBACK)

    def build (self, tz) :
        """
            FixedOffsetTimezone -> None
            pytz.timezone -> tz_name
        """
        
        # special-case for auto/no explicit timezone
        if self.is_default(tz) :
            return self.default

        else :
            # pytz.timezone zone name
            return tz.zone
    
    def process (self, prefs, tz) :
        """
            If this timezone is given, simply build that. Otherwise, try and use TimezoneOffset, and if that fails,
            just return the default.

            None -> FixedOffsetTimezone/PREF_TIMEZONE_FALLBACK
            pytz.timezone -> pytz.timezone
        """
        
        # specific timezone set?
        if tz != self.default :
            return tz
        
        # fixed offset?
        elif prefs[timezone_offset] is not None :
            return utils.FixedOffsetTimezone(prefs[timezone_offset])
        
        # default
        else :
            return config.PREF_TIMEZONE_FALLBACK

class ImageFont (Preference) :
    """
        Font for ImageFormatter
    """

    # set name
    name = 'image_font'
    
    def __init__ (self, font_dict, default_name) :
        """
            Use the given { name: (path, title) } dict and default the given name
        """

        self.font_dict = font_dict
        self.default = self.parse(default_name)
    
    def parse (self, name) :
        """
            name -> (name, path, title)
        """

        path, title = self.font_dict[name]

        return name, path, title
    
    def build (self, font_info) :
        """
            (name, path, title) -> name
        """

        name, path, title = font_info

        return name

class ImageFontSize (urltree.URLIntegerType, Preference) :
    # set name, default
    name = 'image_font_size'
    default = config.PREF_IMAGE_FONT_SIZE_DEFAULT
    
    # XXX: constraints for valid values

class Formatter (Preference) :
    """
        LogFormatter to use
    """

    # set name
    name = 'formatter'

    def __init__ (self, formatters, default) :
        """
            Use the given { name -> class LogFormatter } dict and default (a LogFormatter class)
        """

        self.formatters = formatters
        self.default = default
    
    def parse (self, fmt_name) :
        """
            fmt_name -> class LogFormatter
        """

        return self.formatters[fmt_name]
    
    def build (self, fmt_cls) :
        """
            class LogFormatter -> fmt_name
        """

        return fmt_cls.name
    
    def process (self, prefs, fmt_cls) :
        """
            class LogFormatter -> LogFormatter(tz, time_fmt, image_font.path)
        """

        # time stuff
        tz = prefs[timezone]
        time_fmt = prefs[time_format]
        
        # font stuff
        font_name, font_path, font_title = prefs[image_font]
        font_size = prefs[image_font_size]

        return fmt_cls(tz, time_fmt, font_path, font_size)

class Count (urltree.URLIntegerType, Preference) :
    """
        Number of lines of log data to display per page
    """

    # set name
    name = "count"
    
    # default
    default = config.PREF_COUNT_DEFAULT
    
    def __init__ (self) :
        super(Count, self).__init__(allow_negative=False, allow_zero=False, max=config.PREF_COUNT_MAX)

# and then build the Preferences object
time_format     = TimeFormat()
date_format     = DateFormat()
timezone_offset = TimezoneOffset()
timezone        = Timezone()
image_font      = ImageFont(config.FORMATTER_IMAGE_FONTS, config.PREF_IMAGE_FONT_DEFAULT)
image_font_size = ImageFontSize()
formatter       = Formatter(config.LOG_FORMATTERS, config.PREF_FORMATTER_DEFAULT)
count           = Count()

preferences = Preferences([
    time_format,
    date_format,
    timezone_offset,
    timezone,
    image_font,
    image_font_size,
    formatter,
    count,
])