--- /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,
+])
+