diff -r 9c7769850195 -r 6db2527b67cf preferences.py --- a/preferences.py Sun Sep 13 00:49:55 2009 +0300 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,534 +0,0 @@ -""" - 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, -]) -