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 ' % (flatatt(final_attrs), escape(value)) + +class CheckboxInput(Widget): + def __init__(self, attrs=None, check_test=bool): + # 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): + final_attrs = self.build_attrs(attrs, type='checkbox', name=name) + try: + result = self.check_test(value) + except: # Silently catch exceptions + result = False + 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) + +class Select(Widget): + def __init__(self, attrs=None, choices=()): + self.attrs = attrs or {} + # 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. + self.choices = list(choices) + + def render(self, name, value, attrs=None, choices=()): + 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. + for option_value, option_label in chain(self.choices, choices): + option_value = smart_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'') + return 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'))) + super(NullBooleanSelect, self).__init__(attrs, choices) + + def render(self, name, value, attrs=None, choices=()): + try: + value = {True: u'2', False: u'3', u'2': u'2', u'3': u'3'}[value] + except KeyError: + value = u'1' + return super(NullBooleanSelect, self).render(name, value, attrs, choices) + + def value_from_datadict(self, data, 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=()): + # 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) + + def value_from_datadict(self, data, 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 ." + 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.index = index + + def __unicode__(self): + return u'' % (self.tag(), self.choice_label) + + def is_checked(self): + return self.value == self.choice_value + + def tag(self): + if self.attrs.has_key('id'): + 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) + +class RadioFieldRenderer(StrAndUnicode): + "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 + + def __iter__(self): + for i, choice in enumerate(self.choices): + yield RadioInput(self.name, self.value, self.attrs.copy(), choice, i) + + def __getitem__(self, idx): + choice = self.choices[idx] # Let the IndexError propogate + 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]) + +class RadioSelect(Select): + def render(self, name, value, attrs=None, choices=()): + "Returns a RadioFieldRenderer instance rather than a Unicode string." + 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))) + + def id_for_label(self, id_): + # RadioSelect is represented by multiple fields, + # each of which has a distinct ID. The IDs are made distinct by a "_X" + # suffix, where X is the zero-based index of the radio field. Thus, + # the label for a RadioSelect should reference the first one ('_0'). + if id_: + id_ += '_0' + return id_ + id_for_label = classmethod(id_for_label) + +class CheckboxSelectMultiple(SelectMultiple): + def render(self, name, value, attrs=None, choices=()): + if value is None: value = [] + has_id = attrs and attrs.has_key('id') + final_attrs = self.build_attrs(attrs, name=name) + output = [u'
          '] + str_values = set([smart_unicode(v) for v in value]) # Normalize to strings. + 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) + rendered_cb = cb.render(name, option_value) + output.append(u'
        • ' % (rendered_cb, escape(smart_unicode(option_label)))) + output.append(u'
        ') + return u'\n'.join(output) + + 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) + +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. + + 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(). + + Subclasses may implement format_output(), which takes the list of rendered + widgets and returns HTML that formats them any way you'd like. + + You'll probably want to use this with MultiValueField. + """ + def __init__(self, widgets, attrs=None): + self.widgets = [isinstance(w, type) and w() or w for w in widgets] + super(MultiWidget, self).__init__(attrs) + + def render(self, name, value, attrs=None): + # value is a list of values, each corresponding to a widget + # in self.widgets. + if not isinstance(value, list): + value = self.decompress(value) + output = [] + for i, widget in enumerate(self.widgets): + try: + widget_value = value[i] + except KeyError: + widget_value = None + output.append(widget.render(name + '_%s' % i, widget_value, attrs)) + return 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 format_output(self, rendered_widgets): + return u''.join(rendered_widgets) + + def decompress(self, value): + """ + Returns a list of decompressed values for the given compressed value. + The given value can be assumed to be valid, but not necessarily + non-empty. + """ + raise NotImplementedError('Subclasses must implement this method.') + +class SplitDateTimeWidget(MultiWidget): + """ + A Widget that splits datetime input into two boxes. + """ + def __init__(self, attrs=None): + widgets = (TextInput(attrs=attrs), TextInput(attrs=attrs)) + super(SplitDateTimeWidget, self).__init__(widgets, attrs) + + def decompress(self, value): + if value: + return [value.date(), value.time()] + return [None, None] diff --git a/deluge/ui/webui/webui_plugin/lib/readme.txt b/deluge/ui/webui/webui_plugin/lib/readme.txt index 16c5eee85..50a6a66c2 100644 --- a/deluge/ui/webui/webui_plugin/lib/readme.txt +++ b/deluge/ui/webui/webui_plugin/lib/readme.txt @@ -6,3 +6,9 @@ Disclaimer: Some may have been adapted to work better with deluge. But they will import other parts of deluge or Webui. +LICENCE: +All components are GPL compatible. +All these components are Licensed under their original license. +See docstring or LICENSE files. + + diff --git a/deluge/ui/webui/webui_plugin/pages.py b/deluge/ui/webui/webui_plugin/pages.py index 9c0c96562..86761b05e 100644 --- a/deluge/ui/webui/webui_plugin/pages.py +++ b/deluge/ui/webui/webui_plugin/pages.py @@ -35,6 +35,9 @@ from webserver_common import ws from utils import * from render import render, error_page import page_decorators as deco +import config_tabs_webui #auto registers +import config_tabs_deluge #auto registers +from config import config_page #import forms import lib.webpy022 as web @@ -64,7 +67,7 @@ urls = ( "/resume_all", "resume_all", "/refresh/set", "refresh_set", "/refresh/(.*)", "refresh", - "/config", "config", + "/config/(.*)", "config_page", "/home", "home", "/about", "about", "/logout", "logout", @@ -91,7 +94,7 @@ class login: def POST(self): vars = web.input(pwd = None, redir = None) - if check_pwd(vars.pwd): + if ws.check_pwd(vars.pwd): #start new session start_session() do_redirect() @@ -215,7 +218,7 @@ class remote_torrent_add: vars = web.input(pwd = None, torrent = {}, data_b64 = None , torrent_name= None) - if not check_pwd(vars.pwd): + if not ws.check_pwd(vars.pwd): return 'error:wrong password' if vars.data_b64: #b64 post (greasemonkey) @@ -305,21 +308,6 @@ class refresh_set: else: error_page(_('refresh must be > 0')) -class config: #namespace clash? - """core config - TODO:good validation. - """ - @deco.deluge_page - def GET(self, name): - return render.config(forms.bandwith()) - - def POST(self): - vars = web.input(max_download=None, max_upload=None) - - #self.config.set("max_download_speed", float(str_bwdown)) - raise NotImplementedError('todo') - - class home: @deco.check_session def GET(self, name): diff --git a/deluge/ui/webui/webui_plugin/static/simple_site_style.css b/deluge/ui/webui/webui_plugin/static/simple_site_style.css index 3776994e8..092b79eb9 100755 --- a/deluge/ui/webui/webui_plugin/static/simple_site_style.css +++ b/deluge/ui/webui/webui_plugin/static/simple_site_style.css @@ -1,5 +1,5 @@ -/* ----------------------------------------------------------- Theme Name: Simple Theme URI: http://deluge-torrent.org Description: Deluge Theme Version: 1.0 ----------------------------------------------------------- */ BODY { background: #304663 url(images/simple_bg.jpg) repeat-x; font-family: trebuchet ms; font-size: 10pt; margin: 0; } /* GENERIC STYLES */ a img {border: 0px} hr {color: #627082; margin: 15px 0 15px 0;} /* STRUCTURE */ #page { min-width: 800px; margin-left: auto; margin-right: auto; } #main_content { background:url(images/simple_line.jpg) repeat-x; } #simple_logo { background:url(images/simple_logo.jpg) no-repeat; } #main { padding-top: 20px; padding-left: 20px; color: #fff; } #main form table { border: #2a425c 1px solid; } #main form table tr { border: 0px; } #main form table tr th { background: #1f3044; font-size: 16px; border: 0px; - white-space: nowrap; } #main form table tr td{ border: 0px; color: #fff; font-size: 12px; white-space: nowrap; } #main form table tr th a { color: #8fa6c3; font-size: 16px; white-space: nowrap; } #main form table tr th a, a:active, a:visited { color: #8fa6c3; text-decoration: none; } #main form table tr th a:hover {color: #fff; text-decoration: underline;} #main form table tr td a { color: #fff; font-size: 12px; white-space: nowrap; } #main form table tr td a, a:active, a:visited { color: #fff; text-decoration: none;} #main form table tr td a:hover {color: #fff; text-decoration: underline;} #main a { color: #fff; font-size: 12px; } #main a, a:active, a:visited { color: #fff; text-decoration: none;} #main a:hover {color: #fff; text-decoration: underline;} .info { text-align: right; padding: 0 50px 0 0; color: #8fa6c3; font-size: 16px; letter-spacing: 4px; font-weight: bold; } .title { color: #dce4ee; font-size: 32px; padding: 10px 50px 0 0; text-align: right; } .title a, a:active, a:visited { color: #dce4ee; text-decoration: none;} .title a:hover {color: #fff; text-decoration: underline;} #button { border:1px solid #23344b; background: #99acc3; color: #000; font-family:verdana, arial, helvetica, sans-serif; font-size:10px; margin-top:5px; } INPUT{ border:1px solid #23344b; background: #99acc3; color: #000; } TEXTAREA{ border:1px solid #23344b; background: #99acc3; width:480px; } .footertext a { color: #c0c0c0; text-decoration:none;} .footertext a:visited { color: #c0c0c0; text-decoration:none;} .footertext a:active { color: #c0c0c0; text-decoration:none;} .footertext a:hover {color: #fff; text-decoration: underline;} .footertext { text-align: center; padding: 60px 0 0 0; font-size: 8pt; left: -100px; font-family: trebuchet MS; color: #fff; position: relative; } .clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } div.progress_bar{ background-color:#4573a5; /*color:blue;*/ -moz-border-radius:5px; /*ff only setting*/ } +/* ----------------------------------------------------------- Theme Name: Simple Theme URI: http://deluge-torrent.org Description: Deluge Theme Version: 1.0 ----------------------------------------------------------- */ BODY { background: #304663 url(images/simple_bg.jpg) repeat-x; font-family: trebuchet ms; font-size: 10pt; margin: 0; } /* GENERIC STYLES */ a img {border: 0px} hr {color: #627082; margin: 15px 0 15px 0;} /* STRUCTURE */ #page { min-width: 800px; margin-left: auto; margin-right: auto; } #main_content { background:url(images/simple_line.jpg) repeat-x; } #simple_logo { background:url(images/simple_logo.jpg) no-repeat; } #main { padding-top: 20px; padding-left: 20px; color: #fff; } #main form table { border: #2a425c 1px solid; } #main form table tr { border: 0px; } #main form table tr { background: #1f3044; font-size: 16px; border: 0px; + white-space: nowrap; } #main form table tr td{ border: 0px; color: #fff; font-size: 12px; white-space: nowrap; } #main form table tr a { color: #8fa6c3; font-size: 16px; white-space: nowrap; } #main form table tr a, a:active, a:visited { color: #8fa6c3; text-decoration: none; } #main form table tr a:hover {color: #fff; text-decoration: underline;} #main form table tr td a { color: #fff; font-size: 12px; white-space: nowrap; } #main form table tr td a, a:active, a:visited { color: #fff; text-decoration: none;} #main form table tr td a:hover {color: #fff; text-decoration: underline;} #main a { color: #fff; font-size: 12px; } #main a, a:active, a:visited { color: #fff; text-decoration: none;} #main a:hover {color: #fff; text-decoration: underline;} .info { text-align: right; padding: 0 50px 0 0; color: #8fa6c3; font-size: 16px; letter-spacing: 4px; font-weight: bold; } .title { color: #dce4ee; font-size: 32px; padding: 10px 50px 0 0; text-align: right; } .title a, a:active, a:visited { color: #dce4ee; text-decoration: none;} .title a:hover {color: #fff; text-decoration: underline;} #button { border:1px solid #23344b; background: #99acc3; color: #000; font-family:verdana, arial, helvetica, sans-serif; font-size:10px; margin-top:5px; } INPUT{ border:1px solid #23344b; background: #99acc3; color: #000; } TEXTAREA{ border:1px solid #23344b; background: #99acc3; width:480px; } .footertext a { color: #c0c0c0; text-decoration:none;} .footertext a:visited { color: #c0c0c0; text-decoration:none;} .footertext a:active { color: #c0c0c0; text-decoration:none;} .footertext a:hover {color: #fff; text-decoration: underline;} .footertext { text-align: center; padding: 60px 0 0 0; font-size: 8pt; left: -100px; font-family: trebuchet MS; color: #fff; position: relative; } .clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } div.progress_bar{ background-color:#4573a5; /*color:blue;*/ -moz-border-radius:5px; /*ff only setting*/ } div.progress_bar_outer { /*used in table-view*/ width:150px; @@ -88,4 +88,36 @@ th { border: #2a425c 1px solid; } +#config_chooser { + float: left; + width:150px; + text-align:left; + height:60%; +} + +#config_chooser ul { + list-style-type: none; +} + +#config_chooser li:hover { + background-color:#68a; +} + +#config_chooser li.selected { + background-color:#900; +} + +#config_panel { + height:60%; +} +#config_panel th { + font-size: 12px; + text-align:right; + color:#FFFFFF; +} + +#config_panel table { + background-color:none; +} + /* Hides from IE-mac \*/ * html .clearfix {height: 1%;} .clearfix {display: block;} /* End hide from IE-mac */ diff --git a/deluge/ui/webui/webui_plugin/templates/advanced/static/advanced.css b/deluge/ui/webui/webui_plugin/templates/advanced/static/advanced.css index b4fa9db10..2b801a1b8 100644 --- a/deluge/ui/webui/webui_plugin/templates/advanced/static/advanced.css +++ b/deluge/ui/webui/webui_plugin/templates/advanced/static/advanced.css @@ -10,23 +10,20 @@ table {font-family: Bitstream Vera,Verdana;} div {font-family: Bitstream Vera,Ve margin: 0; padding:0; } #simple_logo { background:url(../../static/images/simple_logo.jpg) no-repeat; } #main { margin: 0; - padding:0; padding-top: 6px; color: #fff; } #main form table { border: #2a425c 1px solid; } #main form table tr { border: 0px; } #main form table tr th { background: #1f3044; font-size: 16px; border: 0px; + padding:0; padding-top: 6px; color: #fff; } #main form table { border: #2a425c 1px solid; } #main form table tr { border: 0px; } #main form table tr { font-size: 16px; border: 0px; white-space: nowrap; } #main form table tr td{ border: 0px; color: #fff; font-size: 12px; white-space: nowrap; - font-family: Bitstream Vera,Verdana; } #main form table tr th a { color: #8fa6c3; font-size: 16px; white-space: nowrap; } #main form table tr th a, a:active, a:visited { color: #8fa6c3; text-decoration: none; } #main form table tr th a:hover {color: #fff; text-decoration: underline;} #main form table tr td a { color: #fff; font-size: 12px; white-space: nowrap; - font-family: Bitstream Vera,Verdana; } #main form table tr td a, a:active, a:visited { color: #fff; text-decoration: none;} #main form table tr td a:hover {color: #fff; text-decoration: underline;} #main a { color: #fff; font-size: 12px; } #main a, a:active, a:visited { color: #fff; text-decoration: none;} #main a:hover {color: #fff; text-decoration: underline;} .info { text-align: right; padding: 0 50px 0 0; color: #8fa6c3; font-size: 16px; letter-spacing: 4px; font-weight: bold; } .title { color: #dce4ee; font-size: 32px; padding: 10px 50px 0 0; text-align: right; } .title a, a:active, a:visited { color: #dce4ee; text-decoration: none;} .title a:hover {color: #fff; text-decoration: underline;} input{ + font-family: Bitstream Vera,Verdana; } #main form table tr a { color: #8fa6c3; font-size: 16px; white-space: nowrap; } #main form table tr th a, a:active, a:visited { color: #8fa6c3; text-decoration: none; } #main form table tr th a:hover {color: #fff; text-decoration: underline;} #main form table tr td a { color: #fff; font-size: 12px; white-space: nowrap; + font-family: Bitstream Vera,Verdana; } #main form table tr td a, a:active, a:visited { color: #fff; text-decoration: none;} #main form table tr td a:hover {color: #fff; text-decoration: underline;} #main a { color: #fff; font-size: 12px; } #main a, a:active, a:visited { color: #fff; text-decoration: none;} #main a:hover {color: #fff; text-decoration: underline;} .info { text-align: right; padding: 0 50px 0 0; color: #8fa6c3; font-size: 16px; letter-spacing: 4px; font-weight: bold; } .title { color: #dce4ee; font-size: 32px; padding: 10px 50px 0 0; text-align: right; } .title a, a:active, a:visited { color: #dce4ee; text-decoration: none;} .title a:hover {color: #fff; text-decoration: underline;} /*DISABLED! +input{ background-color: #37506f; border:1px solid #68a; - background: #99acc3; color: #000; - /*vertical-align:middle;*/ - -moz-border-radius:5px; - /*margin-top:5px;*/ - } + -moz-border-radius:5px; } input:hover { background-color:#68a; -} TEXTAREA{ border:1px solid #23344b; background: #99acc3; width:480px; } .footertext a { color: #c0c0c0; text-decoration:none;} .footertext a:visited { color: #c0c0c0; text-decoration:none;} .footertext a:active { color: #c0c0c0; text-decoration:none;} .footertext a:hover {color: #fff; text-decoration: underline;} .footertext { text-align: center; padding: 60px 0 0 0; font-size: 8pt; left: -100px; font-family: Bitstream Vera,Verdana; color: #fff; position: relative; } .clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } div.progress_bar{ background-color:#4573a5; /*color:blue;*/ -moz-border-radius:5px; /*ff only setting*/ } +} TEXTAREA{ border:1px solid #23344b; background: #99acc3; width:480px; } */ .footertext a { color: #c0c0c0; text-decoration:none;} .footertext a:visited { color: #c0c0c0; text-decoration:none;} .footertext a:active { color: #c0c0c0; text-decoration:none;} .footertext a:hover {color: #fff; text-decoration: underline;} .footertext { text-align: center; padding: 60px 0 0 0; font-size: 8pt; left: -100px; font-family: Bitstream Vera,Verdana; color: #fff; position: relative; } .clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } div.progress_bar{ background-color:#4573a5; /*color:blue;*/ -moz-border-radius:5px; /*ff only setting*/ } div.progress_bar_outer { /*used in table-view*/ width:150px; @@ -255,6 +252,43 @@ form { /*all forms!*/ border:0; } +#config_chooser { + float: left; + width:150px; + text-align:left; + height:60%; +} + +#config_chooser ul { + list-style-type: none; +} + +#config_chooser li:hover { + background-color:#68a; +} + +#config_chooser li.selected { + background-color:#900; +} + +#config_panel { + height:60%; +} +#config_panel th { + font-size: 12px; + text-align:right; + color:#FFFFFF; +} + +#config_panel table { + background-color:none; +} + +ul.errorlist { + display:hidden; +} + + #torrent_list { -moz-border-radius:7px; } diff --git a/deluge/ui/webui/webui_plugin/templates/deluge/config.html b/deluge/ui/webui/webui_plugin/templates/deluge/config.html index e2670d0ae..4ee2d75cc 100644 --- a/deluge/ui/webui/webui_plugin/templates/deluge/config.html +++ b/deluge/ui/webui/webui_plugin/templates/deluge/config.html @@ -1,10 +1,39 @@ -$def with (form) -$:render.header(_('Config')) +$def with (groups, pages, form, selected, message, error) -
        Not Implemented!
        +$:render.header(_("Config")) + + +
        +$for group in groups: +

        $group

        + +
        + + +
        +

        $form.group / $form.title

        +
        $form.info
        -$:form.render() - + +$:form.as_table() +
        +$if message: +
        $message
        +$if error: +
        $error
        + + + + +
        +
        $:render.footer() diff --git a/deluge/ui/webui/webui_plugin/tests/test_all.py b/deluge/ui/webui/webui_plugin/tests/test_all.py index a5ca186ee..4c163c344 100644 --- a/deluge/ui/webui/webui_plugin/tests/test_all.py +++ b/deluge/ui/webui/webui_plugin/tests/test_all.py @@ -12,8 +12,6 @@ import operator ws.init_06() print 'test-env=',ws.env - - #CONFIG: BASE_URL = 'http://localhost:8112' PWD = 'deluge' @@ -358,6 +356,11 @@ class TestIntegration(TestWebUiBase): # +if True: + cfg = ws.proxy.get_config() + for key in sorted(cfg.keys()): + print key,cfg[key] + if False: suiteFew = unittest.TestSuite() diff --git a/deluge/ui/webui/webui_plugin/utils.py b/deluge/ui/webui/webui_plugin/utils.py index 6cafac4f1..78bcfa75b 100644 --- a/deluge/ui/webui/webui_plugin/utils.py +++ b/deluge/ui/webui/webui_plugin/utils.py @@ -44,7 +44,6 @@ import random from operator import attrgetter import datetime import pickle -from md5 import md5 from urlparse import urlparse from webserver_common import REVNO, VERSION, TORRENT_KEYS, STATE_MESSAGES @@ -107,12 +106,6 @@ def getcookie(key, default = None): ck = cookies() return ck.get(key, default) -#utils: -def check_pwd(pwd): - m = md5() - m.update(ws.config.get('pwd_salt')) - m.update(pwd) - return (m.digest() == ws.config.get('pwd_md5')) def get_stats(): stats = Storage({ @@ -260,8 +253,3 @@ def get_category_choosers(torrent_list): #/utils -__all__ = [ - 'do_redirect', 'start_session','getcookie' - ,'setcookie','end_session', - 'get_torrent_status', 'check_pwd','get_categories' - ,'filter_torrent_state','web','get_category_choosers','get_stats'] diff --git a/deluge/ui/webui/webui_plugin/webserver_common.py b/deluge/ui/webui/webui_plugin/webserver_common.py index c1aa93294..743ad20e2 100644 --- a/deluge/ui/webui/webui_plugin/webserver_common.py +++ b/deluge/ui/webui/webui_plugin/webserver_common.py @@ -41,6 +41,7 @@ import random import pickle import sys import base64 +from md5 import md5 random.seed() @@ -203,6 +204,39 @@ class Ws: format="[%(levelname)s] %(message)s") self.log = logging + + #utils for config: + def get_templates(self): + template_path = os.path.join(os.path.dirname(__file__), 'templates') + return [dirname for dirname + in os.listdir(template_path) + if os.path.isdir(os.path.join(template_path, dirname)) + and not dirname.startswith('.')] + + def save_config(self): + self.log.debug('Save Webui Config') + data = pickle.dumps(self.config) + f = open(self.config_file,'wb') + f.write(data) + f.close() + + def update_pwd(self,pwd): + sm = md5() + sm.update(str(random.getrandbits(5000))) + salt = sm.digest() + self.config["pwd_salt"] = salt + # + m = md5() + m.update(salt) + m.update(pwd) + self.config["pwd_md5"] = m.digest() + + def check_pwd(self,pwd): + m = md5() + m.update(self.config.get('pwd_salt')) + m.update(pwd) + return (m.digest() == self.config.get('pwd_md5')) + ws =Ws()