qmsk/irclogs/preferences.py
changeset 140 6db2527b67cf
parent 131 67f5d2fdca1d
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/qmsk/irclogs/preferences.py	Sun Sep 13 01:15:56 2009 +0300
@@ -0,0 +1,534 @@
+"""
+    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,
+])
+