From b87089c7d2b48d89ca91c911b7d9881e88d35f2f Mon Sep 17 00:00:00 2001 From: Martijn Voncken Date: Sat, 22 Mar 2008 12:55:49 +0000 Subject: [PATCH] newforms->newforms_portable --- deluge/ui/webui/components.py | 2 +- deluge/ui/webui/config_tabs_deluge.py | 2 +- deluge/ui/webui/lib/newforms/fields.py | 492 ----------- deluge/ui/webui/lib/newforms/util.py | 78 -- deluge/ui/webui/lib/newforms/utils/html.py | 7 - deluge/ui/webui/lib/newforms_plus.py | 12 +- .../{newforms => newforms_portable}/LICENSE | 0 .../__init__.py | 2 + .../ui/webui/lib/newforms_portable/about.txt | 3 + .../django}/__init__.py | 0 .../newforms_portable/django/core/__init__.py | 0 .../django/core/exceptions.py | 29 + .../django/utils/__init__.py | 0 .../django}/utils/datastructures.py | 213 +++-- .../django/utils/encoding.py | 102 +++ .../django/utils/functional.py | 241 ++++++ .../newforms_portable/django/utils/html.py | 163 ++++ .../newforms_portable/django/utils/http.py | 67 ++ .../django/utils/safestring.py | 119 +++ .../django/utils/translation.py | 9 + .../ui/webui/lib/newforms_portable/fields.py | 784 ++++++++++++++++++ .../{newforms => newforms_portable}/forms.py | 205 +++-- .../ui/webui/lib/newforms_portable/models.py | 398 +++++++++ deluge/ui/webui/lib/newforms_portable/util.py | 69 ++ .../widgets.py | 266 ++++-- deluge/ui/webui/pages.py | 1 - deluge/ui/webui/torrent_add.py | 2 +- deluge/ui/webui/torrent_move.py | 2 +- 28 files changed, 2460 insertions(+), 808 deletions(-) delete mode 100644 deluge/ui/webui/lib/newforms/fields.py delete mode 100644 deluge/ui/webui/lib/newforms/util.py delete mode 100644 deluge/ui/webui/lib/newforms/utils/html.py rename deluge/ui/webui/lib/{newforms => newforms_portable}/LICENSE (100%) rename deluge/ui/webui/lib/{newforms => newforms_portable}/__init__.py (91%) create mode 100644 deluge/ui/webui/lib/newforms_portable/about.txt rename deluge/ui/webui/lib/{newforms/utils => newforms_portable/django}/__init__.py (100%) create mode 100644 deluge/ui/webui/lib/newforms_portable/django/core/__init__.py create mode 100644 deluge/ui/webui/lib/newforms_portable/django/core/exceptions.py create mode 100644 deluge/ui/webui/lib/newforms_portable/django/utils/__init__.py rename deluge/ui/webui/lib/{newforms => newforms_portable/django}/utils/datastructures.py (50%) create mode 100644 deluge/ui/webui/lib/newforms_portable/django/utils/encoding.py create mode 100644 deluge/ui/webui/lib/newforms_portable/django/utils/functional.py create mode 100644 deluge/ui/webui/lib/newforms_portable/django/utils/html.py create mode 100644 deluge/ui/webui/lib/newforms_portable/django/utils/http.py create mode 100644 deluge/ui/webui/lib/newforms_portable/django/utils/safestring.py create mode 100644 deluge/ui/webui/lib/newforms_portable/django/utils/translation.py create mode 100644 deluge/ui/webui/lib/newforms_portable/fields.py rename deluge/ui/webui/lib/{newforms => newforms_portable}/forms.py (60%) create mode 100644 deluge/ui/webui/lib/newforms_portable/models.py create mode 100644 deluge/ui/webui/lib/newforms_portable/util.py rename deluge/ui/webui/lib/{newforms => newforms_portable}/widgets.py (56%) diff --git a/deluge/ui/webui/components.py b/deluge/ui/webui/components.py index 19e12d6f2..4f877a31b 100644 --- a/deluge/ui/webui/components.py +++ b/deluge/ui/webui/components.py @@ -153,7 +153,7 @@ class ConfigPageManager(component.Component): def __init__(self): component.Component.__init__(self, "ConfigPageManager") self.groups = [] - self.blocks = forms.utils.datastructures.SortedDict() + self.blocks = forms.django.utils.datastructures.SortedDict() def register(self, group, name, form): if not group in self.groups: diff --git a/deluge/ui/webui/config_tabs_deluge.py b/deluge/ui/webui/config_tabs_deluge.py index 44696c7ac..0b7562162 100644 --- a/deluge/ui/webui/config_tabs_deluge.py +++ b/deluge/ui/webui/config_tabs_deluge.py @@ -57,7 +57,7 @@ class NetworkPorts(config_forms.CfgForm ): data['listen_ports'] = [data['_port_from'] , data['_port_to'] ] del(data['_port_from']) del(data['_port_to']) - config_forms.config.CfgForm.save(self, data) + config_forms.CfgForm.save(self, data) def validate(self, data): if (data['_port_to'] < data['_port_from']): diff --git a/deluge/ui/webui/lib/newforms/fields.py b/deluge/ui/webui/lib/newforms/fields.py deleted file mode 100644 index 5076c5824..000000000 --- a/deluge/ui/webui/lib/newforms/fields.py +++ /dev/null @@ -1,492 +0,0 @@ -""" -Field classes -""" - -from gettext import gettext -from util import ErrorList, ValidationError, smart_unicode -from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple -import datetime -import re -import time - -__all__ = ( - 'Field', 'CharField', 'IntegerField', - 'DEFAULT_DATE_INPUT_FORMATS', 'DateField', - 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', - 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', - 'RegexField', 'EmailField', 'URLField', 'BooleanField', - 'ChoiceField', 'NullBooleanField', 'MultipleChoiceField', - 'ComboField', 'MultiValueField', - 'SplitDateTimeField', -) - -# These values, if given to to_python(), will trigger the self.required check. -EMPTY_VALUES = (None, '') - -try: - set # Only available in Python 2.4+ -except NameError: - from sets import Set as set # Python 2.3 fallback - -class Field(object): - widget = TextInput # Default widget to use when rendering this type of Field. - hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden". - - # Tracks each time a Field instance is created. Used to retain order. - creation_counter = 0 - - def __init__(self, required=True, widget=None, label=None, initial=None, help_text=None): - # required -- Boolean that specifies whether the field is required. - # True by default. - # widget -- A Widget class, or instance of a Widget class, that should be - # used for this Field when displaying it. Each Field has a default - # Widget that it'll use if you don't specify this. In most cases, - # the default widget is TextInput. - # label -- A verbose name for this field, for use in displaying this field in - # a form. By default, Django will use a "pretty" version of the form - # field name, if the Field is part of a Form. - # initial -- A value to use in this Field's initial display. This value is - # *not* used as a fallback if data isn't given. - # help_text -- An optional string to use as "help text" for this Field. - if label is not None: - label = smart_unicode(label) - self.required, self.label, self.initial = required, label, initial - self.help_text = smart_unicode(help_text or '') - widget = widget or self.widget - if isinstance(widget, type): - widget = widget() - - # Hook into self.widget_attrs() for any Field-specific HTML attributes. - extra_attrs = self.widget_attrs(widget) - if extra_attrs: - widget.attrs.update(extra_attrs) - - self.widget = widget - - # Increase the creation counter, and save our local copy. - self.creation_counter = Field.creation_counter - Field.creation_counter += 1 - - def clean(self, value): - """ - Validates the given value and returns its "cleaned" value as an - appropriate Python object. - - Raises ValidationError for any errors. - """ - if self.required and value in EMPTY_VALUES: - raise ValidationError(gettext(u'This field is required.')) - return value - - def widget_attrs(self, widget): - """ - Given a Widget instance (*not* a Widget class), returns a dictionary of - any HTML attributes that should be added to the Widget, based on this - Field. - """ - return {} - -class CharField(Field): - def __init__(self, max_length=None, min_length=None, *args, **kwargs): - self.max_length, self.min_length = max_length, min_length - super(CharField, self).__init__(*args, **kwargs) - - def clean(self, value): - "Validates max_length and min_length. Returns a Unicode object." - super(CharField, self).clean(value) - if value in EMPTY_VALUES: - return u'' - value = smart_unicode(value) - if self.max_length is not None and len(value) > self.max_length: - raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) - if self.min_length is not None and len(value) < self.min_length: - raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length) - return value - - def widget_attrs(self, widget): - if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): - return {'maxlength': str(self.max_length)} - -class IntegerField(Field): - def __init__(self, max_value=None, min_value=None, *args, **kwargs): - self.max_value, self.min_value = max_value, min_value - super(IntegerField, self).__init__(*args, **kwargs) - - def clean(self, value): - """ - Validates that int() can be called on the input. Returns the result - of int(). Returns None for empty values. - """ - super(IntegerField, self).clean(value) - if value in EMPTY_VALUES: - return None - try: - value = int(value) - except (ValueError, TypeError): - raise ValidationError(gettext(u'Enter a whole number.')) - if self.max_value is not None and value > self.max_value: - raise ValidationError(gettext(u'Ensure this value is less than or equal to %s.') % self.max_value) - if self.min_value is not None and value < self.min_value: - raise ValidationError(gettext(u'Ensure this value is greater than or equal to %s.') % self.min_value) - return value - -DEFAULT_DATE_INPUT_FORMATS = ( - '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' - '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006' - '%d %b %Y', '%d %b, %Y', # '25 Oct 2006', '25 Oct, 2006' - '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006' - '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006' -) - -class DateField(Field): - def __init__(self, input_formats=None, *args, **kwargs): - super(DateField, self).__init__(*args, **kwargs) - self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS - - def clean(self, value): - """ - Validates that the input can be converted to a date. Returns a Python - datetime.date object. - """ - super(DateField, self).clean(value) - if value in EMPTY_VALUES: - return None - if isinstance(value, datetime.datetime): - return value.date() - if isinstance(value, datetime.date): - return value - for format in self.input_formats: - try: - return datetime.date(*time.strptime(value, format)[:3]) - except ValueError: - continue - raise ValidationError(gettext(u'Enter a valid date.')) - -DEFAULT_TIME_INPUT_FORMATS = ( - '%H:%M:%S', # '14:30:59' - '%H:%M', # '14:30' -) - -class TimeField(Field): - def __init__(self, input_formats=None, *args, **kwargs): - super(TimeField, self).__init__(*args, **kwargs) - self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS - - def clean(self, value): - """ - Validates that the input can be converted to a time. Returns a Python - datetime.time object. - """ - super(TimeField, self).clean(value) - if value in EMPTY_VALUES: - return None - if isinstance(value, datetime.time): - return value - for format in self.input_formats: - try: - return datetime.time(*time.strptime(value, format)[3:6]) - except ValueError: - continue - raise ValidationError(gettext(u'Enter a valid time.')) - -DEFAULT_DATETIME_INPUT_FORMATS = ( - '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' - '%Y-%m-%d %H:%M', # '2006-10-25 14:30' - '%Y-%m-%d', # '2006-10-25' - '%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59' - '%m/%d/%Y %H:%M', # '10/25/2006 14:30' - '%m/%d/%Y', # '10/25/2006' - '%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59' - '%m/%d/%y %H:%M', # '10/25/06 14:30' - '%m/%d/%y', # '10/25/06' -) - -class DateTimeField(Field): - def __init__(self, input_formats=None, *args, **kwargs): - super(DateTimeField, self).__init__(*args, **kwargs) - self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS - - def clean(self, value): - """ - Validates that the input can be converted to a datetime. Returns a - Python datetime.datetime object. - """ - super(DateTimeField, self).clean(value) - if value in EMPTY_VALUES: - return None - if isinstance(value, datetime.datetime): - return value - if isinstance(value, datetime.date): - return datetime.datetime(value.year, value.month, value.day) - for format in self.input_formats: - try: - return datetime.datetime(*time.strptime(value, format)[:6]) - except ValueError: - continue - raise ValidationError(gettext(u'Enter a valid date/time.')) - -class RegexField(Field): - def __init__(self, regex, max_length=None, min_length=None, error_message=None, *args, **kwargs): - """ - regex can be either a string or a compiled regular expression object. - error_message is an optional error message to use, if - 'Enter a valid value' is too generic for you. - """ - super(RegexField, self).__init__(*args, **kwargs) - if isinstance(regex, basestring): - regex = re.compile(regex) - self.regex = regex - self.max_length, self.min_length = max_length, min_length - self.error_message = error_message or gettext(u'Enter a valid value.') - - def clean(self, value): - """ - Validates that the input matches the regular expression. Returns a - Unicode object. - """ - super(RegexField, self).clean(value) - if value in EMPTY_VALUES: - value = u'' - value = smart_unicode(value) - if value == u'': - return value - if self.max_length is not None and len(value) > self.max_length: - raise ValidationError(gettext(u'Ensure this value has at most %d characters.') % self.max_length) - if self.min_length is not None and len(value) < self.min_length: - raise ValidationError(gettext(u'Ensure this value has at least %d characters.') % self.min_length) - if not self.regex.search(value): - raise ValidationError(self.error_message) - return value - -email_re = re.compile( - r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom - r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string - r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain - -class EmailField(RegexField): - def __init__(self, max_length=None, min_length=None, *args, **kwargs): - RegexField.__init__(self, email_re, max_length, min_length, - gettext(u'Enter a valid e-mail address.'), *args, **kwargs) - -url_re = re.compile( - r'^https?://' # http:// or https:// - r'(?:[A-Z0-9-]+\.)+[A-Z]{2,6}' # domain - r'(?::\d+)?' # optional port - r'(?:/?|/\S+)$', re.IGNORECASE) - -try: - from django.conf import settings - URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT -except ImportError: - # It's OK if Django settings aren't configured. - URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' - -class URLField(RegexField): - def __init__(self, max_length=None, min_length=None, verify_exists=False, - validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs): - super(URLField, self).__init__(url_re, max_length, min_length, gettext(u'Enter a valid URL.'), *args, **kwargs) - self.verify_exists = verify_exists - self.user_agent = validator_user_agent - - def clean(self, value): - value = super(URLField, self).clean(value) - if value == u'': - return value - if self.verify_exists: - import urllib2 - from django.conf import settings - headers = { - "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5", - "Accept-Language": "en-us,en;q=0.5", - "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", - "Connection": "close", - "User-Agent": self.user_agent, - } - try: - req = urllib2.Request(value, None, headers) - u = urllib2.urlopen(req) - except ValueError: - raise ValidationError(gettext(u'Enter a valid URL.')) - except: # urllib2.URLError, httplib.InvalidURL, etc. - raise ValidationError(gettext(u'This URL appears to be a broken link.')) - return value - -class BooleanField(Field): - widget = CheckboxInput - - def clean(self, value): - "Returns a Python boolean object." - super(BooleanField, self).clean(value) - return bool(value) - -class NullBooleanField(BooleanField): - """ - A field whose valid values are None, True and False. Invalid values are - cleaned to None. - """ - widget = NullBooleanSelect - - def clean(self, value): - return {True: True, False: False}.get(value, None) - -class ChoiceField(Field): - def __init__(self, choices=(), required=True, widget=Select, label=None, initial=None, help_text=None): - super(ChoiceField, self).__init__(required, widget, label, initial, help_text) - self.choices = choices - - def _get_choices(self): - return self._choices - - def _set_choices(self, value): - # Setting choices also sets the choices on the widget. - # choices can be any iterable, but we call list() on it because - # it will be consumed more than once. - self._choices = self.widget.choices = list(value) - - choices = property(_get_choices, _set_choices) - - def clean(self, value): - """ - Validates that the input is in self.choices. - """ - value = super(ChoiceField, self).clean(value) - if value in EMPTY_VALUES: - value = u'' - value = smart_unicode(value) - if value == u'': - return value - valid_values = set([str(k) for k, v in self.choices]) - if value not in valid_values: - raise ValidationError(gettext(u'Select a valid choice. That choice is not one of the available choices.')) - return value - -class MultipleChoiceField(ChoiceField): - hidden_widget = MultipleHiddenInput - - def __init__(self, choices=(), required=True, widget=SelectMultiple, label=None, initial=None, help_text=None): - super(MultipleChoiceField, self).__init__(choices, required, widget, label, initial, help_text) - - def clean(self, value): - """ - Validates that the input is a list or tuple. - """ - if self.required and not value: - raise ValidationError(gettext(u'This field is required.')) - elif not self.required and not value: - return [] - if not isinstance(value, (list, tuple)): - raise ValidationError(gettext(u'Enter a list of values.')) - new_value = [] - for val in value: - val = smart_unicode(val) - new_value.append(val) - # Validate that each value in the value list is in self.choices. - valid_values = set([smart_unicode(k) for k, v in self.choices]) - for val in new_value: - if val not in valid_values: - raise ValidationError(gettext(u'Select a valid choice. %s is not one of the available choices.') % val) - return new_value - -class ComboField(Field): - """ - A Field whose clean() method calls multiple Field clean() methods. - """ - def __init__(self, fields=(), *args, **kwargs): - super(ComboField, self).__init__(*args, **kwargs) - # Set 'required' to False on the individual fields, because the - # required validation will be handled by ComboField, not by those - # individual fields. - for f in fields: - f.required = False - self.fields = fields - - def clean(self, value): - """ - Validates the given value against all of self.fields, which is a - list of Field instances. - """ - super(ComboField, self).clean(value) - for field in self.fields: - value = field.clean(value) - return value - -class MultiValueField(Field): - """ - A Field that is composed of multiple Fields. - - Its clean() method takes a "decompressed" list of values. Each value in - this list is cleaned by the corresponding field -- the first value is - cleaned by the first field, the second value is cleaned by the second - field, etc. Once all fields are cleaned, the list of clean values is - "compressed" into a single value. - - Subclasses should implement compress(), which specifies how a list of - valid values should be converted to a single value. Subclasses should not - have to implement clean(). - - You'll probably want to use this with MultiWidget. - """ - def __init__(self, fields=(), *args, **kwargs): - super(MultiValueField, self).__init__(*args, **kwargs) - # Set 'required' to False on the individual fields, because the - # required validation will be handled by MultiValueField, not by those - # individual fields. - for f in fields: - f.required = False - self.fields = fields - - def clean(self, value): - """ - Validates every value in the given list. A value is validated against - the corresponding Field in self.fields. - - For example, if this MultiValueField was instantiated with - fields=(DateField(), TimeField()), clean() would call - DateField.clean(value[0]) and TimeField.clean(value[1]). - """ - clean_data = [] - errors = ErrorList() - if self.required and not value: - raise ValidationError(gettext(u'This field is required.')) - elif not self.required and not value: - return self.compress([]) - if not isinstance(value, (list, tuple)): - raise ValidationError(gettext(u'Enter a list of values.')) - for i, field in enumerate(self.fields): - try: - field_value = value[i] - except KeyError: - field_value = None - if self.required and field_value in EMPTY_VALUES: - raise ValidationError(gettext(u'This field is required.')) - try: - clean_data.append(field.clean(field_value)) - except ValidationError, e: - # Collect all validation errors in a single list, which we'll - # raise at the end of clean(), rather than raising a single - # exception for the first error we encounter. - errors.extend(e.messages) - if errors: - raise ValidationError(errors) - return self.compress(clean_data) - - def compress(self, data_list): - """ - Returns a single value for the given list of values. The values can be - assumed to be valid. - - For example, if this MultiValueField was instantiated with - fields=(DateField(), TimeField()), this might return a datetime - object created by combining the date and time in data_list. - """ - raise NotImplementedError('Subclasses must implement this method.') - -class SplitDateTimeField(MultiValueField): - def __init__(self, *args, **kwargs): - fields = (DateField(), TimeField()) - super(SplitDateTimeField, self).__init__(fields, *args, **kwargs) - - def compress(self, data_list): - if data_list: - return datetime.datetime.combine(*data_list) - return None diff --git a/deluge/ui/webui/lib/newforms/util.py b/deluge/ui/webui/lib/newforms/util.py deleted file mode 100644 index 8b234df04..000000000 --- a/deluge/ui/webui/lib/newforms/util.py +++ /dev/null @@ -1,78 +0,0 @@ -from utils.html import escape - -class settings(object): - DEFAULT_CHARSET = 'utf-8' - - -# Converts a dictionary to a single string with key="value", XML-style with -# a leading space. Assumes keys do not need to be XML-escaped. -flatatt = lambda attrs: u''.join([u' %s="%s"' % (k, escape(v)) for k, v in attrs.items()]) - -def smart_unicode(s): - if not isinstance(s, basestring): - if hasattr(s, '__unicode__'): - s = unicode(s) - else: - s = unicode(str(s), settings.DEFAULT_CHARSET) - elif not isinstance(s, unicode): - s = unicode(s, settings.DEFAULT_CHARSET) - return s - -class StrAndUnicode(object): - """ - A class whose __str__ returns its __unicode__ as a bytestring - according to settings.DEFAULT_CHARSET. - - Useful as a mix-in. - """ - def __str__(self): - return self.__unicode__().encode(settings.DEFAULT_CHARSET) - -class ErrorDict(dict): - """ - A collection of errors that knows how to display itself in various formats. - - The dictionary keys are the field names, and the values are the errors. - """ - def __str__(self): - return self.as_ul() - - def as_ul(self): - if not self: return u'' - return u'' % ''.join([u'
  • %s%s
  • ' % (k, v) for k, v in self.items()]) - - def as_text(self): - return u'\n'.join([u'* %s\n%s' % (k, u'\n'.join([u' * %s' % i for i in v])) for k, v in self.items()]) - -class ErrorList(list): - """ - A collection of errors that knows how to display itself in various formats. - """ - def __str__(self): - return self.as_ul() - - def as_ul(self): - if not self: return u'' - return u'' % ''.join([u'
  • %s
  • ' % e for e in self]) - - def as_text(self): - if not self: return u'' - return u'\n'.join([u'* %s' % e for e in self]) - -class ValidationError(Exception): - def __init__(self, message): - "ValidationError can be passed a string or a list." - self.message = message - if isinstance(message, list): - self.messages = ErrorList([smart_unicode(msg) for msg in message]) - else: - assert isinstance(message, basestring), ("%s should be a basestring" % repr(message)) - message = smart_unicode(message) - self.messages = ErrorList([message]) - - def __str__(self): - # This is needed because, without a __str__(), printing an exception - # instance would result in this: - # AttributeError: ValidationError instance has no attribute 'args' - # See http://www.python.org/doc/current/tut/node10.html#handling - return repr(self.messages) diff --git a/deluge/ui/webui/lib/newforms/utils/html.py b/deluge/ui/webui/lib/newforms/utils/html.py deleted file mode 100644 index e1f67cd43..000000000 --- a/deluge/ui/webui/lib/newforms/utils/html.py +++ /dev/null @@ -1,7 +0,0 @@ -"HTML utilities suitable for global use." - -def escape(html): - "Returns the given HTML with ampersands, quotes and carets encoded" - if not isinstance(html, basestring): - html = str(html) - return html.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''') diff --git a/deluge/ui/webui/lib/newforms_plus.py b/deluge/ui/webui/lib/newforms_plus.py index 7e7877670..e1e8ffa4d 100644 --- a/deluge/ui/webui/lib/newforms_plus.py +++ b/deluge/ui/webui/lib/newforms_plus.py @@ -1,12 +1,14 @@ # -*- coding: utf-8 -*- # # Copyright (C) Martijn Voncken 2008 -# Django Lisence, see ./newforms/LICENCE +# Django Licence, see ./newforms_portable/LICENCE # -from newforms import * -import newforms -from newforms.forms import BoundField +from newforms_portable import * +import newforms_portable as newforms +from newforms_portable.forms import BoundField +from newforms_portable.util import ErrorList, escape + import sys, os @@ -83,7 +85,7 @@ class Form(FilteredForm): def start_save(self): "called by config_page" - data = web.Storage(self.clean_data) + data = web.Storage(self.cleaned_data) self.validate(data) self.save(data) self.post_save() diff --git a/deluge/ui/webui/lib/newforms/LICENSE b/deluge/ui/webui/lib/newforms_portable/LICENSE similarity index 100% rename from deluge/ui/webui/lib/newforms/LICENSE rename to deluge/ui/webui/lib/newforms_portable/LICENSE diff --git a/deluge/ui/webui/lib/newforms/__init__.py b/deluge/ui/webui/lib/newforms_portable/__init__.py similarity index 91% rename from deluge/ui/webui/lib/newforms/__init__.py rename to deluge/ui/webui/lib/newforms_portable/__init__.py index 62125e218..a34f46c8e 100644 --- a/deluge/ui/webui/lib/newforms/__init__.py +++ b/deluge/ui/webui/lib/newforms_portable/__init__.py @@ -14,3 +14,5 @@ from util import ValidationError from widgets import * from fields import * from forms import * +from models import * +import django diff --git a/deluge/ui/webui/lib/newforms_portable/about.txt b/deluge/ui/webui/lib/newforms_portable/about.txt new file mode 100644 index 000000000..333869ab8 --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/about.txt @@ -0,0 +1,3 @@ +based on django rev.7350 +,/django/ contains the parts of django required to run newforms. + diff --git a/deluge/ui/webui/lib/newforms/utils/__init__.py b/deluge/ui/webui/lib/newforms_portable/django/__init__.py similarity index 100% rename from deluge/ui/webui/lib/newforms/utils/__init__.py rename to deluge/ui/webui/lib/newforms_portable/django/__init__.py diff --git a/deluge/ui/webui/lib/newforms_portable/django/core/__init__.py b/deluge/ui/webui/lib/newforms_portable/django/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/deluge/ui/webui/lib/newforms_portable/django/core/exceptions.py b/deluge/ui/webui/lib/newforms_portable/django/core/exceptions.py new file mode 100644 index 000000000..d9fc326cf --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/django/core/exceptions.py @@ -0,0 +1,29 @@ +"Global Django exceptions" + +class ObjectDoesNotExist(Exception): + "The requested object does not exist" + silent_variable_failure = True + +class MultipleObjectsReturned(Exception): + "The query returned multiple objects when only one was expected." + pass + +class SuspiciousOperation(Exception): + "The user did something suspicious" + pass + +class PermissionDenied(Exception): + "The user did not have permission to do that" + pass + +class ViewDoesNotExist(Exception): + "The requested view does not exist" + pass + +class MiddlewareNotUsed(Exception): + "This middleware is not used in this server configuration" + pass + +class ImproperlyConfigured(Exception): + "Django is somehow improperly configured" + pass diff --git a/deluge/ui/webui/lib/newforms_portable/django/utils/__init__.py b/deluge/ui/webui/lib/newforms_portable/django/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/deluge/ui/webui/lib/newforms/utils/datastructures.py b/deluge/ui/webui/lib/newforms_portable/django/utils/datastructures.py similarity index 50% rename from deluge/ui/webui/lib/newforms/utils/datastructures.py rename to deluge/ui/webui/lib/newforms_portable/django/utils/datastructures.py index 7b7fa2b0f..4c278c0d8 100644 --- a/deluge/ui/webui/lib/newforms/utils/datastructures.py +++ b/deluge/ui/webui/lib/newforms_portable/django/utils/datastructures.py @@ -1,24 +1,24 @@ class MergeDict(object): """ - A simple class for creating new "virtual" dictionaries that actualy look + A simple class for creating new "virtual" dictionaries that actually look up values in more than one dictionary, passed in the constructor. + + If a key appears in more than one of the given dictionaries, only the + first occurrence will be used. """ def __init__(self, *dicts): self.dicts = dicts def __getitem__(self, key): - for dict in self.dicts: + for dict_ in self.dicts: try: - return dict[key] + return dict_[key] except KeyError: pass raise KeyError - def __contains__(self, key): - return self.has_key(key) - - def __copy__(self): - return self.__class__(*self.dicts) + def __copy__(self): + return self.__class__(*self.dicts) def get(self, key, default=None): try: @@ -27,84 +27,145 @@ class MergeDict(object): return default def getlist(self, key): - for dict in self.dicts: - try: - return dict.getlist(key) - except KeyError: - pass - raise KeyError + for dict_ in self.dicts: + if key in dict_.keys(): + return dict_.getlist(key) + return [] def items(self): item_list = [] - for dict in self.dicts: - item_list.extend(dict.items()) + for dict_ in self.dicts: + item_list.extend(dict_.items()) return item_list def has_key(self, key): - for dict in self.dicts: - if dict.has_key(key): + for dict_ in self.dicts: + if key in dict_: return True return False - - def copy(self): - """ returns a copy of this object""" + + __contains__ = has_key + + def copy(self): + """Returns a copy of this object.""" return self.__copy__() class SortedDict(dict): - "A dictionary that keeps its keys in the order in which they're inserted." + """ + A dictionary that keeps its keys in the order in which they're inserted. + """ def __init__(self, data=None): - if data is None: data = {} - dict.__init__(self, data) - self.keyOrder = data.keys() + if data is None: + data = {} + super(SortedDict, self).__init__(data) + if isinstance(data, dict): + self.keyOrder = data.keys() + else: + self.keyOrder = [] + for key, value in data: + if key not in self.keyOrder: + self.keyOrder.append(key) + + def __deepcopy__(self, memo): + from copy import deepcopy + return self.__class__([(key, deepcopy(value, memo)) + for key, value in self.iteritems()]) def __setitem__(self, key, value): - dict.__setitem__(self, key, value) + super(SortedDict, self).__setitem__(key, value) if key not in self.keyOrder: self.keyOrder.append(key) def __delitem__(self, key): - dict.__delitem__(self, key) + super(SortedDict, self).__delitem__(key) self.keyOrder.remove(key) def __iter__(self): for k in self.keyOrder: yield k + def pop(self, k, *args): + result = super(SortedDict, self).pop(k, *args) + try: + self.keyOrder.remove(k) + except ValueError: + # Key wasn't in the dictionary in the first place. No problem. + pass + return result + + def popitem(self): + result = super(SortedDict, self).popitem() + self.keyOrder.remove(result[0]) + return result + def items(self): return zip(self.keyOrder, self.values()) + def iteritems(self): + for key in self.keyOrder: + yield key, super(SortedDict, self).__getitem__(key) + def keys(self): return self.keyOrder[:] - def values(self): - return [dict.__getitem__(self, k) for k in self.keyOrder] + def iterkeys(self): + return iter(self.keyOrder) - def update(self, dict): - for k, v in dict.items(): + def values(self): + return [super(SortedDict, self).__getitem__(k) for k in self.keyOrder] + + def itervalues(self): + for key in self.keyOrder: + yield super(SortedDict, self).__getitem__(key) + + def update(self, dict_): + for k, v in dict_.items(): self.__setitem__(k, v) def setdefault(self, key, default): if key not in self.keyOrder: self.keyOrder.append(key) - return dict.setdefault(self, key, default) + return super(SortedDict, self).setdefault(key, default) def value_for_index(self, index): - "Returns the value of the item at the given zero-based index." + """Returns the value of the item at the given zero-based index.""" return self[self.keyOrder[index]] + def insert(self, index, key, value): + """Inserts the key, value pair before the item with the given index.""" + if key in self.keyOrder: + n = self.keyOrder.index(key) + del self.keyOrder[n] + if n < index: + index -= 1 + self.keyOrder.insert(index, key) + super(SortedDict, self).__setitem__(key, value) + def copy(self): - "Returns a copy of this object." + """Returns a copy of this object.""" # This way of initializing the copy means it works for subclasses, too. obj = self.__class__(self) - obj.keyOrder = self.keyOrder + obj.keyOrder = self.keyOrder[:] return obj + def __repr__(self): + """ + Replaces the normal dict.__repr__ with a version that returns the keys + in their sorted order. + """ + return '{%s}' % ', '.join(['%r: %r' % (k, v) for k, v in self.items()]) + + def clear(self): + super(SortedDict, self).clear() + self.keyOrder = [] + class MultiValueDictKeyError(KeyError): pass class MultiValueDict(dict): """ - A subclass of dictionary customized to handle multiple values for the same key. + A subclass of dictionary customized to handle multiple values for the + same key. >>> d = MultiValueDict({'name': ['Adrian', 'Simon'], 'position': ['Developer']}) >>> d['name'] @@ -120,10 +181,11 @@ class MultiValueDict(dict): single name-value pairs. """ def __init__(self, key_to_list_mapping=()): - dict.__init__(self, key_to_list_mapping) + super(MultiValueDict, self).__init__(key_to_list_mapping) def __repr__(self): - return "" % dict.__repr__(self) + return "<%s: %s>" % (self.__class__.__name__, + super(MultiValueDict, self).__repr__()) def __getitem__(self, key): """ @@ -131,7 +193,7 @@ class MultiValueDict(dict): raises KeyError if not found. """ try: - list_ = dict.__getitem__(self, key) + list_ = super(MultiValueDict, self).__getitem__(key) except KeyError: raise MultiValueDictKeyError, "Key %r not found in %r" % (key, self) try: @@ -140,22 +202,27 @@ class MultiValueDict(dict): return [] def __setitem__(self, key, value): - dict.__setitem__(self, key, [value]) + super(MultiValueDict, self).__setitem__(key, [value]) def __copy__(self): - return self.__class__(dict.items(self)) + return self.__class__(super(MultiValueDict, self).items()) def __deepcopy__(self, memo=None): import copy - if memo is None: memo = {} + if memo is None: + memo = {} result = self.__class__() memo[id(self)] = result for key, value in dict.items(self): - dict.__setitem__(result, copy.deepcopy(key, memo), copy.deepcopy(value, memo)) + dict.__setitem__(result, copy.deepcopy(key, memo), + copy.deepcopy(value, memo)) return result def get(self, key, default=None): - "Returns the default value if the requested data doesn't exist" + """ + Returns the last data value for the passed key. If key doesn't exist + or value is an empty list, then default is returned. + """ try: val = self[key] except KeyError: @@ -165,14 +232,17 @@ class MultiValueDict(dict): return val def getlist(self, key): - "Returns an empty list if the requested data doesn't exist" + """ + Returns the list of values for the passed key. If key doesn't exist, + then an empty list is returned. + """ try: - return dict.__getitem__(self, key) + return super(MultiValueDict, self).__getitem__(key) except KeyError: return [] def setlist(self, key, list_): - dict.__setitem__(self, key, list_) + super(MultiValueDict, self).__setitem__(key, list_) def setdefault(self, key, default=None): if key not in self: @@ -185,9 +255,9 @@ class MultiValueDict(dict): return self.getlist(key) def appendlist(self, key, value): - "Appends an item to the internal list associated with key" + """Appends an item to the internal list associated with key.""" self.setlistdefault(key, []) - dict.__setitem__(self, key, self.getlist(key) + [value]) + super(MultiValueDict, self).__setitem__(key, self.getlist(key) + [value]) def items(self): """ @@ -197,21 +267,24 @@ class MultiValueDict(dict): return [(key, self[key]) for key in self.keys()] def lists(self): - "Returns a list of (key, list) pairs." - return dict.items(self) + """Returns a list of (key, list) pairs.""" + return super(MultiValueDict, self).items() def values(self): - "Returns a list of the last value on every key list." + """Returns a list of the last value on every key list.""" return [self[key] for key in self.keys()] def copy(self): - "Returns a copy of this object." + """Returns a copy of this object.""" return self.__deepcopy__() def update(self, *args, **kwargs): - "update() extends rather than replaces existing key lists. Also accepts keyword args." + """ + update() extends rather than replaces existing key lists. + Also accepts keyword args. + """ if len(args) > 1: - raise TypeError, "update expected at most 1 arguments, got %d", len(args) + raise TypeError, "update expected at most 1 arguments, got %d" % len(args) if args: other_dict = args[0] if isinstance(other_dict, MultiValueDict): @@ -232,22 +305,20 @@ class DotExpandedDict(dict): may contain dots to specify inner dictionaries. It's confusing, but this example should make sense. - >>> d = DotExpandedDict({'person.1.firstname': ['Simon'], - 'person.1.lastname': ['Willison'], - 'person.2.firstname': ['Adrian'], + >>> d = DotExpandedDict({'person.1.firstname': ['Simon'], \ + 'person.1.lastname': ['Willison'], \ + 'person.2.firstname': ['Adrian'], \ 'person.2.lastname': ['Holovaty']}) >>> d - {'person': {'1': {'lastname': ['Willison'], 'firstname': ['Simon']}, - '2': {'lastname': ['Holovaty'], 'firstname': ['Adrian']}}} + {'person': {'1': {'lastname': ['Willison'], 'firstname': ['Simon']}, '2': {'lastname': ['Holovaty'], 'firstname': ['Adrian']}}} >>> d['person'] - {'1': {'firstname': ['Simon'], 'lastname': ['Willison'], - '2': {'firstname': ['Adrian'], 'lastname': ['Holovaty']} + {'1': {'lastname': ['Willison'], 'firstname': ['Simon']}, '2': {'lastname': ['Holovaty'], 'firstname': ['Adrian']}} >>> d['person']['1'] - {'firstname': ['Simon'], 'lastname': ['Willison']} + {'lastname': ['Willison'], 'firstname': ['Simon']} # Gotcha: Results are unpredictable if the dots are "uneven": >>> DotExpandedDict({'c.1': 2, 'c.2': 3, 'c': 1}) - >>> {'c': 1} + {'c': 1} """ def __init__(self, key_to_list_mapping): for k, v in key_to_list_mapping.items(): @@ -259,4 +330,16 @@ class DotExpandedDict(dict): try: current[bits[-1]] = v except TypeError: # Special-case if current isn't a dict. - current = {bits[-1] : v} + current = {bits[-1]: v} + +class FileDict(dict): + """ + A dictionary used to hold uploaded file contents. The only special feature + here is that repr() of this object won't dump the entire contents of the + file to the output. A handy safeguard for a large file upload. + """ + def __repr__(self): + if 'content' in self: + d = dict(self, content='') + return dict.__repr__(d) + return dict.__repr__(self) diff --git a/deluge/ui/webui/lib/newforms_portable/django/utils/encoding.py b/deluge/ui/webui/lib/newforms_portable/django/utils/encoding.py new file mode 100644 index 000000000..2b5219cc6 --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/django/utils/encoding.py @@ -0,0 +1,102 @@ +import types +import urllib +import datetime + +from functional import Promise +from safestring import SafeData, mark_safe + +class DjangoUnicodeDecodeError(UnicodeDecodeError): + def __init__(self, obj, *args): + self.obj = obj + UnicodeDecodeError.__init__(self, *args) + + def __str__(self): + original = UnicodeDecodeError.__str__(self) + return '%s. You passed in %r (%s)' % (original, self.obj, + type(self.obj)) + +class StrAndUnicode(object): + """ + A class whose __str__ returns its __unicode__ as a UTF-8 bytestring. + + Useful as a mix-in. + """ + def __str__(self): + return self.__unicode__().encode('utf-8') + +def smart_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): + """ + Returns a unicode object representing 's'. Treats bytestrings using the + 'encoding' codec. + + If strings_only is True, don't convert (some) non-string-like objects. + """ + if isinstance(s, Promise): + # The input is the result of a gettext_lazy() call. + return s + return force_unicode(s, encoding, strings_only, errors) + +def force_unicode(s, encoding='utf-8', strings_only=False, errors='strict'): + """ + Similar to smart_unicode, except that lazy instances are resolved to + strings, rather than kept as lazy objects. + + If strings_only is True, don't convert (some) non-string-like objects. + """ + if strings_only and isinstance(s, (types.NoneType, int, long, datetime.datetime, datetime.date, datetime.time, float)): + return s + try: + if not isinstance(s, basestring,): + if hasattr(s, '__unicode__'): + s = unicode(s) + else: + s = unicode(str(s), encoding, errors) + elif not isinstance(s, unicode): + # Note: We use .decode() here, instead of unicode(s, encoding, + # errors), so that if s is a SafeString, it ends up being a + # SafeUnicode at the end. + s = s.decode(encoding, errors) + except UnicodeDecodeError, e: + raise DjangoUnicodeDecodeError(s, *e.args) + return s + +def smart_str(s, encoding='utf-8', strings_only=False, errors='strict'): + """ + Returns a bytestring version of 's', encoded as specified in 'encoding'. + + If strings_only is True, don't convert (some) non-string-like objects. + """ + if strings_only and isinstance(s, (types.NoneType, int)): + return s + if isinstance(s, Promise): + return unicode(s).encode(encoding, errors) + elif not isinstance(s, basestring): + try: + return str(s) + except UnicodeEncodeError: + return unicode(s).encode(encoding, errors) + elif isinstance(s, unicode): + return s.encode(encoding, errors) + elif s and encoding != 'utf-8': + return s.decode('utf-8', errors).encode(encoding, errors) + else: + return s + +def iri_to_uri(iri): + """ + Convert an Internationalized Resource Identifier (IRI) portion to a URI + portion that is suitable for inclusion in a URL. + + This is the algorithm from section 3.1 of RFC 3987. However, since we are + assuming input is either UTF-8 or unicode already, we can simplify things a + little from the full method. + + Returns an ASCII string containing the encoded result. + """ + # The list of safe characters here is constructed from the printable ASCII + # characters that are not explicitly excluded by the list at the end of + # section 3.1 of RFC 3987. + if iri is None: + return iri + return urllib.quote(smart_str(iri), safe='/#%[]=:;$&()+,!?*') + diff --git a/deluge/ui/webui/lib/newforms_portable/django/utils/functional.py b/deluge/ui/webui/lib/newforms_portable/django/utils/functional.py new file mode 100644 index 000000000..3de693e18 --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/django/utils/functional.py @@ -0,0 +1,241 @@ +# License for code in this file that was taken from Python 2.5. + +# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +# -------------------------------------------- +# +# 1. This LICENSE AGREEMENT is between the Python Software Foundation +# ("PSF"), and the Individual or Organization ("Licensee") accessing and +# otherwise using this software ("Python") in source or binary form and +# its associated documentation. +# +# 2. Subject to the terms and conditions of this License Agreement, PSF +# hereby grants Licensee a nonexclusive, royalty-free, world-wide +# license to reproduce, analyze, test, perform and/or display publicly, +# prepare derivative works, distribute, and otherwise use Python +# alone or in any derivative version, provided, however, that PSF's +# License Agreement and PSF's notice of copyright, i.e., "Copyright (c) +# 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software Foundation; +# All Rights Reserved" are retained in Python alone or in any derivative +# version prepared by Licensee. +# +# 3. In the event Licensee prepares a derivative work that is based on +# or incorporates Python or any part thereof, and wants to make +# the derivative work available to others as provided herein, then +# Licensee hereby agrees to include in any such work a brief summary of +# the changes made to Python. +# +# 4. PSF is making Python available to Licensee on an "AS IS" +# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +# INFRINGE ANY THIRD PARTY RIGHTS. +# +# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. +# +# 6. This License Agreement will automatically terminate upon a material +# breach of its terms and conditions. +# +# 7. Nothing in this License Agreement shall be deemed to create any +# relationship of agency, partnership, or joint venture between PSF and +# Licensee. This License Agreement does not grant permission to use PSF +# trademarks or trade name in a trademark sense to endorse or promote +# products or services of Licensee, or any third party. +# +# 8. By copying, installing or otherwise using Python, Licensee +# agrees to be bound by the terms and conditions of this License +# Agreement. + + +def curry(_curried_func, *args, **kwargs): + def _curried(*moreargs, **morekwargs): + return _curried_func(*(args+moreargs), **dict(kwargs, **morekwargs)) + return _curried + +### Begin from Python 2.5 functools.py ######################################## + +# Summary of changes made to the Python 2.5 code below: +# * swapped ``partial`` for ``curry`` to maintain backwards-compatibility +# in Django. +# * Wrapped the ``setattr`` call in ``update_wrapper`` with a try-except +# block to make it compatible with Python 2.3, which doesn't allow +# assigning to ``__name__``. + +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007 Python Software Foundation. +# All Rights Reserved. + +############################################################################### + +# update_wrapper() and wraps() are tools to help write +# wrapper functions that can handle naive introspection + +WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__') +WRAPPER_UPDATES = ('__dict__',) +def update_wrapper(wrapper, + wrapped, + assigned = WRAPPER_ASSIGNMENTS, + updated = WRAPPER_UPDATES): + """Update a wrapper function to look like the wrapped function + + wrapper is the function to be updated + wrapped is the original function + assigned is a tuple naming the attributes assigned directly + from the wrapped function to the wrapper function (defaults to + functools.WRAPPER_ASSIGNMENTS) + updated is a tuple naming the attributes off the wrapper that + are updated with the corresponding attribute from the wrapped + function (defaults to functools.WRAPPER_UPDATES) + """ + for attr in assigned: + try: + setattr(wrapper, attr, getattr(wrapped, attr)) + except TypeError: # Python 2.3 doesn't allow assigning to __name__. + pass + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr)) + # Return the wrapper so this can be used as a decorator via curry() + return wrapper + +def wraps(wrapped, + assigned = WRAPPER_ASSIGNMENTS, + updated = WRAPPER_UPDATES): + """Decorator factory to apply update_wrapper() to a wrapper function + + Returns a decorator that invokes update_wrapper() with the decorated + function as the wrapper argument and the arguments to wraps() as the + remaining arguments. Default arguments are as for update_wrapper(). + This is a convenience function to simplify applying curry() to + update_wrapper(). + """ + return curry(update_wrapper, wrapped=wrapped, + assigned=assigned, updated=updated) + +### End from Python 2.5 functools.py ########################################## + +def memoize(func, cache, num_args): + """ + Wrap a function so that results for any argument tuple are stored in + 'cache'. Note that the args to the function must be usable as dictionary + keys. + + Only the first num_args are considered when creating the key. + """ + def wrapper(*args): + mem_args = args[:num_args] + if mem_args in cache: + return cache[mem_args] + result = func(*args) + cache[mem_args] = result + return result + return wraps(func)(wrapper) + +class Promise(object): + """ + This is just a base class for the proxy class created in + the closure of the lazy function. It can be used to recognize + promises in code. + """ + pass + +def lazy(func, *resultclasses): + """ + Turns any callable into a lazy evaluated callable. You need to give result + classes or types -- at least one is needed so that the automatic forcing of + the lazy evaluation code is triggered. Results are not memoized; the + function is evaluated on every access. + """ + class __proxy__(Promise): + # This inner class encapsulates the code that should be evaluated + # lazily. On calling of one of the magic methods it will force + # the evaluation and store the result. Afterwards, the result + # is delivered directly. So the result is memoized. + def __init__(self, args, kw): + self.__func = func + self.__args = args + self.__kw = kw + self.__dispatch = {} + for resultclass in resultclasses: + self.__dispatch[resultclass] = {} + for (k, v) in resultclass.__dict__.items(): + setattr(self, k, self.__promise__(resultclass, k, v)) + self._delegate_str = str in resultclasses + self._delegate_unicode = unicode in resultclasses + assert not (self._delegate_str and self._delegate_unicode), "Cannot call lazy() with both str and unicode return types." + if self._delegate_unicode: + # Each call to lazy() makes a new __proxy__ object, so this + # doesn't interfere with any other lazy() results. + __proxy__.__unicode__ = __proxy__.__unicode_cast + elif self._delegate_str: + __proxy__.__str__ = __proxy__.__str_cast + + def __promise__(self, klass, funcname, func): + # Builds a wrapper around some magic method and registers that magic + # method for the given type and method name. + def __wrapper__(*args, **kw): + # Automatically triggers the evaluation of a lazy value and + # applies the given magic method of the result type. + res = self.__func(*self.__args, **self.__kw) + return self.__dispatch[type(res)][funcname](res, *args, **kw) + + if klass not in self.__dispatch: + self.__dispatch[klass] = {} + self.__dispatch[klass][funcname] = func + return __wrapper__ + + def __unicode_cast(self): + return self.__func(*self.__args, **self.__kw) + + def __str_cast(self): + return str(self.__func(*self.__args, **self.__kw)) + + def __cmp__(self, rhs): + if self._delegate_str: + s = str(self.__func(*self.__args, **self.__kw)) + elif self._delegate_unicode: + s = unicode(self.__func(*self.__args, **self.__kw)) + else: + s = self.__func(*self.__args, **self.__kw) + if isinstance(rhs, Promise): + return -cmp(rhs, s) + else: + return cmp(s, rhs) + + def __mod__(self, rhs): + if self._delegate_str: + return str(self) % rhs + elif self._delegate_unicode: + return unicode(self) % rhs + else: + raise AssertionError('__mod__ not supported for non-string types') + + def __deepcopy__(self, memo): + # Instances of this class are effectively immutable. It's just a + # collection of functions. So we don't need to do anything + # complicated for copying. + memo[id(self)] = self + return self + + def __wrapper__(*args, **kw): + # Creates the proxy object, instead of the actual value. + return __proxy__(args, kw) + + return wraps(func)(__wrapper__) + +def allow_lazy(func, *resultclasses): + """ + A decorator that allows a function to be called with one or more lazy + arguments. If none of the args are lazy, the function is evaluated + immediately, otherwise a __proxy__ is returned that will evaluate the + function when needed. + """ + def wrapper(*args, **kwargs): + for arg in list(args) + kwargs.values(): + if isinstance(arg, Promise): + break + else: + return func(*args, **kwargs) + return lazy(func, *resultclasses)(*args, **kwargs) + return wraps(func)(wrapper) diff --git a/deluge/ui/webui/lib/newforms_portable/django/utils/html.py b/deluge/ui/webui/lib/newforms_portable/django/utils/html.py new file mode 100644 index 000000000..3ac39864d --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/django/utils/html.py @@ -0,0 +1,163 @@ +"""HTML utilities suitable for global use.""" + +import re +import string + +from safestring import SafeData, mark_safe +from encoding import force_unicode +from functional import allow_lazy +from http import urlquote + +# Configuration for urlize() function. +LEADING_PUNCTUATION = ['(', '<', '<'] +TRAILING_PUNCTUATION = ['.', ',', ')', '>', '\n', '>'] + +# List of possible strings used for bullets in bulleted lists. +DOTS = ['·', '*', '\xe2\x80\xa2', '•', '•', '•'] + +unencoded_ampersands_re = re.compile(r'&(?!(\w+|#\d+);)') +word_split_re = re.compile(r'(\s+)') +punctuation_re = re.compile('^(?P(?:%s)*)(?P.*?)(?P(?:%s)*)$' % \ + ('|'.join([re.escape(x) for x in LEADING_PUNCTUATION]), + '|'.join([re.escape(x) for x in TRAILING_PUNCTUATION]))) +simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$') +link_target_attribute_re = re.compile(r'(]*?)target=[^\s>]+') +html_gunk_re = re.compile(r'(?:
    |<\/i>|<\/b>|<\/em>|<\/strong>|<\/?smallcaps>|<\/?uppercase>)', re.IGNORECASE) +hard_coded_bullets_re = re.compile(r'((?:

    (?:%s).*?[a-zA-Z].*?

    \s*)+)' % '|'.join([re.escape(x) for x in DOTS]), re.DOTALL) +trailing_empty_content_re = re.compile(r'(?:

    (?: |\s|
    )*?

    \s*)+\Z') +del x # Temporary variable + +def escape(html): + """Returns the given HTML with ampersands, quotes and carets encoded.""" + return mark_safe(force_unicode(html).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')) +escape = allow_lazy(escape, unicode) + +def conditional_escape(html): + """ + Similar to escape(), except that it doesn't operate on pre-escaped strings. + """ + if isinstance(html, SafeData): + return html + else: + return escape(html) + +def linebreaks(value, autoescape=False): + """Converts newlines into

    and
    s.""" + value = re.sub(r'\r\n|\r|\n', '\n', force_unicode(value)) # normalize newlines + paras = re.split('\n{2,}', value) + if autoescape: + paras = [u'

    %s

    ' % escape(p.strip()).replace('\n', '
    ') for p in paras] + else: + paras = [u'

    %s

    ' % p.strip().replace('\n', '
    ') for p in paras] + return u'\n\n'.join(paras) +linebreaks = allow_lazy(linebreaks, unicode) + +def strip_tags(value): + """Returns the given HTML with all tags stripped.""" + return re.sub(r'<[^>]*?>', '', force_unicode(value)) +strip_tags = allow_lazy(strip_tags) + +def strip_spaces_between_tags(value): + """Returns the given HTML with spaces between tags removed.""" + return re.sub(r'>\s+<', '><', force_unicode(value)) +strip_spaces_between_tags = allow_lazy(strip_spaces_between_tags, unicode) + +def strip_entities(value): + """Returns the given HTML with all entities (&something;) stripped.""" + return re.sub(r'&(?:\w+|#\d+);', '', force_unicode(value)) +strip_entities = allow_lazy(strip_entities, unicode) + +def fix_ampersands(value): + """Returns the given HTML with all unencoded ampersands encoded correctly.""" + return unencoded_ampersands_re.sub('&', force_unicode(value)) +fix_ampersands = allow_lazy(fix_ampersands, unicode) + +def urlize(text, trim_url_limit=None, nofollow=False, autoescape=False): + """ + Converts any URLs in text into clickable links. + + Works on http://, https://, and www. links. Links can have trailing + punctuation (periods, commas, close-parens) and leading punctuation + (opening parens) and it'll still do the right thing. + + If trim_url_limit is not None, the URLs in link text longer than this limit + will truncated to trim_url_limit-3 characters and appended with an elipsis. + + If nofollow is True, the URLs in link text will get a rel="nofollow" + attribute. + """ + if autoescape: + trim_url = lambda x, limit=trim_url_limit: conditional_escape(limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x) + else: + trim_url = lambda x, limit=trim_url_limit: limit is not None and (len(x) > limit and ('%s...' % x[:max(0, limit - 3)])) or x + safe_input = isinstance(text, SafeData) + words = word_split_re.split(force_unicode(text)) + nofollow_attr = nofollow and ' rel="nofollow"' or '' + for i, word in enumerate(words): + match = punctuation_re.match(word) + if match: + lead, middle, trail = match.groups() + if safe_input: + middle = mark_safe(middle) + if middle.startswith('www.') or ('@' not in middle and not middle.startswith('http://') and \ + len(middle) > 0 and middle[0] in string.ascii_letters + string.digits and \ + (middle.endswith('.org') or middle.endswith('.net') or middle.endswith('.com'))): + middle = 'http://%s' % middle + if middle.startswith('http://') or middle.startswith('https://'): + url = urlquote(middle, safe='/&=:;#?+*') + if autoescape and not safe_input: + url = escape(url) + trimmed_url = trim_url(middle) + middle = '
    %s' % (url, nofollow_attr, + trimmed_url) + elif '@' in middle and not middle.startswith('www.') and \ + not ':' in middle and simple_email_re.match(middle): + if autoescape: + middle = conditional_escape(middle) + middle = '%s' % (middle, middle) + if lead + middle + trail != word: + if autoescape and not safe_input: + lead, trail = escape(lead), escape(trail) + words[i] = mark_safe('%s%s%s' % (lead, middle, trail)) + elif autoescape and not safe_input: + words[i] = escape(word) + elif safe_input: + words[i] = mark_safe(word) + elif autoescape: + words[i] = escape(word) + return u''.join(words) +urlize = allow_lazy(urlize, unicode) + +def clean_html(text): + """ + Clean the given HTML. Specifically, do the following: + * Convert and to and . + * Encode all ampersands correctly. + * Remove all "target" attributes from tags. + * Remove extraneous HTML, such as presentational tags that open and + immediately close and
    . + * Convert hard-coded bullets into HTML unordered lists. + * Remove stuff like "

      

    ", but only if it's at the + bottom of the text. + """ + from django.utils.text import normalize_newlines + text = normalize_newlines(force_unicode(text)) + text = re.sub(r'<(/?)\s*b\s*>', '<\\1strong>', text) + text = re.sub(r'<(/?)\s*i\s*>', '<\\1em>', text) + text = fix_ampersands(text) + # Remove all target="" attributes from
    tags. + text = link_target_attribute_re.sub('\\1', text) + # Trim stupid HTML such as
    . + text = html_gunk_re.sub('', text) + # Convert hard-coded bullets into HTML unordered lists. + def replace_p_tags(match): + s = match.group().replace('

    ', '') + for d in DOTS: + s = s.replace('

    %s' % d, '

  • ') + return u'
      \n%s\n
    ' % s + text = hard_coded_bullets_re.sub(replace_p_tags, text) + # Remove stuff like "

      

    ", but only if it's at the bottom + # of the text. + text = trailing_empty_content_re.sub('', text) + return text +clean_html = allow_lazy(clean_html, unicode) diff --git a/deluge/ui/webui/lib/newforms_portable/django/utils/http.py b/deluge/ui/webui/lib/newforms_portable/django/utils/http.py new file mode 100644 index 000000000..db8bb9644 --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/django/utils/http.py @@ -0,0 +1,67 @@ +import urllib +from email.Utils import formatdate + +from encoding import smart_str, force_unicode +from functional import allow_lazy + +def urlquote(url, safe='/'): + """ + A version of Python's urllib.quote() function that can operate on unicode + strings. The url is first UTF-8 encoded before quoting. The returned string + can safely be used as part of an argument to a subsequent iri_to_uri() call + without double-quoting occurring. + """ + return force_unicode(urllib.quote(smart_str(url), safe)) + +urlquote = allow_lazy(urlquote, unicode) + +def urlquote_plus(url, safe=''): + """ + A version of Python's urllib.quote_plus() function that can operate on + unicode strings. The url is first UTF-8 encoded before quoting. The + returned string can safely be used as part of an argument to a subsequent + iri_to_uri() call without double-quoting occurring. + """ + return force_unicode(urllib.quote_plus(smart_str(url), safe)) +urlquote_plus = allow_lazy(urlquote_plus, unicode) + +def urlencode(query, doseq=0): + """ + A version of Python's urllib.urlencode() function that can operate on + unicode strings. The parameters are first case to UTF-8 encoded strings and + then encoded as per normal. + """ + if hasattr(query, 'items'): + query = query.items() + return urllib.urlencode( + [(smart_str(k), + isinstance(v, (list,tuple)) and [smart_str(i) for i in v] or smart_str(v)) + for k, v in query], + doseq) + +def cookie_date(epoch_seconds=None): + """ + Formats the time to ensure compatibility with Netscape's cookie standard. + + Accepts a floating point number expressed in seconds since the epoch, in + UTC - such as that outputted by time.time(). If set to None, defaults to + the current time. + + Outputs a string in the format 'Wdy, DD-Mon-YYYY HH:MM:SS GMT'. + """ + rfcdate = formatdate(epoch_seconds) + return '%s-%s-%s GMT' % (rfcdate[:7], rfcdate[8:11], rfcdate[12:25]) + +def http_date(epoch_seconds=None): + """ + Formats the time to match the RFC1123 date format as specified by HTTP + RFC2616 section 3.3.1. + + Accepts a floating point number expressed in seconds since the epoch, in + UTC - such as that outputted by time.time(). If set to None, defaults to + the current time. + + Outputs a string in the format 'Wdy, DD Mon YYYY HH:MM:SS GMT'. + """ + rfcdate = formatdate(epoch_seconds) + return '%s GMT' % rfcdate[:25] diff --git a/deluge/ui/webui/lib/newforms_portable/django/utils/safestring.py b/deluge/ui/webui/lib/newforms_portable/django/utils/safestring.py new file mode 100644 index 000000000..99658fb8b --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/django/utils/safestring.py @@ -0,0 +1,119 @@ +""" +Functions for working with "safe strings": strings that can be displayed safely +without further escaping in HTML. Marking something as a "safe string" means +that the producer of the string has already turned characters that should not +be interpreted by the HTML engine (e.g. '<') into the appropriate entities. +""" +from functional import curry, Promise + +class EscapeData(object): + pass + +class EscapeString(str, EscapeData): + """ + A string that should be HTML-escaped when output. + """ + pass + +class EscapeUnicode(unicode, EscapeData): + """ + A unicode object that should be HTML-escaped when output. + """ + pass + +class SafeData(object): + pass + +class SafeString(str, SafeData): + """ + A string subclass that has been specifically marked as "safe" (requires no + further escaping) for HTML output purposes. + """ + def __add__(self, rhs): + """ + Concatenating a safe string with another safe string or safe unicode + object is safe. Otherwise, the result is no longer safe. + """ + t = super(SafeString, self).__add__(rhs) + if isinstance(rhs, SafeUnicode): + return SafeUnicode(t) + elif isinstance(rhs, SafeString): + return SafeString(t) + return t + + def _proxy_method(self, *args, **kwargs): + """ + Wrap a call to a normal unicode method up so that we return safe + results. The method that is being wrapped is passed in the 'method' + argument. + """ + method = kwargs.pop('method') + data = method(self, *args, **kwargs) + if isinstance(data, str): + return SafeString(data) + else: + return SafeUnicode(data) + + decode = curry(_proxy_method, method = str.decode) + +class SafeUnicode(unicode, SafeData): + """ + A unicode subclass that has been specifically marked as "safe" for HTML + output purposes. + """ + def __add__(self, rhs): + """ + Concatenating a safe unicode object with another safe string or safe + unicode object is safe. Otherwise, the result is no longer safe. + """ + t = super(SafeUnicode, self).__add__(rhs) + if isinstance(rhs, SafeData): + return SafeUnicode(t) + return t + + def _proxy_method(self, *args, **kwargs): + """ + Wrap a call to a normal unicode method up so that we return safe + results. The method that is being wrapped is passed in the 'method' + argument. + """ + method = kwargs.pop('method') + data = method(self, *args, **kwargs) + if isinstance(data, str): + return SafeString(data) + else: + return SafeUnicode(data) + + encode = curry(_proxy_method, method = unicode.encode) + +def mark_safe(s): + """ + Explicitly mark a string as safe for (HTML) output purposes. The returned + object can be used everywhere a string or unicode object is appropriate. + + Can be called multiple times on a single string. + """ + if isinstance(s, SafeData): + return s + if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str): + return SafeString(s) + if isinstance(s, (unicode, Promise)): + return SafeUnicode(s) + return SafeString(str(s)) + +def mark_for_escaping(s): + """ + Explicitly mark a string as requiring HTML escaping upon output. Has no + effect on SafeData subclasses. + + Can be called multiple times on a single string (the resulting escaping is + only applied once). + """ + if isinstance(s, (SafeData, EscapeData)): + return s + if isinstance(s, str) or (isinstance(s, Promise) and s._delegate_str): + return EscapeString(s) + if isinstance(s, (unicode, Promise)): + return EscapeUnicode(s) + return EscapeString(str(s)) + diff --git a/deluge/ui/webui/lib/newforms_portable/django/utils/translation.py b/deluge/ui/webui/lib/newforms_portable/django/utils/translation.py new file mode 100644 index 000000000..ad65bd959 --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/django/utils/translation.py @@ -0,0 +1,9 @@ +try: + _('translate something') +except: + import gettext + gettext.install('locale') + +ugettext = _ +ugettext_lazy = _ + diff --git a/deluge/ui/webui/lib/newforms_portable/fields.py b/deluge/ui/webui/lib/newforms_portable/fields.py new file mode 100644 index 000000000..c20899ada --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/fields.py @@ -0,0 +1,784 @@ +""" +Field classes. +""" + +import copy +import datetime +import os +import re +import time +# Python 2.3 fallbacks +try: + from decimal import Decimal, DecimalException +except ImportError: + from django.utils._decimal import Decimal, DecimalException +try: + set +except NameError: + from sets import Set as set + +from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import StrAndUnicode, smart_unicode, smart_str + +from util import ErrorList, ValidationError +from widgets import TextInput, PasswordInput, HiddenInput, MultipleHiddenInput, FileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateTimeInput + + +__all__ = ( + 'Field', 'CharField', 'IntegerField', + 'DEFAULT_DATE_INPUT_FORMATS', 'DateField', + 'DEFAULT_TIME_INPUT_FORMATS', 'TimeField', + 'DEFAULT_DATETIME_INPUT_FORMATS', 'DateTimeField', + 'RegexField', 'EmailField', 'FileField', 'ImageField', 'URLField', + 'BooleanField', 'NullBooleanField', 'ChoiceField', 'MultipleChoiceField', + 'ComboField', 'MultiValueField', 'FloatField', 'DecimalField', + 'SplitDateTimeField', 'IPAddressField', 'FilePathField', +) + +# These values, if given to to_python(), will trigger the self.required check. +EMPTY_VALUES = (None, '') + + +class Field(object): + widget = TextInput # Default widget to use when rendering this type of Field. + hidden_widget = HiddenInput # Default widget to use when rendering this as "hidden". + default_error_messages = { + 'required': _(u'This field is required.'), + 'invalid': _(u'Enter a valid value.'), + } + + # Tracks each time a Field instance is created. Used to retain order. + creation_counter = 0 + + def __init__(self, required=True, widget=None, label=None, initial=None, + help_text=None, error_messages=None): + # required -- Boolean that specifies whether the field is required. + # True by default. + # widget -- A Widget class, or instance of a Widget class, that should + # be used for this Field when displaying it. Each Field has a + # default Widget that it'll use if you don't specify this. In + # most cases, the default widget is TextInput. + # label -- A verbose name for this field, for use in displaying this + # field in a form. By default, Django will use a "pretty" + # version of the form field name, if the Field is part of a + # Form. + # initial -- A value to use in this Field's initial display. This value + # is *not* used as a fallback if data isn't given. + # help_text -- An optional string to use as "help text" for this Field. + if label is not None: + label = smart_unicode(label) + self.required, self.label, self.initial = required, label, initial + self.help_text = smart_unicode(help_text or '') + widget = widget or self.widget + if isinstance(widget, type): + widget = widget() + + # Hook into self.widget_attrs() for any Field-specific HTML attributes. + extra_attrs = self.widget_attrs(widget) + if extra_attrs: + widget.attrs.update(extra_attrs) + + self.widget = widget + + # Increase the creation counter, and save our local copy. + self.creation_counter = Field.creation_counter + Field.creation_counter += 1 + + def set_class_error_messages(messages, klass): + for base_class in klass.__bases__: + set_class_error_messages(messages, base_class) + messages.update(getattr(klass, 'default_error_messages', {})) + + messages = {} + set_class_error_messages(messages, self.__class__) + messages.update(error_messages or {}) + self.error_messages = messages + + def clean(self, value): + """ + Validates the given value and returns its "cleaned" value as an + appropriate Python object. + + Raises ValidationError for any errors. + """ + if self.required and value in EMPTY_VALUES: + raise ValidationError(self.error_messages['required']) + return value + + def widget_attrs(self, widget): + """ + Given a Widget instance (*not* a Widget class), returns a dictionary of + any HTML attributes that should be added to the Widget, based on this + Field. + """ + return {} + + def __deepcopy__(self, memo): + result = copy.copy(self) + memo[id(self)] = result + result.widget = copy.deepcopy(self.widget, memo) + return result + +class CharField(Field): + default_error_messages = { + 'max_length': _(u'Ensure this value has at most %(max)d characters (it has %(length)d).'), + 'min_length': _(u'Ensure this value has at least %(min)d characters (it has %(length)d).'), + } + + def __init__(self, max_length=None, min_length=None, *args, **kwargs): + self.max_length, self.min_length = max_length, min_length + super(CharField, self).__init__(*args, **kwargs) + + def clean(self, value): + "Validates max_length and min_length. Returns a Unicode object." + super(CharField, self).clean(value) + if value in EMPTY_VALUES: + return u'' + value = smart_unicode(value) + value_length = len(value) + if self.max_length is not None and value_length > self.max_length: + raise ValidationError(self.error_messages['max_length'] % {'max': self.max_length, 'length': value_length}) + if self.min_length is not None and value_length < self.min_length: + raise ValidationError(self.error_messages['min_length'] % {'min': self.min_length, 'length': value_length}) + return value + + def widget_attrs(self, widget): + if self.max_length is not None and isinstance(widget, (TextInput, PasswordInput)): + # The HTML attribute is maxlength, not max_length. + return {'maxlength': str(self.max_length)} + +class IntegerField(Field): + default_error_messages = { + 'invalid': _(u'Enter a whole number.'), + 'max_value': _(u'Ensure this value is less than or equal to %s.'), + 'min_value': _(u'Ensure this value is greater than or equal to %s.'), + } + + def __init__(self, max_value=None, min_value=None, *args, **kwargs): + self.max_value, self.min_value = max_value, min_value + super(IntegerField, self).__init__(*args, **kwargs) + + def clean(self, value): + """ + Validates that int() can be called on the input. Returns the result + of int(). Returns None for empty values. + """ + super(IntegerField, self).clean(value) + if value in EMPTY_VALUES: + return None + try: + value = int(str(value)) + except (ValueError, TypeError): + raise ValidationError(self.error_messages['invalid']) + if self.max_value is not None and value > self.max_value: + raise ValidationError(self.error_messages['max_value'] % self.max_value) + if self.min_value is not None and value < self.min_value: + raise ValidationError(self.error_messages['min_value'] % self.min_value) + return value + +class FloatField(Field): + default_error_messages = { + 'invalid': _(u'Enter a number.'), + 'max_value': _(u'Ensure this value is less than or equal to %s.'), + 'min_value': _(u'Ensure this value is greater than or equal to %s.'), + } + + def __init__(self, max_value=None, min_value=None, *args, **kwargs): + self.max_value, self.min_value = max_value, min_value + Field.__init__(self, *args, **kwargs) + + def clean(self, value): + """ + Validates that float() can be called on the input. Returns a float. + Returns None for empty values. + """ + super(FloatField, self).clean(value) + if not self.required and value in EMPTY_VALUES: + return None + try: + value = float(value) + except (ValueError, TypeError): + raise ValidationError(self.error_messages['invalid']) + if self.max_value is not None and value > self.max_value: + raise ValidationError(self.error_messages['max_value'] % self.max_value) + if self.min_value is not None and value < self.min_value: + raise ValidationError(self.error_messages['min_value'] % self.min_value) + return value + +class DecimalField(Field): + default_error_messages = { + 'invalid': _(u'Enter a number.'), + 'max_value': _(u'Ensure this value is less than or equal to %s.'), + 'min_value': _(u'Ensure this value is greater than or equal to %s.'), + 'max_digits': _('Ensure that there are no more than %s digits in total.'), + 'max_decimal_places': _('Ensure that there are no more than %s decimal places.'), + 'max_whole_digits': _('Ensure that there are no more than %s digits before the decimal point.') + } + + def __init__(self, max_value=None, min_value=None, max_digits=None, decimal_places=None, *args, **kwargs): + self.max_value, self.min_value = max_value, min_value + self.max_digits, self.decimal_places = max_digits, decimal_places + Field.__init__(self, *args, **kwargs) + + def clean(self, value): + """ + Validates that the input is a decimal number. Returns a Decimal + instance. Returns None for empty values. Ensures that there are no more + than max_digits in the number, and no more than decimal_places digits + after the decimal point. + """ + super(DecimalField, self).clean(value) + if not self.required and value in EMPTY_VALUES: + return None + value = smart_str(value).strip() + try: + value = Decimal(value) + except DecimalException: + raise ValidationError(self.error_messages['invalid']) + pieces = str(value).lstrip("-").split('.') + decimals = (len(pieces) == 2) and len(pieces[1]) or 0 + digits = len(pieces[0]) + if self.max_value is not None and value > self.max_value: + raise ValidationError(self.error_messages['max_value'] % self.max_value) + if self.min_value is not None and value < self.min_value: + raise ValidationError(self.error_messages['min_value'] % self.min_value) + if self.max_digits is not None and (digits + decimals) > self.max_digits: + raise ValidationError(self.error_messages['max_digits'] % self.max_digits) + if self.decimal_places is not None and decimals > self.decimal_places: + raise ValidationError(self.error_messages['max_decimal_places'] % self.decimal_places) + if self.max_digits is not None and self.decimal_places is not None and digits > (self.max_digits - self.decimal_places): + raise ValidationError(self.error_messages['max_whole_digits'] % (self.max_digits - self.decimal_places)) + return value + +DEFAULT_DATE_INPUT_FORMATS = ( + '%Y-%m-%d', '%m/%d/%Y', '%m/%d/%y', # '2006-10-25', '10/25/2006', '10/25/06' + '%b %d %Y', '%b %d, %Y', # 'Oct 25 2006', 'Oct 25, 2006' + '%d %b %Y', '%d %b, %Y', # '25 Oct 2006', '25 Oct, 2006' + '%B %d %Y', '%B %d, %Y', # 'October 25 2006', 'October 25, 2006' + '%d %B %Y', '%d %B, %Y', # '25 October 2006', '25 October, 2006' +) + +class DateField(Field): + default_error_messages = { + 'invalid': _(u'Enter a valid date.'), + } + + def __init__(self, input_formats=None, *args, **kwargs): + super(DateField, self).__init__(*args, **kwargs) + self.input_formats = input_formats or DEFAULT_DATE_INPUT_FORMATS + + def clean(self, value): + """ + Validates that the input can be converted to a date. Returns a Python + datetime.date object. + """ + super(DateField, self).clean(value) + if value in EMPTY_VALUES: + return None + if isinstance(value, datetime.datetime): + return value.date() + if isinstance(value, datetime.date): + return value + for format in self.input_formats: + try: + return datetime.date(*time.strptime(value, format)[:3]) + except ValueError: + continue + raise ValidationError(self.error_messages['invalid']) + +DEFAULT_TIME_INPUT_FORMATS = ( + '%H:%M:%S', # '14:30:59' + '%H:%M', # '14:30' +) + +class TimeField(Field): + default_error_messages = { + 'invalid': _(u'Enter a valid time.') + } + + def __init__(self, input_formats=None, *args, **kwargs): + super(TimeField, self).__init__(*args, **kwargs) + self.input_formats = input_formats or DEFAULT_TIME_INPUT_FORMATS + + def clean(self, value): + """ + Validates that the input can be converted to a time. Returns a Python + datetime.time object. + """ + super(TimeField, self).clean(value) + if value in EMPTY_VALUES: + return None + if isinstance(value, datetime.time): + return value + for format in self.input_formats: + try: + return datetime.time(*time.strptime(value, format)[3:6]) + except ValueError: + continue + raise ValidationError(self.error_messages['invalid']) + +DEFAULT_DATETIME_INPUT_FORMATS = ( + '%Y-%m-%d %H:%M:%S', # '2006-10-25 14:30:59' + '%Y-%m-%d %H:%M', # '2006-10-25 14:30' + '%Y-%m-%d', # '2006-10-25' + '%m/%d/%Y %H:%M:%S', # '10/25/2006 14:30:59' + '%m/%d/%Y %H:%M', # '10/25/2006 14:30' + '%m/%d/%Y', # '10/25/2006' + '%m/%d/%y %H:%M:%S', # '10/25/06 14:30:59' + '%m/%d/%y %H:%M', # '10/25/06 14:30' + '%m/%d/%y', # '10/25/06' +) + +class DateTimeField(Field): + widget = DateTimeInput + default_error_messages = { + 'invalid': _(u'Enter a valid date/time.'), + } + + def __init__(self, input_formats=None, *args, **kwargs): + super(DateTimeField, self).__init__(*args, **kwargs) + self.input_formats = input_formats or DEFAULT_DATETIME_INPUT_FORMATS + + def clean(self, value): + """ + Validates that the input can be converted to a datetime. Returns a + Python datetime.datetime object. + """ + super(DateTimeField, self).clean(value) + if value in EMPTY_VALUES: + return None + if isinstance(value, datetime.datetime): + return value + if isinstance(value, datetime.date): + return datetime.datetime(value.year, value.month, value.day) + if isinstance(value, list): + # Input comes from a SplitDateTimeWidget, for example. So, it's two + # components: date and time. + if len(value) != 2: + raise ValidationError(self.error_messages['invalid']) + value = '%s %s' % tuple(value) + for format in self.input_formats: + try: + return datetime.datetime(*time.strptime(value, format)[:6]) + except ValueError: + continue + raise ValidationError(self.error_messages['invalid']) + +class RegexField(CharField): + def __init__(self, regex, max_length=None, min_length=None, error_message=None, *args, **kwargs): + """ + regex can be either a string or a compiled regular expression object. + error_message is an optional error message to use, if + 'Enter a valid value' is too generic for you. + """ + # error_message is just kept for backwards compatibility: + if error_message: + error_messages = kwargs.get('error_messages') or {} + error_messages['invalid'] = error_message + kwargs['error_messages'] = error_messages + super(RegexField, self).__init__(max_length, min_length, *args, **kwargs) + if isinstance(regex, basestring): + regex = re.compile(regex) + self.regex = regex + + def clean(self, value): + """ + Validates that the input matches the regular expression. Returns a + Unicode object. + """ + value = super(RegexField, self).clean(value) + if value == u'': + return value + if not self.regex.search(value): + raise ValidationError(self.error_messages['invalid']) + return value + +email_re = re.compile( + r"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" # dot-atom + r'|^"([\001-\010\013\014\016-\037!#-\[\]-\177]|\\[\001-011\013\014\016-\177])*"' # quoted-string + r')@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$', re.IGNORECASE) # domain + +class EmailField(RegexField): + default_error_messages = { + 'invalid': _(u'Enter a valid e-mail address.'), + } + + def __init__(self, max_length=None, min_length=None, *args, **kwargs): + RegexField.__init__(self, email_re, max_length, min_length, *args, + **kwargs) + +try: + from django.conf import settings + URL_VALIDATOR_USER_AGENT = settings.URL_VALIDATOR_USER_AGENT +except ImportError: + # It's OK if Django settings aren't configured. + URL_VALIDATOR_USER_AGENT = 'Django (http://www.djangoproject.com/)' + +class UploadedFile(StrAndUnicode): + "A wrapper for files uploaded in a FileField" + def __init__(self, filename, content): + self.filename = filename + self.content = content + + def __unicode__(self): + """ + The unicode representation is the filename, so that the pre-database-insertion + logic can use UploadedFile objects + """ + return self.filename + +class FileField(Field): + widget = FileInput + default_error_messages = { + 'invalid': _(u"No file was submitted. Check the encoding type on the form."), + 'missing': _(u"No file was submitted."), + 'empty': _(u"The submitted file is empty."), + } + + def __init__(self, *args, **kwargs): + super(FileField, self).__init__(*args, **kwargs) + + def clean(self, data, initial=None): + super(FileField, self).clean(initial or data) + if not self.required and data in EMPTY_VALUES: + return None + elif not data and initial: + return initial + try: + f = UploadedFile(data['filename'], data['content']) + except TypeError: + raise ValidationError(self.error_messages['invalid']) + except KeyError: + raise ValidationError(self.error_messages['missing']) + if not f.content: + raise ValidationError(self.error_messages['empty']) + return f + +class ImageField(FileField): + default_error_messages = { + 'invalid_image': _(u"Upload a valid image. The file you uploaded was either not an image or a corrupted image."), + } + + def clean(self, data, initial=None): + """ + Checks that the file-upload field data contains a valid image (GIF, JPG, + PNG, possibly others -- whatever the Python Imaging Library supports). + """ + f = super(ImageField, self).clean(data, initial) + if f is None: + return None + elif not data and initial: + return initial + from PIL import Image + from cStringIO import StringIO + try: + # load() is the only method that can spot a truncated JPEG, + # but it cannot be called sanely after verify() + trial_image = Image.open(StringIO(f.content)) + trial_image.load() + # verify() is the only method that can spot a corrupt PNG, + # but it must be called immediately after the constructor + trial_image = Image.open(StringIO(f.content)) + trial_image.verify() + except Exception: # Python Imaging Library doesn't recognize it as an image + raise ValidationError(self.error_messages['invalid_image']) + return f + +url_re = re.compile( + r'^https?://' # http:// or https:// + r'(?:(?:[A-Z0-9-]+\.)+[A-Z]{2,6}|' #domain... + r'localhost|' #localhost... + r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip + r'(?::\d+)?' # optional port + r'(?:/?|/\S+)$', re.IGNORECASE) + +class URLField(RegexField): + default_error_messages = { + 'invalid': _(u'Enter a valid URL.'), + 'invalid_link': _(u'This URL appears to be a broken link.'), + } + + def __init__(self, max_length=None, min_length=None, verify_exists=False, + validator_user_agent=URL_VALIDATOR_USER_AGENT, *args, **kwargs): + super(URLField, self).__init__(url_re, max_length, min_length, *args, + **kwargs) + self.verify_exists = verify_exists + self.user_agent = validator_user_agent + + def clean(self, value): + # If no URL scheme given, assume http:// + if value and '://' not in value: + value = u'http://%s' % value + value = super(URLField, self).clean(value) + if value == u'': + return value + if self.verify_exists: + import urllib2 + from django.conf import settings + headers = { + "Accept": "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5", + "Accept-Language": "en-us,en;q=0.5", + "Accept-Charset": "ISO-8859-1,utf-8;q=0.7,*;q=0.7", + "Connection": "close", + "User-Agent": self.user_agent, + } + try: + req = urllib2.Request(value, None, headers) + u = urllib2.urlopen(req) + except ValueError: + raise ValidationError(self.error_messages['invalid']) + except: # urllib2.URLError, httplib.InvalidURL, etc. + raise ValidationError(self.error_messages['invalid_link']) + return value + +class BooleanField(Field): + widget = CheckboxInput + + def clean(self, value): + """Returns a Python boolean object.""" + super(BooleanField, self).clean(value) + # Explicitly check for the string 'False', which is what a hidden field + # will submit for False. Because bool("True") == True, we don't need to + # handle that explicitly. + if value == 'False': + return False + return bool(value) + +class NullBooleanField(BooleanField): + """ + A field whose valid values are None, True and False. Invalid values are + cleaned to None. + """ + widget = NullBooleanSelect + + def clean(self, value): + return {True: True, False: False}.get(value, None) + +class ChoiceField(Field): + widget = Select + default_error_messages = { + 'invalid_choice': _(u'Select a valid choice. That choice is not one of the available choices.'), + } + + def __init__(self, choices=(), required=True, widget=None, label=None, + initial=None, help_text=None, *args, **kwargs): + super(ChoiceField, self).__init__(required, widget, label, initial, + help_text, *args, **kwargs) + self.choices = choices + + def _get_choices(self): + return self._choices + + def _set_choices(self, value): + # Setting choices also sets the choices on the widget. + # choices can be any iterable, but we call list() on it because + # it will be consumed more than once. + self._choices = self.widget.choices = list(value) + + choices = property(_get_choices, _set_choices) + + def clean(self, value): + """ + Validates that the input is in self.choices. + """ + value = super(ChoiceField, self).clean(value) + if value in EMPTY_VALUES: + value = u'' + value = smart_unicode(value) + if value == u'': + return value + valid_values = set([smart_unicode(k) for k, v in self.choices]) + if value not in valid_values: + raise ValidationError(self.error_messages['invalid_choice'] % {'value': value}) + return value + +class MultipleChoiceField(ChoiceField): + hidden_widget = MultipleHiddenInput + widget = SelectMultiple + default_error_messages = { + 'invalid_choice': _(u'Select a valid choice. %(value)s is not one of the available choices.'), + 'invalid_list': _(u'Enter a list of values.'), + } + + def clean(self, value): + """ + Validates that the input is a list or tuple. + """ + if self.required and not value: + raise ValidationError(self.error_messages['required']) + elif not self.required and not value: + return [] + if not isinstance(value, (list, tuple)): + raise ValidationError(self.error_messages['invalid_list']) + new_value = [smart_unicode(val) for val in value] + # Validate that each value in the value list is in self.choices. + valid_values = set([smart_unicode(k) for k, v in self.choices]) + for val in new_value: + if val not in valid_values: + raise ValidationError(self.error_messages['invalid_choice'] % {'value': val}) + return new_value + +class ComboField(Field): + """ + A Field whose clean() method calls multiple Field clean() methods. + """ + def __init__(self, fields=(), *args, **kwargs): + super(ComboField, self).__init__(*args, **kwargs) + # Set 'required' to False on the individual fields, because the + # required validation will be handled by ComboField, not by those + # individual fields. + for f in fields: + f.required = False + self.fields = fields + + def clean(self, value): + """ + Validates the given value against all of self.fields, which is a + list of Field instances. + """ + super(ComboField, self).clean(value) + for field in self.fields: + value = field.clean(value) + return value + +class MultiValueField(Field): + """ + A Field that aggregates the logic of multiple Fields. + + Its clean() method takes a "decompressed" list of values, which are then + cleaned into a single value according to self.fields. Each value in + this list is cleaned by the corresponding field -- the first value is + cleaned by the first field, the second value is cleaned by the second + field, etc. Once all fields are cleaned, the list of clean values is + "compressed" into a single value. + + Subclasses should not have to implement clean(). Instead, they must + implement compress(), which takes a list of valid values and returns a + "compressed" version of those values -- a single value. + + You'll probably want to use this with MultiWidget. + """ + default_error_messages = { + 'invalid': _(u'Enter a list of values.'), + } + + def __init__(self, fields=(), *args, **kwargs): + super(MultiValueField, self).__init__(*args, **kwargs) + # Set 'required' to False on the individual fields, because the + # required validation will be handled by MultiValueField, not by those + # individual fields. + for f in fields: + f.required = False + self.fields = fields + + def clean(self, value): + """ + Validates every value in the given list. A value is validated against + the corresponding Field in self.fields. + + For example, if this MultiValueField was instantiated with + fields=(DateField(), TimeField()), clean() would call + DateField.clean(value[0]) and TimeField.clean(value[1]). + """ + cleaned_data = [] + errors = ErrorList() + if not value or isinstance(value, (list, tuple)): + if not value or not [v for v in value if v not in EMPTY_VALUES]: + if self.required: + raise ValidationError(self.error_messages['required']) + else: + return self.compress([]) + else: + raise ValidationError(self.error_messages['invalid']) + for i, field in enumerate(self.fields): + try: + field_value = value[i] + except IndexError: + field_value = None + if self.required and field_value in EMPTY_VALUES: + raise ValidationError(self.error_messages['required']) + try: + cleaned_data.append(field.clean(field_value)) + except ValidationError, e: + # Collect all validation errors in a single list, which we'll + # raise at the end of clean(), rather than raising a single + # exception for the first error we encounter. + errors.extend(e.messages) + if errors: + raise ValidationError(errors) + return self.compress(cleaned_data) + + def compress(self, data_list): + """ + Returns a single value for the given list of values. The values can be + assumed to be valid. + + For example, if this MultiValueField was instantiated with + fields=(DateField(), TimeField()), this might return a datetime + object created by combining the date and time in data_list. + """ + raise NotImplementedError('Subclasses must implement this method.') + +class FilePathField(ChoiceField): + def __init__(self, path, match=None, recursive=False, required=True, + widget=Select, label=None, initial=None, help_text=None, + *args, **kwargs): + self.path, self.match, self.recursive = path, match, recursive + super(FilePathField, self).__init__(choices=(), required=required, + widget=widget, label=label, initial=initial, help_text=help_text, + *args, **kwargs) + self.choices = [] + if self.match is not None: + self.match_re = re.compile(self.match) + if recursive: + for root, dirs, files in os.walk(self.path): + for f in files: + if self.match is None or self.match_re.search(f): + f = os.path.join(root, f) + self.choices.append((f, f.replace(path, "", 1))) + else: + try: + for f in os.listdir(self.path): + full_file = os.path.join(self.path, f) + if os.path.isfile(full_file) and (self.match is None or self.match_re.search(f)): + self.choices.append((full_file, f)) + except OSError: + pass + self.widget.choices = self.choices + +class SplitDateTimeField(MultiValueField): + default_error_messages = { + 'invalid_date': _(u'Enter a valid date.'), + 'invalid_time': _(u'Enter a valid time.'), + } + + def __init__(self, *args, **kwargs): + errors = self.default_error_messages.copy() + if 'error_messages' in kwargs: + errors.update(kwargs['error_messages']) + fields = ( + DateField(error_messages={'invalid': errors['invalid_date']}), + TimeField(error_messages={'invalid': errors['invalid_time']}), + ) + super(SplitDateTimeField, self).__init__(fields, *args, **kwargs) + + def compress(self, data_list): + if data_list: + # Raise a validation error if time or date is empty + # (possible if SplitDateTimeField has required=False). + if data_list[0] in EMPTY_VALUES: + raise ValidationError(self.error_messages['invalid_date']) + if data_list[1] in EMPTY_VALUES: + raise ValidationError(self.error_messages['invalid_time']) + return datetime.datetime.combine(*data_list) + return None + +ipv4_re = re.compile(r'^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$') + +class IPAddressField(RegexField): + default_error_messages = { + 'invalid': _(u'Enter a valid IPv4 address.'), + } + + def __init__(self, *args, **kwargs): + super(IPAddressField, self).__init__(ipv4_re, *args, **kwargs) diff --git a/deluge/ui/webui/lib/newforms/forms.py b/deluge/ui/webui/lib/newforms_portable/forms.py similarity index 60% rename from deluge/ui/webui/lib/newforms/forms.py rename to deluge/ui/webui/lib/newforms_portable/forms.py index df4af3b54..2c481e47a 100644 --- a/deluge/ui/webui/lib/newforms/forms.py +++ b/deluge/ui/webui/lib/newforms_portable/forms.py @@ -2,14 +2,18 @@ Form classes """ -from utils.datastructures import SortedDict, MultiValueDict -from utils.html import escape -from fields import Field -from widgets import TextInput, Textarea, HiddenInput, MultipleHiddenInput -from util import flatatt, StrAndUnicode, ErrorDict, ErrorList, ValidationError -import copy +from copy import deepcopy -#__all__ = ('BaseForm', 'Form') +from django.utils.datastructures import SortedDict +from django.utils.html import escape +from django.utils.encoding import StrAndUnicode, smart_unicode, force_unicode +from django.utils.safestring import mark_safe + +from fields import Field, FileField +from widgets import TextInput, Textarea +from util import flatatt, ErrorDict, ErrorList, ValidationError + +__all__ = ('BaseForm', 'Form') NON_FIELD_ERRORS = '__all__' @@ -18,17 +22,32 @@ def pretty_name(name): name = name[0].upper() + name[1:] return name.replace('_', ' ') -class SortedDictFromList(SortedDict): - "A dictionary that keeps its keys in the order in which they're inserted." - # This is different than django.utils.datastructures.SortedDict, because - # this takes a list/tuple as the argument to __init__(). - def __init__(self, data=None): - if data is None: data = [] - self.keyOrder = [d[0] for d in data] - dict.__init__(self, dict(data)) +def get_declared_fields(bases, attrs, with_base_fields=True): + """ + Create a list of form field instances from the passed in 'attrs', plus any + similar fields on the base classes (in 'bases'). This is used by both the + Form and ModelForm metclasses. - def copy(self): - return SortedDictFromList([(k, copy.copy(v)) for k, v in self.items()]) + If 'with_base_fields' is True, all fields from the bases are used. + Otherwise, only fields in the 'declared_fields' attribute on the bases are + used. The distinction is useful in ModelForm subclassing. + """ + fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] + fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) + + # If this class is subclassing another Form, add that Form's fields. + # Note that we loop over the bases in *reverse*. This is necessary in + # order to preserve the correct order of fields. + if with_base_fields: + for base in bases[::-1]: + if hasattr(base, 'base_fields'): + fields = base.base_fields.items() + fields + else: + for base in bases[::-1]: + if hasattr(base, 'declared_fields'): + fields = base.declared_fields.items() + fields + + return SortedDict(fields) class DeclarativeFieldsMetaclass(type): """ @@ -36,17 +55,7 @@ class DeclarativeFieldsMetaclass(type): 'base_fields', taking into account parent class 'base_fields' as well. """ def __new__(cls, name, bases, attrs): - fields = [(field_name, attrs.pop(field_name)) for field_name, obj in attrs.items() if isinstance(obj, Field)] - fields.sort(lambda x, y: cmp(x[1].creation_counter, y[1].creation_counter)) - - # If this class is subclassing another Form, add that Form's fields. - # Note that we loop over the bases in *reverse*. This is necessary in - # order to preserve the correct order of fields. - for base in bases[::-1]: - if hasattr(base, 'base_fields'): - fields = base.base_fields.items() + fields - - attrs['base_fields'] = SortedDictFromList(fields) + attrs['base_fields'] = get_declared_fields(bases, attrs) return type.__new__(cls, name, bases, attrs) class BaseForm(StrAndUnicode): @@ -54,20 +63,24 @@ class BaseForm(StrAndUnicode): # class is different than Form. See the comments by the Form class for more # information. Any improvements to the form API should be made to *this* # class, not to the Form class. - def __init__(self, data=None, auto_id='id_%s', prefix=None, initial=None): - self.is_bound = data is not None + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, + initial=None, error_class=ErrorList, label_suffix=':'): + self.is_bound = data is not None or files is not None self.data = data or {} + self.files = files or {} self.auto_id = auto_id self.prefix = prefix self.initial = initial or {} - self.__errors = None # Stores the errors after clean() has been called. + self.error_class = error_class + self.label_suffix = label_suffix + self._errors = None # Stores the errors after clean() has been called. # The base_fields class attribute is the *class-wide* definition of # fields. Because a particular *instance* of the class might want to # alter self.fields, we create self.fields here by copying base_fields. # Instances should always modify self.fields; they should not modify # self.base_fields. - self.fields = self.base_fields.copy() + self.fields = deepcopy(self.base_fields) def __unicode__(self): return self.as_table() @@ -84,12 +97,12 @@ class BaseForm(StrAndUnicode): raise KeyError('Key %r not found in Form' % name) return BoundField(self, field, name) - def _errors(self): - "Returns an ErrorDict for self.data" - if self.__errors is None: + def _get_errors(self): + "Returns an ErrorDict for the data provided for the form" + if self._errors is None: self.full_clean() - return self.__errors - errors = property(_errors) + return self._errors + errors = property(_get_errors) def is_valid(self): """ @@ -113,31 +126,43 @@ class BaseForm(StrAndUnicode): output, hidden_fields = [], [] for name, field in self.fields.items(): bf = BoundField(self, field, name) - bf_errors = ErrorList([escape(error) for error in bf.errors]) # Escape and cache in local variable. + bf_errors = self.error_class([escape(error) for error in bf.errors]) # Escape and cache in local variable. if bf.is_hidden: if bf_errors: - top_errors.extend(['(Hidden field %s) %s' % (name, e) for e in bf_errors]) + top_errors.extend([u'(Hidden field %s) %s' % (name, force_unicode(e)) for e in bf_errors]) hidden_fields.append(unicode(bf)) else: if errors_on_separate_row and bf_errors: - output.append(error_row % bf_errors) - label = bf.label and bf.label_tag(escape(bf.label + ':')) or '' + output.append(error_row % force_unicode(bf_errors)) + if bf.label: + label = escape(force_unicode(bf.label)) + # Only add the suffix if the label does not end in + # punctuation. + if self.label_suffix: + if label[-1] not in ':?.!': + label += self.label_suffix + label = bf.label_tag(label) or '' + else: + label = '' if field.help_text: - help_text = help_text_html % field.help_text + help_text = help_text_html % force_unicode(field.help_text) else: help_text = u'' - output.append(normal_row % {'errors': bf_errors, 'label': label, 'field': unicode(bf), 'help_text': help_text}) + output.append(normal_row % {'errors': force_unicode(bf_errors), 'label': force_unicode(label), 'field': unicode(bf), 'help_text': help_text}) if top_errors: - output.insert(0, error_row % top_errors) + output.insert(0, error_row % force_unicode(top_errors)) if hidden_fields: # Insert any hidden fields in the last row. str_hidden = u''.join(hidden_fields) if output: last_row = output[-1] - # Chop off the trailing row_ender (e.g. '') and insert the hidden fields. + # Chop off the trailing row_ender (e.g. '') and + # insert the hidden fields. output[-1] = last_row[:-len(row_ender)] + str_hidden + row_ender - else: # If there aren't any rows in the output, just append the hidden fields. + else: + # If there aren't any rows in the output, just append the + # hidden fields. output.append(str_hidden) - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) def as_table(self): "Returns this form rendered as HTML s -- excluding the
    ." @@ -149,7 +174,7 @@ class BaseForm(StrAndUnicode): def as_p(self): "Returns this form rendered as HTML

    s." - return self._html_output(u'

    %(label)s %(field)s%(help_text)s

    ', u'

    %s

    ', '

    ', u' %s', True) + return self._html_output(u'

    %(label)s %(field)s%(help_text)s

    ', u'%s', '

    ', u' %s', True) def non_field_errors(self): """ @@ -157,37 +182,42 @@ class BaseForm(StrAndUnicode): field -- i.e., from Form.clean(). Returns an empty ErrorList if there are none. """ - return self.errors.get(NON_FIELD_ERRORS, ErrorList()) + return self.errors.get(NON_FIELD_ERRORS, self.error_class()) def full_clean(self): """ - Cleans all of self.data and populates self.__errors and self.clean_data. + Cleans all of self.data and populates self._errors and + self.cleaned_data. """ - errors = ErrorDict() + self._errors = ErrorDict() if not self.is_bound: # Stop further processing. - self.__errors = errors return - self.clean_data = {} + self.cleaned_data = {} for name, field in self.fields.items(): - # value_from_datadict() gets the data from the dictionary. + # value_from_datadict() gets the data from the data dictionaries. # Each widget type knows how to retrieve its own data, because some # widgets split data over several HTML fields. - value = field.widget.value_from_datadict(self.data, self.add_prefix(name)) + value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name)) try: - value = field.clean(value) - self.clean_data[name] = value + if isinstance(field, FileField): + initial = self.initial.get(name, field.initial) + value = field.clean(value, initial) + else: + value = field.clean(value) + self.cleaned_data[name] = value if hasattr(self, 'clean_%s' % name): value = getattr(self, 'clean_%s' % name)() - self.clean_data[name] = value + self.cleaned_data[name] = value except ValidationError, e: - errors[name] = e.messages + self._errors[name] = e.messages + if name in self.cleaned_data: + del self.cleaned_data[name] try: - self.clean_data = self.clean() + self.cleaned_data = self.clean() except ValidationError, e: - errors[NON_FIELD_ERRORS] = e.messages - if errors: - delattr(self, 'clean_data') - self.__errors = errors + self._errors[NON_FIELD_ERRORS] = e.messages + if self._errors: + delattr(self, 'cleaned_data') def clean(self): """ @@ -196,7 +226,17 @@ class BaseForm(StrAndUnicode): not be associated with a particular field; it will have a special-case association with the field named '__all__'. """ - return self.clean_data + return self.cleaned_data + + def is_multipart(self): + """ + Returns True if the form needs to be multipart-encrypted, i.e. it has + FileInput. Otherwise, False. + """ + for field in self.fields.values(): + if field.widget.needs_multipart_form: + return True + return False class Form(BaseForm): "A collection of Fields, plus their associated data." @@ -221,32 +261,33 @@ class BoundField(StrAndUnicode): self.help_text = field.help_text or '' def __unicode__(self): - "Renders this field as an HTML widget." - # Use the 'widget' attribute on the field to determine which type - # of HTML widget to use. - value = self.as_widget(self.field.widget) - if not isinstance(value, basestring): - # Some Widget render() methods -- notably RadioSelect -- return a - # "special" object rather than a string. Call the __str__() on that - # object to get its rendered value. - value = value.__str__() - return value + """Renders this field as an HTML widget.""" + return self.as_widget() def _errors(self): """ Returns an ErrorList for this field. Returns an empty ErrorList if there are none. """ - return self.form.errors.get(self.name, ErrorList()) + return self.form.errors.get(self.name, self.form.error_class()) errors = property(_errors) - def as_widget(self, widget, attrs=None): + def as_widget(self, widget=None, attrs=None): + """ + Renders the field by rendering the passed widget, adding any HTML + attributes passed as attrs. If no widget is specified, then the + field's default widget will be used. + """ + if not widget: + widget = self.field.widget attrs = attrs or {} auto_id = self.auto_id - if auto_id and not attrs.has_key('id') and not widget.attrs.has_key('id'): + if auto_id and 'id' not in attrs and 'id' not in widget.attrs: attrs['id'] = auto_id if not self.form.is_bound: data = self.form.initial.get(self.name, self.field.initial) + if callable(data): + data = data() else: data = self.data return widget.render(self.html_name, data, attrs=attrs) @@ -271,7 +312,7 @@ class BoundField(StrAndUnicode): """ Returns the data for this BoundField, or None if it wasn't given. """ - return self.field.widget.value_from_datadict(self.form.data, self.html_name) + return self.field.widget.value_from_datadict(self.form.data, self.form.files, self.html_name) data = property(_data) def label_tag(self, contents=None, attrs=None): @@ -288,7 +329,7 @@ class BoundField(StrAndUnicode): if id_: attrs = attrs and flatatt(attrs) or '' contents = '' % (widget.id_for_label(id_), attrs, contents) - return contents + return mark_safe(contents) def _is_hidden(self): "Returns True if this BoundField's widget is hidden." @@ -301,8 +342,8 @@ class BoundField(StrAndUnicode): associated Form has specified auto_id. Returns an empty string otherwise. """ auto_id = self.form.auto_id - if auto_id and '%s' in str(auto_id): - return str(auto_id) % self.html_name + if auto_id and '%s' in smart_unicode(auto_id): + return smart_unicode(auto_id) % self.html_name elif auto_id: return self.html_name return '' diff --git a/deluge/ui/webui/lib/newforms_portable/models.py b/deluge/ui/webui/lib/newforms_portable/models.py new file mode 100644 index 000000000..0590839b2 --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/models.py @@ -0,0 +1,398 @@ +""" +Helper functions for creating Form classes from Django models +and database field objects. +""" + +from warnings import warn + +from django.utils.translation import ugettext_lazy as _ +from django.utils.encoding import smart_unicode +from django.utils.datastructures import SortedDict +from django.core.exceptions import ImproperlyConfigured + +from util import ValidationError, ErrorList +from forms import BaseForm, get_declared_fields +from fields import Field, ChoiceField, EMPTY_VALUES +from widgets import Select, SelectMultiple, MultipleHiddenInput + +__all__ = ( + 'ModelForm', 'BaseModelForm', 'model_to_dict', 'fields_for_model', + 'save_instance', 'form_for_model', 'form_for_instance', 'form_for_fields', + 'ModelChoiceField', 'ModelMultipleChoiceField' +) + +def save_instance(form, instance, fields=None, fail_message='saved', + commit=True): + """ + Saves bound Form ``form``'s cleaned_data into model instance ``instance``. + + If commit=True, then the changes to ``instance`` will be saved to the + database. Returns ``instance``. + """ + from django.db import models + opts = instance.__class__._meta + if form.errors: + raise ValueError("The %s could not be %s because the data didn't" + " validate." % (opts.object_name, fail_message)) + cleaned_data = form.cleaned_data + for f in opts.fields: + if not f.editable or isinstance(f, models.AutoField) \ + or not f.name in cleaned_data: + continue + if fields and f.name not in fields: + continue + f.save_form_data(instance, cleaned_data[f.name]) + # Wrap up the saving of m2m data as a function. + def save_m2m(): + opts = instance.__class__._meta + cleaned_data = form.cleaned_data + for f in opts.many_to_many: + if fields and f.name not in fields: + continue + if f.name in cleaned_data: + f.save_form_data(instance, cleaned_data[f.name]) + if commit: + # If we are committing, save the instance and the m2m data immediately. + instance.save() + save_m2m() + else: + # We're not committing. Add a method to the form to allow deferred + # saving of m2m data. + form.save_m2m = save_m2m + return instance + +def make_model_save(model, fields, fail_message): + """Returns the save() method for a Form.""" + def save(self, commit=True): + return save_instance(self, model(), fields, fail_message, commit) + return save + +def make_instance_save(instance, fields, fail_message): + """Returns the save() method for a Form.""" + def save(self, commit=True): + return save_instance(self, instance, fields, fail_message, commit) + return save + +def form_for_model(model, form=BaseForm, fields=None, + formfield_callback=lambda f: f.formfield()): + """ + Returns a Form class for the given Django model class. + + Provide ``form`` if you want to use a custom BaseForm subclass. + + Provide ``formfield_callback`` if you want to define different logic for + determining the formfield for a given database field. It's a callable that + takes a database Field instance and returns a form Field instance. + """ + warn("form_for_model is deprecated. Use ModelForm instead.", + PendingDeprecationWarning, stacklevel=3) + opts = model._meta + field_list = [] + for f in opts.fields + opts.many_to_many: + if not f.editable: + continue + if fields and not f.name in fields: + continue + formfield = formfield_callback(f) + if formfield: + field_list.append((f.name, formfield)) + base_fields = SortedDict(field_list) + return type(opts.object_name + 'Form', (form,), + {'base_fields': base_fields, '_model': model, + 'save': make_model_save(model, fields, 'created')}) + +def form_for_instance(instance, form=BaseForm, fields=None, + formfield_callback=lambda f, **kwargs: f.formfield(**kwargs)): + """ + Returns a Form class for the given Django model instance. + + Provide ``form`` if you want to use a custom BaseForm subclass. + + Provide ``formfield_callback`` if you want to define different logic for + determining the formfield for a given database field. It's a callable that + takes a database Field instance, plus **kwargs, and returns a form Field + instance with the given kwargs (i.e. 'initial'). + """ + warn("form_for_instance is deprecated. Use ModelForm instead.", + PendingDeprecationWarning, stacklevel=3) + model = instance.__class__ + opts = model._meta + field_list = [] + for f in opts.fields + opts.many_to_many: + if not f.editable: + continue + if fields and not f.name in fields: + continue + current_value = f.value_from_object(instance) + formfield = formfield_callback(f, initial=current_value) + if formfield: + field_list.append((f.name, formfield)) + base_fields = SortedDict(field_list) + return type(opts.object_name + 'InstanceForm', (form,), + {'base_fields': base_fields, '_model': model, + 'save': make_instance_save(instance, fields, 'changed')}) + +def form_for_fields(field_list): + """ + Returns a Form class for the given list of Django database field instances. + """ + fields = SortedDict([(f.name, f.formfield()) + for f in field_list if f.editable]) + return type('FormForFields', (BaseForm,), {'base_fields': fields}) + + +# ModelForms ################################################################# + +def model_to_dict(instance, fields=None, exclude=None): + """ + Returns a dict containing the data in ``instance`` suitable for passing as + a Form's ``initial`` keyword argument. + + ``fields`` is an optional list of field names. If provided, only the named + fields will be included in the returned dict. + + ``exclude`` is an optional list of field names. If provided, the named + fields will be excluded from the returned dict, even if they are listed in + the ``fields`` argument. + """ + # avoid a circular import + from django.db.models.fields.related import ManyToManyField + opts = instance._meta + data = {} + for f in opts.fields + opts.many_to_many: + if not f.editable: + continue + if fields and not f.name in fields: + continue + if exclude and f.name in exclude: + continue + if isinstance(f, ManyToManyField): + # If the object doesn't have a primry key yet, just use an empty + # list for its m2m fields. Calling f.value_from_object will raise + # an exception. + if instance.pk is None: + data[f.name] = [] + else: + # MultipleChoiceWidget needs a list of pks, not object instances. + data[f.name] = [obj.pk for obj in f.value_from_object(instance)] + else: + data[f.name] = f.value_from_object(instance) + return data + +def fields_for_model(model, fields=None, exclude=None, formfield_callback=lambda f: f.formfield()): + """ + Returns a ``SortedDict`` containing form fields for the given model. + + ``fields`` is an optional list of field names. If provided, only the named + fields will be included in the returned fields. + + ``exclude`` is an optional list of field names. If provided, the named + fields will be excluded from the returned fields, even if they are listed + in the ``fields`` argument. + """ + # TODO: if fields is provided, it would be nice to return fields in that order + field_list = [] + opts = model._meta + for f in opts.fields + opts.many_to_many: + if not f.editable: + continue + if fields and not f.name in fields: + continue + if exclude and f.name in exclude: + continue + formfield = formfield_callback(f) + if formfield: + field_list.append((f.name, formfield)) + return SortedDict(field_list) + +class ModelFormOptions(object): + def __init__(self, options=None): + self.model = getattr(options, 'model', None) + self.fields = getattr(options, 'fields', None) + self.exclude = getattr(options, 'exclude', None) + + +class ModelFormMetaclass(type): + def __new__(cls, name, bases, attrs, + formfield_callback=lambda f: f.formfield()): + try: + parents = [b for b in bases if issubclass(b, ModelForm)] + except NameError: + # We are defining ModelForm itself. + parents = None + if not parents: + return super(ModelFormMetaclass, cls).__new__(cls, name, bases, + attrs) + + new_class = type.__new__(cls, name, bases, attrs) + declared_fields = get_declared_fields(bases, attrs, False) + opts = new_class._meta = ModelFormOptions(getattr(new_class, 'Meta', None)) + if opts.model: + # If a model is defined, extract form fields from it. + fields = fields_for_model(opts.model, opts.fields, + opts.exclude, formfield_callback) + # Override default model fields with any custom declared ones + # (plus, include all the other declared fields). + fields.update(declared_fields) + else: + fields = declared_fields + new_class.declared_fields = declared_fields + new_class.base_fields = fields + return new_class + +class BaseModelForm(BaseForm): + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, + initial=None, error_class=ErrorList, label_suffix=':', + instance=None): + opts = self._meta + if instance is None: + # if we didn't get an instance, instantiate a new one + self.instance = opts.model() + object_data = {} + else: + self.instance = instance + object_data = model_to_dict(instance, opts.fields, opts.exclude) + # if initial was provided, it should override the values from instance + if initial is not None: + object_data.update(initial) + BaseForm.__init__(self, data, files, auto_id, prefix, object_data, error_class, label_suffix) + + def save(self, commit=True): + """ + Saves this ``form``'s cleaned_data into model instance + ``self.instance``. + + If commit=True, then the changes to ``instance`` will be saved to the + database. Returns ``instance``. + """ + if self.instance.pk is None: + fail_message = 'created' + else: + fail_message = 'changed' + return save_instance(self, self.instance, self._meta.fields, fail_message, commit) + +class ModelForm(BaseModelForm): + __metaclass__ = ModelFormMetaclass + + +# Fields ##################################################################### + +class ModelChoiceIterator(object): + def __init__(self, field): + self.field = field + self.queryset = field.queryset + + def __iter__(self): + if self.field.empty_label is not None: + yield (u"", self.field.empty_label) + for obj in self.queryset: + yield (obj.pk, self.field.label_from_instance(obj)) + # Clear the QuerySet cache if required. + if not self.field.cache_choices: + self.queryset._result_cache = None + +class ModelChoiceField(ChoiceField): + """A ChoiceField whose choices are a model QuerySet.""" + # This class is a subclass of ChoiceField for purity, but it doesn't + # actually use any of ChoiceField's implementation. + default_error_messages = { + 'invalid_choice': _(u'Select a valid choice. That choice is not one of' + u' the available choices.'), + } + + def __init__(self, queryset, empty_label=u"---------", cache_choices=False, + required=True, widget=Select, label=None, initial=None, + help_text=None, *args, **kwargs): + self.empty_label = empty_label + self.cache_choices = cache_choices + + # Call Field instead of ChoiceField __init__() because we don't need + # ChoiceField.__init__(). + Field.__init__(self, required, widget, label, initial, help_text, + *args, **kwargs) + self.queryset = queryset + + def _get_queryset(self): + return self._queryset + + def _set_queryset(self, queryset): + self._queryset = queryset + self.widget.choices = self.choices + + queryset = property(_get_queryset, _set_queryset) + + # this method will be used to create object labels by the QuerySetIterator. + # Override it to customize the label. + def label_from_instance(self, obj): + """ + This method is used to convert objects into strings; it's used to + generate the labels for the choices presented by this object. Subclasses + can override this method to customize the display of the choices. + """ + return smart_unicode(obj) + + def _get_choices(self): + # If self._choices is set, then somebody must have manually set + # the property self.choices. In this case, just return self._choices. + if hasattr(self, '_choices'): + return self._choices + + # Otherwise, execute the QuerySet in self.queryset to determine the + # choices dynamically. Return a fresh QuerySetIterator that has not been + # consumed. Note that we're instantiating a new QuerySetIterator *each* + # time _get_choices() is called (and, thus, each time self.choices is + # accessed) so that we can ensure the QuerySet has not been consumed. This + # construct might look complicated but it allows for lazy evaluation of + # the queryset. + return ModelChoiceIterator(self) + + def _set_choices(self, value): + # This method is copied from ChoiceField._set_choices(). It's necessary + # because property() doesn't allow a subclass to overwrite only + # _get_choices without implementing _set_choices. + self._choices = self.widget.choices = list(value) + + choices = property(_get_choices, _set_choices) + + def clean(self, value): + Field.clean(self, value) + if value in EMPTY_VALUES: + return None + try: + value = self.queryset.get(pk=value) + except self.queryset.model.DoesNotExist: + raise ValidationError(self.error_messages['invalid_choice']) + return value + +class ModelMultipleChoiceField(ModelChoiceField): + """A MultipleChoiceField whose choices are a model QuerySet.""" + hidden_widget = MultipleHiddenInput + default_error_messages = { + 'list': _(u'Enter a list of values.'), + 'invalid_choice': _(u'Select a valid choice. %s is not one of the' + u' available choices.'), + } + + def __init__(self, queryset, cache_choices=False, required=True, + widget=SelectMultiple, label=None, initial=None, + help_text=None, *args, **kwargs): + super(ModelMultipleChoiceField, self).__init__(queryset, None, + cache_choices, required, widget, label, initial, help_text, + *args, **kwargs) + + def clean(self, value): + if self.required and not value: + raise ValidationError(self.error_messages['required']) + elif not self.required and not value: + return [] + if not isinstance(value, (list, tuple)): + raise ValidationError(self.error_messages['list']) + final_values = [] + for val in value: + try: + obj = self.queryset.get(pk=val) + except self.queryset.model.DoesNotExist: + raise ValidationError(self.error_messages['invalid_choice'] % val) + else: + final_values.append(obj) + return final_values diff --git a/deluge/ui/webui/lib/newforms_portable/util.py b/deluge/ui/webui/lib/newforms_portable/util.py new file mode 100644 index 000000000..b3edf41ad --- /dev/null +++ b/deluge/ui/webui/lib/newforms_portable/util.py @@ -0,0 +1,69 @@ +from django.utils.html import escape +from django.utils.encoding import smart_unicode, StrAndUnicode, force_unicode +from django.utils.functional import Promise +from django.utils.safestring import mark_safe + +def flatatt(attrs): + """ + Convert a dictionary of attributes to a single string. + The returned string will contain a leading space followed by key="value", + XML-style pairs. It is assumed that the keys do not need to be XML-escaped. + If the passed dictionary is empty, then return an empty string. + """ + return u''.join([u' %s="%s"' % (k, escape(v)) for k, v in attrs.items()]) + +class ErrorDict(dict, StrAndUnicode): + """ + A collection of errors that knows how to display itself in various formats. + + The dictionary keys are the field names, and the values are the errors. + """ + def __unicode__(self): + return self.as_ul() + + def as_ul(self): + if not self: return u'' + return mark_safe(u'
      %s
    ' + % ''.join([u'
  • %s%s
  • ' % (k, force_unicode(v)) + for k, v in self.items()])) + + def as_text(self): + return u'\n'.join([u'* %s\n%s' % (k, u'\n'.join([u' * %s' % force_unicode(i) for i in v])) for k, v in self.items()]) + +class ErrorList(list, StrAndUnicode): + """ + A collection of errors that knows how to display itself in various formats. + """ + def __unicode__(self): + return self.as_ul() + + def as_ul(self): + if not self: return u'' + return mark_safe(u'
      %s
    ' + % ''.join([u'
  • %s
  • ' % force_unicode(e) for e in self])) + + def as_text(self): + if not self: return u'' + return u'\n'.join([u'* %s' % force_unicode(e) for e in self]) + + def __repr__(self): + return repr([force_unicode(e) for e in self]) + +class ValidationError(Exception): + def __init__(self, message): + """ + ValidationError can be passed any object that can be printed (usually + a string) or a list of objects. + """ + if isinstance(message, list): + self.messages = ErrorList([smart_unicode(msg) for msg in message]) + else: + message = smart_unicode(message) + self.messages = ErrorList([message]) + + def __str__(self): + # This is needed because, without a __str__(), printing an exception + # instance would result in this: + # AttributeError: ValidationError instance has no attribute 'args' + # See http://www.python.org/doc/current/tut/node10.html#handling + return repr(self.messages) diff --git a/deluge/ui/webui/lib/newforms/widgets.py b/deluge/ui/webui/lib/newforms_portable/widgets.py similarity index 56% rename from deluge/ui/webui/lib/newforms/widgets.py rename to deluge/ui/webui/lib/newforms_portable/widgets.py index f58c137d0..20a7cab46 100644 --- a/deluge/ui/webui/lib/newforms/widgets.py +++ b/deluge/ui/webui/lib/newforms_portable/widgets.py @@ -2,29 +2,44 @@ HTML Widget classes """ -__all__ = ( - 'Widget', 'TextInput', 'PasswordInput', 'HiddenInput', 'MultipleHiddenInput', - 'FileInput', 'Textarea', 'CheckboxInput', - 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 'CheckboxSelectMultiple', - 'MultiWidget', 'SplitDateTimeWidget', -) +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback -from util import flatatt, StrAndUnicode, smart_unicode -from utils.datastructures import MultiValueDict -from utils.html import escape -from gettext import gettext +import copy from itertools import chain -try: - set # Only available in Python 2.4+ -except NameError: - from sets import Set as set # Python 2.3 fallback +from django.utils.datastructures import MultiValueDict +from django.utils.html import escape, conditional_escape +from django.utils.translation import ugettext +from django.utils.encoding import StrAndUnicode, force_unicode +from django.utils.safestring import mark_safe +from util import flatatt + +__all__ = ( + 'Widget', 'TextInput', 'PasswordInput', + 'HiddenInput', 'MultipleHiddenInput', + 'FileInput', 'DateTimeInput', 'Textarea', 'CheckboxInput', + 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', + 'CheckboxSelectMultiple', 'MultiWidget', 'SplitDateTimeWidget', +) class Widget(object): is_hidden = False # Determines whether this corresponds to an . + needs_multipart_form = False # Determines does this widget need multipart-encrypted form def __init__(self, attrs=None): - self.attrs = attrs or {} + if attrs is not None: + self.attrs = attrs.copy() + else: + self.attrs = {} + + def __deepcopy__(self, memo): + obj = copy.copy(self) + obj.attrs = self.attrs.copy() + memo[id(self)] = obj + return obj def render(self, name, value, attrs=None): """ @@ -42,7 +57,7 @@ class Widget(object): attrs.update(extra_attrs) return attrs - def value_from_datadict(self, data, name): + def value_from_datadict(self, data, files, name): """ Given a dictionary of data and this widget's name, returns the value of this widget. Returns None if it's not provided. @@ -72,8 +87,10 @@ class Input(Widget): def render(self, name, value, attrs=None): if value is None: value = '' final_attrs = self.build_attrs(attrs, type=self.input_type, name=name) - if value != '': final_attrs['value'] = smart_unicode(value) # Only add the 'value' attribute if a value is non-empty. - return u'' % flatatt(final_attrs) + if value != '': + # Only add the 'value' attribute if a value is non-empty. + final_attrs['value'] = force_unicode(value) + return mark_safe(u'' % flatatt(final_attrs)) class TextInput(Input): input_type = 'text' @@ -82,7 +99,7 @@ class PasswordInput(Input): input_type = 'password' def __init__(self, attrs=None, render_value=True): - self.attrs = attrs or {} + super(PasswordInput, self).__init__(attrs) self.render_value = render_value def render(self, name, value, attrs=None): @@ -99,35 +116,68 @@ class MultipleHiddenInput(HiddenInput): of values. """ def __init__(self, attrs=None, choices=()): + super(MultipleHiddenInput, self).__init__(attrs) # choices can be any iterable - self.attrs = attrs or {} self.choices = choices def render(self, name, value, attrs=None, choices=()): if value is None: value = [] final_attrs = self.build_attrs(attrs, type=self.input_type, name=name) - return u'\n'.join([(u'' % flatatt(dict(value=smart_unicode(v), **final_attrs))) for v in value]) + return mark_safe(u'\n'.join([(u'' % + flatatt(dict(value=force_unicode(v), **final_attrs))) + for v in value])) - def value_from_datadict(self, data, name): + def value_from_datadict(self, data, files, name): if isinstance(data, MultiValueDict): return data.getlist(name) return data.get(name, None) class FileInput(Input): input_type = 'file' + needs_multipart_form = True + + def render(self, name, value, attrs=None): + return super(FileInput, self).render(name, None, attrs=attrs) + + def value_from_datadict(self, data, files, name): + "File widgets take data from FILES, not POST" + return files.get(name, None) class Textarea(Widget): + def __init__(self, attrs=None): + # The 'rows' and 'cols' attributes are required for HTML correctness. + self.attrs = {'cols': '40', 'rows': '10'} + if attrs: + self.attrs.update(attrs) + def render(self, name, value, attrs=None): if value is None: value = '' - value = smart_unicode(value) + value = force_unicode(value) final_attrs = self.build_attrs(attrs, name=name) - return u'%s' % (flatatt(final_attrs), escape(value)) + return mark_safe(u'%s' % (flatatt(final_attrs), + conditional_escape(force_unicode(value)))) + +class DateTimeInput(Input): + input_type = 'text' + format = '%Y-%m-%d %H:%M:%S' # '2006-10-25 14:30:59' + + def __init__(self, attrs=None, format=None): + super(DateTimeInput, self).__init__(attrs) + if format: + self.format = format + + def render(self, name, value, attrs=None): + if value is None: + value = '' + elif hasattr(value, 'strftime'): + value = value.strftime(self.format) + return super(DateTimeInput, self).render(name, value, attrs) class CheckboxInput(Widget): def __init__(self, attrs=None, check_test=bool): + super(CheckboxInput, self).__init__(attrs) # check_test is a callable that takes a value and returns True # if the checkbox should be checked for that value. - self.attrs = attrs or {} self.check_test = check_test def render(self, name, value, attrs=None): @@ -139,12 +189,20 @@ class CheckboxInput(Widget): if result: final_attrs['checked'] = 'checked' if value not in ('', True, False, None): - final_attrs['value'] = smart_unicode(value) # Only add the 'value' attribute if a value is non-empty. - return u'' % flatatt(final_attrs) + # Only add the 'value' attribute if a value is non-empty. + final_attrs['value'] = force_unicode(value) + return mark_safe(u'' % flatatt(final_attrs)) + + def value_from_datadict(self, data, files, name): + if name not in data: + # A missing value means False because HTML form submission does not + # send results for unselected checkboxes. + return False + return super(CheckboxInput, self).value_from_datadict(data, files, name) class Select(Widget): def __init__(self, attrs=None, choices=()): - self.attrs = attrs or {} + super(Select, self).__init__(attrs) # choices can be any iterable, but we may need to render this widget # multiple times. Thus, collapse it into a list so it can be consumed # more than once. @@ -154,20 +212,23 @@ class Select(Widget): if value is None: value = '' final_attrs = self.build_attrs(attrs, name=name) output = [u'' % flatatt(final_attrs)] - str_value = smart_unicode(value) # Normalize to string. + # Normalize to string. + str_value = force_unicode(value) for option_value, option_label in chain(self.choices, choices): - option_value = smart_unicode(option_value) + option_value = force_unicode(option_value) selected_html = (option_value == str_value) and u' selected="selected"' or '' - output.append(u'' % (escape(option_value), selected_html, escape(smart_unicode(option_label)))) + output.append(u'' % ( + escape(option_value), selected_html, + conditional_escape(force_unicode(option_label)))) output.append(u'') - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) class NullBooleanSelect(Select): """ A Select Widget intended to be used with NullBooleanField. """ def __init__(self, attrs=None): - choices = ((u'1', gettext('Unknown')), (u'2', gettext('Yes')), (u'3', gettext('No'))) + choices = ((u'1', ugettext('Unknown')), (u'2', ugettext('Yes')), (u'3', ugettext('No'))) super(NullBooleanSelect, self).__init__(attrs, choices) def render(self, name, value, attrs=None, choices=()): @@ -177,58 +238,68 @@ class NullBooleanSelect(Select): value = u'1' return super(NullBooleanSelect, self).render(name, value, attrs, choices) - def value_from_datadict(self, data, name): + def value_from_datadict(self, data, files, name): value = data.get(name, None) return {u'2': True, u'3': False, True: True, False: False}.get(value, None) class SelectMultiple(Widget): def __init__(self, attrs=None, choices=()): + super(SelectMultiple, self).__init__(attrs) # choices can be any iterable - self.attrs = attrs or {} self.choices = choices def render(self, name, value, attrs=None, choices=()): if value is None: value = [] final_attrs = self.build_attrs(attrs, name=name) output = [u'') - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) - def value_from_datadict(self, data, name): + def value_from_datadict(self, data, files, name): if isinstance(data, MultiValueDict): return data.getlist(name) return data.get(name, None) class RadioInput(StrAndUnicode): - "An object used by RadioFieldRenderer that represents a single ." + """ + An object used by RadioFieldRenderer that represents a single + . + """ + def __init__(self, name, value, attrs, choice, index): self.name, self.value = name, value self.attrs = attrs - self.choice_value = smart_unicode(choice[0]) - self.choice_label = smart_unicode(choice[1]) + self.choice_value = force_unicode(choice[0]) + self.choice_label = force_unicode(choice[1]) self.index = index def __unicode__(self): - return u'' % (self.tag(), self.choice_label) + return mark_safe(u'' % (self.tag(), + conditional_escape(force_unicode(self.choice_label)))) def is_checked(self): return self.value == self.choice_value def tag(self): - if self.attrs.has_key('id'): + if 'id' in self.attrs: self.attrs['id'] = '%s_%s' % (self.attrs['id'], self.index) final_attrs = dict(self.attrs, type='radio', name=self.name, value=self.choice_value) if self.is_checked(): final_attrs['checked'] = 'checked' - return u'' % flatatt(final_attrs) + return mark_safe(u'' % flatatt(final_attrs)) class RadioFieldRenderer(StrAndUnicode): - "An object used by RadioSelect to enable customization of radio widgets." + """ + An object used by RadioSelect to enable customization of radio widgets. + """ + def __init__(self, name, value, attrs, choices): self.name, self.value, self.attrs = name, value, attrs self.choices = choices @@ -242,16 +313,33 @@ class RadioFieldRenderer(StrAndUnicode): return RadioInput(self.name, self.value, self.attrs.copy(), choice, idx) def __unicode__(self): - "Outputs a
      for this set of radio fields." - return u'
        \n%s\n
      ' % u'\n'.join([u'
    • %s
    • ' % w for w in self]) + return self.render() + + def render(self): + """Outputs a
        for this set of radio fields.""" + return mark_safe(u'
          \n%s\n
        ' % u'\n'.join([u'
      • %s
      • ' + % force_unicode(w) for w in self])) class RadioSelect(Select): - def render(self, name, value, attrs=None, choices=()): - "Returns a RadioFieldRenderer instance rather than a Unicode string." + renderer = RadioFieldRenderer + + def __init__(self, *args, **kwargs): + # Override the default renderer if we were passed one. + renderer = kwargs.pop('renderer', None) + if renderer: + self.renderer = renderer + super(RadioSelect, self).__init__(*args, **kwargs) + + def get_renderer(self, name, value, attrs=None, choices=()): + """Returns an instance of the renderer.""" if value is None: value = '' - str_value = smart_unicode(value) # Normalize to string. - attrs = attrs or {} - return RadioFieldRenderer(name, str_value, attrs, list(chain(self.choices, choices))) + str_value = force_unicode(value) # Normalize to string. + final_attrs = self.build_attrs(attrs) + choices = list(chain(self.choices, choices)) + return self.renderer(name, str_value, final_attrs, choices) + + def render(self, name, value, attrs=None, choices=()): + return self.get_renderer(name, value, attrs, choices).render() def id_for_label(self, id_): # RadioSelect is represented by multiple fields, @@ -266,21 +354,23 @@ class RadioSelect(Select): class CheckboxSelectMultiple(SelectMultiple): def render(self, name, value, attrs=None, choices=()): if value is None: value = [] - has_id = attrs and attrs.has_key('id') + has_id = attrs and 'id' in attrs final_attrs = self.build_attrs(attrs, name=name) output = [u'
          '] - str_values = set([smart_unicode(v) for v in value]) # Normalize to strings. + # Normalize to strings + str_values = set([force_unicode(v) for v in value]) for i, (option_value, option_label) in enumerate(chain(self.choices, choices)): # If an ID attribute was given, add a numeric index as a suffix, # so that the checkboxes don't all have the same ID attribute. if has_id: final_attrs = dict(final_attrs, id='%s_%s' % (attrs['id'], i)) cb = CheckboxInput(final_attrs, check_test=lambda value: value in str_values) - option_value = smart_unicode(option_value) + option_value = force_unicode(option_value) rendered_cb = cb.render(name, option_value) - output.append(u'
        • ' % (rendered_cb, escape(smart_unicode(option_label)))) + output.append(u'
        • ' % (rendered_cb, + conditional_escape(force_unicode(option_label)))) output.append(u'
        ') - return u'\n'.join(output) + return mark_safe(u'\n'.join(output)) def id_for_label(self, id_): # See the comment for RadioSelect.id_for_label() @@ -293,19 +383,28 @@ class MultiWidget(Widget): """ A widget that is composed of multiple widgets. - Its render() method takes a "decompressed" list of values, not a single - value. Each value in this list is rendered in the corresponding widget -- - the first value is rendered in the first widget, the second value is - rendered in the second widget, etc. + Its render() method is different than other widgets', because it has to + figure out how to split a single value for display in multiple widgets. + The ``value`` argument can be one of two things: - Subclasses should implement decompress(), which specifies how a single - value should be converted to a list of values. Subclasses should not - have to implement clean(). + * A list. + * A normal value (e.g., a string) that has been "compressed" from + a list of values. + + In the second case -- i.e., if the value is NOT a list -- render() will + first "decompress" the value into a list before rendering it. It does so by + calling the decompress() method, which MultiWidget subclasses must + implement. This method takes a single "compressed" value and returns a + list. + + When render() does its HTML rendering, each value in the list is rendered + with the corresponding widget -- the first value is rendered in the first + widget, the second value is rendered in the second widget, etc. Subclasses may implement format_output(), which takes the list of rendered - widgets and returns HTML that formats them any way you'd like. + widgets and returns a string of HTML that formats them any way you'd like. - You'll probably want to use this with MultiValueField. + You'll probably want to use this class with MultiValueField. """ def __init__(self, widgets, attrs=None): self.widgets = [isinstance(w, type) and w() or w for w in widgets] @@ -317,18 +416,36 @@ class MultiWidget(Widget): if not isinstance(value, list): value = self.decompress(value) output = [] + final_attrs = self.build_attrs(attrs) + id_ = final_attrs.get('id', None) for i, widget in enumerate(self.widgets): try: widget_value = value[i] - except KeyError: + except IndexError: widget_value = None - output.append(widget.render(name + '_%s' % i, widget_value, attrs)) - return self.format_output(output) + if id_: + final_attrs = dict(final_attrs, id='%s_%s' % (id_, i)) + output.append(widget.render(name + '_%s' % i, widget_value, final_attrs)) + return mark_safe(self.format_output(output)) - def value_from_datadict(self, data, name): - return [data.get(name + '_%s' % i) for i in range(len(self.widgets))] + def id_for_label(self, id_): + # See the comment for RadioSelect.id_for_label() + if id_: + id_ += '_0' + return id_ + id_for_label = classmethod(id_for_label) + + def value_from_datadict(self, data, files, name): + return [widget.value_from_datadict(data, files, name + '_%s' % i) for i, widget in enumerate(self.widgets)] def format_output(self, rendered_widgets): + """ + Given a list of rendered widgets (as strings), returns a Unicode string + representing the HTML for the whole lot. + + This hook allows you to format the HTML design of the widgets, if + needed. + """ return u''.join(rendered_widgets) def decompress(self, value): @@ -349,5 +466,6 @@ class SplitDateTimeWidget(MultiWidget): def decompress(self, value): if value: - return [value.date(), value.time()] + return [value.date(), value.time().replace(microsecond=0)] return [None, None] + diff --git a/deluge/ui/webui/pages.py b/deluge/ui/webui/pages.py index 2e67656bb..53bddfc08 100644 --- a/deluge/ui/webui/pages.py +++ b/deluge/ui/webui/pages.py @@ -103,7 +103,6 @@ urls = [ ] #/routing - #pages: class login: @deco.deluge_page_noauth diff --git a/deluge/ui/webui/torrent_add.py b/deluge/ui/webui/torrent_add.py index 35f1b19dc..bc81c063e 100644 --- a/deluge/ui/webui/torrent_add.py +++ b/deluge/ui/webui/torrent_add.py @@ -102,7 +102,7 @@ class torrent_add: if not options_form.is_valid(): print self.add_page(error = _("Error in torrent options.")) return - options = options_form.clean_data + options = options_form.cleaned_data vars = web.input(url = None, torrent = {}) diff --git a/deluge/ui/webui/torrent_move.py b/deluge/ui/webui/torrent_move.py index 0afb04209..4fb16696a 100644 --- a/deluge/ui/webui/torrent_move.py +++ b/deluge/ui/webui/torrent_move.py @@ -70,6 +70,6 @@ class torrent_move: if not form.is_valid(): print self.move_page(name, error = _("Error in Path.")) return - save_path = form.clean_data["save_path"] + save_path = form.cleaned_data["save_path"] proxy.move_torrent(torrent_ids, save_path) utils.do_redirect()