restructure into package format - the qmsk.* stuff doesn't work so well though, requires a symlink for qmsk.web to work...
"""
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,
])