terom@53: """ terom@53: Handling user preferences terom@53: """ terom@53: terom@53: import functools terom@53: import Cookie terom@53: terom@53: from qmsk.web import urltree terom@129: import utils terom@53: terom@53: class Preference (urltree.URLType) : terom@53: """ terom@53: A specific preference terom@53: """ terom@53: terom@53: # the name to use terom@53: name = None terom@53: terom@129: # the default value, as from parse() terom@53: default = None terom@53: terom@129: def is_default (self, value) : terom@129: """ terom@129: Returns True if the given post-value is the default value for this preference. terom@129: terom@129: Defaults to just compare against self.default terom@129: """ terom@129: terom@129: return (value == self.default) terom@129: terom@53: def process (self, preferences, value) : terom@53: """ terom@129: Post-process this preference value. This can access the post-processed values of all other preferences that terom@129: were defined before this one in the list given to Preferences. terom@53: terom@53: Defaults to just return value. terom@53: """ terom@53: terom@53: return value terom@53: terom@53: class RequestPreferences (object) : terom@53: """ terom@53: Represents the specific preferences for some request terom@53: """ terom@53: terom@53: def __init__ (self, preferences, request, value_map=None) : terom@53: """ terom@129: Initialize with the given Preferences object, http Request, and { key: value } mapping of raw preference values. terom@129: terom@129: This will build a mapping of { name: pre-value } using Preference.parse/Preference.default, and then terom@129: post-process them into the final { name: value } mapping using Preference.process, in strict pref_list terom@129: order. Note that the process() method will only have access to those preferences processed before it was. terom@53: """ terom@53: terom@53: # store terom@53: self.preferences = preferences terom@53: self.request = request terom@53: terom@53: # initialize terom@53: self.values = {} terom@53: self.set_cookies = {} terom@53: terom@129: # initial value map terom@129: pre_values = {} terom@129: terom@53: # load preferences terom@53: for pref in preferences.pref_list : terom@53: # got a value for it? terom@53: if value_map and pref.name in value_map : terom@53: # get value terom@53: value = value_map[pref.name] terom@53: terom@53: # parse it terom@53: value = pref.parse(value) terom@53: terom@53: else : terom@53: # use default value terom@53: value = pref.default terom@129: terom@53: # add terom@129: pre_values[pref.name] = value terom@53: terom@129: # then post-process using Preferences.process(), in strict pref_list order terom@129: for pref in preferences.pref_list : terom@129: # store into self.values, so that pref.get(...) will be able to access the still-incomplete self.values terom@129: # dict terom@129: self.values[pref.name] = pref.process(self, pre_values[pref.name]) terom@59: terom@129: def _get_name (self, pref) : terom@129: """ terom@129: Look up a Preference's name, either by class, object or name. terom@129: """ terom@129: terom@129: # Preference -> name terom@129: if isinstance(pref, Preference) : terom@129: pref = pref.name terom@129: terom@129: return pref terom@129: terom@129: def pref (self, name) : terom@129: """ terom@129: Look up a Preference by object, name terom@129: """ terom@129: terom@129: # Preference terom@129: if isinstance(name, Preference) : terom@129: return name terom@129: terom@129: # Preference.name terom@129: elif isinstance(name, basestring) : terom@129: return self.preferences.pref_map[name] terom@129: terom@129: # XXX: class? terom@129: else : terom@129: assert False terom@129: terom@53: def get (self, pref) : terom@53: """ terom@62: Return the value for the given Preference, or preference name terom@53: """ terom@53: terom@62: # look up terom@129: return self.values[self._get_name(pref)] terom@59: terom@59: # support dict-access terom@59: __getitem__ = get terom@129: terom@129: def is_default (self, pref) : terom@129: """ terom@129: Returns True if the given preference is at its default value terom@129: """ terom@129: terom@129: # determine using Preference.is_default terom@129: return self.pref(pref).is_default(self.get(pref)) terom@59: terom@129: def build (self, pref) : terom@53: """ terom@129: Like 'get', but return the raw cookie value terom@129: """ terom@129: terom@129: # the Preference terom@129: pref = self.pref(pref) terom@129: terom@129: # build terom@129: return pref.build(self.get(pref)) terom@129: terom@129: def parse (self, pref, value=None) : terom@129: """ terom@129: Parse+process the raw value for some pref into a value object. terom@129: terom@129: Is the given raw value is None, this uses Preference.default terom@129: """ terom@129: terom@129: # lookup pref terom@129: pref = self.pref(pref) terom@129: terom@129: # build value terom@129: if value is not None : terom@129: # parse terom@129: value = pref.parse(value) terom@129: terom@129: else : terom@129: # default terom@129: value = pref.default terom@129: terom@129: # post-process terom@129: value = pref.process(self, value) terom@129: terom@129: # return terom@129: return value terom@129: terom@129: def set (self, name, value_obj=None) : terom@129: """ terom@129: Set a new value for the given preference (by str name). terom@129: terom@129: If value_obj is None, then the preference cookie is unset terom@53: """ terom@53: terom@53: # sanity-check to make sure we're not setting it twice... terom@53: assert name not in self.set_cookies terom@129: terom@129: # None? terom@129: if value_obj is not None : terom@129: # encode using the Preference object terom@129: value_str = self.preferences.pref_map[name].build(value_obj) terom@129: terom@129: else : terom@129: # unset as None terom@129: value_str = None terom@53: terom@53: # update in our dict terom@53: self.values[name] = value_obj terom@53: terom@53: # add to set_cookies terom@53: self.set_cookies[name] = value_str terom@53: terom@53: class Preferences (object) : terom@53: """ terom@53: Handle user preferences using cookies terom@53: """ terom@53: terom@53: def __init__ (self, pref_list) : terom@53: """ terom@129: Use the given list of Preference objects. terom@129: terom@129: The ordering of the given pref_list is significant for the process() implementation, as the terom@129: Preferences are process()'d in order. terom@53: """ terom@53: terom@53: # store terom@53: self.pref_list = pref_list terom@53: terom@53: # translate to mapping as well terom@53: self.pref_map = dict((pref.name, pref) for pref in pref_list) terom@53: terom@53: def load (self, request, ) : terom@53: """ terom@53: Load the set of preferences for the given request, and return as a { name -> value } dict terom@53: """ terom@53: terom@53: # the dict of values terom@53: values = {} terom@53: terom@53: # load the cookies terom@53: cookie_data = request.env.get('HTTP_COOKIE') terom@53: terom@53: # got any? terom@53: if cookie_data : terom@53: # parse into a SimpleCookie terom@53: cookies = Cookie.SimpleCookie(cookie_data) terom@53: terom@53: # update the the values terom@53: values.update((morsel.key, morsel.value) for morsel in cookies.itervalues()) terom@53: terom@53: else : terom@53: cookies = None terom@53: terom@53: # apply any query parameters terom@53: for pref in self.pref_list : terom@53: # look for a query param terom@53: value = request.get_arg(pref.name) terom@53: terom@53: if value : terom@53: # override terom@53: values[pref.name] = value terom@53: terom@53: # build the RequestPreferences object terom@53: return cookies, RequestPreferences(self, request, values) terom@53: terom@53: def handler (self, *pref_list) : terom@53: """ terom@53: Intended to be used as a decorator for a request handler, this will load the give Preferences and pass terom@53: them to the wrapped handler as keyword arguments, in addition to any others given. terom@53: """ terom@53: terom@53: def _decorator (func) : terom@53: @functools.wraps(func) terom@53: def _handler (request, **args) : terom@53: # load preferences terom@53: cookies, prefs = self.load(request) terom@53: terom@59: # bind to request.prefs terom@59: # XXX: better way to do this? :/ terom@59: request.prefs = prefs terom@59: terom@53: # update args with new ones terom@53: args.update(((pref.name, prefs.get(pref)) for pref in pref_list)) terom@53: terom@53: # handle to get response terom@53: response = func(request, **args) terom@53: terom@53: # set cookies? terom@53: if prefs.set_cookies : terom@129: # default, empty, cookiejar terom@59: if not cookies : terom@59: cookies = Cookie.SimpleCookie('') terom@59: terom@53: # update cookies terom@53: for key, value in prefs.set_cookies.iteritems() : terom@129: if value is None : terom@129: assert False, "Not implemented yet..." terom@129: terom@129: else : terom@129: # set terom@129: cookies[key] = value terom@131: cookies[key]["path"] = config.PREF_COOKIE_PATH terom@131: cookies[key]["expires"] = config.PREF_COOKIE_EXPIRE_SECONDS terom@53: terom@53: # add headers terom@53: for morsel in cookies.itervalues() : terom@53: response.add_header('Set-cookie', morsel.OutputString()) terom@53: terom@53: return response terom@53: terom@53: # return wrapped handler terom@53: return _handler terom@53: terom@53: # return decorator... terom@53: return _decorator terom@53: terom@53: # now for our defined preferences.... terom@53: import pytz terom@73: import config terom@53: terom@53: class TimeFormat (urltree.URLStringType, Preference) : terom@53: """ terom@53: Time format terom@53: """ terom@53: terom@53: # set name terom@53: name = 'time_format' terom@53: terom@53: # default value terom@73: default = config.PREF_TIME_FMT_DEFAULT terom@53: terom@53: class DateFormat (urltree.URLStringType, Preference) : terom@53: """ terom@53: Date format terom@53: """ terom@53: terom@53: # set name terom@53: name = 'date_format' terom@53: terom@53: # default value terom@73: default = config.PREF_DATE_FMT_DEFAULT terom@53: terom@129: class TimezoneOffset (Preference) : terom@129: """ terom@129: If the DST-aware 'timezone' is missing, we can fallback to a fixed-offset timezone as detected by terom@129: Javascript. terom@129: terom@129: This is read-only, and None by default terom@129: """ terom@129: terom@129: name = 'timezone_offset' terom@129: default = None terom@129: terom@129: def parse (self, offset) : terom@129: """ terom@129: Offset in minutes -> said minutes terom@129: """ terom@129: terom@129: return int(offset) terom@129: terom@53: class Timezone (Preference) : terom@53: """ terom@53: Timezone terom@53: """ terom@53: terom@53: # set name terom@53: name = 'timezone' terom@53: terom@129: # default is handled via process() terom@129: default = 'auto' terom@129: terom@129: # the list of available (value, name) options for use with helpers.select_options terom@129: OPTIONS = [('auto', "Autodetect")] + [(None, tz_name) for tz_name in pytz.common_timezones] terom@53: terom@53: def parse (self, name) : terom@53: """ terom@129: default -> default terom@53: tz_name -> pytz.timezone terom@53: """ terom@129: terom@129: # special-case for 'auto' terom@129: if name == self.default : terom@129: return self.default terom@53: terom@129: else : terom@129: return pytz.timezone(name) terom@129: terom@129: def is_default (self, tz) : terom@129: """ terom@131: True if it's a FixedOffsetTimezone or PREF_TIMEZONE_FALLBACK terom@129: """ terom@129: terom@131: return (isinstance(tz, utils.FixedOffsetTimezone) or tz == config.PREF_TIMEZONE_FALLBACK) terom@53: terom@53: def build (self, tz) : terom@53: """ terom@129: FixedOffsetTimezone -> None terom@53: pytz.timezone -> tz_name terom@53: """ terom@129: terom@129: # special-case for auto/no explicit timezone terom@129: if self.is_default(tz) : terom@129: return self.default terom@53: terom@129: else : terom@129: # pytz.timezone zone name terom@129: return tz.zone terom@129: terom@129: def process (self, prefs, tz) : terom@129: """ terom@129: If this timezone is given, simply build that. Otherwise, try and use TimezoneOffset, and if that fails, terom@129: just return the default. terom@129: terom@131: None -> FixedOffsetTimezone/PREF_TIMEZONE_FALLBACK terom@129: pytz.timezone -> pytz.timezone terom@129: """ terom@129: terom@129: # specific timezone set? terom@129: if tz != self.default : terom@129: return tz terom@129: terom@129: # fixed offset? terom@129: elif prefs[timezone_offset] is not None : terom@129: return utils.FixedOffsetTimezone(prefs[timezone_offset]) terom@129: terom@129: # default terom@129: else : terom@131: return config.PREF_TIMEZONE_FALLBACK terom@53: terom@79: class ImageFont (Preference) : terom@79: """ terom@79: Font for ImageFormatter terom@79: """ terom@79: terom@79: # set name terom@79: name = 'image_font' terom@79: terom@79: def __init__ (self, font_dict, default_name) : terom@79: """ terom@79: Use the given { name: (path, title) } dict and default the given name terom@79: """ terom@79: terom@79: self.font_dict = font_dict terom@79: self.default = self.parse(default_name) terom@79: terom@79: def parse (self, name) : terom@79: """ terom@79: name -> (name, path, title) terom@79: """ terom@79: terom@79: path, title = self.font_dict[name] terom@79: terom@79: return name, path, title terom@79: terom@79: def build (self, font_info) : terom@79: """ terom@79: (name, path, title) -> name terom@79: """ terom@79: terom@79: name, path, title = font_info terom@79: terom@79: return name terom@79: terom@79: class ImageFontSize (urltree.URLIntegerType, Preference) : terom@79: # set name, default terom@79: name = 'image_font_size' terom@79: default = config.PREF_IMAGE_FONT_SIZE_DEFAULT terom@79: terom@129: # XXX: constraints for valid values terom@79: terom@53: class Formatter (Preference) : terom@53: """ terom@53: LogFormatter to use terom@53: """ terom@53: terom@53: # set name terom@53: name = 'formatter' terom@53: terom@53: def __init__ (self, formatters, default) : terom@53: """ terom@53: Use the given { name -> class LogFormatter } dict and default (a LogFormatter class) terom@53: """ terom@53: terom@53: self.formatters = formatters terom@53: self.default = default terom@53: terom@53: def parse (self, fmt_name) : terom@53: """ terom@53: fmt_name -> class LogFormatter terom@53: """ terom@53: terom@53: return self.formatters[fmt_name] terom@53: terom@53: def build (self, fmt_cls) : terom@53: """ terom@53: class LogFormatter -> fmt_name terom@53: """ terom@53: terom@59: return fmt_cls.name terom@53: terom@53: def process (self, prefs, fmt_cls) : terom@53: """ terom@79: class LogFormatter -> LogFormatter(tz, time_fmt, image_font.path) terom@53: """ terom@53: terom@129: # time stuff terom@129: tz = prefs[timezone] terom@129: time_fmt = prefs[time_format] terom@129: terom@129: # font stuff terom@79: font_name, font_path, font_title = prefs[image_font] terom@79: font_size = prefs[image_font_size] terom@79: terom@129: return fmt_cls(tz, time_fmt, font_path, font_size) terom@53: terom@73: class Count (urltree.URLIntegerType, Preference) : terom@73: """ terom@73: Number of lines of log data to display per page terom@73: """ terom@53: terom@73: # set name terom@73: name = "count" terom@73: terom@73: # default terom@73: default = config.PREF_COUNT_DEFAULT terom@73: terom@73: def __init__ (self) : terom@73: super(Count, self).__init__(allow_negative=False, allow_zero=False, max=config.PREF_COUNT_MAX) terom@73: terom@73: # and then build the Preferences object terom@73: time_format = TimeFormat() terom@73: date_format = DateFormat() terom@129: timezone_offset = TimezoneOffset() terom@73: timezone = Timezone() terom@79: image_font = ImageFont(config.FORMATTER_IMAGE_FONTS, config.PREF_IMAGE_FONT_DEFAULT) terom@79: image_font_size = ImageFontSize() terom@73: formatter = Formatter(config.LOG_FORMATTERS, config.PREF_FORMATTER_DEFAULT) terom@73: count = Count() terom@59: terom@53: preferences = Preferences([ terom@59: time_format, terom@59: date_format, terom@129: timezone_offset, terom@59: timezone, terom@79: image_font, terom@79: image_font_size, terom@59: formatter, terom@73: count, terom@53: ]) terom@53: