qmsk/irclogs/preferences.py
changeset 140 6db2527b67cf
parent 131 67f5d2fdca1d
equal deleted inserted replaced
139:9c7769850195 140:6db2527b67cf
       
     1 """
       
     2     Handling user preferences
       
     3 """
       
     4 
       
     5 import functools
       
     6 import Cookie
       
     7 
       
     8 from qmsk.web import urltree
       
     9 import utils
       
    10 
       
    11 class Preference (urltree.URLType) :
       
    12     """
       
    13         A specific preference
       
    14     """
       
    15 
       
    16     # the name to use
       
    17     name = None
       
    18 
       
    19     # the default value, as from parse()
       
    20     default = None
       
    21     
       
    22     def is_default (self, value) :
       
    23         """
       
    24             Returns True if the given post-value is the default value for this preference.
       
    25 
       
    26             Defaults to just compare against self.default
       
    27         """
       
    28 
       
    29         return (value == self.default)
       
    30         
       
    31     def process (self, preferences, value) :
       
    32         """
       
    33             Post-process this preference value. This can access the post-processed values of all other preferences that
       
    34             were defined before this one in the list given to Preferences. 
       
    35 
       
    36             Defaults to just return value.
       
    37         """
       
    38 
       
    39         return value
       
    40 
       
    41 class RequestPreferences (object) :
       
    42     """
       
    43         Represents the specific preferences for some request
       
    44     """
       
    45 
       
    46     def __init__ (self, preferences, request, value_map=None) :
       
    47         """
       
    48             Initialize with the given Preferences object, http Request, and { key: value } mapping of raw preference values.
       
    49 
       
    50             This will build a mapping of { name: pre-value } using Preference.parse/Preference.default, and then
       
    51             post-process them into the final { name: value } mapping using Preference.process, in strict pref_list
       
    52             order. Note that the process() method will only have access to those preferences processed before it was.
       
    53         """
       
    54         
       
    55         # store
       
    56         self.preferences = preferences
       
    57         self.request = request
       
    58 
       
    59         # initialize
       
    60         self.values = {}
       
    61         self.set_cookies = {}
       
    62 
       
    63         # initial value map
       
    64         pre_values = {}
       
    65 
       
    66         # load preferences
       
    67         for pref in preferences.pref_list :
       
    68             # got a value for it?
       
    69             if value_map and pref.name in value_map :
       
    70                 # get value
       
    71                 value = value_map[pref.name]
       
    72 
       
    73                 # parse it
       
    74                 value = pref.parse(value)
       
    75 
       
    76             else :
       
    77                 # use default value
       
    78                 value = pref.default
       
    79                 
       
    80             # add
       
    81             pre_values[pref.name] = value
       
    82         
       
    83         # then post-process using Preferences.process(), in strict pref_list order
       
    84         for pref in preferences.pref_list :
       
    85             # store into self.values, so that pref.get(...) will be able to access the still-incomplete self.values
       
    86             # dict
       
    87             self.values[pref.name] = pref.process(self, pre_values[pref.name])
       
    88     
       
    89     def _get_name (self, pref) :
       
    90         """
       
    91             Look up a Preference's name, either by class, object or name.
       
    92         """
       
    93 
       
    94         # Preference -> name
       
    95         if isinstance(pref, Preference) :
       
    96             pref = pref.name
       
    97 
       
    98         return pref
       
    99     
       
   100     def pref (self, name) :
       
   101         """
       
   102             Look up a Preference by object, name
       
   103         """
       
   104 
       
   105         # Preference
       
   106         if isinstance(name, Preference) :
       
   107             return name
       
   108         
       
   109         # Preference.name
       
   110         elif isinstance(name, basestring) :
       
   111             return self.preferences.pref_map[name]
       
   112         
       
   113         # XXX: class?
       
   114         else :
       
   115             assert False
       
   116 
       
   117     def get (self, pref) :
       
   118         """
       
   119             Return the value for the given Preference, or preference name
       
   120         """
       
   121         
       
   122         # look up
       
   123         return self.values[self._get_name(pref)]
       
   124 
       
   125     # support dict-access
       
   126     __getitem__ = get
       
   127     
       
   128     def is_default (self, pref) :
       
   129         """
       
   130             Returns True if the given preference is at its default value
       
   131         """
       
   132         
       
   133         # determine using Preference.is_default
       
   134         return self.pref(pref).is_default(self.get(pref))
       
   135 
       
   136     def build (self, pref) :
       
   137         """
       
   138             Like 'get', but return the raw cookie value
       
   139         """
       
   140         
       
   141         # the Preference
       
   142         pref = self.pref(pref)
       
   143         
       
   144         # build
       
   145         return pref.build(self.get(pref))
       
   146     
       
   147     def parse (self, pref, value=None) :
       
   148         """
       
   149             Parse+process the raw value for some pref into a value object.
       
   150 
       
   151             Is the given raw value is None, this uses Preference.default
       
   152         """
       
   153 
       
   154         # lookup pref
       
   155         pref = self.pref(pref)
       
   156         
       
   157         # build value
       
   158         if value is not None :
       
   159             # parse
       
   160             value = pref.parse(value)
       
   161         
       
   162         else :
       
   163             # default
       
   164             value = pref.default
       
   165         
       
   166         # post-process
       
   167         value = pref.process(self, value)
       
   168 
       
   169         # return
       
   170         return value
       
   171 
       
   172     def set (self, name, value_obj=None) :
       
   173         """
       
   174             Set a new value for the given preference (by str name).
       
   175 
       
   176             If value_obj is None, then the preference cookie is unset
       
   177         """
       
   178 
       
   179         # sanity-check to make sure we're not setting it twice...
       
   180         assert name not in self.set_cookies
       
   181         
       
   182         # None?
       
   183         if value_obj is not None :
       
   184             # encode using the Preference object
       
   185             value_str = self.preferences.pref_map[name].build(value_obj)
       
   186         
       
   187         else :
       
   188             # unset as None
       
   189             value_str = None
       
   190 
       
   191         # update in our dict
       
   192         self.values[name] = value_obj
       
   193 
       
   194         # add to set_cookies
       
   195         self.set_cookies[name] = value_str
       
   196 
       
   197 class Preferences (object) :
       
   198     """
       
   199         Handle user preferences using cookies
       
   200     """
       
   201 
       
   202     def __init__ (self, pref_list) :
       
   203         """
       
   204             Use the given list of Preference objects.
       
   205 
       
   206             The ordering of the given pref_list is significant for the process() implementation, as the
       
   207             Preferences are process()'d in order.
       
   208         """
       
   209 
       
   210         # store
       
   211         self.pref_list = pref_list
       
   212 
       
   213         # translate to mapping as well
       
   214         self.pref_map = dict((pref.name, pref) for pref in pref_list)
       
   215 
       
   216     def load (self, request, ) :
       
   217         """
       
   218             Load the set of preferences for the given request, and return as a { name -> value } dict
       
   219         """
       
   220 
       
   221         # the dict of values
       
   222         values = {}
       
   223 
       
   224         # load the cookies
       
   225         cookie_data = request.env.get('HTTP_COOKIE')
       
   226 
       
   227         # got any?
       
   228         if cookie_data :
       
   229             # parse into a SimpleCookie
       
   230             cookies = Cookie.SimpleCookie(cookie_data)
       
   231 
       
   232             # update the the values
       
   233             values.update((morsel.key, morsel.value) for morsel in cookies.itervalues())
       
   234         
       
   235         else :
       
   236             cookies = None
       
   237 
       
   238         # apply any query parameters
       
   239         for pref in self.pref_list :
       
   240             # look for a query param
       
   241             value = request.get_arg(pref.name)
       
   242 
       
   243             if value :
       
   244                 # override
       
   245                 values[pref.name] = value
       
   246 
       
   247         # build the RequestPreferences object
       
   248         return cookies, RequestPreferences(self, request, values)
       
   249 
       
   250     def handler (self, *pref_list) :
       
   251         """
       
   252             Intended to be used as a decorator for a request handler, this will load the give Preferences and pass
       
   253             them to the wrapped handler as keyword arguments, in addition to any others given.
       
   254         """
       
   255 
       
   256         def _decorator (func) :
       
   257             @functools.wraps(func)
       
   258             def _handler (request, **args) :
       
   259                 # load preferences
       
   260                 cookies, prefs = self.load(request)
       
   261 
       
   262                 # bind to request.prefs
       
   263                 # XXX: better way to do this? :/
       
   264                 request.prefs = prefs
       
   265 
       
   266                 # update args with new ones
       
   267                 args.update(((pref.name, prefs.get(pref)) for pref in pref_list))
       
   268 
       
   269                 # handle to get response
       
   270                 response = func(request, **args)
       
   271 
       
   272                 # set cookies?
       
   273                 if prefs.set_cookies :
       
   274                     # default, empty, cookiejar
       
   275                     if not cookies :
       
   276                         cookies = Cookie.SimpleCookie('')
       
   277 
       
   278                     # update cookies
       
   279                     for key, value in prefs.set_cookies.iteritems() :
       
   280                         if value is None :
       
   281                             assert False, "Not implemented yet..."
       
   282 
       
   283                         else :
       
   284                             # set
       
   285                             cookies[key] = value
       
   286                             cookies[key]["path"] = config.PREF_COOKIE_PATH
       
   287                             cookies[key]["expires"] = config.PREF_COOKIE_EXPIRE_SECONDS
       
   288 
       
   289                     # add headers
       
   290                     for morsel in cookies.itervalues() :
       
   291                         response.add_header('Set-cookie', morsel.OutputString())
       
   292 
       
   293                 return response
       
   294             
       
   295             # return wrapped handler
       
   296             return _handler
       
   297         
       
   298         # return decorator...
       
   299         return _decorator
       
   300 
       
   301 # now for our defined preferences....
       
   302 import pytz
       
   303 import config
       
   304 
       
   305 class TimeFormat (urltree.URLStringType, Preference) :
       
   306     """
       
   307         Time format
       
   308     """
       
   309 
       
   310     # set name
       
   311     name = 'time_format'
       
   312 
       
   313     # default value
       
   314     default = config.PREF_TIME_FMT_DEFAULT
       
   315 
       
   316 class DateFormat (urltree.URLStringType, Preference) :
       
   317     """
       
   318         Date format
       
   319     """
       
   320 
       
   321     # set name
       
   322     name = 'date_format'
       
   323 
       
   324     # default value
       
   325     default = config.PREF_DATE_FMT_DEFAULT
       
   326 
       
   327 class TimezoneOffset (Preference) :
       
   328     """
       
   329         If the DST-aware 'timezone' is missing, we can fallback to a fixed-offset timezone as detected by
       
   330         Javascript.
       
   331 
       
   332         This is read-only, and None by default
       
   333     """
       
   334 
       
   335     name = 'timezone_offset'
       
   336     default = None
       
   337 
       
   338     def parse (self, offset) :
       
   339         """
       
   340             Offset in minutes -> said minutes
       
   341         """
       
   342 
       
   343         return int(offset)
       
   344 
       
   345 class Timezone (Preference) :
       
   346     """
       
   347         Timezone
       
   348     """
       
   349     
       
   350     # set name
       
   351     name = 'timezone'
       
   352 
       
   353     # default is handled via process()
       
   354     default = 'auto'
       
   355 
       
   356     # the list of available (value, name) options for use with helpers.select_options
       
   357     OPTIONS = [('auto', "Autodetect")] + [(None, tz_name) for tz_name in pytz.common_timezones]
       
   358 
       
   359     def parse (self, name) :
       
   360         """
       
   361             default -> default
       
   362             tz_name -> pytz.timezone
       
   363         """
       
   364         
       
   365         # special-case for 'auto'
       
   366         if name == self.default :
       
   367             return self.default
       
   368 
       
   369         else :
       
   370             return pytz.timezone(name)
       
   371 
       
   372     def is_default (self, tz) :
       
   373         """
       
   374             True if it's a FixedOffsetTimezone or PREF_TIMEZONE_FALLBACK
       
   375         """
       
   376 
       
   377         return (isinstance(tz, utils.FixedOffsetTimezone) or tz == config.PREF_TIMEZONE_FALLBACK)
       
   378 
       
   379     def build (self, tz) :
       
   380         """
       
   381             FixedOffsetTimezone -> None
       
   382             pytz.timezone -> tz_name
       
   383         """
       
   384         
       
   385         # special-case for auto/no explicit timezone
       
   386         if self.is_default(tz) :
       
   387             return self.default
       
   388 
       
   389         else :
       
   390             # pytz.timezone zone name
       
   391             return tz.zone
       
   392     
       
   393     def process (self, prefs, tz) :
       
   394         """
       
   395             If this timezone is given, simply build that. Otherwise, try and use TimezoneOffset, and if that fails,
       
   396             just return the default.
       
   397 
       
   398             None -> FixedOffsetTimezone/PREF_TIMEZONE_FALLBACK
       
   399             pytz.timezone -> pytz.timezone
       
   400         """
       
   401         
       
   402         # specific timezone set?
       
   403         if tz != self.default :
       
   404             return tz
       
   405         
       
   406         # fixed offset?
       
   407         elif prefs[timezone_offset] is not None :
       
   408             return utils.FixedOffsetTimezone(prefs[timezone_offset])
       
   409         
       
   410         # default
       
   411         else :
       
   412             return config.PREF_TIMEZONE_FALLBACK
       
   413 
       
   414 class ImageFont (Preference) :
       
   415     """
       
   416         Font for ImageFormatter
       
   417     """
       
   418 
       
   419     # set name
       
   420     name = 'image_font'
       
   421     
       
   422     def __init__ (self, font_dict, default_name) :
       
   423         """
       
   424             Use the given { name: (path, title) } dict and default the given name
       
   425         """
       
   426 
       
   427         self.font_dict = font_dict
       
   428         self.default = self.parse(default_name)
       
   429     
       
   430     def parse (self, name) :
       
   431         """
       
   432             name -> (name, path, title)
       
   433         """
       
   434 
       
   435         path, title = self.font_dict[name]
       
   436 
       
   437         return name, path, title
       
   438     
       
   439     def build (self, font_info) :
       
   440         """
       
   441             (name, path, title) -> name
       
   442         """
       
   443 
       
   444         name, path, title = font_info
       
   445 
       
   446         return name
       
   447 
       
   448 class ImageFontSize (urltree.URLIntegerType, Preference) :
       
   449     # set name, default
       
   450     name = 'image_font_size'
       
   451     default = config.PREF_IMAGE_FONT_SIZE_DEFAULT
       
   452     
       
   453     # XXX: constraints for valid values
       
   454 
       
   455 class Formatter (Preference) :
       
   456     """
       
   457         LogFormatter to use
       
   458     """
       
   459 
       
   460     # set name
       
   461     name = 'formatter'
       
   462 
       
   463     def __init__ (self, formatters, default) :
       
   464         """
       
   465             Use the given { name -> class LogFormatter } dict and default (a LogFormatter class)
       
   466         """
       
   467 
       
   468         self.formatters = formatters
       
   469         self.default = default
       
   470     
       
   471     def parse (self, fmt_name) :
       
   472         """
       
   473             fmt_name -> class LogFormatter
       
   474         """
       
   475 
       
   476         return self.formatters[fmt_name]
       
   477     
       
   478     def build (self, fmt_cls) :
       
   479         """
       
   480             class LogFormatter -> fmt_name
       
   481         """
       
   482 
       
   483         return fmt_cls.name
       
   484     
       
   485     def process (self, prefs, fmt_cls) :
       
   486         """
       
   487             class LogFormatter -> LogFormatter(tz, time_fmt, image_font.path)
       
   488         """
       
   489 
       
   490         # time stuff
       
   491         tz = prefs[timezone]
       
   492         time_fmt = prefs[time_format]
       
   493         
       
   494         # font stuff
       
   495         font_name, font_path, font_title = prefs[image_font]
       
   496         font_size = prefs[image_font_size]
       
   497 
       
   498         return fmt_cls(tz, time_fmt, font_path, font_size)
       
   499 
       
   500 class Count (urltree.URLIntegerType, Preference) :
       
   501     """
       
   502         Number of lines of log data to display per page
       
   503     """
       
   504 
       
   505     # set name
       
   506     name = "count"
       
   507     
       
   508     # default
       
   509     default = config.PREF_COUNT_DEFAULT
       
   510     
       
   511     def __init__ (self) :
       
   512         super(Count, self).__init__(allow_negative=False, allow_zero=False, max=config.PREF_COUNT_MAX)
       
   513 
       
   514 # and then build the Preferences object
       
   515 time_format     = TimeFormat()
       
   516 date_format     = DateFormat()
       
   517 timezone_offset = TimezoneOffset()
       
   518 timezone        = Timezone()
       
   519 image_font      = ImageFont(config.FORMATTER_IMAGE_FONTS, config.PREF_IMAGE_FONT_DEFAULT)
       
   520 image_font_size = ImageFontSize()
       
   521 formatter       = Formatter(config.LOG_FORMATTERS, config.PREF_FORMATTER_DEFAULT)
       
   522 count           = Count()
       
   523 
       
   524 preferences = Preferences([
       
   525     time_format,
       
   526     date_format,
       
   527     timezone_offset,
       
   528     timezone,
       
   529     image_font,
       
   530     image_font_size,
       
   531     formatter,
       
   532     count,
       
   533 ])
       
   534