diff --git a/deluge/ui/webui/webui_plugin/config.py b/deluge/ui/webui/webui_plugin/config.py
new file mode 100644
index 000000000..5cd403ba9
--- /dev/null
+++ b/deluge/ui/webui/webui_plugin/config.py
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# deluge_webserver.py
+#
+# Copyright (C) Martijn Voncken 2008
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, write to:
+# The Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+import lib.newforms as forms
+import page_decorators as deco
+import lib.webpy022 as web
+from webserver_common import ws
+from render import render
+from lib.webpy022.http import seeother
+
+groups = []
+blocks = forms.utils.datastructures.SortedDict()
+
+class Form(forms.Form):
+ info = ""
+ title = "No Title"
+ def __init__(self,data = None):
+ if data == None:
+ data = self.initial_data()
+ forms.Form.__init__(self,data)
+
+ def initial_data(self):
+ "override in subclass"
+ raise NotImplementedError()
+
+ def start_save(self):
+ "called by config_page"
+ self.save(web.Storage(self.clean_data))
+ self.post_save()
+
+ def save(self, vars):
+ "override in subclass"
+ raise NotImplementedError()
+
+ def post_save(self):
+ "override in subclass"
+ pass
+
+
+class WebCfgForm(Form):
+ "config base for webui"
+ def initial_data(self):
+ return ws.config
+
+ def save(self, data):
+ ws.config.update(data)
+ ws.save_config()
+ self.post_save()
+
+ def post_save(self):
+ pass
+
+
+class CfgForm(Form):
+ "config base for deluge-cfg"
+ def initial_data(self):
+ return ws.proxy.get_config()
+ def save(data):
+ ws.proxy.set_config(data)
+
+
+class config_page:
+ """
+ web.py config page
+ """
+ def get_form_class(self,name):
+ try:
+ return blocks[name]
+ except KeyError:
+ raise Exception('no config page named:"%s"')
+
+ @deco.deluge_page
+ def GET(self, name):
+ if name == '':
+ return seeother('/config/template')
+
+ form_class = self.get_form_class(name)
+ f = form_class()
+ f.full_clean()
+ return self.render(f , name)
+
+ @deco.deluge_page
+ def POST(self,name):
+
+ form_class = self.get_form_class(name)
+ fields = form_class.base_fields.keys()
+ form_data = web.Storage()
+ vars = web.input()
+ for field in fields:
+ form_data[field] = vars.get(field)
+
+ form = form_class(form_data)
+ if form.is_valid():
+ ws.log.debug('save config %s' % form_data)
+ try:
+ form.start_save()
+ return self.render(form , name, _('These changes were saved'))
+ except forms.ValidationError, e:
+ ws.log.debug(e.message)
+ return self.render(form , name, error = e.message)
+ else:
+ return self.render(form , name, _('Please correct errors and try again'))
+
+ def render(self, f , name , message = '' , error=''):
+ return render.config(groups, blocks, f, name , message , error)
+
+def register_block(group, name, form):
+ if not group in groups:
+ groups.append(group)
+ form.group = group
+ blocks[name] = form
+
+
+
diff --git a/deluge/ui/webui/webui_plugin/config_tabs_deluge.py b/deluge/ui/webui/webui_plugin/config_tabs_deluge.py
new file mode 100644
index 000000000..bf961a2de
--- /dev/null
+++ b/deluge/ui/webui/webui_plugin/config_tabs_deluge.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# deluge_webserver.py
+#
+# Copyright (C) Martijn Voncken 2008
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, write to:
+# The Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+import lib.newforms as forms
+import config
+import utils
+
+
+class BandWidth(config.CfgForm):
+ title = _("Bandwidth")
+ up = forms.IntegerField(label = "TODO")
+
+config.register_block('deluge','bandwidth',BandWidth)
+
+
+
diff --git a/deluge/ui/webui/webui_plugin/config_tabs_webui.py b/deluge/ui/webui/webui_plugin/config_tabs_webui.py
new file mode 100644
index 000000000..dbaaba10a
--- /dev/null
+++ b/deluge/ui/webui/webui_plugin/config_tabs_webui.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# deluge_webserver.py
+#
+# Copyright (C) Martijn Voncken 2008
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2, or (at your option)
+# any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, write to:
+# The Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor
+# Boston, MA 02110-1301, USA.
+#
+# In addition, as a special exception, the copyright holders give
+# permission to link the code of portions of this program with the OpenSSL
+# library.
+# You must obey the GNU General Public License in all respects for all of
+# the code used other than OpenSSL. If you modify file(s) with this
+# exception, you may extend this exception to your version of the file(s),
+# but you are not obligated to do so. If you do not wish to do so, delete
+# this exception statement from your version. If you delete this exception
+# statement from all source files in the program, then also delete it here.
+
+import lib.newforms as forms
+import config
+import utils
+from render import render
+from webserver_common import ws
+
+
+class Template(config.WebCfgForm):
+ title = _("Template")
+
+ template = forms.ChoiceField( label=_("Template"),
+ choices = [(t,t) for t in ws.get_templates()])
+
+ button_style = forms.ChoiceField( label=_("Button style"),
+ choices=[
+ (0,_('Text and image')),
+ (1, _('Image Only')),
+ (2, _('Text Only'))])
+
+ cache_templates = forms.BooleanField(label = _("Cache templates"),
+ required=False)
+
+ def post_save(self):
+ render.apply_cfg()
+
+
+class Server(config.WebCfgForm):
+ info = _("Restart webui after changing these values.")
+ title = _("Server")
+
+ port = forms.IntegerField(label = _("Port"),min_value=80)
+ use_https = forms.BooleanField(label = _("Use https") , required=False)
+
+class Password(config.Form):
+ title = _("Password")
+ old_pwd = forms.CharField(widget = forms.PasswordInput
+ ,label = _("Current Password"), required=False)
+
+ new1 = forms.CharField(widget = forms.PasswordInput
+ ,label = _("New Password"), required=False)
+
+ new2 = forms.CharField(widget = forms.PasswordInput
+ ,label = _("New Password (Confirm)"), required=False)
+
+ def initial_data(self):
+ return {}
+
+ def save(self,data):
+ if not ws.check_pwd(data.old_pwd):
+ raise forms.ValidationError(_("Old password is invalid"))
+ if data.new1 <> data.new2:
+ raise forms.ValidationError(_("New Password is not equal to New Password(confirm)"))
+
+ ws.update_pwd(data.new1)
+ ws.save_config()
+
+ def post_save(self):
+ utils.end_session()
+
+config.register_block('webui','template', Template)
+config.register_block('webui','server',Server)
+config.register_block('webui','password',Password)
diff --git a/deluge/ui/webui/webui_plugin/lib/newforms/LICENSE b/deluge/ui/webui/webui_plugin/lib/newforms/LICENSE
new file mode 100644
index 000000000..ba3e68a06
--- /dev/null
+++ b/deluge/ui/webui/webui_plugin/lib/newforms/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2005, the Lawrence Journal-World
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of Django nor the names of its contributors may be used
+ to endorse or promote products derived from this software without
+ specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/deluge/ui/webui/webui_plugin/lib/newforms/__init__.py b/deluge/ui/webui/webui_plugin/lib/newforms/__init__.py
new file mode 100644
index 000000000..62125e218
--- /dev/null
+++ b/deluge/ui/webui/webui_plugin/lib/newforms/__init__.py
@@ -0,0 +1,16 @@
+"""
+Django validation and HTML form handling.
+
+TODO:
+ Default value for field
+ Field labels
+ Nestable Forms
+ FatalValidationError -- short-circuits all other validators on a form
+ ValidationWarning
+ "This form field requires foo.js" and form.js_includes()
+"""
+
+from util import ValidationError
+from widgets import *
+from fields import *
+from forms import *
diff --git a/deluge/ui/webui/webui_plugin/lib/newforms/fields.py b/deluge/ui/webui/webui_plugin/lib/newforms/fields.py
new file mode 100644
index 000000000..5076c5824
--- /dev/null
+++ b/deluge/ui/webui/webui_plugin/lib/newforms/fields.py
@@ -0,0 +1,492 @@
+"""
+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/webui_plugin/lib/newforms/forms.py b/deluge/ui/webui/webui_plugin/lib/newforms/forms.py
new file mode 100644
index 000000000..97f0efcb6
--- /dev/null
+++ b/deluge/ui/webui/webui_plugin/lib/newforms/forms.py
@@ -0,0 +1,309 @@
+"""
+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
+
+__all__ = ('BaseForm', 'Form')
+
+NON_FIELD_ERRORS = '__all__'
+
+def pretty_name(name):
+ "Converts 'first_name' to 'First 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 copy(self):
+ return SortedDictFromList([(k, copy.copy(v)) for k, v in self.items()])
+
+class DeclarativeFieldsMetaclass(type):
+ """
+ Metaclass that converts Field attributes to a dictionary called
+ '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)
+ return type.__new__(cls, name, bases, attrs)
+
+class BaseForm(StrAndUnicode):
+ # This is the main implementation of all the Form logic. Note that this
+ # 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
+ self.data = data or {}
+ self.auto_id = auto_id
+ self.prefix = prefix
+ self.initial = initial or {}
+ 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()
+
+ def __unicode__(self):
+ return self.as_table()
+
+ def __iter__(self):
+ for name, field in self.fields.items():
+ yield BoundField(self, field, name)
+
+ def __getitem__(self, name):
+ "Returns a BoundField with the given name."
+ try:
+ field = self.fields[name]
+ except KeyError:
+ 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:
+ self.full_clean()
+ return self.__errors
+ errors = property(_errors)
+
+ def is_valid(self):
+ """
+ Returns True if the form has no errors. Otherwise, False. If errors are
+ being ignored, returns False.
+ """
+ return self.is_bound and not bool(self.errors)
+
+ def add_prefix(self, field_name):
+ """
+ Returns the field name with a prefix appended, if this Form has a
+ prefix set.
+
+ Subclasses may wish to override.
+ """
+ return self.prefix and ('%s-%s' % (self.prefix, field_name)) or field_name
+
+ def _html_output(self, normal_row, error_row, row_ender, help_text_html, errors_on_separate_row):
+ "Helper function for outputting HTML. Used by as_table(), as_ul(), as_p()."
+ top_errors = self.non_field_errors() # Errors that should be displayed above all fields.
+ 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.
+ if bf.is_hidden:
+ if bf_errors:
+ top_errors.extend(['(Hidden field %s) %s' % (name, 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 ''
+ if field.help_text:
+ help_text = help_text_html % field.help_text
+ else:
+ help_text = u''
+ output.append(normal_row % {'errors': bf_errors, 'label': label, 'field': unicode(bf), 'help_text': help_text})
+ if top_errors:
+ output.insert(0, error_row % 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.
+ 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.
+ output.append(str_hidden)
+ return u'\n'.join(output)
+
+ def as_table(self):
+ "Returns this form rendered as HTML
s -- excluding the
."
+ return self._html_output(u'
%(label)s
%(errors)s%(field)s%(help_text)s
', u'
%s
', '', u' %s', False)
+
+ def as_ul(self):
+ "Returns this form rendered as HTML
s -- excluding the
."
+ return self._html_output(u'
%(errors)s%(label)s %(field)s%(help_text)s
', u'
%s
', '', u' %s', False)
+
+ 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)
+
+ def non_field_errors(self):
+ """
+ Returns an ErrorList of errors that aren't associated with a particular
+ field -- i.e., from Form.clean(). Returns an empty ErrorList if there
+ are none.
+ """
+ return self.errors.get(NON_FIELD_ERRORS, ErrorList())
+
+ def full_clean(self):
+ """
+ Cleans all of self.data and populates self.__errors and self.clean_data.
+ """
+ errors = ErrorDict()
+ if not self.is_bound: # Stop further processing.
+ self.__errors = errors
+ return
+ self.clean_data = {}
+ for name, field in self.fields.items():
+ # value_from_datadict() gets the data from the dictionary.
+ # 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))
+ try:
+ value = field.clean(value)
+ self.clean_data[name] = value
+ if hasattr(self, 'clean_%s' % name):
+ value = getattr(self, 'clean_%s' % name)()
+ self.clean_data[name] = value
+ except ValidationError, e:
+ errors[name] = e.messages
+ try:
+ self.clean_data = self.clean()
+ except ValidationError, e:
+ errors[NON_FIELD_ERRORS] = e.messages
+ if errors:
+ delattr(self, 'clean_data')
+ self.__errors = errors
+
+ def clean(self):
+ """
+ Hook for doing any extra form-wide cleaning after Field.clean() been
+ called on every field. Any ValidationError raised by this method will
+ not be associated with a particular field; it will have a special-case
+ association with the field named '__all__'.
+ """
+ return self.clean_data
+
+class Form(BaseForm):
+ "A collection of Fields, plus their associated data."
+ # This is a separate class from BaseForm in order to abstract the way
+ # self.fields is specified. This class (Form) is the one that does the
+ # fancy metaclass stuff purely for the semantic sugar -- it allows one
+ # to define a form using declarative syntax.
+ # BaseForm itself has no way of designating self.fields.
+ __metaclass__ = DeclarativeFieldsMetaclass
+
+class BoundField(StrAndUnicode):
+ "A Field plus data"
+ def __init__(self, form, field, name):
+ self.form = form
+ self.field = field
+ self.name = name
+ self.html_name = form.add_prefix(name)
+ if self.field.label is None:
+ self.label = pretty_name(name)
+ else:
+ self.label = self.field.label
+ 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
+
+ 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())
+ errors = property(_errors)
+
+ def as_widget(self, widget, attrs=None):
+ attrs = attrs or {}
+ auto_id = self.auto_id
+ if auto_id and not attrs.has_key('id') and not widget.attrs.has_key('id'):
+ attrs['id'] = auto_id
+ if not self.form.is_bound:
+ data = self.form.initial.get(self.name, self.field.initial)
+ else:
+ data = self.data
+ return widget.render(self.html_name, data, attrs=attrs)
+
+ def as_text(self, attrs=None):
+ """
+ Returns a string of HTML for representing this as an .
+ """
+ return self.as_widget(TextInput(), attrs)
+
+ def as_textarea(self, attrs=None):
+ "Returns a string of HTML for representing this as a