diff -r 9c7769850195 -r 6db2527b67cf qmsk/irclogs/preferences.py --- /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, +]) +