"""I Want Options

*** NOTE ***

 This is a copy of the I Want Options library as it was on March 18,
 2003. Development has almost surely progressed by then. You can get a
 more recent version from the NewsBruiser development site at
 http://newsbruiser.tigris.org/.

*** NOTE ***

A library for managing an application's configuration, user
preferences, and/or other miscellaneous options through the application
itself. You provide the application, it provides the configuration
management.

This library has no external dependencies. It defines the following
classes:

* OptionConfig: reads from the option definition store. You can
  extend this class to read from a different format (e.g. from an XML
  file or a database).

* OptionGroup: A container for options. You probably don't need to
  extend this class.

* Option: Defines the semantics for an option (e.g. a boolean or a
  string option). You can extend this class to create options with
  application-specific semantics (see TimeZoneOption for a
  complex but generic example).

* Context: Defines the unit of configuration (e.g. for a user
  preference engine, this is your application's notion of "user"). You
  should make some existing class in your application extend this. A
  simple FileBackedContext, which simply writes key-value pairs to a
  file, is provided.

* OptionWrapper: Defines display semantics for your application.
  If you don't extend this, your option screen will look very boring
  and not like the rest of your application.

* ConfigurationCGI: Very simple logic for an option-setting CGI. You
  probably want to write your own CGI that fits in with whatever Web
  framework you're using, but this will get you started (and is great
  for demos).

* StringOption, LongStringOption, BooleanOption, SelectionOption,
  IntegerOption, EmailOption, HTMLColorOption, LanguageOption,
  TimeZoneOption, etc: Extensions of Option which implement different
  display, gathering, and validation interfaces.

To use, initialize an OptionConfig object with the path to your option
definition store and (optionally) an instance of OptionWrapper. Pass
an instance of Context to all OptionConfig methods which require one.

The option definition store looks like this:

group=OptionGroup1
This is the first option group.

name=option1
displayName=This is the first option in option group 1.
property=Options can have arbitrary properties, but the first property must be "name".
#Comments are allowed as well.

name=option2
displayName=This is the second option in option group 1.

group=OptionGroup2
This is the second option group.

name=option1
displayName=This is the first option in option group 2.

...

"""

__author__ = "Leonard Richardson (leonardr@segfault.org)"
__version__ = "$Revision: 1.31 $"
__date__ = "$Date: 2003/03/19 03:56:06 $"
__copyright__ = "Copyright (c) 2002-2003 Leonard Richardson"
__license__ = "Python"

import cgi
import math
import os
import re
import string
import sys
import time
import types

DEFAULT_OPTION_TYPE = 'string'

class OptionConfig:

    """This class reads from the option definition store. It instantiates
    OptionGroup objects, which contain Option objects."""

    def __init__(self, filename, wrapper=None):
        """Initialize variables; the data structures are populated
        lazily."""

        self.filename = filename
        if not wrapper:
            wrapper = OptionWrapper()
        self.wrapper = wrapper
        self.wrapper.config = self

        #A list of OptionGroup objects in definition order.
        self.optionGroupList = None

        #A mapping of option group names to OptionGroup objects.
        self.optionGroupMap = None

        #A mapping of option names to Option objects. If multiple option
        #groups define options with the same name, the contents of this
        #map are undefined.
        self.optionMap = None

    def getOptionMap(self):
        """Returns a mapping of option names to Option objects. If multiple
        option groups define options with the same name, the contents of this
        map are undefined; use the option group interface instead."""
        if not self.optionMap:
            self.__getOptionDefinitions()
        return self.optionMap

    def getOptionGroupList(self):
        """Returns a list of OptionGroup objects, in the order in which they
        were defined."""
        if not self.optionGroupList:
            self.__getOptionDefinitions()
        return self.optionGroupList

    def getOptionGroupMap(self):
        """Returns a mapping of names of option groups to their corresponding
        OptionGroup objects."""
        if not self.optionGroupMap:
            self.__getOptionDefinitions()
        return self.optionGroupMap

    def getOption(self, name):
        """Convenience method to retrieve an option by name. This
        method's behavior is undefined if multiple option groups
        define options with the same name."""
        return self.getOptionMap()[name]

    def getOptionValue(self, name, context):
        """Convenience method to retrieve the value of a named option
        in the given context. This method's behavior is undefined if
        multiple option groups define options with the same name."""
        return self.getOptionMap()[name].getValue(context)

    def getOptionDefault(self, name, context):
        """Convenience method to obtain the default value of the named
        option in the given context. This method's behavior is undefined if
        multiple option groups define options with the same name."""
        return self.getOptionMap()[name].getDefault(context)

    def getRelevantGroups(self, context):
        """Returns only those option groups which contain options
        relevant to the given context."""
        groups = []
        for group in self.getOptionGroupList():
            if group.containsRelevantOptions(context):
                groups.append(group)
        return groups

    def __getOptionDefinitions(self):
        """Loads option definitions into the data structures. If the
        option definitions have already been loaded, does nothing."""
        if not self.optionGroupList:
            self.optionGroupList = []
            self.optionMap = {}
            self.optionGroupMap = {}
            self.loadOptionDefinitions(self.filename)

    def loadOptionDefinitions(self, filename):
        """Called by the internal initialization on the file you
        specify in the OptionConfig constructor. You can call this
        manually on additional files to dynamically add options to the
        option configuration."""
        file = open(filename)
        currentGroup = None
        optionProperties = None
        for line in file.readlines():
            if line[0] != '\n' and line[0] != '#':
                if line[:6] == 'group=':                        
                    if currentGroup and optionProperties:
                        self.__createOption(optionProperties, currentGroup)
                    currentGroup = OptionGroup(line[6:-1], self.wrapper)
                    self.optionGroupList.append(currentGroup)
                    self.optionGroupMap[currentGroup.name] = currentGroup
                    optionProperties = None
                elif self.optionGroupList and line[:7] == 'option=':
                    if optionProperties:
                        #Time to create a new option.
                        self.__createOption(optionProperties, currentGroup)
                    else:
                        optionProperties = {}                    
                    optionProperties['name'] = line[7:-1]
                elif self.optionGroupList and optionProperties:
                    #The file is defining another property of
                    #the current option.
                    i = string.find(line, '=')
                    if i > 0:
                        optionProperties[line[:i]] = line[i+1:-1]
                    elif currentGroup and not currentGroup.options:
                        text = line[:-1]
                        currentGroup.setText(text)
        if optionProperties:
            self.__createOption(optionProperties, currentGroup)
        file.close()

    def __createOption(self, properties, defaultGroup):
        """Transforms a set of option properties and an OptionGroup object
        into an Option object, and registers the Option object with its
        group and the map of options by name."""

        #First, see if the option itself specifies an (already defined)
        #option group, which would override any current group.
        groupName = properties.get('optionGroup', None)
        group = self.optionGroupMap.get(groupName, defaultGroup)
        if not group:
            raise "Option %s is not in any group!" % properties['name']

        optionType = properties.get('type', DEFAULT_OPTION_TYPE)
        optionClass = self.wrapper.optionClasses.get(optionType, None)
        if not optionClass:
            optionClass = sys.modules[self.wrapper.__module__].__dict__[optionType]
        option = optionClass(self.wrapper, properties)
        self.optionMap[option.name] = option
        group.addOption(option)
        properties.clear()

class OptionGroup:    
    def __init__(self, name, wrapper):
        self.name = name
        self.options = []
        self.text = ''
        self.wrapper = wrapper

        #Whether or not this option group contains settable options for
        #various contexts.
        self.settable = {}

    def containsRelevantOptions(self, context):
        """Returns true iff this option group contains at least one option
        which can be set in the given context."""
        settable = self.settable.get(context, None)
        if settable == None:
            settable = 0
            for option in self.options:
                if option.isRelevant(context):
                    settable = 1
                    break
            self.settable[context] = settable
        return settable

    def validate(self, context, formVariables):
        """Validate all the options in this group against the given
        set of form variables. Returns a list of errors."""
        errors = []
        for option in self.options:
            if option.isRelevant(context):
                newErrors = option.validate \
                            (context, option.processFormValue(formVariables))
                if newErrors:
                    if type(newErrors) == types.ListType:
                        errors.extend(newErrors)
                    elif type(newErrors) == types.StringType:
                        errors.append(newErrors)
        return errors
    
    def setText(self, text):
        """Sets the descriptive text for this option group."""
        self.text = text

    def getText(self, context):
        """Returns the descriptive text for this option group."""
        return self.wrapper.getGroupDescription(self, context)

    def addOption(self, option):
        """Adds an option to the end of the list of options defined in
        this group."""
        self.options.append(option)

    def getOptions(self):
        """Returns the list of options defined in this group."""
        return self.options

    def render(self, context, presetValues):
        """Renders this option group as HTML."""
        self.wrapper.renderGroup(self, context, presetValues)

class Option:

    def __init__(self, wrapper, properties):
        self.wrapper = wrapper
        for (key, value) in properties.items():
            setattr(self, key, value)

    ### Some methods are specific to the implementation, and delegate
    ### to the OptionWrapper (defined below). Subclasses may add logic
    ### of their own in addition to or instead of delegating to the
    ### wrapper.
            
    def isRelevant(self, context):
        """Returns true iff this option can be set in the given context."""
        return self.wrapper.isRelevant(self, context)

    def renderControl(self, context, presetValue=None):
        """Renders the control to edit this option, as well as running
        any application-specific UI wrapper code."""
        self.wrapper.renderControl(self, context, presetValue)

    def getControllingOption(self, context):
        """Returns the boolean option, if any, whose value controls
        the appearance of this option. This gives you an easy way of
        making one option depend on another without having to define a
        special isRelevant() method for it."""
        return self.wrapper.getControllingOption(self, context)

    ### Some methods are specific to the type of option.

    def getValue(self, context, override=None):
        """Returns the value of this option as set in the given context."""
        val = override
        if not val:
            val = context.get(self.name, None)
        if val == None:
            val = self.getDefault(context)
        return val

    def renderEditControl(self, context, presetValue=None):
        pass

    def validate(self, context, value):
        """Return a list of problems with the given value."""
        pass

    def getDefault(self, context):
        """Returns the default value of this attribute in the given context."""
        default = self.getStandardDefault(context)
        if hasattr(self, 'default'):
            default = self.wrapper.getOptionDefault(self, context)
        return default

    def getStandardDefault(self, context):
        """Returns the default value of this attribute as defined in its
        definition (before it's given to wrapper.getOptionDefault)."""
        return ''

    def processFormValue(self, vars):
        """Obtain the value of this option from a list of CGI variables,
        perform any neccessary processing, and return the processed value."""
        return vars.get(self.name, '')

class Context:

    """The context is the unit of configuration. It represents some
    resource controlled by your application, such that each different
    instance of the resource can have its own configuration settings.
    In NewsBruiser this is the Notebook object (since each notebook
    has its own distinct settings). If you are using this library to
    implement user options, this would be your user object.

    The get(), set(), and save() methods implement the interface to
    your configuration store."""

    def get(self, key, default=None):
        self.__abstract()

    def set(self, key, value):
        self.__abstract()

    def save(self, authenticationToken=None):
        """A context which saves values on set() doesn't need a save(). You
        also might want to override this with a version of save() which takes
        an authentication token."""
        pass    

    def __abstract(self):
        raise "This is an abstract class."

    def __getitem__(self, key):
        return self.get(key)

    def __setitem__(self, key, value):
        return self.set(key, value)

### Very simple sample implementations of various classes which you
### should implement yourself in a way the fits in with your
### application framework.

class OptionWrapper:

    """The Option class tree implements different types of options.
    Rather than making you subclass each Option subclass to get your
    application-specific behavior for each option, or change each
    Option subclass to subclass a superclass you define, implement
    your application-specific Option and OptionGroup behavior in an
    OptionWrapper subclass and pass in an instance of it to the
    OptionConfig constructor.

    From this class you can define new option classes, change the way
    options are rendered, and the like.

    External example: NewsBruiser extends this with the
    Options.NBOptionWrapper class."""

    def __init__(self):
        self.optionClasses = {}
        self.scanModuleForOptionClasses(sys.modules[OptionWrapper.__module__])

    def scanModuleForOptionClasses(self, module):
        """Scans the given module for classes that provide an
        OPTION_KEY member, and registers them in the optionClasses map
        under their OPTION_KEY. If you define additional option
        classes in OptionWrapper and you want to use key names for
        them rather than their full class names, you can define
        OPTION_KEY in each Option subclass in the module and call
        scanModuleForOptionClasses(sys.modules[self.__module__]) in
        the constructor of your OptionWrapper subclass."""
        for c in module.__dict__.values():
            if hasattr(c, 'OPTION_KEY'):
                self.optionClasses[c.OPTION_KEY] = c
                
    def renderGroup(self, group, context, presetValues={}):
        """Render a group of options. Does not print out the group
        text; if you want that, you need to do it separately."""

        for option in group.getOptions():
            if option.isRelevant(context):
                option.renderControl(context, presetValues.get(option.name))

    def renderControl(self, option, context, presetValue=None):
        """Render an HTML depiction of the given option."""
        print '<p>'
        if hasattr(option, 'displayName'):
            print '<b>%s</b>:' % self.getOptionDisplayName(option, context)
        option.renderEditControl(context, presetValue)
        print '<br/>'
        if hasattr(option, 'description'):
            print self.getOptionDescription(option, context)
        print '</p>'

    def isRelevant(self, option, context):
        """Returns true iff this option can be set in the given context."""
        relevant = 1
        controllingOption = option.getControllingOption(context)
        if controllingOption:
            relevant = controllingOption.getValue(context)
        return relevant

    def getControllingOption(self, option, none):
        """A hook method to retrieve any option whose value controls whether
        or not this option is relevant."""
        controllingOption = None
        optionName = getattr(option, 'controllingOption', None)
        if optionName:
            controllingOption = self.config.getOption(optionName)
        return controllingOption

    def getOptionDisplayName(self, option, context):
        """A hook method to retrieve and possibly transform an option's
        display name."""
        return option.displayName

    def getOptionDescription(self, option, context):
        """A hook method to retrieve and possibly transform an option's
        description."""
        return option.description

    def getOptionDefault(self, option, context):
        """A hook method to retrieve and possibly transform an option's
        default value."""
        return option.default

    def getGroupDescription(self, group, context):
        """A hook method to retrieve and possibly transform a group's
        description."""
        return group.text

class FileBackedContext(Context):
    """For applications in which the configuration is global, affecting the
    entire application, you can use this simple context which keeps
    its configuration options in a file whose path you specify."""

    def __init__(self, config, path):
        self.path = path
        self.map = {}
        self.config = config
        if os.path.exists(self.path):
            file = open(self.path, 'r')
            for line in file.readlines():
                (key,value) = string.split(line[:-1], '=', 1)
                self.map[key] = value

    def get(self, name, default=None):
        if not default:
            default = self.config.getOptionDefault(name, self)
        return self.map.get(name, default)

    def set(self, name, value):
        self.map[name] = value

    def save(self):
        file = open(self.path, 'w')
        for (key, value) in self.map.items():
            file.write('%s=%s\n' % (key, string.replace(value, '\n', ' ')))
        file.close()

class ConfigurationCGI:

    """An ultra-simple configuration CGI class. Pass in an
    OptionConfig object and it will print out a form. Change items on
    the form, submit, and they're validated and propagated to the
    Context in your OptionConfig. You probably want to write your own
    CGI to make it fit in with your framework, but this is good for demos
    and quick starts.

    External example: see NewsBruiser's configure.cgi, which has
    similar logic."""
    
    import cgi

    SELECTED_GROUP = 'group'
    SUBMIT_BUTTON = 'submit'
    
    def __init__(self, config, context, formURL, title=''):
        self.config = config
        self.context = context
        self.formURL = formURL
        self.title = title

        if not hasattr(self, 'variables'):
            self.getVariables()

        self.groups = self.config.getOptionGroupList()
        groupName = self.variables.get(self.SELECTED_GROUP)
        self.selectedGroup = self.config.getOptionGroupMap().get \
                             (groupName, self.groups[0])

        print "Content-type: text/html\n"
        if self.variables.get(self.SUBMIT_BUTTON):
            errors = self.selectedGroup.validate(self.context, self.variables)
            if errors:
                print '<h1>Errors:</h1>'
                print '<ul>'
                for error in errors:
                    print '<li>%s</li>' % error
                print '</ul>'
            else:
                self.saveChanges()        
        self.startPage()
        self.printForm()
        self.endPage()

    def getVariables(self):
        self.variables = {}
        form = cgi.FieldStorage()
        if form.list:
            for key in form.keys():            
                if type(form[key]) == types.ListType:
                    #This is to handle one CGI field with multiple
                    #values. We want a list of the string values
                    #rather than a list of MiniFieldStorage objects.
                    value = []
                    for field in form[key]:
                        value.append(field.value)
                else:
                    value = form[key].value
                self.variables[key] = value
    
    def saveChanges(self):
        for option in self.selectedGroup.getOptions():
            val = option.processFormValue(self.variables)
            self.context.set(option.name, val)
        self.context.save()

    def startPage(self):
        print '<html>'
        if self.title:
            print '<head><title>%s</title></head>' % self.title
        print '<body>'                

    def endPage(self):
        print '</body></html>'        

    def makeSelfURL(self, extraQueryString):
        addOn = '?'
        if addOn in self.formURL:
            addOn = '&'
        return self.formURL + addOn + extraQueryString            

    def startForm(self):
        print '<form action="%s" method="post">' % self.formURL

    def endForm(self):
        print '</form>'

    def printForm(self):
        self.startForm()
        print '<input type="hidden" name="%s" value="%s">' % \
              (self.SELECTED_GROUP, self.selectedGroup.name)
        if len(self.groups) > 1:
            print '['
            for i in range(0, len(self.groups)):
                group = self.groups[i]
                if group.name == self.selectedGroup.name:
                    print group.name
                else:
                    url = self.makeSelfURL\
                          (self.SELECTED_GROUP + '=' + group.name)
                    print '<a href="%s">%s</a>' % (url, group.name)
                if i < len(self.groups) - 1:
                    print ' | '
            print ']'
        text = self.selectedGroup.getText(self.context)
        if text:
            print '<p><i>%s</i></p>' % text
        self.selectedGroup.render(self.context, self.variables)
        print '<input type="submit" name="%s" value="Save changes">' \
              % self.SUBMIT_BUTTON
        self.endForm()

    def __getitem__(self, key):
        "Returns a value from the configuration."
        return self.config.getOptionValue(key, self.context)

#### Everything below this line is an Option implementation

### Some simple types of options

class StringOption(Option):

    OPTION_KEY = DEFAULT_OPTION_TYPE

    def renderEditControl(self, context, presetValue=None):
        """The edit control for this option is a simple text field."""
        if not hasattr(self, 'size'):
            self.size = 50
        if not hasattr(self, 'maxLength'):
            self.maxLength = None
        if self.maxLength and int(self.size) > int(self.maxLength):
            self.size = self.maxLength
        print '<input type="text" name="%s" value="%s" size="%s" maxlength="%s">' % (self.name, cgi.escape(self.getEditableValue(context, presetValue), 1), self.size, self.maxLength)

    def validate(self, context, value):
        max = getattr(self, 'maxLength', None)
        if max and len(value) > int(max):
            return '"%s" is too long to be a valid value for for "%s" (maximum length is %s).' % (value, self.displayName, max)

    def getEditableValue(self, context, presetValue):
        """Returns the value of this option, suitable for display in an HTML
        form text box."""
        value = str(self.getValue(context, presetValue))
        return value

class LongStringOption(StringOption):

    OPTION_KEY = 'longString'

    def renderEditControl(self, context, presetValue):
        """The edit control for this option is a text area."""
        if not hasattr(self, 'rows'):
            self.rows = 3
        print '<p><textarea wrap="virtual" rows="%s" cols="%s" name="%s">%s</textarea></p>' % (self.rows, 80, self.name, self.getEditableValue(context, presetValue))

class BooleanOption(Option):

    OPTION_KEY = 'boolean'

    def getStandardDefault(self, context):
        return 0

    def getValue(self, context, override=None):
        "Returns the value of this option as set for the given context."
        val = override
        if not val:
            val = Option.getValue(self, context)
        if type(val) == types.StringType:
            val = string.upper(val)
            if val == 'Y' or val == 'TRUE':
                val = 1
            else:
                val = 0
        return val

    def processFormValue(self, vars):
        val = 'false'
        if vars.has_key(self.name):
            checkbox = '%s-checkbox' % self.name
            if vars.has_key(checkbox):
                if vars[self.name]:
                    val = 'true'
        return val

    def renderEditControl(self, context, presetValue):
        print '<input type="hidden" name="%s" value="true">' % self.name
        print '<input type="checkbox" name="%s-checkbox"' % self.name,
        if self.getValue(context):
            print 'checked="checked"',
        print '>'

class SelectionOption(StringOption):

    OPTION_KEY = 'selection'

    def isMultiple(self):
        return hasattr(self, 'multiple')

    def isSelected(self, value, context, presetValue=None):
        selected = 0
        if presetValue:
            multiple = self.isMultiple()
            if multiple:
                selected = value in presetValue
            else:
                selected = value == presetValue
        else:
            selected = value in self.getSelectedValues(context)
        return selected
    
    def getSelectionValues(self):
        return string.split(self.values, ',')

    def getSelectionValueDescriptions(self):
        return string.split(self.valueDescriptions, ',')

    def getSelectedValues(self, context):
        "Returns all selected values."
        values = self.getValue(context)
        if self.isMultiple():            
            values = string.split(values, ',')
        else:
            values = [values]
        return values

    def getDescriptionForValue(self, value):
        "Returns the description for a given value."
        descriptions = self.getSelectionValueDescriptions()
        values = self.getSelectionValues()
        description = None
        for i in range(0, len(values)):
            if values[i] == value:
                description = descriptions[i]
                break
        return description

    def processFormValue(self, vars):
        if self.isMultiple():
            value = vars.get(self.name, '')
            if type(value) == types.StringType:
                value = value
            else:
                value = string.join(value, ',')
        else:
            value = StringOption.processFormValue(self, vars)
        return value

    def validate(self, context, value):
        "Throw an exception if this value is invalid."
        errors = []
        values = [value]
        if self.isMultiple() and ',' in value:
            values = string.split(value, ',')
        for value in values:
            if value and not value in self.getSelectionValues():
                errors.append('"%s" is not a valid selection value for the "%s" option.' % (value, self.displayName))
        return errors

    def getSize(self):
        if self.isMultiple():
            size = len(self.getSelectionValues())
            if size > 10:
                size = 10
            size = getattr(self, 'size', size)
        else:        
            size = 1
        return size

    def getMultiple(self):
        pass

    def renderEditControl(self, context, presetValue):
        selectedValue = self.getValue(context, presetValue)
        [descriptions, values] = [self.getSelectionValueDescriptions(),
                                  self.getSelectionValues()]

        multiple = ''
        if self.isMultiple():
            multiple = 'multiple="multiple"'

        print '<select name="%s" size="%s"%s>' % (self.name, self.getSize(),
                                                  multiple)
        for i in range(0, len(descriptions)):
            value = values[i]
            print '<option value="%s"' % value,
            if self.isSelected(value, context, presetValue):
                print ' selected="selected"',
            print '>%s</option>' % descriptions[i]
        print '</select>'

    def isRelevant(self, context):
        return Option.isRelevant(self, context) and len(self.getSelectionValues()) > 1

### Some fancy options which do validation

class IntegerOption(StringOption):

    OPTION_KEY = 'integer'

    def __init__(self, wrapper, properties):
        StringOption.__init__(self, wrapper, properties)

        if hasattr(self, 'maxValue'):
            self.maxLength = 1 + math.log10(int(self.maxValue))
            self.size = self.maxLength

    def validate(self, context, value):
        try:
            value = int(value)
        except:
            return '"%s" is not an integer.' % value
        if hasattr(self, 'maxValue') and value > int(self.maxValue):
            return '%s is greater than the maximum "%s" value of %s.' % (value, self.displayName, self.maxValue)

class EmailOption(StringOption):

    OPTION_KEY = 'email'

    """An option whose value must be an email address."""

    def validate(self, context, value):
        if not re.match("^.+@.+\..{2,3}$", value):
            return '"%s" is not a well-formed email address.' % value

class HTMLColorOption(StringOption):

    OPTION_KEY = 'htmlColor'

    """An option whose value must be an HTML color or color constant."""

    COLOR_CONSTANTS = string.split("aliceblue antiquewhite aqua aquamarine azure beige bisque black blanchedalmond blue blueviolet brown burlywood cadetblue chartreuse chocolate coral cornflowerblue cornsilk crimson cyan darkblue darkcyan darkgoldenrod darkgray darkgreen darkkhaki darkmagenta darkolivegreen darkorange darkorchid darkred darksalmon darkseagreen darkslateblue darkslategray darkturquoise darkviolet deeppink deepskyblue dimgray dodgerblue firebrick floralwhite forestgreen fuchsia gainsboro ghostwhite gold goldenrod gray green greenyellow honeydew hotpink indianred indigo ivory khaki lavender lavenderblush lawngreen lemonchiffon lightblue lightcoral lightcyan lightgoldenrodyellow lightgreen lightgrey lightpink lightsalmon lightseagreen lightskyblue lightslategray lightsteelblue lightyellow lime limegreen linen magenta maroon mediumaquamarine mediumblue mediumorchid mediumpurple mediumseagreen mediumslateblue mediumspringgreen mediumturquoise mediumvioletred midnightblue mintcream mistyrose mistyrose navajowhite navy oldlace olive olivedrab orange orangered orchid palegoldenrod palegreen paleturquoise palevioletred papayawhip peachpuff peru pink plum powderblue purple red rosybrown royalblue saddlebrown salmon sandybrown seagreen seashell sienna silver skyblue slateblue slategray snow springgreen steelblue tan teal thistle tomato turquoise violet wheat white whitesmoke yellow yellowgreen")
    def __init__(self, wrapper, properties):
        StringOption.__init__(self, wrapper, properties)
        self.maxLength = len('lightgoldenrodyellow')
        self.size = self.maxLength

    def validate(self, context, value):
        if not re.match("#?[A-Fa-f0-9]{6}$", value) and value not in self.COLOR_CONSTANTS:
            return '"%s" is not an HTML color code or color constant.' % value

class LanguageOption(SelectionOption):

    """A selection drop-down for language/locale, which yields values
    in the "language[-locale]" format used by RSS, and is restricted
    those languages acceptable to RSS:
    http://backend.userland.com/stories/storyReader$16"""

    OPTION_KEY = 'language'

    LANGUAGES = { 'Afrikaans' : 'af',
                  'Albanian' : 'sq ',
                  'Basque' : 'eu',
                  'Belarusian' : 'be',
                  'Bulgarian' : 'bg',
                  'Catalan' : 'ca',
                  'Chinese (Simplified)' : 'zh-cn',
                  'Chinese (Traditional)' : 'zh-tw',
                  'Croatian' : 'hr',
                  'Czech' : 'cs',
                  'Danish' : 'da',
                  'Dutch' : 'nl',
                  'Dutch (Belgium)' : 'nl-be',
                  'Dutch (Netherlands)' : 'nl-nl',
                  'English' : 'en',
                  'English (Australia)' : 'en-au',
                  'English (Belize)' : 'en-bz',
                  'English (Canada)' : 'en-ca',
                  'English (Ireland)' : 'en-ie',
                  'English (Jamaica)' : 'en-jm',
                  'English (New Zealand)' : 'en-nz',
                  'English (Phillipines)' : 'en-ph',
                  'English (South Africa)' : 'en-za',
                  'English (Trinidad)' : 'en-tt',
                  'English (United Kingdom)' : 'en-gb',
                  'English (United States)' : 'en-us',
                  'English (Zimbabwe)' : 'en-zw',
                  'Estonian' : ' et',
                  'Faeroese' : 'fo',
                  'Finnish' : 'fi',
                  'French' : 'fr',
                  'French (Belgium)' : 'fr-be',
                  'French (Canada)' : 'fr-ca',
                  'French (France)' : 'fr-fr',
                  'French (Luxembourg)' : 'fr-lu',
                  'French (Monaco)' : 'fr-mc',
                  'French (Switzerland)' : 'fr-ch',
                  'Galician' : 'gl',
                  'Gaelic' : 'gd',
                  'German' : 'de',
                  'German (Austria)' : 'de-at',
                  'German (Germany)' : 'de-de',
                  'German (Liechtenstein)' : 'de-li',
                  'German (Luxembourg)' : 'de-lu',
                  'German (Switzerland)' : 'de-ch',
                  'Greek' : 'el',
                  'Hawaiian' : 'haw',
                  'Hungarian' : 'hu',
                  'Icelandic' : 'is',
                  'Indonesian' : 'in',
                  'Irish' : 'ga',
                  'Italian' : 'it',
                  'Italian (Italy)' : 'it-it',
                  'Italian (Switzerland)' : 'it-ch',
                  'Japanese' : 'ja',
                  'Korean' : 'ko',
                  'Macedonian' : 'mk',
                  'Norwegian' : 'no',
                  'Polish' : 'pl',
                  'Portuguese' : 'pt',
                  'Portuguese (Brazil)' : 'pt-br',
                  'Portuguese (Portugal)' : 'pt-pt',
                  'Romanian' : 'ro',
                  'Romanian (Moldova)' : 'ro-mo',
                  'Romanian (Romania)' : 'ro-ro',
                  'Russian' : 'ru',
                  'Russian (Moldova)' : 'ru-mo',
                  'Russian (Russia)' : 'ru-ru',
                  'Serbian' : 'sr',
                  'Slovak' : 'sk',
                  'Slovenian' : 'sl',
                  'Spanish' : 'es',
                  'Spanish (Argentina)' : 'es-ar',
                  'Spanish (Bolivia)' : 'es-bo',
                  'Spanish (Chile)' : 'es-cl',
                  'Spanish (Colombia)' : 'es-co',
                  'Spanish (Costa Rica)' : 'es-cr',
                  'Spanish (Dominican Republic)' : 'es-do',
                  'Spanish (Ecuador)' : 'es-ec',
                  'Spanish (El Salvador)' : 'es-sv',
                  'Spanish (Guatemala)' : 'es-gt',
                  'Spanish (Honduras)' : 'es-hn',
                  'Spanish (Mexico)' : 'es-mx',
                  'Spanish (Nicaragua)' : 'es-ni',
                  'Spanish (Panama)' : 'es-pa',
                  'Spanish (Paraguay)' : 'es-py',
                  'Spanish (Peru)' : 'es-pe',
                  'Spanish (Puerto Rico)' : 'es-pr',
                  'Spanish (Spain)' : 'es-es',
                  'Spanish (Uruguay)' : 'es-uy',
                  'Spanish (Venezuela)' : 'es-ve',
                  'Swedish' : 'sv',
                  'Swedish (Finland)' : 'sv-fi',
                  'Swedish (Sweden)' : 'sv-se',
                  'Turkish' : 'tr',
                  'Ukranian' : 'uk'}

    def getSelectionValues(self):
        if not hasattr(self, 'values'):
            self.values = []
            for i in self.getSelectionValueDescriptions():
                self.values.append(self.LANGUAGES[i])
        return self.values

    def getSelectionValueDescriptions(self):
        if not hasattr(self, 'descriptions'):
            self.descriptions = self.LANGUAGES.keys()
            self.descriptions.sort()
        return self.descriptions

class TimeZoneOption(SelectionOption):

    OPTION_KEY = 'timezone'

    """A selection drop-down for time zone selection, which yields
    values in the format accepted by the Unix TZ environment variable.

    I've tried to choose time zone-appropriate cities to illustrate
    each time zone, but since I've been to very few of these places I
    may have made some mistakes. I also don't have abbreviations for
    most of the time zones.    

    The abbreviation stuff is a mess. I think the best way to go might
    be to put a "Daylight savings?" checkbox next to the selection box
    and, if it's set, generate a bogus daylight savings time zone
    abbreviation to go along with the bogus regular time zone
    abbreviation. This would allow you to use the TZ environment variable
    as a simple GMT offset, and if you needed the name of the timezone you
    could define the TZ format in a text box."""

    #The TZ environment variable requires a timezone abbreviation. We
    #use this bogus abbreviation for cases where there is no
    #known abbreviation.
    DEFAULT_TIMEZONE_ABBR = 'AAA'

    systemTimezone = "GMT" + str(-time.timezone/60/60) + ' '
    if len(time.tzname) == 1:
        systemTimezone = time.tzname[0] + systemTimezone
    elif len(time.tzname) == 2:
        systemTimezone = systemTimezone + time.tzname[0] + '/' + time.tzname[1]

    TIME_ZONES = [ ('',      'Use system time zone (%s)' % systemTimezone),
                   ('-12',   'Eniwetok, Kwajalein'),
                   ('-11',   'Alofi, Apia'),
                   ('-10',   'Avura, Ulupalakua', 'HST'),
                   ('-9',    'Bora Bora, Nome', 'AKST', 'AKDT'),
                   ('-8',    'Pacific (Berkeley, Vancouver)', 'PST', 'PDT'),
                   ('-7',    'Mountain (Helena, Provo)', 'MST', 'MDT'),
                   ('-6',    'Central (Decatur, Repulse Bay)', 'CST', 'CDT'),
                   ('-5',    'Eastern (Boca Raton, Seacoast)', 'EST', 'EDT'),
                   ('-4',    "Atlantic (Bishop's Falls, Halifax)", 'AST',
                              'ADT'),
                   ('-3:30', "Newfoundland (St. John's, Cow Head)",
                    'NST', 'NDT'),
                   ('-3',    'Montevideo, New Amsterdam'),
                   ('-2',    'Belgrano, Fernando de Noronha'),
                   ('-1',    'Azores, Scoresbysund'),
                   ( '0',    'Bath, Hekla', 'GMT', 'BST'),
                   ('+1',    'Belgrade, Szeged'),
                   ('+2',    'Minsk, Thessaloniki'),
                   ('+3',    'Manama, Nakuru'),
                   ('+3:30', 'Chabahar, Qom'),
                   ('+4',    'Abu Dabhi, Sevastopol'),
                   ('+4:30', 'Kandahar, Mazar-e-Sharif'),
                   ('+5',    'Hyderabad, Turkmenbasy'),
                   ('+5:30', 'Chennai, Kathmandu'),
                   ('+6',    'Petropavlovsk, Thimphu'),
                   ('+7',    'Irkutsk, Xiangkhoang'),
                   ('+8',    'Bandar Seri Begawan, Ulaanbaatar', 'AWST'),
                   ('+9',    'Kyoto, Maluku'),
                   ('+9:30', 'Darwin', 'ACST', 'ACDT'),
                   ('+10',   'Saipan, Vladivostok', 'AEST', 'AEDT'),
                   ('+11',   'Luganville, Noumea'),
                   ('+12',   'Suva, Dunedin'),
                   ]    

    def getSelectionValues(self):
        if not hasattr(self, 'values'):
            self.values = []
            for i in self.TIME_ZONES:
                value = i[0]
                #The TZ var treats positive offsets as west of GMT,
                #which is the opposite of what you'd expect and of
                #what we display. We strip off '-' and change '+' to
                #'-'.
                if value:
                    if value[0] == '-':
                        value = value[1:]
                    elif value[0] == '+':
                        value = '-' + value[1:]
                # TZ var needs *some* abbreviation, so try a bogus one if
                # none provided.
                normalAbbr = 'AAA' 
                daylightAbbr = ''
                if i[0]:
                    #Add the standard timezone name, or the default if none
                    #provided.
                    if len(i) > 2:                        
                        value = i[2] + value
                    else:
                        value = self.DEFAULT_TIMEZONE_ABBR + value
                    #Add the daylight savings time zone name, if any.
                    if len(i) > 3:
                        value = value + i[3] 
                self.values.append(value)

        return self.values

    def getSelectionValueDescriptions(self):
        if not hasattr(self, 'descriptions'):
            self.descriptions = [self.TIME_ZONES[0][1]]            
            for i in self.TIME_ZONES[1:]:                
                offset, cities = i[:2]
                if offset == '0':
                    offset = '+' + offset
                if ':' not in offset:
                    offset = offset + ':00'
                if '(' not in cities:
                    cities = '(%s)' % cities
                self.descriptions.append('GMT %s %s' % (offset, cities))
        return self.descriptions


    
#  LocalWords:  OptionConfig OptionWrapper cgi OptionGroup init optionGroupList
#  LocalWords:  optionGroupMap optionMap getOptionMap getOptionDefinitions
#  LocalWords:  getOptionGroupList getOptionGroupMap getOption
