fix up webui

This commit is contained in:
Marcos Pinto 2007-12-22 19:58:46 +00:00
parent 5ccca41ea6
commit 75684cde8d
89 changed files with 11285 additions and 0 deletions

350
plugins/WebUi/LICENSE Normal file
View File

@ -0,0 +1,350 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 of the License, 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
Public License instead of this License.
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.

20
plugins/WebUi/TODO Normal file
View File

@ -0,0 +1,20 @@
0.5.7
SSL
torrent/add http-post for private sites
rename reannounce->update-tracker.
queued displays as seeding/downloading
0.5.7 advanced layout
fonts
fix auto-refresh-layout
buttons
hide 0.0 kbps like in gtk-ui
update-tracker.
0.6
prepare for cat:
filters on status (prepare for cat)
filters on tracker
categories
greasemonkey : private sites.

281
plugins/WebUi/__init__.py Normal file
View File

@ -0,0 +1,281 @@
# -*- coding: utf-8 -*-
#
#
# Copyright (C) Martijn Voncken 2007 <mvoncken@gmail.com>
#
# 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
plugin_name = "Web User Interface"
plugin_author = "Martijn Voncken"
plugin_version = "rev."
plugin_description = """A Web based User Interface
Firefox greasemonkey script: http://userscripts.org/scripts/show/12639
Remotely add a file: "curl -F torrent=@./test1.torrent -F pwd=deluge http://localhost:8112/remote/torrent/add"
Advanced template is only tested on firefox and garanteed not to work in IE6
ssl keys are located in WebUi/ssl/
Other contributors:
*somedude : template enhancements.
*markybob : stability : synced with his changes in deluge-svn.
"""
import deluge.common
try:
import deluge.pref
from deluge.dialogs import show_popup_warning
import webserver_common
except ImportError:
print 'WebUi:not imported as a plugin'
try:
from dbus_interface import get_dbus_manager
except:
print 'error importing dbus-interface!'
pass #for unit-test.
import time
import gtk
import os
from subprocess import Popen
from md5 import md5
from threading import Thread
import random
random.seed()
plugin_version += open(os.path.join(os.path.dirname(__file__),'revno')).read()
plugin_description += (
open(os.path.join(os.path.dirname(__file__),'version')).read())
def deluge_init(deluge_path):
global path
path = deluge_path
def enable(core, interface):
global path
return plugin_WebUi(path, core, interface)
class plugin_WebUi(object):
def __init__(self, path, deluge_core, deluge_interface):
self.path = path
self.core = deluge_core
self.interface = deluge_interface
self.proc = None
self.web_server = None
if not deluge.common.windows_check():
import commands
status = commands.getstatusoutput(
'ps x |grep -v grep |grep run_webserver')
if status[0] == 0:
os.kill(int(status[1].split()[0]), 9)
time.sleep(1) #safe time to wait for kill to finish.
self.config_file = deluge.common.CONFIG_DIR + "/webui.conf"
self.config = deluge.pref.Preferences(self.config_file, False)
try:
self.config.load()
except IOError:
# File does not exist
pass
if not self.config.get('port'): #ugly way to detect new config file.
#set default values:
self.config.set("port", 8112)
self.config.set("button_style", 2)
self.config.set("auto_refresh", False)
self.config.set("auto_refresh_secs", 4)
self.config.set("template", "deluge")
self.config.save(self.config_file)
if not self.config.get("pwd_salt"):
self.config.set("pwd_salt", "invalid")
self.config.set("pwd_md5", "invalid")
if self.config.get("cache_templates") == None:
self.config.set("cache_templates", True)
if deluge.common.windows_check():
self.config.set("run_in_thread", True)
else:
self.config.set("run_in_thread", False)
if self.config.get("use_https") == None:
self.config.set("use_https", False)
self.dbus_manager = get_dbus_manager(deluge_core, deluge_interface,
self.config, self.config_file)
self.start_server()
def unload(self):
print 'WebUI:unload..'
self.kill_server()
def update(self):
pass
## This will be only called if your plugin is configurable
def configure(self,parent_dialog):
d = ConfigDialog(self.config, self, parent_dialog)
if d.run() == gtk.RESPONSE_OK:
d.save_config()
d.destroy()
def start_server(self):
self.kill_server()
if self.config.get("run_in_thread"):
print 'Start Webui(inside gtk)..'
webserver_common.init_gtk_05() #reload changed config.
from deluge_webserver import WebServer #only import in threaded mode
self.web_server = WebServer()
self.web_server.start_gtk()
else:
print 'Start Webui(in process)..'
server_bin = os.path.dirname(__file__) + '/run_webserver'
self.proc = Popen((server_bin,'env=0.5'))
def kill_server(self):
if self.web_server:
print "webserver: stop"
self.web_server.stop_gtk()
self.web_server = None
if self.proc:
print "webserver: kill %i" % self.proc.pid
os.system("kill -9 %i" % self.proc.pid)
time.sleep(1) #safe time to wait for kill to finish.
self.proc = None
def __del__(self):
self.kill_server()
class ConfigDialog(gtk.Dialog):
"""
sorry, can't get used to gui builders.
from what I read glade is better, but i dont want to invest time in them.
"""
def __init__(self, config, plugin, parent):
gtk.Dialog.__init__(self ,parent=parent)
self.config = config
self.plugin = plugin
self.vb = gtk.VBox()
self.set_title(_("WebUi Config"))
template_path = os.path.join(os.path.dirname(__file__), 'templates')
self.templates = [dirname for dirname
in os.listdir(template_path)
if os.path.isdir(os.path.join(template_path, dirname))
and not dirname.startswith('.')]
self.port = self.add_widget(_('Port Number'), gtk.SpinButton())
self.pwd1 = self.add_widget(_('New Password'), gtk.Entry())
self.pwd2 = self.add_widget(_('New Password(confirm)'), gtk.Entry())
self.template = self.add_widget(_('Template'), gtk.combo_box_new_text())
self.button_style = self.add_widget(_('Button Style'),
gtk.combo_box_new_text())
self.cache_templates = self.add_widget(_('Cache Templates'),
gtk.CheckButton())
self.use_https = self.add_widget(_('https://'),
gtk.CheckButton())
#self.share_downloads = self.add_widget(_('Share Download Directory'),
# gtk.CheckButton())
self.port.set_range(80, 65536)
self.port.set_increments(1, 10)
self.pwd1.set_visibility(False)
self.pwd2.set_visibility(False)
for item in self.templates:
self.template.append_text(item)
if not self.config.get("template") in self.templates:
self.config.set("template","deluge")
for item in [_('Text and image'), _('Image Only'), _('Text Only')]:
self.button_style.append_text(item)
if self.config.get("button_style") == None:
self.config.set("button_style", 2)
self.port.set_value(int(self.config.get("port")))
self.template.set_active(
self.templates.index(self.config.get("template")))
self.button_style.set_active(self.config.get("button_style"))
#self.share_downloads.set_active(
# bool(self.config.get("share_downloads")))
self.cache_templates.set_active(self.config.get("cache_templates"))
self.use_https.set_active(self.config.get("use_https"))
self.vbox.pack_start(self.vb, True, True, 0)
self.vb.show_all()
self.add_buttons(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL
,gtk.STOCK_OK, gtk.RESPONSE_OK)
def add_widget(self,label,w=None):
hb = gtk.HBox()
lbl = gtk.Label(label)
lbl.set_size_request(200,20)
hb.pack_start(lbl,False,False, 0)
hb.pack_start(w,True,True, 0)
self.vb.pack_start(hb,False,False, 0)
return w
self.add_buttons(dgtk.STOCK_CLOSE, dgtk.RESPONSE_CLOSE)
def save_config(self):
if self.pwd1.get_text() > '':
if self.pwd1.get_text() <> self.pwd2.get_text():
show_popup_warning(self,_("Confirmed Password <> New Password\n"
+ "Password was not changed"))
else:
sm = md5()
sm.update(str(random.getrandbits(5000)))
salt = sm.digest()
self.config.set("pwd_salt", salt)
#
m = md5()
m.update(salt)
m.update(unicode(self.pwd1.get_text()))
self.config.set("pwd_md5", m.digest())
self.config.set("port", int(self.port.get_value()))
self.config.set("template", self.template.get_active_text())
self.config.set("button_style", self.button_style.get_active())
self.config.set("cache_templates", self.cache_templates.get_active())
self.config.set("use_https", self.use_https.get_active())
#self.config.set("share_downloads", self.share_downloads.get_active())
self.config.save(self.plugin.config_file)
self.plugin.start_server() #restarts server

View File

@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
# Dbus Ipc for experimental web interface
#
# dbus_interface.py
#
# Copyright (C) Martijn Voncken 2007 <mvoncken@gmail.com>
# Contains copy and pasted code from other parts of deluge,see deluge AUTHORS
#
# 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 os
import gtk
import dbus
import deluge.common as common
from lib.pythonize import pythonize
import base64
import random
random.seed()
dbus_interface="org.deluge_torrent.dbusplugin"
dbus_service="/org/deluge_torrent/DelugeDbusPlugin"
dbus_manager = None
def get_dbus_manager(*args):
#another way to make a singleton.
global dbus_manager
if not dbus_manager:
dbus_manager = DbusManager(*args)
return dbus_manager
class DbusManager(dbus.service.Object):
def __init__(self, core, interface,config,config_file):
self.core = core
self.interface = interface
self.config = config
self.config_file = config_file
self.bus = dbus.SessionBus()
bus_name = dbus.service.BusName(dbus_interface,bus=self.bus)
dbus.service.Object.__init__(self, bus_name,dbus_service)
#
#todo : add: get_interface_version in=i,get_config_value in=s out=s
#
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="",out_signature="as")
def get_session_state(self):
"""Returns a list of torrent_ids in the session.
same as 0.6, but returns type "as" instead of a pickle
"""
torrent_list = [str(key) for key in self.core.unique_IDs]
return torrent_list
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="sas",out_signature="a{sv}")
def get_torrent_status(self, torrent_id, keys):
"""return torrent metadata of a single torrent as a dict
0.6 returns a pickle, this returns a dbus-type.
+added some more values to the dict
"""
torrent_id = int(torrent_id)
# Convert the array of strings to a python list of strings
nkeys = [str(key) for key in keys]
state = self.core.get_torrent_state(torrent_id)
torrent = self.core.unique_IDs[torrent_id]
status = {
"name": state["name"],
"total_size": state["total_size"],
"num_pieces": state["num_pieces"],
"state": state['state'],
"user_paused": self.core.is_user_paused(torrent_id),
"paused":state['is_paused'],
"progress": int(state["progress"] * 100),
"next_announce": state["next_announce"],
"total_payload_download":state["total_payload_download"],
"total_payload_upload": state["total_payload_upload"],
"download_payload_rate": state["download_rate"],
"upload_payload_rate": state["upload_rate"],
"num_peers": state["num_peers"],
"num_seeds": state["num_seeds"],
"total_wanted": state["total_wanted"],
"eta": common.estimate_eta(state),
"ratio": self.interface.manager.calc_ratio(torrent_id,state),
#non 0.6 values follow here:
"tracker_status": state.get("tracker_status","?"),
"uploaded_memory": torrent.uploaded_memory,
}
#more non 0.6 values
for key in ["total_seeds", "total_peers","is_seed", "total_done",
"total_download", "total_upload"
#, "download_rate","upload_rate"
, "num_files", "piece_length", "distributed_copies"
,"next_announce","tracker","queue_pos"]:
status[key] = state[key]
#print 'all_keys:',sorted(status.keys())
status_subset = {}
for key in keys:
if key in status:
status_subset[key] = status[key]
else:
print 'mbus error,no key named:', key
return status_subset
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="as",out_signature="")
def pause_torrent(self, torrents):
"""same as 0.6 interface"""
for torrent_id in torrents:
torrent_id = int(torrent_id)
self.core.set_user_pause(torrent_id, True)
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="as", out_signature="")
def resume_torrent(self, torrents):
"""same as 0.6 interface"""
for torrent_id in torrents:
torrent_id = int(torrent_id)
self.core.set_user_pause(torrent_id, False)
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="as", out_signature="")
def force_reannounce(self, torrents):
"""same as 0.6 interface"""
for torrent_id in torrents:
torrent_id = int(torrent_id)
self.core.update_tracker(torrent_id)
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="asbb", out_signature="")
def remove_torrent(self, torrent_ids, data_also, torrent_also):
"""remove a torrent,and optionally data and torrent
additions compared to 0.6 interface: (data_also, torrent_also)
"""
for torrent_id in torrent_ids:
torrent_id = int(torrent_id)
self.core.remove_torrent(torrent_id, bool(data_also)
,bool( torrent_also))
#this should not be needed:
gtk.gdk.threads_enter()
try:
self.interface.torrent_model_remove(torrent_id)
except:
pass
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="s", out_signature="b")
def add_torrent_url(self, url):
filename = fetch_url(url)
self._add_torrent(filename)
return True
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="s", out_signature="b")
def queue_up(self, torrent_id):
self.core.queue_up(int(torrent_id))
return True
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="s", out_signature="b")
def queue_down(self, torrent_id):
self.core.queue_down(int(torrent_id))
return True
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="ss", out_signature="b")
def add_torrent_filecontent(self, name, filecontent_b64):
"""not available in deluge 0.6 interface"""
#name = fillename without directory
name = name.replace('\\','/')
name = 'deluge_' + str(random.random()) + '_' + name.split('/')[-1]
filename = os.path.join(self.core.config.get("default_download_path"), name)
filecontent = base64.b64decode(filecontent_b64)
f = open(filename,"wb") #no with statement, that's py 2.5+
f.write(filecontent)
f.close()
print 'write:',filename
self._add_torrent(filename)
return True
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="", out_signature="a{sv}")
def get_config(self):
return self.core.config.mapping
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="s", out_signature="v")
def get_config_value(self,key):
return self.core.config.mapping[pythonize(key)] #ugly!
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="a{sv}", out_signature="")
def set_config(self, config):
"""Set the config with values from dictionary"""
config = deluge.common.pythonize(config)
# Load all the values into the configuration
for key in self.core.config.keys():
self.core.config[key] = config[key]
self.core.apply_prefs()
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="", out_signature="v")
def get_download_rate(self):
return self.core.get_state()['download_rate']
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="", out_signature="v")
def get_upload_rate(self):
return self.core.get_state()['upload_rate']
@dbus.service.method(dbus_interface=dbus_interface,
in_signature="", out_signature="v")
def get_num_connections(self):
core_state = self.core.get_state()
return core_state['num_connections']
#internal
def _add_torrent(self, filename):
filename = unicode(filename)
target = self.core.config.get("default_download_path")
torrent_id = self.core.add_torrent(filename, target,
self.interface.config.get("use_compact_storage"))
#update gtk-ui This should not be needed!!
gtk.gdk.threads_enter()
try:
self.interface.torrent_model_append(torrent_id)
except:
pass
#finally is 2.5 only!
gtk.gdk.threads_leave()
return True
def fetch_url(url):
import urllib
try:
filename, headers = urllib.urlretrieve(url)
except IOError:
raise Exception( "Network error while trying to fetch torrent from %s"
% url)
else:
if (filename.endswith(".torrent") or
headers["content-type"]=="application/x-bittorrent"):
return filename
else:
raise Exception("URL doesn't appear to be a valid torrent file:%s"
% url)
return None

373
plugins/WebUi/debugerror.py Normal file
View File

@ -0,0 +1,373 @@
"""
pretty debug errors
(part of web.py)
adapted from Django <djangoproject.com>
Copyright (c) 2005, the Lawrence Journal-World
Used under the modified BSD license:
http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
"""
__all__ = ["debugerror", "djangoerror"]
import sys, urlparse, pprint
from lib.webpy022.net import websafe
from lib.webpy022.template import Template
import lib.webpy022.webapi as web
import webserver_common as ws
from traceback import format_tb
import os, os.path
whereami = os.path.join(os.getcwd(), __file__)
whereami = os.path.sep.join(whereami.split(os.path.sep)[:-1])
djangoerror_t = """\
$def with (exception_type, exception_value, frames, exception_message, version_info, tback_txt)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="robots" content="NONE,NOARCHIVE" />
<title>$exception_type at $ctx.path</title>
<style type="text/css">
html * { padding:0; margin:0; }
body * { padding:10px 20px; }
body * * { padding:0; }
body { font:small sans-serif; }
body>div { border-bottom:1px solid #ddd; }
h1 { font-weight:normal; }
h2 { margin-bottom:.8em; }
h2 span { font-size:80%; color:#666; font-weight:normal; }
h3 { margin:1em 0 .5em 0; }
h4 { margin:0 0 .5em 0; font-weight: normal; }
table {
border:1px solid #ccc; border-collapse: collapse; background:white; }
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
thead th {
padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
font-weight:normal; font-size:11px; border:1px solid #ddd; }
tbody th { text-align:right; color:#666; padding-right:.5em; }
table.vars { margin:5px 0 2px 40px; }
table.vars td, table.req td { font-family:monospace; }
table td.code { width:100%;}
table td.code div { overflow:hidden; }
table.source th { color:#666; }
table.source td {
font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
ul.traceback { list-style-type:none; }
ul.traceback li.frame { margin-bottom:1em; }
div.context { margin: 10px 0; }
div.context ol {
padding-left:30px; margin:0 10px; list-style-position: inside; }
div.context ol li {
font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
div.context ol.context-line li { color:black; background-color:#ccc; }
div.context ol.context-line li span { float: right; }
div.commands { margin-left: 40px; }
div.commands a { color:black; text-decoration:none; }
#summary { background: #ffc; }
#summary h2 { font-weight: normal; color: #666; }
#explanation { background:#eee; }
#template, #template-not-exist { background:#f6f6f6; }
#template-not-exist ul { margin: 0 0 0 20px; }
#traceback { background:#eee; }
#requestinfo { background:#f6f6f6; padding-left:120px; }
#summary table { border:none; background:transparent; }
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
#requestinfo h3 { margin-bottom:-1em; }
.error { background: #ffc; }
.specific { color:#cc3300; font-weight:bold; }
</style>
<script type="text/javascript">
//<!--
function getElementsByClassName(oElm, strTagName, strClassName){
// Written by Jonathan Snook, http://www.snook.ca/jon;
// Add-ons by Robert Nyman, http://www.robertnyman.com
var arrElements = (strTagName == "*" && document.all)? document.all :
oElm.getElementsByTagName(strTagName);
var arrReturnElements = new Array();
strClassName = strClassName.replace(/\-/g, "\\-");
var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)");
var oElement;
for(var i=0; i<arrElements.length; i++){
oElement = arrElements[i];
if(oRegExp.test(oElement.className)){
arrReturnElements.push(oElement);
}
}
return (arrReturnElements)
}
function hideAll(elems) {
for (var e = 0; e < elems.length; e++) {
elems[e].style.display = 'none';
}
}
window.onload = function() {
hideAll(getElementsByClassName(document, 'table', 'vars'));
hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
hideAll(getElementsByClassName(document, 'ol', 'post-context'));
}
function toggle() {
for (var i = 0; i < arguments.length; i++) {
var e = document.getElementById(arguments[i]);
if (e) {
e.style.display = e.style.display == 'none' ? 'block' : 'none';
}
}
return false;
}
function varToggle(link, id) {
toggle('v' + id);
var s = link.getElementsByTagName('span')[0];
var uarr = String.fromCharCode(0x25b6);
var darr = String.fromCharCode(0x25bc);
s.innerHTML = s.innerHTML == uarr ? darr : uarr;
return false;
}
//-->
</script>
</head>
<body>
<div id="summary">
<h1>$exception_type : $exception_value</h2>
</div>
<div id="explanation">
<p>
<!--ERROR-MARKER-->
Oops, Deluge Broke :-( , You might have found a bug, or you did something really stupid ;-).
<br />If the error persists :<br />
Read the <a href="http://deluge-torrent.org/faq">Faq</a>.<br />
Try downloading the latest version at
<a href="http://deluge-torrent.org">deluge-torrent.org</a>
<br />Visit the <a href="http://forum.deluge-torrent.org">forum</a>
or the <a href="http://dev.deluge-torrent.org/query">buglist</a> for more info.
</p>
</div>
<div id="summary">
Paste the contents of this text-box when you are asked for a traceback:<br>
<!--
<form action="http://pastebin.ca/index.php" method=POST>
option to paste to a pastebin disabled, need to find out about pastebin etiquette first.
Or ask markybob to make a deluge pastebin.
--->
<textarea cols=80 rows=20 name="content">
--Deluge Error--
$exception_type : $exception_value
path : $ctx.path
file : $frames[0].filename in $frames[0].function, line $frames[0].lineno
--Input--
$web.input()
--Versions--
$version_info
--Traceback--
$tback_txt
</textarea><br />
<font color=red>Use a <a href="http://pastebin.ca/">pastebin</a> on IRC!</font><br>
<!--
<input type=submit name="Submit" value="Click here to paste to http://pastebin.ca">
-->
</form>
</div>
<div id="traceback">
<h2>Traceback <span>(innermost first)</span></h2>
<ul class="traceback">
$for frame in frames:
<li class="frame">
<code>$frame.filename</code> in <code>$frame.function</code>
$if frame.context_line:
<div class="context" id="c$frame.id">
$if frame.pre_context:
<ol start="$frame.pre_context_lineno" class="pre-context" id="pre$frame.id">
$for line in frame.pre_context:
<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>
</ol>
<ol start="$frame.lineno" class="context-line"><li onclick="toggle('pre$frame.id', 'post$frame.id')">$frame.context_line <span>...</span></li></ol>
$if frame.post_context:
<ol start='${frame.lineno + 1}' class="post-context" id="post$frame.id">
$for line in frame.post_context:
<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>
</ol>
</div>
$if frame.vars:
<div class="commands">
<a href='#' onclick="return varToggle(this, '$frame.id')"><span>&#x25b6;</span> Local vars</a>
$# $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame))
</div>
$:dicttable(frame.vars, kls='vars', id=('v' + str(frame.id)))
</li>
</ul>
</div>
<div id="requestinfo">
$if ctx.output or ctx.headers:
<h2>Response so far</h2>
<h3>HEADERS</h3>
<p class="req"><code>
$for kv in ctx.headers:
$kv[0]: $kv[1]<br />
$else:
[no headers]
</code></p>
<h3>BODY</h3>
<p class="req" style="padding-bottom: 2em"><code>
$ctx.output
</code></p>
<h2>Request information</h2>
<h3>INPUT</h3>
$:dicttable(web.input())
<h3 id="cookie-info">COOKIES</h3>
$:dicttable(web.cookies())
<h3 id="meta-info">META</h3>
$ newctx = []
$# ) and (k not in ['env', 'output', 'headers', 'environ', 'status', 'db_execute']):
$for k, v in ctx.iteritems():
$if not k.startswith('_') and (k in x):
$newctx.append(kv)
$:dicttable(dict(newctx))
<h3 id="meta-info">ENVIRONMENT</h3>
$:dicttable(ctx.env)
</div>
</body>
</html>
"""
dicttable_t = r"""$def with (d, kls='req', id=None)
$if d:
<table class="$kls"\
$if id: id="$id"\
><thead><tr><th>Variable</th><th>Value</th></tr></thead>
<tbody>
$ temp = d.items()
$temp.sort()
$for kv in temp:
<tr><td>$kv[0]</td><td class="code"><div>$prettify(kv[1])</div></td></tr>
</tbody>
</table>
$else:
<p>No data.</p>
"""
dicttable_r = Template(dicttable_t, filter=websafe)
djangoerror_r = Template(djangoerror_t, filter=websafe)
def djangoerror():
def _get_lines_from_file(filename, lineno, context_lines):
"""
Returns context_lines before and after lineno from file.
Returns (pre_context_lineno, pre_context, context_line, post_context).
"""
try:
source = open(filename).readlines()
lower_bound = max(0, lineno - context_lines)
upper_bound = lineno + context_lines
pre_context = \
[line.strip('\n') for line in source[lower_bound:lineno]]
context_line = source[lineno].strip('\n')
post_context = \
[line.strip('\n') for line in source[lineno + 1:upper_bound]]
return lower_bound, pre_context, context_line, post_context
except (OSError, IOError):
return None, [], None, []
exception_type, exception_value, tback = sys.exc_info()
exception_message = 'Error'
try:
exception_message = exception_value.message
except AttributeError:
exception_message = 'no message'
exception_type = exception_type.__name__
version_info = (
"WebUi : rev." + ws.REVNO
+ "Python : " + str(sys.version)
)
try:
import dbus
version_info += '\ndbus:' + str(dbus.__version__)
except:
pass
tback_txt = ''.join(format_tb(tback))
frames = []
while tback is not None:
filename = tback.tb_frame.f_code.co_filename
function = tback.tb_frame.f_code.co_name
lineno = tback.tb_lineno - 1
pre_context_lineno, pre_context, context_line, post_context = \
_get_lines_from_file(filename, lineno, 7)
frames.append(web.storage({
'tback': tback,
'filename': filename,
'function': function,
'lineno': lineno,
'vars': tback.tb_frame.f_locals,
'id': id(tback),
'pre_context': pre_context,
'context_line': context_line,
'post_context': post_context,
'pre_context_lineno': pre_context_lineno,
}))
tback = tback.tb_next
frames.reverse()
urljoin = urlparse.urljoin
def prettify(x):
try:
out = pprint.pformat(x)
except Exception, e:
out = '[could not display: <' + e.__class__.__name__ + \
': '+str(e)+'>]'
return out
dt = dicttable_r
dt.globals = {'prettify': prettify}
t = djangoerror_r
t.globals = {'ctx': web.ctx, 'web':web, 'dicttable':dt, 'dict':dict, 'str':str}
return t(exception_type, exception_value, frames, exception_message, version_info, tback_txt)
def deluge_debugerror():
"""
A replacement for `internalerror` that presents a nice page with lots
of debug information for the programmer.
(Based on the beautiful 500 page from [Django](http://djangoproject.com/),
designed by [Wilson Miner](http://wilsonminer.com/).)
"""
web.ctx.headers = [
('Content-Type', 'text/html')
]
web.ctx.output = djangoerror()
if __name__ == "__main__":
urls = (
'/', 'index'
)
class index:
def GET(self):
thisdoesnotexist
web.internalerror = web.debugerror
web.run(urls)

View File

@ -0,0 +1,382 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# deluge_webserver.py
#
# Copyright (C) Martijn Voncken 2007 <mvoncken@gmail.com>
#
# 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 webserver_common as ws
from webserver_framework import *
import lib.webpy022 as web
from lib.webpy022.http import seeother, url
import base64
from operator import attrgetter
import os
from json_api import json_api
#routing:
urls = (
"/login", "login",
"/index", "index",
"/torrent/info/(.*)", "torrent_info",
"/torrent/info_inner/(.*)", "torrent_info_inner",
"/torrent/stop/(.*)", "torrent_stop",
"/torrent/start/(.*)", "torrent_start",
"/torrent/reannounce/(.*)", "torrent_reannounce",
"/torrent/add(.*)", "torrent_add",
"/torrent/delete/(.*)", "torrent_delete",
"/torrent/queue/up/(.*)", "torrent_queue_up",
"/torrent/queue/down/(.*)", "torrent_queue_down",
"/pause_all", "pause_all",
"/resume_all", "resume_all",
"/refresh/set", "refresh_set",
"/refresh/(.*)", "refresh",
"/config", "config_",
"/home", "home",
"/about", "about",
"/logout", "logout",
#remote-api:
"/remote/torrent/add(.*)", "remote_torrent_add",
"/json/(.*)","json_api",
#static:
"/static/(.*)", "static",
"/template/static/(.*)", "template_static",
#"/downloads/(.*)","downloads" disabled until it can handle large downloads
#default-pages
"/", "home",
"", "home"
)
#/routing
#pages:
class login:
@deluge_page_noauth
def GET(self, name):
vars = web.input(error = None)
return ws.render.login(vars.error)
def POST(self):
vars = web.input(pwd = None, redir = None)
if check_pwd(vars.pwd):
#start new session
start_session()
do_redirect()
elif vars.redir:
seeother(url('/login', error=1, redir=vars.redir))
else:
seeother('/login?error=1')
class index:
"page containing the torrent list."
@deluge_page
@auto_refreshed
def GET(self, name):
vars = web.input(sort=None, order=None ,filter=None , category=None)
torrent_list = [get_torrent_status(torrent_id)
for torrent_id in ws.proxy.get_session_state()]
all_torrents = torrent_list[:]
#filter-state
if vars.filter:
torrent_list = filter_torrent_state(torrent_list, vars.filter)
setcookie("filter", vars.filter)
else:
setcookie("filter", "")
#filter-cat
if vars.category:
torrent_list = [t for t in torrent_list if t.category == vars.category]
setcookie("category", vars.category)
else:
setcookie("category", "")
#sorting:
if vars.sort:
torrent_list.sort(key=attrgetter(vars.sort))
if vars.order == 'up':
torrent_list = reversed(torrent_list)
setcookie("order", vars.order)
setcookie("sort", vars.sort)
return ws.render.index(torrent_list, all_torrents)
class torrent_info:
@deluge_page
@auto_refreshed
def GET(self, name):
torrent_id = name.split(',')[0]
return ws.render.torrent_info(get_torrent_status(torrent_id))
class torrent_info_inner:
@deluge_page
def GET(self, torrent_ids):
torrent_ids = torrent_ids.split(',')
info = get_torrent_status(torrent_ids[0])
if len(torrent_ids) > 1:
#todo : hmm, lots of manual stuff here :(
pass
return ws.render.torrent_info_inner(info)
class torrent_start:
@check_session
def POST(self, name):
torrent_ids = name.split(',')
ws.proxy.resume_torrent(torrent_ids)
do_redirect()
class torrent_stop:
@check_session
def POST(self, name):
torrent_ids = name.split(',')
ws.proxy.pause_torrent(torrent_ids)
do_redirect()
class torrent_reannounce:
@check_session
def POST(self, torrent_id):
ws.proxy.force_reannounce([torrent_id])
do_redirect()
class torrent_add:
@deluge_page
def GET(self, name):
return ws.render.torrent_add()
@check_session
def POST(self, name):
"""
allows:
*posting of url
*posting file-upload
*posting of data as string(for greasemonkey-private)
"""
vars = web.input(url = None, torrent = {})
torrent_name = None
torrent_data = None
if vars.torrent.filename:
torrent_name = vars.torrent.filename
torrent_data = vars.torrent.file.read()
if vars.url and torrent_name:
error_page(_("Choose an url or a torrent, not both."))
if vars.url:
ws.proxy.add_torrent_url(vars.url)
do_redirect()
elif torrent_name:
data_b64 = base64.b64encode(torrent_data)
#b64 because of strange bug-reports related to binary data
ws.proxy.add_torrent_filecontent(vars.torrent.filename, data_b64)
do_redirect()
else:
error_page(_("no data."))
class remote_torrent_add:
"""
For use in remote scripts etc.
curl ->POST pwd and torrent as file
greasemonkey: POST pwd torrent_name and data_b64
"""
@remote
def POST(self, name):
vars = web.input(pwd = None, torrent = {},
data_b64 = None , torrent_name= None)
if not check_pwd(vars.pwd):
return 'error:wrong password'
if vars.data_b64: #b64 post (greasemonkey)
data_b64 = unicode(vars.data_b64)
torrent_name = vars.torrent_name
else: #file-post (curl)
data_b64 = base64.b64encode(vars.torrent.file.read())
torrent_name = vars.torrent.filename
ws.proxy.add_torrent_filecontent(torrent_name, data_b64)
return 'ok'
class torrent_delete:
@deluge_page
def GET(self, name):
torrent_ids = name.split(',')
torrent_list = [get_torrent_status(id) for id in torrent_ids]
return ws.render.torrent_delete(name, torrent_list)
@check_session
def POST(self, name):
torrent_ids = name.split(',')
vars = web.input(data_also = None, torrent_also = None)
data_also = bool(vars.data_also)
torrent_also = bool(vars.torrent_also)
ws.proxy.remove_torrent(torrent_ids, data_also, torrent_also)
do_redirect()
class torrent_queue_up:
@check_session
def POST(self, name):
#a bit too verbose..
torrent_ids = name.split(',')
torrents = [get_torrent_status(id) for id in torrent_ids]
torrents.sort(lambda x, y : x.queue_pos - y.queue_pos)
torrent_ids = [t.id for t in torrents]
for torrent_id in torrent_ids:
ws.proxy.queue_up(torrent_id)
do_redirect()
class torrent_queue_down:
@check_session
def POST(self, name):
#a bit too verbose..
torrent_ids = name.split(',')
torrents = [get_torrent_status(id) for id in torrent_ids]
torrents.sort(lambda x, y : x.queue_pos - y.queue_pos)
torrent_ids = [t.id for t in torrents]
for torrent_id in reversed(torrent_ids):
ws.proxy.queue_down(torrent_id)
do_redirect()
class pause_all:
@check_session
def POST(self, name):
ws.proxy.pause_torrent(ws.proxy.get_session_state())
do_redirect()
class resume_all:
@check_session
def POST(self, name):
ws.proxy.resume_torrent(ws.proxy.get_session_state())
do_redirect()
class refresh:
@check_session
def POST(self, name):
auto_refresh = {'off': '0', 'on': '1'}[name]
setcookie('auto_refresh', auto_refresh)
if not getcookie('auto_refresh_secs'):
setcookie('auto_refresh_secs', 10)
do_redirect()
class refresh_set:
@deluge_page
def GET(self, name):
return ws.render.refresh_form()
@check_session
def POST(self, name):
vars = web.input(refresh = 0)
refresh = int(vars.refresh)
if refresh > 0:
setcookie('auto_refresh', '1')
setcookie('auto_refresh_secs', str(refresh))
do_redirect()
else:
error_page(_('refresh must be > 0'))
class config_: #namespace clash?
"""core config
TODO:good validation.
"""
"""
SOMEHOW ONLY BREAKS 0.6 ??
cfg_form = web.form.Form(
web.form.Dropdown('max_download', ws.SPEED_VALUES,
description=_('Download Speed Limit'),
post='%s Kib/sec' % ws.proxy.get_config_value('max_download_speed')
)
,web.form.Dropdown('max_upload', ws.SPEED_VALUES,
description=_('Upload Speed Limit'),
post='%s Kib/sec' % ws.proxy.get_config_value('max_upload_speed')
)
)
@deluge_page
def GET(self, name):
return ws.render.config(self.cfg_form())
def POST(self, name):
vars = web.input(max_download=None, max_upload=None)
#self.config.set("max_download_speed", float(str_bwdown))
raise NotImplementedError('todo')
"""
class home:
@check_session
def GET(self, name):
do_redirect()
class about:
@deluge_page_noauth
def GET(self, name):
return ws.render.about()
class logout:
@check_session
def POST(self, name):
end_session()
seeother('/login')
class static(static_handler):
base_dir = os.path.join(os.path.dirname(__file__), 'static')
class template_static(static_handler):
def get_base_dir(self):
return os.path.join(os.path.dirname(__file__),
'templates/%s/static' % ws.config.get('template'))
class downloads(static_handler):
def GET(self, name):
self.base_dir = ws.proxy.get_config_value('default_download_path')
if not ws.config.get('share_downloads'):
raise Exception('Access to downloads is forbidden.')
return static_handler.GET(self, name)
#/pages
def WebServer():
return create_webserver(urls, globals())
def run():
server = WebServer()
try:
server.start()
except KeyboardInterrupt:
server.stop()
if __name__ == "__main__":
run()

210
plugins/WebUi/json_api.py Normal file
View File

@ -0,0 +1,210 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# webserver_framework.py
#
# Copyright (C) Martijn Voncken 2007 <mvoncken@gmail.com>
#
# 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.
"""
json api.
only used for XUL and/or external scripts
it would be possible not to incluse the python-json dependency.
"""
from new import instancemethod
from inspect import getargspec
from webserver_framework import remote,ws,get_torrent_status,log,get_category_choosers, get_stats,log,filter_torrent_state,fsize,fspeed
from operator import attrgetter
import lib.webpy022 as web
proxy = ws.proxy
def to_json(obj):
from lib.pythonize import pythonize
obj = pythonize(obj)
try:
import json
return json.write(obj)
except ImportError:
raise ImportError("""Install python-json using your package-manager
http://sourceforge.net/projects/json-py/""")
class json_api:
"""
eperimental json api
generic proxy for all methods onm self.
"""
illegal_methods = ['shutdown', 'socket', 'xmlrpclib','pickle','os',
'is_localhost','CoreProxy','connect_on_new_core', 'connect_on_no_core',
'connected','deluge','GET','POST']
def __init__(self):
self._add_proxy_methods()
#extra exposed:
get_torrent_status = get_torrent_status
@remote
def POST(self,name):
import json
if name.startswith('_'):
raise AttributeError('_ methods are illegal.')
if name in self.illegal_methods:
raise AttributeError('Illegal method.')
if not(hasattr(self,name)):
raise AttributeError('No such method')
method = getattr(self,name)
vars = web.input(kwargs= None)
log.debug('vars=%s' % vars)
if vars.kwargs:
kwargs = json.read(vars.kwargs)
else:
kwargs = {}
result = method(**kwargs)
return "(" + to_json(result) + ")"
def list_methods(self):
"""
list all json methods
returns a dict of {methodname:{args:[list of kwargs],doc:'string'},..}
"""
methods = [getattr(self,m) for m in dir(self)
if not m.startswith('_')
and (not m in self.illegal_methods)
and callable(getattr(self,m))
]
return dict([(f.__name__,
{'args':getargspec(f)[0],'doc':(f.__doc__ or '').strip()})
for f in methods])
def _add_proxy_methods(self):
methods = [getattr(proxy,m) for m in dir(proxy)
if not m.startswith('_')
and (not m in self.illegal_methods)
and callable(getattr(proxy,m))
]
for m in methods:
setattr(self,m.__name__,m)
#extra's:
def list_torrents(self):
return [get_torrent_status(torrent_id)
for torrent_id in ws.proxy.get_session_state()]
def simplify_torrent_status(self, torrent):
"""smaller subset and preformatted data for the treelist"""
data = {
"id":torrent.id,
"message":torrent.message,
"name":torrent.name,
"total_size":fsize(torrent.total_size),
"progress":torrent.progress,
"category":torrent.category,
"seeds":"",
"peers":"",
"download_rate":"",
"upload_rate":"",
"eta":"",
"distributed_copies":"",
"ratio":"",
"calc_state_str":torrent.calc_state_str,
"queue_pos":torrent.queue_pos
}
if torrent.total_seeds > 0:
data['seeds'] = "%s (%s)" % (torrent.num_seeds, torrent.total_seeds)
if torrent.total_peers > 0:
data['peers'] = "%s (%s)" % (torrent.num_peers, torrent.total_peers)
if torrent.download_rate > 0:
data['download_rate'] = fspeed(torrent.download_rate)
if torrent.upload_rate > 0:
data['upload_rate'] = fspeed(torrent.upload_rate)
if torrent.eta > 0:
data['eta'] = ("%.3f" % torrent.eta)
if torrent.distributed_copies > 0:
data['distributed_copies'] = "%.3f" % torrent.distributed_copies
if torrent.ratio > 0:
data['ratio'] = "%.3f" % torrent.ratio
return data
def update_ui(self, filter=None, category=None ,sort='name' ,order='down'):
"""
Combines the most important ui calls into 1 composite call.
xmlhttp requests are expensive,max 2 running at the same time.
and performance over the internet is mostly related to the number
of requests (low ping)
returns :
{torrent_list:[{},..],'categories':[],'filters':'','stats':{}}
"""
torrent_list = self.list_torrents();
filter_tabs, category_tabs = get_category_choosers(torrent_list)
#filter-state
if filter:
torrent_list = filter_torrent_state(torrent_list, filter)
#filter-cat
if category:
torrent_list = [t for t in torrent_list if t.category == category]
#sorting
if sort:
torrent_list.sort(key=attrgetter(sort))
if order == 'up':
torrent_list = reversed(torrent_list)
torrent_list = [self.simplify_torrent_status(t) for t in torrent_list]
return {
'torrent_list':torrent_list,
'categories':category_tabs,
'filters':filter_tabs,
'stats':get_stats()
}
if __name__ == '__main__':
from pprint import pprint
#proxy.set_core_uri('http://localhost:58846') #How to configure this?
j = json_api()
if True:
print 'list-methods:'
methods = j.list_methods()
names = methods.keys()
names.sort()
for name in names:
m = methods[name]
print "%s(%s)\n %s\n" % (name , m['args'] , m['doc'])
#j.GET('list_torrents')
j.POST('list_torrents')

View File

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,38 @@
"""
some dbus to python type conversions
-decorator for interface
-wrapper class for proxy
"""
def pythonize(var):
"""translates dbus types back to basic python types."""
if isinstance(var, list):
return [pythonize(value) for value in var]
if isinstance(var, tuple):
return tuple([pythonize(value) for value in var])
if isinstance(var, dict):
return dict(
[(pythonize(key), pythonize(value)) for key, value in var.iteritems()]
)
for klass in [unicode, str, bool, int, float, long]:
if isinstance(var,klass):
return klass(var)
return var
def pythonize_call(func):
def deco(*args,**kwargs):
return pythonize(func(*args, **kwargs))
return deco
def pythonize_interface(func):
def deco(*args, **kwargs):
args = pythonize(args)
kwargs = pythonize(kwargs)
return func(*args, **kwargs)
return deco
class PythonizeProxy(object):
def __init__(self,proxy):
self.proxy = proxy
def __getattr__(self, key):
return pythonize_call(getattr(self.proxy, key))

View File

@ -0,0 +1,8 @@
This folder may only contain general purpose utilities/files/tools.
They should be usable outside of deluge.
Disclaimer:
Some may have been adapted to work better with deluge.
But they will import other parts of deluge or Webui.

View File

@ -0,0 +1,136 @@
#!/usr/bin/env python
#(c) Martijn Voncken, mvoncken@gmail.com
#Same Licence as web.py 0.22 ->Public Domain
#
"""
static fileserving for web.py
without the need for wsgi wrapper magic.
"""
import webpy022 as web
from webpy022.http import seeother, url
import posixpath
import urlparse
import urllib
import mimetypes
import os
import datetime
import cgi
from StringIO import StringIO
mimetypes.init() # try to read system mime.types
class static_handler:
"""
mostly c&p from SimpleHttpServer
serves relative from start location
"""
base_dir = './'
extensions_map = mimetypes.types_map
def get_base_dir(self):
#override this if you have a config that changes the base dir at runtime
#deluge on windows :(
return self.base_dir
def GET(self, path):
path = self.translate_path(path)
if os.path.isdir(path):
if not path.endswith('/'):
path += "/"
return self.list_directory(path)
ctype = self.guess_type(path)
try:
f = open(path, 'rb')
except IOError:
raise Exception('file not found:%s' % path)
#web.header("404", "File not found")
#return
web.header("Content-type", ctype)
fs = os.fstat(f.fileno())
web.header("Content-Length", str(fs[6]))
web.lastmodified(datetime.datetime.fromtimestamp(fs.st_mtime))
print f.read()
def translate_path(self, path):
"""Translate a /-separated PATH to the local filename syntax.
Components that mean special things to the local file system
(e.g. drive or directory names) are ignored. (XXX They should
probably be diagnosed.)
"""
# abandon query parameters
path = urlparse.urlparse(path)[2]
path = posixpath.normpath(urllib.unquote(path))
words = path.split('/')
words = filter(None, words)
path = self.get_base_dir()
for word in words:
drive, word = os.path.splitdrive(word)
head, word = os.path.split(word)
if word in (os.curdir, os.pardir): continue
path = os.path.join(path, word)
return path
def guess_type(self, path):
base, ext = posixpath.splitext(path)
if ext in self.extensions_map:
return self.extensions_map[ext]
ext = ext.lower()
if ext in self.extensions_map:
return self.extensions_map[ext]
else:
return 'application/octet-stream'
def list_directory(self, path):
"""Helper to produce a directory listing (absent index.html).
Return value is either a file object, or None (indicating an
error). In either case, the headers are sent, making the
interface the same as for send_head().
#TODO ->use web.py +template!
"""
try:
list = os.listdir(path)
except os.error:
web.header('404', "No permission to list directory")
return None
list.sort(key=lambda a: a.lower())
f = StringIO()
displaypath = cgi.escape(urllib.unquote(path))
f.write("<title>Directory listing for %s</title>\n" % displaypath)
f.write("<h2>Directory listing for %s</h2>\n" % displaypath)
f.write("<hr>\n<ul>\n")
for name in list:
fullname = os.path.join(path, name)
displayname = linkname = name
# Append / for directories or @ for symbolic links
if os.path.isdir(fullname):
displayname = name + "/"
linkname = name + "/"
if os.path.islink(fullname):
displayname = name + "@"
# Note: a link to a directory displays with @ and links with /
f.write('<li><a href="%s">%s</a>\n'
% (urllib.quote(linkname), cgi.escape(displayname)))
f.write("</ul>\n<hr>\n")
length = f.tell()
f.seek(0)
web.header("Content-type", "text/html")
web.header("Content-Length", str(length))
print f.read()
if __name__ == '__main__':
#example:
class usr_static(static_handler):
base_dir = os.path.expanduser('~')
urls = ('/relative/(.*)','static_handler',
'/(.*)','usr_static')
web.run(urls,globals())

View File

@ -0,0 +1 @@
http://webpy.org/

View File

@ -0,0 +1,62 @@
#!/usr/bin/env python
from __future__ import generators
"""web.py: makes web apps (http://webpy.org)"""
__version__ = "0.22"
__revision__ = "$Rev: 183 $"
__author__ = "Aaron Swartz <me@aaronsw.com>"
__license__ = "public domain"
__contributors__ = "see http://webpy.org/changes"
# todo:
# - some sort of accounts system
import utils, db, net, wsgi, http, webapi, request, httpserver, debugerror
import template, form
from utils import *
from db import *
from net import *
from wsgi import *
from http import *
from webapi import *
from request import *
from httpserver import *
from debugerror import *
try:
import cheetah
from cheetah import *
except ImportError:
pass
def main():
import doctest
doctest.testmod(utils)
doctest.testmod(db)
doctest.testmod(net)
doctest.testmod(wsgi)
doctest.testmod(http)
doctest.testmod(webapi)
doctest.testmod(request)
try:
doctest.testmod(cheetah)
except NameError:
pass
template.test()
import sys
urls = ('/web.py', 'source')
class source:
def GET(self):
header('Content-Type', 'text/python')
print open(sys.argv[0]).read()
if listget(sys.argv, 1) != 'test':
run(urls, locals())
if __name__ == "__main__": main()

View File

@ -0,0 +1,5 @@
1:Commented out some code to enable a relative redirect.
This is not according to HTTP/1.1 Spec
But many deluge users will want to route the webui through firewalls/routers or use apache redirects.
2:Disabled logging in the builtin http-server.

View File

@ -0,0 +1,98 @@
"""
Cheetah API
(from web.py)
"""
__all__ = ["render"]
import re, urlparse, pprint, traceback, sys
from Cheetah.Compiler import Compiler
from Cheetah.Filters import Filter
from utils import re_compile, memoize, dictadd
from net import htmlquote, websafe
from webapi import ctx, header, output, input, cookies, loadhooks
def upvars(level=2):
"""Guido van Rossum sez: don't use this function."""
return dictadd(
sys._getframe(level).f_globals,
sys._getframe(level).f_locals)
r_include = re_compile(r'(?!\\)#include \"(.*?)\"($|#)', re.M)
def __compiletemplate(template, base=None, isString=False):
if isString:
text = template
else:
text = open('templates/'+template).read()
# implement #include at compile-time
def do_include(match):
text = open('templates/'+match.groups()[0]).read()
return text
while r_include.findall(text):
text = r_include.sub(do_include, text)
execspace = _compiletemplate.bases.copy()
tmpl_compiler = Compiler(source=text, mainClassName='GenTemplate')
tmpl_compiler.addImportedVarNames(execspace.keys())
exec str(tmpl_compiler) in execspace
if base:
_compiletemplate.bases[base] = execspace['GenTemplate']
return execspace['GenTemplate']
_compiletemplate = memoize(__compiletemplate)
_compiletemplate.bases = {}
def render(template, terms=None, asTemplate=False, base=None,
isString=False):
"""
Renders a template, caching where it can.
`template` is the name of a file containing the a template in
the `templates/` folder, unless `isString`, in which case it's the
template itself.
`terms` is a dictionary used to fill the template. If it's None, then
the caller's local variables are used instead, plus context, if it's not
already set, is set to `context`.
If asTemplate is False, it `output`s the template directly. Otherwise,
it returns the template object.
If the template is a potential base template (that is, something other templates)
can extend, then base should be a string with the name of the template. The
template will be cached and made available for future calls to `render`.
Requires [Cheetah](http://cheetahtemplate.org/).
"""
# terms=['var1', 'var2'] means grab those variables
if isinstance(terms, list):
new = {}
old = upvars()
for k in terms:
new[k] = old[k]
terms = new
# default: grab all locals
elif terms is None:
terms = {'context': ctx, 'ctx':ctx}
terms.update(sys._getframe(1).f_locals)
# terms=d means use d as the searchList
if not isinstance(terms, tuple):
terms = (terms,)
if 'headers' in ctx and not isString and template.endswith('.html'):
header('Content-Type','text/html; charset=utf-8', unique=True)
if loadhooks.has_key('reloader'):
compiled_tmpl = __compiletemplate(template, base=base, isString=isString)
else:
compiled_tmpl = _compiletemplate(template, base=base, isString=isString)
compiled_tmpl = compiled_tmpl(searchList=terms, filter=WebSafe)
if asTemplate:
return compiled_tmpl
else:
return output(str(compiled_tmpl))
class WebSafe(Filter):
def filter(self, val, **keywords):
return websafe(val)

View File

@ -0,0 +1,703 @@
"""
Database API
(part of web.py)
"""
# todo:
# - test with sqlite
# - a store function?
__all__ = [
"UnknownParamstyle", "UnknownDB",
"sqllist", "sqlors", "aparam", "reparam",
"SQLQuery", "sqlquote",
"SQLLiteral", "sqlliteral",
"connect",
"TransactionError", "transaction", "transact", "commit", "rollback",
"query",
"select", "insert", "update", "delete"
]
import time
try: import datetime
except ImportError: datetime = None
from utils import storage, iters, iterbetter
import webapi as web
try:
from DBUtils import PooledDB
web.config._hasPooling = True
except ImportError:
web.config._hasPooling = False
class _ItplError(ValueError):
def __init__(self, text, pos):
ValueError.__init__(self)
self.text = text
self.pos = pos
def __str__(self):
return "unfinished expression in %s at char %d" % (
repr(self.text), self.pos)
def _interpolate(format):
"""
Takes a format string and returns a list of 2-tuples of the form
(boolean, string) where boolean says whether string should be evaled
or not.
from <http://lfw.org/python/Itpl.py> (public domain, Ka-Ping Yee)
"""
from tokenize import tokenprog
def matchorfail(text, pos):
match = tokenprog.match(text, pos)
if match is None:
raise _ItplError(text, pos)
return match, match.end()
namechars = "abcdefghijklmnopqrstuvwxyz" \
"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
chunks = []
pos = 0
while 1:
dollar = format.find("$", pos)
if dollar < 0:
break
nextchar = format[dollar + 1]
if nextchar == "{":
chunks.append((0, format[pos:dollar]))
pos, level = dollar + 2, 1
while level:
match, pos = matchorfail(format, pos)
tstart, tend = match.regs[3]
token = format[tstart:tend]
if token == "{":
level = level + 1
elif token == "}":
level = level - 1
chunks.append((1, format[dollar + 2:pos - 1]))
elif nextchar in namechars:
chunks.append((0, format[pos:dollar]))
match, pos = matchorfail(format, dollar + 1)
while pos < len(format):
if format[pos] == "." and \
pos + 1 < len(format) and format[pos + 1] in namechars:
match, pos = matchorfail(format, pos + 1)
elif format[pos] in "([":
pos, level = pos + 1, 1
while level:
match, pos = matchorfail(format, pos)
tstart, tend = match.regs[3]
token = format[tstart:tend]
if token[0] in "([":
level = level + 1
elif token[0] in ")]":
level = level - 1
else:
break
chunks.append((1, format[dollar + 1:pos]))
else:
chunks.append((0, format[pos:dollar + 1]))
pos = dollar + 1 + (nextchar == "$")
if pos < len(format):
chunks.append((0, format[pos:]))
return chunks
class UnknownParamstyle(Exception):
"""
raised for unsupported db paramstyles
(currently supported: qmark, numeric, format, pyformat)
"""
pass
def aparam():
"""
Returns the appropriate string to be used to interpolate
a value with the current `web.ctx.db_module` or simply %s
if there isn't one.
>>> aparam()
'%s'
"""
if hasattr(web.ctx, 'db_module'):
style = web.ctx.db_module.paramstyle
else:
style = 'pyformat'
if style == 'qmark':
return '?'
elif style == 'numeric':
return ':1'
elif style in ['format', 'pyformat']:
return '%s'
raise UnknownParamstyle, style
def reparam(string_, dictionary):
"""
Takes a string and a dictionary and interpolates the string
using values from the dictionary. Returns an `SQLQuery` for the result.
>>> reparam("s = $s", dict(s=True))
<sql: "s = 't'">
"""
vals = []
result = []
for live, chunk in _interpolate(string_):
if live:
result.append(aparam())
vals.append(eval(chunk, dictionary))
else: result.append(chunk)
return SQLQuery(''.join(result), vals)
def sqlify(obj):
"""
converts `obj` to its proper SQL version
>>> sqlify(None)
'NULL'
>>> sqlify(True)
"'t'"
>>> sqlify(3)
'3'
"""
# because `1 == True and hash(1) == hash(True)`
# we have to do this the hard way...
if obj is None:
return 'NULL'
elif obj is True:
return "'t'"
elif obj is False:
return "'f'"
elif datetime and isinstance(obj, datetime.datetime):
return repr(obj.isoformat())
else:
return repr(obj)
class SQLQuery:
"""
You can pass this sort of thing as a clause in any db function.
Otherwise, you can pass a dictionary to the keyword argument `vars`
and the function will call reparam for you.
"""
# tested in sqlquote's docstring
def __init__(self, s='', v=()):
self.s, self.v = str(s), tuple(v)
def __getitem__(self, key): # for backwards-compatibility
return [self.s, self.v][key]
def __add__(self, other):
if isinstance(other, str):
self.s += other
elif isinstance(other, SQLQuery):
self.s += other.s
self.v += other.v
return self
def __radd__(self, other):
if isinstance(other, str):
self.s = other + self.s
return self
else:
return NotImplemented
def __str__(self):
try:
return self.s % tuple([sqlify(x) for x in self.v])
except (ValueError, TypeError):
return self.s
def __repr__(self):
return '<sql: %s>' % repr(str(self))
class SQLLiteral:
"""
Protects a string from `sqlquote`.
>>> insert('foo', time=SQLLiteral('NOW()'), _test=True)
<sql: 'INSERT INTO foo (time) VALUES (NOW())'>
"""
def __init__(self, v):
self.v = v
def __repr__(self):
return self.v
sqlliteral = SQLLiteral
def sqlquote(a):
"""
Ensures `a` is quoted properly for use in a SQL query.
>>> 'WHERE x = ' + sqlquote(True) + ' AND y = ' + sqlquote(3)
<sql: "WHERE x = 't' AND y = 3">
"""
return SQLQuery(aparam(), (a,))
class UnknownDB(Exception):
"""raised for unsupported dbms"""
pass
def connect(dbn, **keywords):
"""
Connects to the specified database.
`dbn` currently must be "postgres", "mysql", or "sqlite".
If DBUtils is installed, connection pooling will be used.
"""
if dbn == "postgres":
try:
import psycopg2 as db
except ImportError:
try:
import psycopg as db
except ImportError:
import pgdb as db
if 'pw' in keywords:
keywords['password'] = keywords['pw']
del keywords['pw']
keywords['database'] = keywords['db']
del keywords['db']
elif dbn == "mysql":
import MySQLdb as db
if 'pw' in keywords:
keywords['passwd'] = keywords['pw']
del keywords['pw']
db.paramstyle = 'pyformat' # it's both, like psycopg
elif dbn == "sqlite":
try:
import sqlite3 as db
db.paramstyle = 'qmark'
except ImportError:
try:
from pysqlite2 import dbapi2 as db
db.paramstyle = 'qmark'
except ImportError:
import sqlite as db
web.config._hasPooling = False
keywords['database'] = keywords['db']
del keywords['db']
elif dbn == "firebird":
import kinterbasdb as db
if 'pw' in keywords:
keywords['passwd'] = keywords['pw']
del keywords['pw']
keywords['database'] = keywords['db']
del keywords['db']
else:
raise UnknownDB, dbn
web.ctx.db_name = dbn
web.ctx.db_module = db
web.ctx.db_transaction = 0
web.ctx.db = keywords
def _PooledDB(db, keywords):
# In DBUtils 0.9.3, `dbapi` argument is renamed as `creator`
# see Bug#122112
if PooledDB.__version__.split('.') < '0.9.3'.split('.'):
return PooledDB.PooledDB(dbapi=db, **keywords)
else:
return PooledDB.PooledDB(creator=db, **keywords)
def db_cursor():
if isinstance(web.ctx.db, dict):
keywords = web.ctx.db
if web.config._hasPooling:
if 'db' not in globals():
globals()['db'] = _PooledDB(db, keywords)
web.ctx.db = globals()['db'].connection()
else:
web.ctx.db = db.connect(**keywords)
return web.ctx.db.cursor()
web.ctx.db_cursor = db_cursor
web.ctx.dbq_count = 0
def db_execute(cur, sql_query, dorollback=True):
"""executes an sql query"""
web.ctx.dbq_count += 1
try:
a = time.time()
out = cur.execute(sql_query.s, sql_query.v)
b = time.time()
except:
if web.config.get('db_printing'):
print >> web.debug, 'ERR:', str(sql_query)
if dorollback: rollback(care=False)
raise
if web.config.get('db_printing'):
print >> web.debug, '%s (%s): %s' % (round(b-a, 2), web.ctx.dbq_count, str(sql_query))
return out
web.ctx.db_execute = db_execute
return web.ctx.db
class TransactionError(Exception): pass
class transaction:
"""
A context that can be used in conjunction with "with" statements
to implement SQL transactions. Starts a transaction on enter,
rolls it back if there's an error; otherwise it commits it at the
end.
"""
def __enter__(self):
transact()
def __exit__(self, exctype, excvalue, traceback):
if exctype is not None:
rollback()
else:
commit()
def transact():
"""Start a transaction."""
if not web.ctx.db_transaction:
# commit everything up to now, so we don't rollback it later
if hasattr(web.ctx.db, 'commit'):
web.ctx.db.commit()
else:
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor,
SQLQuery("SAVEPOINT webpy_sp_%s" % web.ctx.db_transaction))
web.ctx.db_transaction += 1
def commit():
"""Commits a transaction."""
web.ctx.db_transaction -= 1
if web.ctx.db_transaction < 0:
raise TransactionError, "not in a transaction"
if not web.ctx.db_transaction:
if hasattr(web.ctx.db, 'commit'):
web.ctx.db.commit()
else:
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor,
SQLQuery("RELEASE SAVEPOINT webpy_sp_%s" % web.ctx.db_transaction))
def rollback(care=True):
"""Rolls back a transaction."""
web.ctx.db_transaction -= 1
if web.ctx.db_transaction < 0:
web.db_transaction = 0
if care:
raise TransactionError, "not in a transaction"
else:
return
if not web.ctx.db_transaction:
if hasattr(web.ctx.db, 'rollback'):
web.ctx.db.rollback()
else:
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor,
SQLQuery("ROLLBACK TO SAVEPOINT webpy_sp_%s" % web.ctx.db_transaction),
dorollback=False)
def query(sql_query, vars=None, processed=False, _test=False):
"""
Execute SQL query `sql_query` using dictionary `vars` to interpolate it.
If `processed=True`, `vars` is a `reparam`-style list to use
instead of interpolating.
>>> query("SELECT * FROM foo", _test=True)
<sql: 'SELECT * FROM foo'>
>>> query("SELECT * FROM foo WHERE x = $x", vars=dict(x='f'), _test=True)
<sql: "SELECT * FROM foo WHERE x = 'f'">
>>> query("SELECT * FROM foo WHERE x = " + sqlquote('f'), _test=True)
<sql: "SELECT * FROM foo WHERE x = 'f'">
"""
if vars is None: vars = {}
if not processed and not isinstance(sql_query, SQLQuery):
sql_query = reparam(sql_query, vars)
if _test: return sql_query
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor, sql_query)
if db_cursor.description:
names = [x[0] for x in db_cursor.description]
def iterwrapper():
row = db_cursor.fetchone()
while row:
yield storage(dict(zip(names, row)))
row = db_cursor.fetchone()
out = iterbetter(iterwrapper())
if web.ctx.db_name != "sqlite":
out.__len__ = lambda: int(db_cursor.rowcount)
out.list = lambda: [storage(dict(zip(names, x))) \
for x in db_cursor.fetchall()]
else:
out = db_cursor.rowcount
if not web.ctx.db_transaction: web.ctx.db.commit()
return out
def sqllist(lst):
"""
Converts the arguments for use in something like a WHERE clause.
>>> sqllist(['a', 'b'])
'a, b'
>>> sqllist('a')
'a'
"""
if isinstance(lst, str):
return lst
else:
return ', '.join(lst)
def sqlors(left, lst):
"""
`left is a SQL clause like `tablename.arg = `
and `lst` is a list of values. Returns a reparam-style
pair featuring the SQL that ORs together the clause
for each item in the lst.
>>> sqlors('foo = ', [])
<sql: '2+2=5'>
>>> sqlors('foo = ', [1])
<sql: 'foo = 1'>
>>> sqlors('foo = ', 1)
<sql: 'foo = 1'>
>>> sqlors('foo = ', [1,2,3])
<sql: '(foo = 1 OR foo = 2 OR foo = 3)'>
"""
if isinstance(lst, iters):
lst = list(lst)
ln = len(lst)
if ln == 0:
return SQLQuery("2+2=5", [])
if ln == 1:
lst = lst[0]
if isinstance(lst, iters):
return SQLQuery('(' + left +
(' OR ' + left).join([aparam() for param in lst]) + ")", lst)
else:
return SQLQuery(left + aparam(), [lst])
def sqlwhere(dictionary, grouping=' AND '):
"""
Converts a `dictionary` to an SQL WHERE clause `SQLQuery`.
>>> sqlwhere({'cust_id': 2, 'order_id':3})
<sql: 'order_id = 3 AND cust_id = 2'>
>>> sqlwhere({'cust_id': 2, 'order_id':3}, grouping=', ')
<sql: 'order_id = 3, cust_id = 2'>
"""
return SQLQuery(grouping.join([
'%s = %s' % (k, aparam()) for k in dictionary.keys()
]), dictionary.values())
def select(tables, vars=None, what='*', where=None, order=None, group=None,
limit=None, offset=None, _test=False):
"""
Selects `what` from `tables` with clauses `where`, `order`,
`group`, `limit`, and `offset`. Uses vars to interpolate.
Otherwise, each clause can be a SQLQuery.
>>> select('foo', _test=True)
<sql: 'SELECT * FROM foo'>
>>> select(['foo', 'bar'], where="foo.bar_id = bar.id", limit=5, _test=True)
<sql: 'SELECT * FROM foo, bar WHERE foo.bar_id = bar.id LIMIT 5'>
"""
if vars is None: vars = {}
qout = ""
def gen_clause(sql, val):
if isinstance(val, (int, long)):
if sql == 'WHERE':
nout = 'id = ' + sqlquote(val)
else:
nout = SQLQuery(val)
elif isinstance(val, (list, tuple)) and len(val) == 2:
nout = SQLQuery(val[0], val[1]) # backwards-compatibility
elif isinstance(val, SQLQuery):
nout = val
elif val:
nout = reparam(val, vars)
else:
return ""
out = ""
if qout: out += " "
out += sql + " " + nout
return out
if web.ctx.get('db_name') == "firebird":
for (sql, val) in (
('FIRST', limit),
('SKIP', offset)
):
qout += gen_clause(sql, val)
if qout:
SELECT = 'SELECT ' + qout
else:
SELECT = 'SELECT'
qout = ""
sql_clauses = (
(SELECT, what),
('FROM', sqllist(tables)),
('WHERE', where),
('GROUP BY', group),
('ORDER BY', order)
)
else:
sql_clauses = (
('SELECT', what),
('FROM', sqllist(tables)),
('WHERE', where),
('GROUP BY', group),
('ORDER BY', order),
('LIMIT', limit),
('OFFSET', offset)
)
for (sql, val) in sql_clauses:
qout += gen_clause(sql, val)
if _test: return qout
return query(qout, processed=True)
def insert(tablename, seqname=None, _test=False, **values):
"""
Inserts `values` into `tablename`. Returns current sequence ID.
Set `seqname` to the ID if it's not the default, or to `False`
if there isn't one.
>>> insert('foo', joe='bob', a=2, _test=True)
<sql: "INSERT INTO foo (a, joe) VALUES (2, 'bob')">
"""
if values:
sql_query = SQLQuery("INSERT INTO %s (%s) VALUES (%s)" % (
tablename,
", ".join(values.keys()),
', '.join([aparam() for x in values])
), values.values())
else:
sql_query = SQLQuery("INSERT INTO %s DEFAULT VALUES" % tablename)
if _test: return sql_query
db_cursor = web.ctx.db_cursor()
if seqname is False:
pass
elif web.ctx.db_name == "postgres":
if seqname is None:
seqname = tablename + "_id_seq"
sql_query += "; SELECT currval('%s')" % seqname
elif web.ctx.db_name == "mysql":
web.ctx.db_execute(db_cursor, sql_query)
sql_query = SQLQuery("SELECT last_insert_id()")
elif web.ctx.db_name == "sqlite":
web.ctx.db_execute(db_cursor, sql_query)
# not really the same...
sql_query = SQLQuery("SELECT last_insert_rowid()")
web.ctx.db_execute(db_cursor, sql_query)
try:
out = db_cursor.fetchone()[0]
except Exception:
out = None
if not web.ctx.db_transaction: web.ctx.db.commit()
return out
def update(tables, where, vars=None, _test=False, **values):
"""
Update `tables` with clause `where` (interpolated using `vars`)
and setting `values`.
>>> joe = 'Joseph'
>>> update('foo', where='name = $joe', name='bob', age=5,
... vars=locals(), _test=True)
<sql: "UPDATE foo SET age = 5, name = 'bob' WHERE name = 'Joseph'">
"""
if vars is None: vars = {}
if isinstance(where, (int, long)):
where = "id = " + sqlquote(where)
elif isinstance(where, (list, tuple)) and len(where) == 2:
where = SQLQuery(where[0], where[1])
elif isinstance(where, SQLQuery):
pass
else:
where = reparam(where, vars)
query = (
"UPDATE " + sqllist(tables) +
" SET " + sqlwhere(values, ', ') +
" WHERE " + where)
if _test: return query
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor, query)
if not web.ctx.db_transaction: web.ctx.db.commit()
return db_cursor.rowcount
def delete(table, where=None, using=None, vars=None, _test=False):
"""
Deletes from `table` with clauses `where` and `using`.
>>> name = 'Joe'
>>> delete('foo', where='name = $name', vars=locals(), _test=True)
<sql: "DELETE FROM foo WHERE name = 'Joe'">
"""
if vars is None: vars = {}
if isinstance(where, (int, long)):
where = "id = " + sqlquote(where)
elif isinstance(where, (list, tuple)) and len(where) == 2:
where = SQLQuery(where[0], where[1])
elif isinstance(where, SQLQuery):
pass
elif where is None:
pass
else:
where = reparam(where, vars)
q = 'DELETE FROM ' + table
if where:
q += ' WHERE ' + where
if using and web.ctx.get('db_name') != "firebird":
q += ' USING ' + sqllist(using)
if _test: return q
db_cursor = web.ctx.db_cursor()
web.ctx.db_execute(db_cursor, q)
if not web.ctx.db_transaction: web.ctx.db.commit()
return db_cursor.rowcount
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@ -0,0 +1,316 @@
"""
pretty debug errors
(part of web.py)
adapted from Django <djangoproject.com>
Copyright (c) 2005, the Lawrence Journal-World
Used under the modified BSD license:
http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
"""
__all__ = ["debugerror", "djangoerror"]
import sys, urlparse, pprint
from net import websafe
from template import Template
import webapi as web
import os, os.path
whereami = os.path.join(os.getcwd(), __file__)
whereami = os.path.sep.join(whereami.split(os.path.sep)[:-1])
djangoerror_t = """\
$def with (exception_type, exception_value, frames)
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<meta name="robots" content="NONE,NOARCHIVE" />
<title>$exception_type at $ctx.path</title>
<style type="text/css">
html * { padding:0; margin:0; }
body * { padding:10px 20px; }
body * * { padding:0; }
body { font:small sans-serif; }
body>div { border-bottom:1px solid #ddd; }
h1 { font-weight:normal; }
h2 { margin-bottom:.8em; }
h2 span { font-size:80%; color:#666; font-weight:normal; }
h3 { margin:1em 0 .5em 0; }
h4 { margin:0 0 .5em 0; font-weight: normal; }
table {
border:1px solid #ccc; border-collapse: collapse; background:white; }
tbody td, tbody th { vertical-align:top; padding:2px 3px; }
thead th {
padding:1px 6px 1px 3px; background:#fefefe; text-align:left;
font-weight:normal; font-size:11px; border:1px solid #ddd; }
tbody th { text-align:right; color:#666; padding-right:.5em; }
table.vars { margin:5px 0 2px 40px; }
table.vars td, table.req td { font-family:monospace; }
table td.code { width:100%;}
table td.code div { overflow:hidden; }
table.source th { color:#666; }
table.source td {
font-family:monospace; white-space:pre; border-bottom:1px solid #eee; }
ul.traceback { list-style-type:none; }
ul.traceback li.frame { margin-bottom:1em; }
div.context { margin: 10px 0; }
div.context ol {
padding-left:30px; margin:0 10px; list-style-position: inside; }
div.context ol li {
font-family:monospace; white-space:pre; color:#666; cursor:pointer; }
div.context ol.context-line li { color:black; background-color:#ccc; }
div.context ol.context-line li span { float: right; }
div.commands { margin-left: 40px; }
div.commands a { color:black; text-decoration:none; }
#summary { background: #ffc; }
#summary h2 { font-weight: normal; color: #666; }
#explanation { background:#eee; }
#template, #template-not-exist { background:#f6f6f6; }
#template-not-exist ul { margin: 0 0 0 20px; }
#traceback { background:#eee; }
#requestinfo { background:#f6f6f6; padding-left:120px; }
#summary table { border:none; background:transparent; }
#requestinfo h2, #requestinfo h3 { position:relative; margin-left:-100px; }
#requestinfo h3 { margin-bottom:-1em; }
.error { background: #ffc; }
.specific { color:#cc3300; font-weight:bold; }
</style>
<script type="text/javascript">
//<!--
function getElementsByClassName(oElm, strTagName, strClassName){
// Written by Jonathan Snook, http://www.snook.ca/jon;
// Add-ons by Robert Nyman, http://www.robertnyman.com
var arrElements = (strTagName == "*" && document.all)? document.all :
oElm.getElementsByTagName(strTagName);
var arrReturnElements = new Array();
strClassName = strClassName.replace(/\-/g, "\\-");
var oRegExp = new RegExp("(^|\\s)" + strClassName + "(\\s|$)");
var oElement;
for(var i=0; i<arrElements.length; i++){
oElement = arrElements[i];
if(oRegExp.test(oElement.className)){
arrReturnElements.push(oElement);
}
}
return (arrReturnElements)
}
function hideAll(elems) {
for (var e = 0; e < elems.length; e++) {
elems[e].style.display = 'none';
}
}
window.onload = function() {
hideAll(getElementsByClassName(document, 'table', 'vars'));
hideAll(getElementsByClassName(document, 'ol', 'pre-context'));
hideAll(getElementsByClassName(document, 'ol', 'post-context'));
}
function toggle() {
for (var i = 0; i < arguments.length; i++) {
var e = document.getElementById(arguments[i]);
if (e) {
e.style.display = e.style.display == 'none' ? 'block' : 'none';
}
}
return false;
}
function varToggle(link, id) {
toggle('v' + id);
var s = link.getElementsByTagName('span')[0];
var uarr = String.fromCharCode(0x25b6);
var darr = String.fromCharCode(0x25bc);
s.innerHTML = s.innerHTML == uarr ? darr : uarr;
return false;
}
//-->
</script>
</head>
<body>
<div id="summary">
<h1>$exception_type at $ctx.path</h1>
<h2>$exception_value</h2>
<table><tr>
<th>Python</th>
<td>$frames[0].filename in $frames[0].function, line $frames[0].lineno</td>
</tr><tr>
<th>Web</th>
<td>$ctx.method $ctx.home$ctx.path</td>
</tr></table>
</div>
<div id="traceback">
<h2>Traceback <span>(innermost first)</span></h2>
<ul class="traceback">
$for frame in frames:
<li class="frame">
<code>$frame.filename</code> in <code>$frame.function</code>
$if frame.context_line:
<div class="context" id="c$frame.id">
$if frame.pre_context:
<ol start="$frame.pre_context_lineno" class="pre-context" id="pre$frame.id">
$for line in frame.pre_context:
<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>
</ol>
<ol start="$frame.lineno" class="context-line"><li onclick="toggle('pre$frame.id', 'post$frame.id')">$frame.context_line <span>...</span></li></ol>
$if frame.post_context:
<ol start='${frame.lineno + 1}' class="post-context" id="post$frame.id">
$for line in frame.post_context:
<li onclick="toggle('pre$frame.id', 'post$frame.id')">$line</li>
</ol>
</div>
$if frame.vars:
<div class="commands">
<a href='#' onclick="return varToggle(this, '$frame.id')"><span>&#x25b6;</span> Local vars</a>
$# $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame))
</div>
$:dicttable(frame.vars, kls='vars', id=('v' + str(frame.id)))
</li>
</ul>
</div>
<div id="requestinfo">
$if ctx.output or ctx.headers:
<h2>Response so far</h2>
<h3>HEADERS</h3>
<p class="req"><code>
$for kv in ctx.headers:
$kv[0]: $kv[1]<br />
$else:
[no headers]
</code></p>
<h3>BODY</h3>
<p class="req" style="padding-bottom: 2em"><code>
$ctx.output
</code></p>
<h2>Request information</h2>
<h3>INPUT</h3>
$:dicttable(web.input())
<h3 id="cookie-info">COOKIES</h3>
$:dicttable(web.cookies())
<h3 id="meta-info">META</h3>
$ newctx = []
$# ) and (k not in ['env', 'output', 'headers', 'environ', 'status', 'db_execute']):
$for k, v in ctx.iteritems():
$if not k.startswith('_') and (k in x):
$newctx.append(kv)
$:dicttable(dict(newctx))
<h3 id="meta-info">ENVIRONMENT</h3>
$:dicttable(ctx.env)
</div>
<div id="explanation">
<p>
You're seeing this error because you have <code>web.internalerror</code>
set to <code>web.debugerror</code>. Change that if you want a different one.
</p>
</div>
</body>
</html>
"""
dicttable_t = r"""$def with (d, kls='req', id=None)
$if d:
<table class="$kls"\
$if id: id="$id"\
><thead><tr><th>Variable</th><th>Value</th></tr></thead>
<tbody>
$ temp = d.items()
$temp.sort()
$for kv in temp:
<tr><td>$kv[0]</td><td class="code"><div>$prettify(kv[1])</div></td></tr>
</tbody>
</table>
$else:
<p>No data.</p>
"""
dicttable_r = Template(dicttable_t, filter=websafe)
djangoerror_r = Template(djangoerror_t, filter=websafe)
def djangoerror():
def _get_lines_from_file(filename, lineno, context_lines):
"""
Returns context_lines before and after lineno from file.
Returns (pre_context_lineno, pre_context, context_line, post_context).
"""
try:
source = open(filename).readlines()
lower_bound = max(0, lineno - context_lines)
upper_bound = lineno + context_lines
pre_context = \
[line.strip('\n') for line in source[lower_bound:lineno]]
context_line = source[lineno].strip('\n')
post_context = \
[line.strip('\n') for line in source[lineno + 1:upper_bound]]
return lower_bound, pre_context, context_line, post_context
except (OSError, IOError):
return None, [], None, []
exception_type, exception_value, tback = sys.exc_info()
frames = []
while tback is not None:
filename = tback.tb_frame.f_code.co_filename
function = tback.tb_frame.f_code.co_name
lineno = tback.tb_lineno - 1
pre_context_lineno, pre_context, context_line, post_context = \
_get_lines_from_file(filename, lineno, 7)
frames.append(web.storage({
'tback': tback,
'filename': filename,
'function': function,
'lineno': lineno,
'vars': tback.tb_frame.f_locals,
'id': id(tback),
'pre_context': pre_context,
'context_line': context_line,
'post_context': post_context,
'pre_context_lineno': pre_context_lineno,
}))
tback = tback.tb_next
frames.reverse()
urljoin = urlparse.urljoin
def prettify(x):
try:
out = pprint.pformat(x)
except Exception, e:
out = '[could not display: <' + e.__class__.__name__ + \
': '+str(e)+'>]'
return out
dt = dicttable_r
dt.globals = {'prettify': prettify}
t = djangoerror_r
t.globals = {'ctx': web.ctx, 'web':web, 'dicttable':dt, 'dict':dict, 'str':str}
return t(exception_type, exception_value, frames)
def debugerror():
"""
A replacement for `internalerror` that presents a nice page with lots
of debug information for the programmer.
(Based on the beautiful 500 page from [Django](http://djangoproject.com/),
designed by [Wilson Miner](http://wilsonminer.com/).)
"""
web.ctx.headers = [('Content-Type', 'text/html')]
web.ctx.output = djangoerror()
if __name__ == "__main__":
urls = (
'/', 'index'
)
class index:
def GET(self):
thisdoesnotexist
web.internalerror = web.debugerror
web.run(urls)

View File

@ -0,0 +1,215 @@
"""
HTML forms
(part of web.py)
"""
import copy, re
import webapi as web
import utils, net
def attrget(obj, attr, value=None):
if hasattr(obj, 'has_key') and obj.has_key(attr): return obj[attr]
if hasattr(obj, attr): return getattr(obj, attr)
return value
class Form:
def __init__(self, *inputs, **kw):
self.inputs = inputs
self.valid = True
self.note = None
self.validators = kw.pop('validators', [])
def __call__(self, x=None):
o = copy.deepcopy(self)
if x: o.validates(x)
return o
def render(self):
out = ''
out += self.rendernote(self.note)
out += '<table>\n'
for i in self.inputs:
out += ' <tr><th><label for="%s">%s</label></th>' % (i.id, i.description)
out += "<td>"+i.pre+i.render()+i.post+"</td>"
out += '<td id="note_%s">%s</td></tr>\n' % (i.id, self.rendernote(i.note))
out += "</table>"
return out
def rendernote(self, note):
if note: return '<strong class="wrong">%s</strong>' % note
else: return ""
def validates(self, source=None, _validate=True, **kw):
source = source or kw or web.input()
out = True
for i in self.inputs:
v = attrget(source, i.name)
if _validate:
out = i.validate(v) and out
else:
i.value = v
if _validate:
out = out and self._validate(source)
self.valid = out
return out
def _validate(self, value):
self.value = value
for v in self.validators:
if not v.valid(value):
self.note = v.msg
return False
return True
def fill(self, source=None, **kw):
return self.validates(source, _validate=False, **kw)
def __getitem__(self, i):
for x in self.inputs:
if x.name == i: return x
raise KeyError, i
def _get_d(self): #@@ should really be form.attr, no?
return utils.storage([(i.name, i.value) for i in self.inputs])
d = property(_get_d)
class Input(object):
def __init__(self, name, *validators, **attrs):
self.description = attrs.pop('description', name)
self.value = attrs.pop('value', None)
self.pre = attrs.pop('pre', "")
self.post = attrs.pop('post', "")
self.id = attrs.setdefault('id', name)
if 'class_' in attrs:
attrs['class'] = attrs['class_']
del attrs['class_']
self.name, self.validators, self.attrs, self.note = name, validators, attrs, None
def validate(self, value):
self.value = value
for v in self.validators:
if not v.valid(value):
self.note = v.msg
return False
return True
def render(self): raise NotImplementedError
def addatts(self):
str = ""
for (n, v) in self.attrs.items():
str += ' %s="%s"' % (n, net.websafe(v))
return str
#@@ quoting
class Textbox(Input):
def render(self):
x = '<input type="text" name="%s"' % net.websafe(self.name)
if self.value: x += ' value="%s"' % net.websafe(self.value)
x += self.addatts()
x += ' />'
return x
class Password(Input):
def render(self):
x = '<input type="password" name="%s"' % net.websafe(self.name)
if self.value: x += ' value="%s"' % net.websafe(self.value)
x += self.addatts()
x += ' />'
return x
class Textarea(Input):
def render(self):
x = '<textarea name="%s"' % net.websafe(self.name)
x += self.addatts()
x += '>'
if self.value is not None: x += net.websafe(self.value)
x += '</textarea>'
return x
class Dropdown(Input):
def __init__(self, name, args, *validators, **attrs):
self.args = args
super(Dropdown, self).__init__(name, *validators, **attrs)
def render(self):
x = '<select name="%s"%s>\n' % (net.websafe(self.name), self.addatts())
for arg in self.args:
if type(arg) == tuple:
value, desc= arg
else:
value, desc = arg, arg
if self.value == value: select_p = ' selected="selected"'
else: select_p = ''
x += ' <option %s value="%s">%s</option>\n' % (select_p, net.websafe(value), net.websafe(desc))
x += '</select>\n'
return x
class Radio(Input):
def __init__(self, name, args, *validators, **attrs):
self.args = args
super(Radio, self).__init__(name, *validators, **attrs)
def render(self):
x = '<span>'
for arg in self.args:
if self.value == arg: select_p = ' checked="checked"'
else: select_p = ''
x += '<input type="radio" name="%s" value="%s"%s%s /> %s ' % (net.websafe(self.name), net.websafe(arg), select_p, self.addatts(), net.websafe(arg))
return x+'</span>'
class Checkbox(Input):
def render(self):
x = '<input name="%s" type="checkbox"' % net.websafe(self.name)
if self.value: x += ' checked="checked"'
x += self.addatts()
x += ' />'
return x
class Button(Input):
def __init__(self, name, *validators, **attrs):
super(Button, self).__init__(name, *validators, **attrs)
self.description = ""
def render(self):
safename = net.websafe(self.name)
x = '<button name="%s"%s>%s</button>' % (safename, self.addatts(), safename)
return x
class Hidden(Input):
def __init__(self, name, *validators, **attrs):
super(Hidden, self).__init__(name, *validators, **attrs)
# it doesnt make sence for a hidden field to have description
self.description = ""
def render(self):
x = '<input type="hidden" name="%s"' % net.websafe(self.name)
if self.value: x += ' value="%s"' % net.websafe(self.value)
x += ' />'
return x
class File(Input):
def render(self):
x = '<input type="file" name="%s"' % net.websafe(self.name)
x += self.addatts()
x += ' />'
return x
class Validator:
def __deepcopy__(self, memo): return copy.copy(self)
def __init__(self, msg, test, jstest=None): utils.autoassign(self, locals())
def valid(self, value):
try: return self.test(value)
except: return False
notnull = Validator("Required", bool)
class regexp(Validator):
def __init__(self, rexp, msg):
self.rexp = re.compile(rexp)
self.msg = msg
def valid(self, value):
return bool(self.rexp.match(value))

View File

@ -0,0 +1,270 @@
"""
HTTP Utilities
(from web.py)
"""
__all__ = [
"expires", "lastmodified",
"prefixurl", "modified",
"redirect", "found", "seeother", "tempredirect",
"write",
"changequery", "url",
"background", "backgrounder",
"Reloader", "reloader", "profiler",
]
import sys, os, threading, urllib, urlparse
try: import datetime
except ImportError: pass
import net, utils, webapi as web
def prefixurl(base=''):
"""
Sorry, this function is really difficult to explain.
Maybe some other time.
"""
url = web.ctx.path.lstrip('/')
for i in xrange(url.count('/')):
base += '../'
if not base:
base = './'
return base
def expires(delta):
"""
Outputs an `Expires` header for `delta` from now.
`delta` is a `timedelta` object or a number of seconds.
"""
if isinstance(delta, (int, long)):
delta = datetime.timedelta(seconds=delta)
date_obj = datetime.datetime.utcnow() + delta
web.header('Expires', net.httpdate(date_obj))
def lastmodified(date_obj):
"""Outputs a `Last-Modified` header for `datetime`."""
web.header('Last-Modified', net.httpdate(date_obj))
def modified(date=None, etag=None):
n = web.ctx.env.get('HTTP_IF_NONE_MATCH')
m = net.parsehttpdate(web.ctx.env.get('HTTP_IF_MODIFIED_SINCE', '').split(';')[0])
validate = False
if etag:
raise NotImplementedError, "no etag support yet"
# should really be a warning
if date and m:
# we subtract a second because
# HTTP dates don't have sub-second precision
if date-datetime.timedelta(seconds=1) <= m:
validate = True
if validate: web.ctx.status = '304 Not Modified'
return not validate
"""
By default, these all return simple error messages that send very short messages
(like "bad request") to the user. They can and should be overridden
to return nicer ones.
"""
def redirect(url, status='301 Moved Permanently'):
"""
Returns a `status` redirect to the new URL.
`url` is joined with the base URL so that things like
`redirect("about") will work properly.
"""
newloc = urlparse.urljoin(web.ctx.path, url)
# if newloc is relative then make it absolute
#mvoncken:Disabled because we don't want to redirect to localhost!
#if newloc.startswith('/'):
# newloc = web.ctx.home + newloc
web.ctx.status = status
web.ctx.output = ''
web.header('Content-Type', 'text/html')
web.header('Location', newloc)
# seems to add a three-second delay for some reason:
# web.output('<a href="'+ newloc + '">moved permanently</a>')
def found(url):
"""A `302 Found` redirect."""
return redirect(url, '302 Found')
def seeother(url):
"""A `303 See Other` redirect."""
return redirect(url, '303 See Other')
def tempredirect(url):
"""A `307 Temporary Redirect` redirect."""
return redirect(url, '307 Temporary Redirect')
def write(cgi_response):
"""
Converts a standard CGI-style string response into `header` and
`output` calls.
"""
cgi_response = str(cgi_response)
cgi_response.replace('\r\n', '\n')
head, body = cgi_response.split('\n\n', 1)
lines = head.split('\n')
for line in lines:
if line.isspace():
continue
hdr, value = line.split(":", 1)
value = value.strip()
if hdr.lower() == "status":
web.ctx.status = value
else:
web.header(hdr, value)
web.output(body)
def urlencode(query):
"""
Same as urllib.urlencode, but supports unicode strings.
>>> urlencode({'text':'foo bar'})
'text=foo+bar'
"""
query = dict([(k, utils.utf8(v)) for k, v in query.items()])
return urllib.urlencode(query)
def changequery(query=None, **kw):
"""
Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return
`/foo?a=3&b=2` -- the same URL but with the arguments you requested
changed.
"""
if query is None:
query = web.input(_method='get')
for k, v in kw.iteritems():
if v is None:
query.pop(k, None)
else:
query[k] = v
out = web.ctx.path
if query:
out += '?' + urlencode(query)
return out
def url(path=None, **kw):
"""
Makes url by concatinating web.ctx.homepath and path and the
query string created using the arguments.
"""
if path is None:
path = web.ctx.path
if path.startswith("/"):
out = web.ctx.homepath + path
else:
out = path
if kw:
out += '?' + urlencode(kw)
return out
def background(func):
"""A function decorator to run a long-running function as a background thread."""
def internal(*a, **kw):
web.data() # cache it
tmpctx = web._context[threading.currentThread()]
web._context[threading.currentThread()] = utils.storage(web.ctx.copy())
def newfunc():
web._context[threading.currentThread()] = tmpctx
func(*a, **kw)
myctx = web._context[threading.currentThread()]
for k in myctx.keys():
if k not in ['status', 'headers', 'output']:
try: del myctx[k]
except KeyError: pass
t = threading.Thread(target=newfunc)
background.threaddb[id(t)] = t
t.start()
web.ctx.headers = []
return seeother(changequery(_t=id(t)))
return internal
background.threaddb = {}
def backgrounder(func):
def internal(*a, **kw):
i = web.input(_method='get')
if '_t' in i:
try:
t = background.threaddb[int(i._t)]
except KeyError:
return web.notfound()
web._context[threading.currentThread()] = web._context[t]
return
else:
return func(*a, **kw)
return internal
class Reloader:
"""
Before every request, checks to see if any loaded modules have changed on
disk and, if so, reloads them.
"""
def __init__(self, func):
self.func = func
self.mtimes = {}
# cheetah:
# b = _compiletemplate.bases
# _compiletemplate = globals()['__compiletemplate']
# _compiletemplate.bases = b
web.loadhooks['reloader'] = self.check
# todo:
# - replace relrcheck with a loadhook
#if reloader in middleware:
# relr = reloader(None)
# relrcheck = relr.check
# middleware.remove(reloader)
#else:
# relr = None
# relrcheck = lambda: None
# if relr:
# relr.func = wsgifunc
# return wsgifunc
#
def check(self):
for mod in sys.modules.values():
try:
mtime = os.stat(mod.__file__).st_mtime
except (AttributeError, OSError, IOError):
continue
if mod.__file__.endswith('.pyc') and \
os.path.exists(mod.__file__[:-1]):
mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime)
if mod not in self.mtimes:
self.mtimes[mod] = mtime
elif self.mtimes[mod] < mtime:
try:
reload(mod)
self.mtimes[mod] = mtime
except ImportError:
pass
return True
def __call__(self, e, o):
self.check()
return self.func(e, o)
reloader = Reloader
def profiler(app):
"""Outputs basic profiling information at the bottom of each response."""
from utils import profile
def profile_internal(e, o):
out, result = profile(app)(e, o)
return out + ['<pre>' + net.websafe(result) + '</pre>']
return profile_internal
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@ -0,0 +1,227 @@
__all__ = ["runsimple"]
import sys, os
import webapi as web
import net
def runbasic(func, server_address=("0.0.0.0", 8080)):
"""
Runs a simple HTTP server hosting WSGI app `func`. The directory `static/`
is hosted statically.
Based on [WsgiServer][ws] from [Colin Stewart][cs].
[ws]: http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html
[cs]: http://www.owlfish.com/
"""
# Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/)
# Modified somewhat for simplicity
# Used under the modified BSD license:
# http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5
import SimpleHTTPServer, SocketServer, BaseHTTPServer, urlparse
import socket, errno
import traceback
class WSGIHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
def run_wsgi_app(self):
protocol, host, path, parameters, query, fragment = \
urlparse.urlparse('http://dummyhost%s' % self.path)
# we only use path, query
env = {'wsgi.version': (1, 0)
,'wsgi.url_scheme': 'http'
,'wsgi.input': self.rfile
,'wsgi.errors': sys.stderr
,'wsgi.multithread': 1
,'wsgi.multiprocess': 0
,'wsgi.run_once': 0
,'REQUEST_METHOD': self.command
,'REQUEST_URI': self.path
,'PATH_INFO': path
,'QUERY_STRING': query
,'CONTENT_TYPE': self.headers.get('Content-Type', '')
,'CONTENT_LENGTH': self.headers.get('Content-Length', '')
,'REMOTE_ADDR': self.client_address[0]
,'SERVER_NAME': self.server.server_address[0]
,'SERVER_PORT': str(self.server.server_address[1])
,'SERVER_PROTOCOL': self.request_version
}
for http_header, http_value in self.headers.items():
env ['HTTP_%s' % http_header.replace('-', '_').upper()] = \
http_value
# Setup the state
self.wsgi_sent_headers = 0
self.wsgi_headers = []
try:
# We have there environment, now invoke the application
result = self.server.app(env, self.wsgi_start_response)
try:
try:
for data in result:
if data:
self.wsgi_write_data(data)
finally:
if hasattr(result, 'close'):
result.close()
except socket.error, socket_err:
# Catch common network errors and suppress them
if (socket_err.args[0] in \
(errno.ECONNABORTED, errno.EPIPE)):
return
except socket.timeout, socket_timeout:
return
except:
print >> web.debug, traceback.format_exc(),
if (not self.wsgi_sent_headers):
# We must write out something!
self.wsgi_write_data(" ")
return
do_POST = run_wsgi_app
do_PUT = run_wsgi_app
do_DELETE = run_wsgi_app
def do_GET(self):
if self.path.startswith('/static/'):
SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
else:
self.run_wsgi_app()
def wsgi_start_response(self, response_status, response_headers,
exc_info=None):
if (self.wsgi_sent_headers):
raise Exception \
("Headers already sent and start_response called again!")
# Should really take a copy to avoid changes in the application....
self.wsgi_headers = (response_status, response_headers)
return self.wsgi_write_data
def wsgi_write_data(self, data):
if (not self.wsgi_sent_headers):
status, headers = self.wsgi_headers
# Need to send header prior to data
status_code = status[:status.find(' ')]
status_msg = status[status.find(' ') + 1:]
self.send_response(int(status_code), status_msg)
for header, value in headers:
self.send_header(header, value)
self.end_headers()
self.wsgi_sent_headers = 1
# Send the data
self.wfile.write(data)
class WSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer):
def __init__(self, func, server_address):
BaseHTTPServer.HTTPServer.__init__(self,
server_address,
WSGIHandler)
self.app = func
self.serverShuttingDown = 0
print "http://%s:%d/" % server_address
WSGIServer(func, server_address).serve_forever()
def runsimple(func, server_address=("0.0.0.0", 8080)):
"""
Runs [CherryPy][cp] WSGI server hosting WSGI app `func`.
The directory `static/` is hosted statically.
[cp]: http://www.cherrypy.org
"""
from wsgiserver import CherryPyWSGIServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
from BaseHTTPServer import BaseHTTPRequestHandler
class StaticApp(SimpleHTTPRequestHandler):
"""WSGI application for serving static files."""
def __init__(self, environ, start_response):
self.headers = []
self.environ = environ
self.start_response = start_response
def send_response(self, status, msg=""):
self.status = str(status) + " " + msg
def send_header(self, name, value):
self.headers.append((name, value))
def end_headers(self):
pass
def log_message(*a): pass
def __iter__(self):
environ = self.environ
self.path = environ.get('PATH_INFO', '')
self.client_address = environ.get('REMOTE_ADDR','-'), \
environ.get('REMOTE_PORT','-')
self.command = environ.get('REQUEST_METHOD', '-')
from cStringIO import StringIO
self.wfile = StringIO() # for capturing error
f = self.send_head()
self.start_response(self.status, self.headers)
if f:
block_size = 16 * 1024
while True:
buf = f.read(block_size)
if not buf:
break
yield buf
f.close()
else:
value = self.wfile.getvalue()
yield value
class WSGIWrapper(BaseHTTPRequestHandler):
"""WSGI wrapper for logging the status and serving static files."""
def __init__(self, app):
self.app = app
self.format = '%s - - [%s] "%s %s %s" - %s'
def __call__(self, environ, start_response):
def xstart_response(status, response_headers, *args):
write = start_response(status, response_headers, *args)
self.log(status, environ)
return write
path = environ.get('PATH_INFO', '')
if path.startswith('/static/'):
return StaticApp(environ, xstart_response)
else:
return self.app(environ, xstart_response)
def log(self, status, environ):
#mvoncken,no logging..
return
outfile = environ.get('wsgi.errors', web.debug)
req = environ.get('PATH_INFO', '_')
protocol = environ.get('ACTUAL_SERVER_PROTOCOL', '-')
method = environ.get('REQUEST_METHOD', '-')
host = "%s:%s" % (environ.get('REMOTE_ADDR','-'),
environ.get('REMOTE_PORT','-'))
#@@ It is really bad to extend from
#@@ BaseHTTPRequestHandler just for this method
time = self.log_date_time_string()
print >> outfile, self.format % (host, time, protocol,
method, req, status)
func = WSGIWrapper(func)
server = CherryPyWSGIServer(server_address, func, server_name="localhost")
print "http://%s:%d/" % server_address
try:
server.start()
except KeyboardInterrupt:
server.stop()

View File

@ -0,0 +1,155 @@
"""
Network Utilities
(from web.py)
"""
__all__ = [
"validipaddr", "validipport", "validip", "validaddr",
"urlquote",
"httpdate", "parsehttpdate",
"htmlquote", "websafe",
]
import urllib, time
try: import datetime
except ImportError: pass
def validipaddr(address):
"""returns True if `address` is a valid IPv4 address"""
try:
octets = address.split('.')
assert len(octets) == 4
for x in octets:
assert 0 <= int(x) <= 255
except (AssertionError, ValueError):
return False
return True
def validipport(port):
"""returns True if `port` is a valid IPv4 port"""
try:
assert 0 <= int(port) <= 65535
except (AssertionError, ValueError):
return False
return True
def validip(ip, defaultaddr="0.0.0.0", defaultport=8080):
"""returns `(ip_address, port)` from string `ip_addr_port`"""
addr = defaultaddr
port = defaultport
ip = ip.split(":", 1)
if len(ip) == 1:
if not ip[0]:
pass
elif validipaddr(ip[0]):
addr = ip[0]
elif validipport(ip[0]):
port = int(ip[0])
else:
raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
elif len(ip) == 2:
addr, port = ip
if not validipaddr(addr) and validipport(port):
raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
port = int(port)
else:
raise ValueError, ':'.join(ip) + ' is not a valid IP address/port'
return (addr, port)
def validaddr(string_):
"""
returns either (ip_address, port) or "/path/to/socket" from string_
>>> validaddr('/path/to/socket')
'/path/to/socket'
>>> validaddr('8000')
('0.0.0.0', 8000)
>>> validaddr('127.0.0.1')
('127.0.0.1', 8080)
>>> validaddr('127.0.0.1:8000')
('127.0.0.1', 8000)
>>> validaddr('fff')
Traceback (most recent call last):
...
ValueError: fff is not a valid IP address/port
"""
if '/' in string_:
return string_
else:
return validip(string_)
def urlquote(val):
"""
Quotes a string for use in a URL.
>>> urlquote('://?f=1&j=1')
'%3A//%3Ff%3D1%26j%3D1'
>>> urlquote(None)
''
>>> urlquote(u'\u203d')
'%E2%80%BD'
"""
if val is None: return ''
if not isinstance(val, unicode): val = str(val)
else: val = val.encode('utf-8')
return urllib.quote(val)
def httpdate(date_obj):
"""
Formats a datetime object for use in HTTP headers.
>>> import datetime
>>> httpdate(datetime.datetime(1970, 1, 1, 1, 1, 1))
'Thu, 01 Jan 1970 01:01:01 GMT'
"""
return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT")
def parsehttpdate(string_):
"""
Parses an HTTP date into a datetime object.
>>> parsehttpdate('Thu, 01 Jan 1970 01:01:01 GMT')
datetime.datetime(1970, 1, 1, 1, 1, 1)
"""
try:
t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z")
except ValueError:
return None
return datetime.datetime(*t[:6])
def htmlquote(text):
"""
Encodes `text` for raw use in HTML.
>>> htmlquote("<'&\\">")
'&lt;&#39;&amp;&quot;&gt;'
"""
text = text.replace("&", "&amp;") # Must be done first!
text = text.replace("<", "&lt;")
text = text.replace(">", "&gt;")
text = text.replace("'", "&#39;")
text = text.replace('"', "&quot;")
return text
def websafe(val):
"""
Converts `val` so that it's safe for use in UTF-8 HTML.
>>> websafe("<'&\\">")
'&lt;&#39;&amp;&quot;&gt;'
>>> websafe(None)
''
>>> websafe(u'\u203d')
'\\xe2\\x80\\xbd'
"""
if val is None:
return ''
if isinstance(val, unicode):
val = val.encode('utf-8')
val = str(val)
return htmlquote(val)
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@ -0,0 +1,153 @@
"""
Request Delegation
(from web.py)
"""
__all__ = ["handle", "nomethod", "autodelegate", "webpyfunc", "run"]
import sys, re, types, os.path, urllib
import http, wsgi, utils, webapi
import webapi as web
def handle(mapping, fvars=None):
"""
Call the appropriate function based on the url to function mapping in `mapping`.
If no module for the function is specified, look up the function in `fvars`. If
`fvars` is empty, using the caller's context.
`mapping` should be a tuple of paired regular expressions with function name
substitutions. `handle` will import modules as necessary.
"""
for url, ofno in utils.group(mapping, 2):
if isinstance(ofno, tuple):
ofn, fna = ofno[0], list(ofno[1:])
else:
ofn, fna = ofno, []
fn, result = utils.re_subm('^' + url + '$', ofn, web.ctx.path)
if result: # it's a match
if fn.split(' ', 1)[0] == "redirect":
url = fn.split(' ', 1)[1]
if web.ctx.method == "GET":
x = web.ctx.env.get('QUERY_STRING', '')
if x:
url += '?' + x
return http.redirect(url)
elif '.' in fn:
x = fn.split('.')
mod, cls = '.'.join(x[:-1]), x[-1]
mod = __import__(mod, globals(), locals(), [""])
cls = getattr(mod, cls)
else:
cls = fn
mod = fvars
if isinstance(mod, types.ModuleType):
mod = vars(mod)
try:
cls = mod[cls]
except KeyError:
return web.notfound()
meth = web.ctx.method
if meth == "HEAD":
if not hasattr(cls, meth):
meth = "GET"
if not hasattr(cls, meth):
return nomethod(cls)
tocall = getattr(cls(), meth)
args = list(result.groups())
for d in re.findall(r'\\(\d+)', ofn):
args.pop(int(d) - 1)
return tocall(*([x and urllib.unquote(x) for x in args] + fna))
return web.notfound()
def nomethod(cls):
"""Returns a `405 Method Not Allowed` error for `cls`."""
web.ctx.status = '405 Method Not Allowed'
web.header('Content-Type', 'text/html')
web.header('Allow', \
', '.join([method for method in \
['GET', 'HEAD', 'POST', 'PUT', 'DELETE'] \
if hasattr(cls, method)]))
# commented out for the same reason redirect is
# return output('method not allowed')
def autodelegate(prefix=''):
"""
Returns a method that takes one argument and calls the method named prefix+arg,
calling `notfound()` if there isn't one. Example:
urls = ('/prefs/(.*)', 'prefs')
class prefs:
GET = autodelegate('GET_')
def GET_password(self): pass
def GET_privacy(self): pass
`GET_password` would get called for `/prefs/password` while `GET_privacy` for
`GET_privacy` gets called for `/prefs/privacy`.
If a user visits `/prefs/password/change` then `GET_password(self, '/change')`
is called.
"""
def internal(self, arg):
if '/' in arg:
first, rest = arg.split('/', 1)
func = prefix + first
args = ['/' + rest]
else:
func = prefix + arg
args = []
if hasattr(self, func):
try:
return getattr(self, func)(*args)
except TypeError:
return web.notfound()
else:
return web.notfound()
return internal
def webpyfunc(inp, fvars, autoreload=False):
"""If `inp` is a url mapping, returns a function that calls handle."""
if not hasattr(inp, '__call__'):
if autoreload:
def modname():
"""find name of the module name from fvars."""
file, name = fvars['__file__'], fvars['__name__']
if name == '__main__':
# Since the __main__ module can't be reloaded, the module has
# to be imported using its file name.
name = os.path.splitext(os.path.basename(file))[0]
return name
mod = __import__(modname(), None, None, [""])
#@@probably should replace this with some inspect magic
name = utils.dictfind(fvars, inp)
func = lambda: handle(getattr(mod, name), mod)
else:
func = lambda: handle(inp, fvars)
else:
func = inp
return func
def run(inp, fvars, *middleware):
"""
Starts handling requests. If called in a CGI or FastCGI context, it will follow
that protocol. If called from the command line, it will start an HTTP
server on the port named in the first command line argument, or, if there
is no argument, on port 8080.
`input` is a callable, then it's called with no arguments.
Otherwise, it's a `mapping` object to be passed to `handle(...)`.
**Caveat:** So that `reloader` will work correctly, input has to be a variable,
it can't be a tuple passed in directly.
`middleware` is a list of WSGI middleware which is applied to the resulting WSGI
function.
"""
autoreload = http.reloader in middleware
return wsgi.runwsgi(webapi.wsgifunc(webpyfunc(inp, fvars, autoreload), *middleware))

View File

@ -0,0 +1,878 @@
"""
simple, elegant templating
(part of web.py)
"""
import re, glob, os, os.path
from types import FunctionType as function
from utils import storage, group, utf8
from net import websafe
# differences from python:
# - for: has an optional else: that gets called if the loop never runs
# differences to add:
# - you can use the expression inside if, while blocks
# - special for loop attributes, like django?
# - you can check to see if a variable is defined (perhaps w/ get func?)
# all these are probably good ideas for python...
# todo:
# inline tuple
# relax constraints on spacing
# continue, break, etc.
# tracebacks
global_globals = {'None':None, 'False':False, 'True': True}
MAX_ITERS = 100000
WHAT = 0
ARGS = 4
KWARGS = 6
NAME = 2
BODY = 4
CLAUSE = 2
ELIF = 6
ELSE = 8
IN = 6
NAME = 2
EXPR = 4
FILTER = 4
THING = 2
ATTR = 4
ITEM = 4
NEGATE = 4
X = 2
OP = 4
Y = 6
LINENO = -1
# http://docs.python.org/ref/identifiers.html
r_var = '[a-zA-Z_][a-zA-Z0-9_]*'
class ParseError(Exception): pass
class Parser:
def __init__(self, text, name=""):
self.t = text
self.p = 0
self._lock = [False]
self.name = name
def lock(self):
self._lock[-1] = True
def curline(self):
return self.t[:self.p].count('\n')+1
def csome(self):
return repr(self.t[self.p:self.p+5]+'...')
def Error(self, x, y=None):
if y is None: y = self.csome()
raise ParseError, "%s: expected %s, got %s (line %s)" % (self.name, x, y, self.curline())
def q(self, f):
def internal(*a, **kw):
checkp = self.p
self._lock.append(False)
try:
q = f(*a, **kw)
except ParseError:
if self._lock[-1]:
raise
self.p = checkp
self._lock.pop()
return False
self._lock.pop()
return q or True
return internal
def tokr(self, t):
text = self.c(len(t))
if text != t:
self.Error(repr(t), repr(text))
return t
def ltokr(self, *l):
for x in l:
o = self.tokq(x)
if o: return o
self.Error('one of '+repr(l))
def rer(self, r):
x = re.match(r, self.t[self.p:]) #@@re_compile
if not x:
self.Error('r'+repr(r))
return self.tokr(x.group())
def endr(self):
if self.p != len(self.t):
self.Error('EOF')
def c(self, n=1):
out = self.t[self.p:self.p+n]
if out == '' and n != 0:
self.Error('character', 'EOF')
self.p += n
return out
def lookbehind(self, t):
return self.t[self.p-len(t):self.p] == t
def __getattr__(self, a):
if a.endswith('q'):
return self.q(getattr(self, a[:-1]+'r'))
raise AttributeError, a
class TemplateParser(Parser):
def __init__(self, *a, **kw):
Parser.__init__(self, *a, **kw)
self.curws = ''
self.curind = ''
def o(self, *a):
return a+('lineno', self.curline())
def go(self):
# maybe try to do some traceback parsing/hacking
return self.gor()
def gor(self):
header = self.defwithq()
results = self.lines(start=True)
self.endr()
return header, results
def ws(self):
n = 0
while self.tokq(" "): n += 1
return " " * n
def defwithr(self):
self.tokr('$def with ')
self.lock()
self.tokr('(')
args = []
kw = []
x = self.req(r_var)
while x:
if self.tokq('='):
v = self.exprr()
kw.append((x, v))
else:
args.append(x)
x = self.tokq(', ') and self.req(r_var)
self.tokr(')\n')
return self.o('defwith', 'null', None, 'args', args, 'kwargs', kw)
def literalr(self):
o = (
self.req('"[^"]*"') or #@@ no support for escapes
self.req("'[^']*'")
)
if o is False:
o = self.req('\-?[0-9]+(\.[0-9]*)?')
if o is not False:
if '.' in o: o = float(o)
else: o = int(o)
if o is False: self.Error('literal')
return self.o('literal', 'thing', o)
def listr(self):
self.tokr('[')
self.lock()
x = []
if not self.tokq(']'):
while True:
t = self.exprr()
x.append(t)
if not self.tokq(', '): break
self.tokr(']')
return self.o('list', 'thing', x)
def dictr(self):
self.tokr('{')
self.lock()
x = {}
if not self.tokq('}'):
while True:
k = self.exprr()
self.tokr(': ')
v = self.exprr()
x[k] = v
if not self.tokq(', '): break
self.tokr('}')
return self.o('dict', 'thing', x)
def parenr(self):
self.tokr('(')
self.lock()
o = self.exprr() # todo: allow list
self.tokr(')')
return self.o('paren', 'thing', o)
def atomr(self):
"""returns var, literal, paren, dict, or list"""
o = (
self.varq() or
self.parenq() or
self.dictq() or
self.listq() or
self.literalq()
)
if o is False: self.Error('atom')
return o
def primaryr(self):
"""returns getattr, call, or getitem"""
n = self.atomr()
while 1:
if self.tokq('.'):
v = self.req(r_var)
if not v:
self.p -= 1 # get rid of the '.'
break
else:
n = self.o('getattr', 'thing', n, 'attr', v)
elif self.tokq('('):
args = []
kw = []
while 1:
# need to see if we're doing a keyword argument
checkp = self.p
k = self.req(r_var)
if k and self.tokq('='): # yup
v = self.exprr()
kw.append((k, v))
else:
self.p = checkp
x = self.exprq()
if x: # at least it's something
args.append(x)
else:
break
if not self.tokq(', '): break
self.tokr(')')
n = self.o('call', 'thing', n, 'args', args, 'kwargs', kw)
elif self.tokq('['):
v = self.exprr()
self.tokr(']')
n = self.o('getitem', 'thing', n, 'item', v)
else:
break
return n
def exprr(self):
negate = self.tokq('not ')
x = self.primaryr()
if self.tokq(' '):
operator = self.ltokr('not in', 'in', 'is not', 'is', '==', '!=', '>=', '<=', '<', '>', 'and', 'or', '*', '+', '-', '/', '%')
self.tokr(' ')
y = self.exprr()
x = self.o('test', 'x', x, 'op', operator, 'y', y)
return self.o('expr', 'thing', x, 'negate', negate)
def varr(self):
return self.o('var', 'name', self.rer(r_var))
def liner(self):
out = []
o = self.curws
while 1:
c = self.c()
self.lock()
if c == '\n':
self.p -= 1
break
if c == '$':
if self.lookbehind('\\$'):
o = o[:-1] + c
else:
filter = not bool(self.tokq(':'))
if self.tokq('{'):
out.append(o)
out.append(self.o('itpl', 'name', self.exprr(), 'filter', filter))
self.tokr('}')
o = ''
else:
g = self.primaryq()
if g:
out.append(o)
out.append(self.o('itpl', 'name', g, 'filter', filter))
o = ''
else:
o += c
else:
o += c
self.tokr('\n')
if not self.lookbehind('\\\n'):
o += '\n'
else:
o = o[:-1]
out.append(o)
return self.o('line', 'thing', out)
def varsetr(self):
self.tokr('$var ')
self.lock()
what = self.rer(r_var)
self.tokr(':')
body = self.lines()
return self.o('varset', 'name', what, 'body', body)
def ifr(self):
self.tokr("$if ")
self.lock()
expr = self.exprr()
self.tokr(":")
ifc = self.lines()
elifs = []
while self.tokq(self.curws + self.curind + '$elif '):
v = self.exprr()
self.tokr(':')
c = self.lines()
elifs.append(self.o('elif', 'clause', v, 'body', c))
if self.tokq(self.curws + self.curind + "$else:"):
elsec = self.lines()
else:
elsec = None
return self.o('if', 'clause', expr, 'then', ifc, 'elif', elifs, 'else', elsec)
def forr(self):
self.tokr("$for ")
self.lock()
v = self.setabler()
self.tokr(" in ")
g = self.exprr()
self.tokr(":")
l = self.lines()
if self.tokq(self.curws + self.curind + '$else:'):
elsec = self.lines()
else:
elsec = None
return self.o('for', 'name', v, 'body', l, 'in', g, 'else', elsec)
def whiler(self):
self.tokr('$while ')
self.lock()
v = self.exprr()
self.tokr(":")
l = self.lines()
if self.tokq(self.curws + self.curind + '$else:'):
elsec = self.lines()
else:
elsec = None
return self.o('while', 'clause', v, 'body', l, 'null', None, 'else', elsec)
def assignr(self):
self.tokr('$ ')
assign = self.rer(r_var) # NOTE: setable
self.tokr(' = ')
expr = self.exprr()
self.tokr('\n')
return self.o('assign', 'name', assign, 'expr', expr)
def commentr(self):
self.tokr('$#')
self.lock()
while self.c() != '\n': pass
return self.o('comment')
def setabler(self):
out = [self.varr()] #@@ not quite right
while self.tokq(', '):
out.append(self.varr())
return out
def lines(self, start=False):
"""
This function gets called from two places:
1. at the start, where it's matching the document itself
2. after any command, where it matches one line or an indented block
"""
o = []
if not start: # try to match just one line
singleline = self.tokq(' ') and self.lineq()
if singleline:
return [singleline]
else:
self.rer(' *') #@@slurp space?
self.tokr('\n')
oldind = self.curind
self.curind += ' '
while 1:
oldws = self.curws
t = self.tokq(oldws + self.curind)
if not t: break
self.curws += self.ws()
x = t and (
self.varsetq() or
self.ifq() or
self.forq() or
self.whileq() or
self.assignq() or
self.commentq() or
self.lineq())
self.curws = oldws
if not x:
break
elif x[WHAT] == 'comment':
pass
else:
o.append(x)
if not start: self.curind = oldind
return o
class Stowage(storage):
def __str__(self): return self.get('_str')
#@@ edits in place
def __add__(self, other):
if isinstance(other, (unicode, str)):
self._str += other
return self
else:
raise TypeError, 'cannot add'
def __radd__(self, other):
if isinstance(other, (unicode, str)):
self._str = other + self._str
return self
else:
raise TypeError, 'cannot add'
class WTF(AssertionError): pass
class SecurityError(Exception):
"""The template seems to be trying to do something naughty."""
pass
Required = object()
class Template:
globals = {}
content_types = {
'.html' : 'text/html; charset=utf-8',
'.txt' : 'text/plain',
}
def __init__(self, text, filter=None, filename=""):
self.filter = filter
self.filename = filename
# universal newlines:
text = text.replace('\r\n', '\n').replace('\r', '\n').expandtabs()
if not text.endswith('\n'): text += '\n'
header, tree = TemplateParser(text, filename).go()
self.tree = tree
if header:
self.h_defwith(header)
else:
self.args, self.kwargs = (), {}
def __call__(self, *a, **kw):
d = self.globals.copy()
d.update(self._parseargs(a, kw))
f = Fill(self.tree, d=d)
if self.filter: f.filter = self.filter
import webapi as web
if 'headers' in web.ctx and self.filename:
content_type = self.find_content_type()
if content_type:
web.header('Content-Type', content_type, unique=True)
return f.go()
def find_content_type(self):
for ext, content_type in self.content_types.iteritems():
if self.filename.endswith(ext):
return content_type
def _parseargs(self, inargs, inkwargs):
# difference from Python:
# no error on setting a keyword arg twice
d = {}
for arg in self.args:
d[arg] = Required
for kw, val in self.kwargs:
d[kw] = val
for n, val in enumerate(inargs):
if n < len(self.args):
d[self.args[n]] = val
elif n < len(self.args)+len(self.kwargs):
kw = self.kwargs[n - len(self.args)][0]
d[kw] = val
for kw, val in inkwargs.iteritems():
d[kw] = val
unset = []
for k, v in d.iteritems():
if v is Required:
unset.append(k)
if unset:
raise TypeError, 'values for %s are required' % unset
return d
def h_defwith(self, header):
assert header[WHAT] == 'defwith'
f = Fill(self.tree, d={})
self.args = header[ARGS]
self.kwargs = []
for var, valexpr in header[KWARGS]:
self.kwargs.append((var, f.h(valexpr)))
def __repr__(self):
return "<Template: %s>" % self.filename
class Handle:
def __init__(self, parsetree, **kw):
self._funccache = {}
self.parsetree = parsetree
for (k, v) in kw.iteritems(): setattr(self, k, v)
def h(self, item):
return getattr(self, 'h_' + item[WHAT])(item)
class Fill(Handle):
builtins = global_globals
def filter(self, text):
if text is None: return ''
else: return utf8(text)
# often replaced with stuff like net.websafe
def h_literal(self, i):
item = i[THING]
if isinstance(item, (unicode, str)) and item[0] in ['"', "'"]:
item = item[1:-1]
elif isinstance(item, (float, int)):
pass
return item
def h_list(self, i):
x = i[THING]
out = []
for item in x:
out.append(self.h(item))
return out
def h_dict(self, i):
x = i[THING]
out = {}
for k, v in x.iteritems():
out[self.h(k)] = self.h(v)
return out
def h_paren(self, i):
item = i[THING]
if isinstance(item, list):
raise NotImplementedError, 'tuples'
return self.h(item)
def h_getattr(self, i):
thing, attr = i[THING], i[ATTR]
thing = self.h(thing)
if attr.startswith('_') or attr.startswith('func_') or attr.startswith('im_'):
raise SecurityError, 'tried to get ' + attr
try:
if thing in self.builtins:
raise SecurityError, 'tried to getattr on ' + repr(thing)
except TypeError:
pass # raised when testing an unhashable object
try:
return getattr(thing, attr)
except AttributeError:
if isinstance(thing, list) and attr == 'join':
return lambda s: s.join(thing)
else:
raise
def h_call(self, i):
call = self.h(i[THING])
args = [self.h(x) for x in i[ARGS]]
kw = dict([(x, self.h(y)) for (x, y) in i[KWARGS]])
return call(*args, **kw)
def h_getitem(self, i):
thing, item = i[THING], i[ITEM]
thing = self.h(thing)
item = self.h(item)
return thing[item]
def h_expr(self, i):
item = self.h(i[THING])
if i[NEGATE]:
item = not item
return item
def h_test(self, item):
ox, op, oy = item[X], item[OP], item[Y]
# for short-circuiting to work, we can't eval these here
e = self.h
if op == 'is':
return e(ox) is e(oy)
elif op == 'is not':
return e(ox) is not e(oy)
elif op == 'in':
return e(ox) in e(oy)
elif op == 'not in':
return e(ox) not in e(oy)
elif op == '==':
return e(ox) == e(oy)
elif op == '!=':
return e(ox) != e(oy)
elif op == '>':
return e(ox) > e(oy)
elif op == '<':
return e(ox) < e(oy)
elif op == '<=':
return e(ox) <= e(oy)
elif op == '>=':
return e(ox) >= e(oy)
elif op == 'and':
return e(ox) and e(oy)
elif op == 'or':
return e(ox) or e(oy)
elif op == '+':
return e(ox) + e(oy)
elif op == '-':
return e(ox) - e(oy)
elif op == '*':
return e(ox) * e(oy)
elif op == '/':
return e(ox) / e(oy)
elif op == '%':
return e(ox) % e(oy)
else:
raise WTF, 'op ' + op
def h_var(self, i):
v = i[NAME]
if v in self.d:
return self.d[v]
elif v in self.builtins:
return self.builtins[v]
elif v == 'self':
return self.output
else:
raise NameError, 'could not find %s (line %s)' % (repr(i[NAME]), i[LINENO])
def h_line(self, i):
out = []
for x in i[THING]:
#@@ what if x is unicode
if isinstance(x, str):
out.append(x)
elif x[WHAT] == 'itpl':
o = self.h(x[NAME])
if x[FILTER]:
o = self.filter(o)
else:
o = (o is not None and utf8(o)) or ""
out.append(o)
else:
raise WTF, x
return ''.join(out)
def h_varset(self, i):
self.output[i[NAME]] = ''.join(self.h_lines(i[BODY]))
return ''
def h_if(self, i):
expr = self.h(i[CLAUSE])
if expr:
do = i[BODY]
else:
for e in i[ELIF]:
expr = self.h(e[CLAUSE])
if expr:
do = e[BODY]
break
else:
do = i[ELSE]
return ''.join(self.h_lines(do))
def h_for(self, i):
out = []
assert i[IN][WHAT] == 'expr'
invar = self.h(i[IN])
forvar = i[NAME]
if invar:
for nv in invar:
if len(forvar) == 1:
fv = forvar[0]
assert fv[WHAT] == 'var'
self.d[fv[NAME]] = nv # same (lack of) scoping as Python
else:
for x, y in zip(forvar, nv):
assert x[WHAT] == 'var'
self.d[x[NAME]] = y
out.extend(self.h_lines(i[BODY]))
else:
if i[ELSE]:
out.extend(self.h_lines(i[ELSE]))
return ''.join(out)
def h_while(self, i):
out = []
expr = self.h(i[CLAUSE])
if not expr:
return ''.join(self.h_lines(i[ELSE]))
c = 0
while expr:
c += 1
if c >= MAX_ITERS:
raise RuntimeError, 'too many while-loop iterations (line %s)' % i[LINENO]
out.extend(self.h_lines(i[BODY]))
expr = self.h(i[CLAUSE])
return ''.join(out)
def h_assign(self, i):
self.d[i[NAME]] = self.h(i[EXPR])
return ''
def h_comment(self, i): pass
def h_lines(self, lines):
if lines is None: return []
return map(self.h, lines)
def go(self):
self.output = Stowage()
self.output._str = ''.join(map(self.h, self.parsetree))
if self.output.keys() == ['_str']:
self.output = self.output['_str']
return self.output
class render:
def __init__(self, loc='templates/', cache=True):
self.loc = loc
if cache:
self.cache = {}
else:
self.cache = False
def _do(self, name, filter=None):
if self.cache is False or name not in self.cache:
tmplpath = os.path.join(self.loc, name)
p = [f for f in glob.glob(tmplpath + '.*') if not f.endswith('~')] # skip backup files
if not p and os.path.isdir(tmplpath):
return render(tmplpath, cache=self.cache)
elif not p:
raise AttributeError, 'no template named ' + name
p = p[0]
c = Template(open(p).read(), filename=p)
if self.cache is not False: self.cache[name] = (p, c)
if self.cache is not False: p, c = self.cache[name]
if p.endswith('.html') or p.endswith('.xml'):
if not filter: c.filter = websafe
return c
def __getattr__(self, p):
return self._do(p)
def frender(fn, *a, **kw):
return Template(open(fn).read(), *a, **kw)
def test():
import sys
verbose = '-v' in sys.argv
def assertEqual(a, b):
if a == b:
if verbose:
sys.stderr.write('.')
sys.stderr.flush()
else:
assert a == b, "\nexpected: %s\ngot: %s" % (repr(b), repr(a))
from utils import storage, group
class t:
def __init__(self, text):
self.text = text
def __call__(self, *a, **kw):
return TestResult(self.text, Template(self.text)(*a, **kw))
class TestResult:
def __init__(self, source, value):
self.source = source
self.value = value
def __eq__(self, other):
if self.value == other:
if verbose:
sys.stderr.write('.')
else:
print >> sys.stderr, 'FAIL:', repr(self.source), 'expected', repr(other), ', got', repr(self.value)
sys.stderr.flush()
t('1')() == '1\n'
t('$def with ()\n1')() == '1\n'
t('$def with (a)\n$a')(1) == '1\n'
t('$def with (a=0)\n$a')(1) == '1\n'
t('$def with (a=0)\n$a')(a=1) == '1\n'
t('$if 1: 1')() == '1\n'
t('$if 1:\n 1')() == '1\n'
t('$if 0: 0\n$elif 1: 1')() == '1\n'
t('$if 0: 0\n$elif None: 0\n$else: 1')() == '1\n'
t('$if (0 < 1) and (1 < 2): 1')() == '1\n'
t('$for x in [1, 2, 3]: $x')() == '1\n2\n3\n'
t('$for x in []: 0\n$else: 1')() == '1\n'
t('$def with (a)\n$while a and a.pop(): 1')([1, 2, 3]) == '1\n1\n1\n'
t('$while 0: 0\n$else: 1')() == '1\n'
t('$ a = 1\n$a')() == '1\n'
t('$# 0')() == ''
t('$def with (d)\n$for k, v in d.iteritems(): $k')({1: 1}) == '1\n'
t('$def with (a)\n$(a)')(1) == '1\n'
t('$def with (a)\n$a')(1) == '1\n'
t('$def with (a)\n$a.b')(storage(b=1)) == '1\n'
t('$def with (a)\n$a[0]')([1]) == '1\n'
t('${0 or 1}')() == '1\n'
t('$ a = [1]\n$a[0]')() == '1\n'
t('$ a = {1: 1}\n$a.keys()[0]')() == '1\n'
t('$ a = []\n$if not a: 1')() == '1\n'
t('$ a = {}\n$if not a: 1')() == '1\n'
t('$ a = -1\n$a')() == '-1\n'
t('$ a = "1"\n$a')() == '1\n'
t('$if 1 is 1: 1')() == '1\n'
t('$if not 0: 1')() == '1\n'
t('$if 1:\n $if 1: 1')() == '1\n'
t('$ a = 1\n$a')() == '1\n'
t('$ a = 1.\n$a')() == '1.0\n'
t('$({1: 1}.keys()[0])')() == '1\n'
t('$for x in [1, 2, 3]:\n\t$x')() == ' 1\n 2\n 3\n'
t('$def with (a)\n$:a')(1) == '1\n'
t('$def with (a)\n$a')(u'\u203d') == '\xe2\x80\xbd\n'
t(u'$def with (f)\n$:f("x")')(lambda x: x) == 'x\n'
j = Template("$var foo: bar")()
assertEqual(str(j), '')
assertEqual(j.foo, 'bar\n')
if verbose: sys.stderr.write('\n')
if __name__ == "__main__":
test()

View File

@ -0,0 +1,796 @@
"""
General Utilities
(part of web.py)
"""
__all__ = [
"Storage", "storage", "storify",
"iters",
"rstrips", "lstrips", "strips", "utf8",
"TimeoutError", "timelimit",
"Memoize", "memoize",
"re_compile", "re_subm",
"group",
"IterBetter", "iterbetter",
"dictreverse", "dictfind", "dictfindall", "dictincr", "dictadd",
"listget", "intget", "datestr",
"numify", "denumify", "dateify",
"CaptureStdout", "capturestdout", "Profile", "profile",
"tryall",
"ThreadedDict",
"autoassign",
"to36",
"safemarkdown"
]
import re, sys, time, threading
try: import datetime
except ImportError: pass
class Storage(dict):
"""
A Storage object is like a dictionary except `obj.foo` can be used
in addition to `obj['foo']`.
>>> o = storage(a=1)
>>> o.a
1
>>> o['a']
1
>>> o.a = 2
>>> o['a']
2
>>> del o.a
>>> o.a
Traceback (most recent call last):
...
AttributeError: 'a'
"""
def __getattr__(self, key):
try:
return self[key]
except KeyError, k:
raise AttributeError, k
def __setattr__(self, key, value):
self[key] = value
def __delattr__(self, key):
try:
del self[key]
except KeyError, k:
raise AttributeError, k
def __repr__(self):
return '<Storage ' + dict.__repr__(self) + '>'
storage = Storage
def storify(mapping, *requireds, **defaults):
"""
Creates a `storage` object from dictionary `mapping`, raising `KeyError` if
d doesn't have all of the keys in `requireds` and using the default
values for keys found in `defaults`.
For example, `storify({'a':1, 'c':3}, b=2, c=0)` will return the equivalent of
`storage({'a':1, 'b':2, 'c':3})`.
If a `storify` value is a list (e.g. multiple values in a form submission),
`storify` returns the last element of the list, unless the key appears in
`defaults` as a list. Thus:
>>> storify({'a':[1, 2]}).a
2
>>> storify({'a':[1, 2]}, a=[]).a
[1, 2]
>>> storify({'a':1}, a=[]).a
[1]
>>> storify({}, a=[]).a
[]
Similarly, if the value has a `value` attribute, `storify will return _its_
value, unless the key appears in `defaults` as a dictionary.
>>> storify({'a':storage(value=1)}).a
1
>>> storify({'a':storage(value=1)}, a={}).a
<Storage {'value': 1}>
>>> storify({}, a={}).a
{}
"""
def getvalue(x):
if hasattr(x, 'value'):
return x.value
else:
return x
stor = Storage()
for key in requireds + tuple(mapping.keys()):
value = mapping[key]
if isinstance(value, list):
if isinstance(defaults.get(key), list):
value = [getvalue(x) for x in value]
else:
value = value[-1]
if not isinstance(defaults.get(key), dict):
value = getvalue(value)
if isinstance(defaults.get(key), list) and not isinstance(value, list):
value = [value]
setattr(stor, key, value)
for (key, value) in defaults.iteritems():
result = value
if hasattr(stor, key):
result = stor[key]
if value == () and not isinstance(result, tuple):
result = (result,)
setattr(stor, key, result)
return stor
iters = [list, tuple]
import __builtin__
if hasattr(__builtin__, 'set'):
iters.append(set)
try:
from sets import Set
iters.append(Set)
except ImportError:
pass
class _hack(tuple): pass
iters = _hack(iters)
iters.__doc__ = """
A list of iterable items (like lists, but not strings). Includes whichever
of lists, tuples, sets, and Sets are available in this version of Python.
"""
def _strips(direction, text, remove):
if direction == 'l':
if text.startswith(remove):
return text[len(remove):]
elif direction == 'r':
if text.endswith(remove):
return text[:-len(remove)]
else:
raise ValueError, "Direction needs to be r or l."
return text
def rstrips(text, remove):
"""
removes the string `remove` from the right of `text`
>>> rstrips("foobar", "bar")
'foo'
"""
return _strips('r', text, remove)
def lstrips(text, remove):
"""
removes the string `remove` from the left of `text`
>>> lstrips("foobar", "foo")
'bar'
"""
return _strips('l', text, remove)
def strips(text, remove):
"""removes the string `remove` from the both sides of `text`
>>> strips("foobarfoo", "foo")
'bar'
"""
return rstrips(lstrips(text, remove), remove)
def utf8(text):
"""Encodes text in utf-8.
>> utf8(u'\u1234') # doctest doesn't seem to like utf-8
'\xe1\x88\xb4'
>>> utf8('hello')
'hello'
>>> utf8(42)
'42'
"""
if isinstance(text, unicode):
return text.encode('utf-8')
elif isinstance(text, str):
return text
else:
return str(text)
class TimeoutError(Exception): pass
def timelimit(timeout):
"""
A decorator to limit a function to `timeout` seconds, raising `TimeoutError`
if it takes longer.
>>> import time
>>> def meaningoflife():
... time.sleep(.2)
... return 42
>>>
>>> timelimit(.1)(meaningoflife)()
Traceback (most recent call last):
...
TimeoutError: took too long
>>> timelimit(1)(meaningoflife)()
42
_Caveat:_ The function isn't stopped after `timeout` seconds but continues
executing in a separate thread. (There seems to be no way to kill a thread.)
inspired by <http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/473878>
"""
def _1(function):
def _2(*args, **kw):
class Dispatch(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.result = None
self.error = None
self.setDaemon(True)
self.start()
def run(self):
try:
self.result = function(*args, **kw)
except:
self.error = sys.exc_info()
c = Dispatch()
c.join(timeout)
if c.isAlive():
raise TimeoutError, 'took too long'
if c.error:
raise c.error[0], c.error[1]
return c.result
return _2
return _1
class Memoize:
"""
'Memoizes' a function, caching its return values for each input.
>>> import time
>>> def meaningoflife():
... time.sleep(.2)
... return 42
>>> fastlife = memoize(meaningoflife)
>>> meaningoflife()
42
>>> timelimit(.1)(meaningoflife)()
Traceback (most recent call last):
...
TimeoutError: took too long
>>> fastlife()
42
>>> timelimit(.1)(fastlife)()
42
"""
def __init__(self, func):
self.func = func
self.cache = {}
def __call__(self, *args, **keywords):
key = (args, tuple(keywords.items()))
if key not in self.cache:
self.cache[key] = self.func(*args, **keywords)
return self.cache[key]
memoize = Memoize
re_compile = memoize(re.compile) #@@ threadsafe?
re_compile.__doc__ = """
A memoized version of re.compile.
"""
class _re_subm_proxy:
def __init__(self):
self.match = None
def __call__(self, match):
self.match = match
return ''
def re_subm(pat, repl, string):
"""
Like re.sub, but returns the replacement _and_ the match object.
>>> t, m = re_subm('g(oo+)fball', r'f\\1lish', 'goooooofball')
>>> t
'foooooolish'
>>> m.groups()
('oooooo',)
"""
compiled_pat = re_compile(pat)
proxy = _re_subm_proxy()
compiled_pat.sub(proxy.__call__, string)
return compiled_pat.sub(repl, string), proxy.match
def group(seq, size):
"""
Returns an iterator over a series of lists of length size from iterable.
>>> list(group([1,2,3,4], 2))
[[1, 2], [3, 4]]
"""
if not hasattr(seq, 'next'):
seq = iter(seq)
while True:
yield [seq.next() for i in xrange(size)]
class IterBetter:
"""
Returns an object that can be used as an iterator
but can also be used via __getitem__ (although it
cannot go backwards -- that is, you cannot request
`iterbetter[0]` after requesting `iterbetter[1]`).
>>> import itertools
>>> c = iterbetter(itertools.count())
>>> c[1]
1
>>> c[5]
5
>>> c[3]
Traceback (most recent call last):
...
IndexError: already passed 3
"""
def __init__(self, iterator):
self.i, self.c = iterator, 0
def __iter__(self):
while 1:
yield self.i.next()
self.c += 1
def __getitem__(self, i):
#todo: slices
if i < self.c:
raise IndexError, "already passed "+str(i)
try:
while i > self.c:
self.i.next()
self.c += 1
# now self.c == i
self.c += 1
return self.i.next()
except StopIteration:
raise IndexError, str(i)
iterbetter = IterBetter
def dictreverse(mapping):
"""
>>> dictreverse({1: 2, 3: 4})
{2: 1, 4: 3}
"""
return dict([(value, key) for (key, value) in mapping.iteritems()])
def dictfind(dictionary, element):
"""
Returns a key whose value in `dictionary` is `element`
or, if none exists, None.
>>> d = {1:2, 3:4}
>>> dictfind(d, 4)
3
>>> dictfind(d, 5)
"""
for (key, value) in dictionary.iteritems():
if element is value:
return key
def dictfindall(dictionary, element):
"""
Returns the keys whose values in `dictionary` are `element`
or, if none exists, [].
>>> d = {1:4, 3:4}
>>> dictfindall(d, 4)
[1, 3]
>>> dictfindall(d, 5)
[]
"""
res = []
for (key, value) in dictionary.iteritems():
if element is value:
res.append(key)
return res
def dictincr(dictionary, element):
"""
Increments `element` in `dictionary`,
setting it to one if it doesn't exist.
>>> d = {1:2, 3:4}
>>> dictincr(d, 1)
3
>>> d[1]
3
>>> dictincr(d, 5)
1
>>> d[5]
1
"""
dictionary.setdefault(element, 0)
dictionary[element] += 1
return dictionary[element]
def dictadd(*dicts):
"""
Returns a dictionary consisting of the keys in the argument dictionaries.
If they share a key, the value from the last argument is used.
>>> dictadd({1: 0, 2: 0}, {2: 1, 3: 1})
{1: 0, 2: 1, 3: 1}
"""
result = {}
for dct in dicts:
result.update(dct)
return result
def listget(lst, ind, default=None):
"""
Returns `lst[ind]` if it exists, `default` otherwise.
>>> listget(['a'], 0)
'a'
>>> listget(['a'], 1)
>>> listget(['a'], 1, 'b')
'b'
"""
if len(lst)-1 < ind:
return default
return lst[ind]
def intget(integer, default=None):
"""
Returns `integer` as an int or `default` if it can't.
>>> intget('3')
3
>>> intget('3a')
>>> intget('3a', 0)
0
"""
try:
return int(integer)
except (TypeError, ValueError):
return default
def datestr(then, now=None):
"""
Converts a (UTC) datetime object to a nice string representation.
>>> from datetime import datetime, timedelta
>>> d = datetime(1970, 5, 1)
>>> datestr(d, now=d)
'0 microseconds ago'
>>> for t, v in {
... timedelta(microseconds=1): '1 microsecond ago',
... timedelta(microseconds=2): '2 microseconds ago',
... -timedelta(microseconds=1): '1 microsecond from now',
... -timedelta(microseconds=2): '2 microseconds from now',
... timedelta(microseconds=2000): '2 milliseconds ago',
... timedelta(seconds=2): '2 seconds ago',
... timedelta(seconds=2*60): '2 minutes ago',
... timedelta(seconds=2*60*60): '2 hours ago',
... timedelta(days=2): '2 days ago',
... }.iteritems():
... assert datestr(d, now=d+t) == v
>>> datestr(datetime(1970, 1, 1), now=d)
'January 1'
>>> datestr(datetime(1969, 1, 1), now=d)
'January 1, 1969'
>>> datestr(datetime(1970, 6, 1), now=d)
'June 1, 1970'
"""
def agohence(n, what, divisor=None):
if divisor: n = n // divisor
out = str(abs(n)) + ' ' + what # '2 day'
if abs(n) != 1: out += 's' # '2 days'
out += ' ' # '2 days '
if n < 0:
out += 'from now'
else:
out += 'ago'
return out # '2 days ago'
oneday = 24 * 60 * 60
if not now: now = datetime.datetime.utcnow()
if type(now).__name__ == "DateTime":
now = datetime.datetime.fromtimestamp(now)
if type(then).__name__ == "DateTime":
then = datetime.datetime.fromtimestamp(then)
delta = now - then
deltaseconds = int(delta.days * oneday + delta.seconds + delta.microseconds * 1e-06)
deltadays = abs(deltaseconds) // oneday
if deltaseconds < 0: deltadays *= -1 # fix for oddity of floor
if deltadays:
if abs(deltadays) < 4:
return agohence(deltadays, 'day')
out = then.strftime('%B %e') # e.g. 'June 13'
if then.year != now.year or deltadays < 0:
out += ', %s' % then.year
return out
if int(deltaseconds):
if abs(deltaseconds) > (60 * 60):
return agohence(deltaseconds, 'hour', 60 * 60)
elif abs(deltaseconds) > 60:
return agohence(deltaseconds, 'minute', 60)
else:
return agohence(deltaseconds, 'second')
deltamicroseconds = delta.microseconds
if delta.days: deltamicroseconds = int(delta.microseconds - 1e6) # datetime oddity
if abs(deltamicroseconds) > 1000:
return agohence(deltamicroseconds, 'millisecond', 1000)
return agohence(deltamicroseconds, 'microsecond')
def numify(string):
"""
Removes all non-digit characters from `string`.
>>> numify('800-555-1212')
'8005551212'
>>> numify('800.555.1212')
'8005551212'
"""
return ''.join([c for c in str(string) if c.isdigit()])
def denumify(string, pattern):
"""
Formats `string` according to `pattern`, where the letter X gets replaced
by characters from `string`.
>>> denumify("8005551212", "(XXX) XXX-XXXX")
'(800) 555-1212'
"""
out = []
for c in pattern:
if c == "X":
out.append(string[0])
string = string[1:]
else:
out.append(c)
return ''.join(out)
def dateify(datestring):
"""
Formats a numified `datestring` properly.
"""
return denumify(datestring, "XXXX-XX-XX XX:XX:XX")
class CaptureStdout:
"""
Captures everything `func` prints to stdout and returns it instead.
>>> def idiot():
... print "foo"
>>> capturestdout(idiot)()
'foo\\n'
**WARNING:** Not threadsafe!
"""
def __init__(self, func):
self.func = func
def __call__(self, *args, **keywords):
from cStringIO import StringIO
# Not threadsafe!
out = StringIO()
oldstdout = sys.stdout
sys.stdout = out
try:
self.func(*args, **keywords)
finally:
sys.stdout = oldstdout
return out.getvalue()
capturestdout = CaptureStdout
class Profile:
"""
Profiles `func` and returns a tuple containing its output
and a string with human-readable profiling information.
>>> import time
>>> out, inf = profile(time.sleep)(.001)
>>> out
>>> inf[:10].strip()
'took 0.0'
"""
def __init__(self, func):
self.func = func
def __call__(self, *args): ##, **kw): kw unused
import hotshot, hotshot.stats, tempfile ##, time already imported
temp = tempfile.NamedTemporaryFile()
prof = hotshot.Profile(temp.name)
stime = time.time()
result = prof.runcall(self.func, *args)
stime = time.time() - stime
prof.close()
stats = hotshot.stats.load(temp.name)
stats.strip_dirs()
stats.sort_stats('time', 'calls')
x = '\n\ntook '+ str(stime) + ' seconds\n'
x += capturestdout(stats.print_stats)(40)
x += capturestdout(stats.print_callers)()
return result, x
profile = Profile
import traceback
# hack for compatibility with Python 2.3:
if not hasattr(traceback, 'format_exc'):
from cStringIO import StringIO
def format_exc(limit=None):
strbuf = StringIO()
traceback.print_exc(limit, strbuf)
return strbuf.getvalue()
traceback.format_exc = format_exc
def tryall(context, prefix=None):
"""
Tries a series of functions and prints their results.
`context` is a dictionary mapping names to values;
the value will only be tried if it's callable.
>>> tryall(dict(j=lambda: True))
j: True
----------------------------------------
results:
True: 1
For example, you might have a file `test/stuff.py`
with a series of functions testing various things in it.
At the bottom, have a line:
if __name__ == "__main__": tryall(globals())
Then you can run `python test/stuff.py` and get the results of
all the tests.
"""
context = context.copy() # vars() would update
results = {}
for (key, value) in context.iteritems():
if not hasattr(value, '__call__'):
continue
if prefix and not key.startswith(prefix):
continue
print key + ':',
try:
r = value()
dictincr(results, r)
print r
except:
print 'ERROR'
dictincr(results, 'ERROR')
print ' ' + '\n '.join(traceback.format_exc().split('\n'))
print '-'*40
print 'results:'
for (key, value) in results.iteritems():
print ' '*2, str(key)+':', value
class ThreadedDict:
"""
Takes a dictionary that maps threads to objects.
When a thread tries to get or set an attribute or item
of the threadeddict, it passes it on to the object
for that thread in dictionary.
"""
def __init__(self, dictionary):
self.__dict__['_ThreadedDict__d'] = dictionary
def __getattr__(self, attr):
return getattr(self.__d[threading.currentThread()], attr)
def __getitem__(self, item):
return self.__d[threading.currentThread()][item]
def __setattr__(self, attr, value):
if attr == '__doc__':
self.__dict__[attr] = value
else:
return setattr(self.__d[threading.currentThread()], attr, value)
def __delattr__(self, item):
try:
del self.__d[threading.currentThread()][item]
except KeyError, k:
raise AttributeError, k
def __delitem__(self, item):
del self.__d[threading.currentThread()][item]
def __setitem__(self, item, value):
self.__d[threading.currentThread()][item] = value
def __hash__(self):
return hash(self.__d[threading.currentThread()])
threadeddict = ThreadedDict
def autoassign(self, locals):
"""
Automatically assigns local variables to `self`.
>>> self = storage()
>>> autoassign(self, dict(a=1, b=2))
>>> self
<Storage {'a': 1, 'b': 2}>
Generally used in `__init__` methods, as in:
def __init__(self, foo, bar, baz=1): autoassign(self, locals())
"""
for (key, value) in locals.iteritems():
if key == 'self':
continue
setattr(self, key, value)
def to36(q):
"""
Converts an integer to base 36 (a useful scheme for human-sayable IDs).
>>> to36(35)
'z'
>>> to36(119292)
'2k1o'
>>> int(to36(939387374), 36)
939387374
>>> to36(0)
'0'
>>> to36(-393)
Traceback (most recent call last):
...
ValueError: must supply a positive integer
"""
if q < 0: raise ValueError, "must supply a positive integer"
letters = "0123456789abcdefghijklmnopqrstuvwxyz"
converted = []
while q != 0:
q, r = divmod(q, 36)
converted.insert(0, letters[r])
return "".join(converted) or '0'
r_url = re_compile('(?<!\()(http://(\S+))')
def safemarkdown(text):
"""
Converts text to HTML following the rules of Markdown, but blocking any
outside HTML input, so that only the things supported by Markdown
can be used. Also converts raw URLs to links.
(requires [markdown.py](http://webpy.org/markdown.py))
"""
from markdown import markdown
if text:
text = text.replace('<', '&lt;')
# TODO: automatically get page title?
text = r_url.sub(r'<\1>', text)
text = markdown(text)
return text
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@ -0,0 +1,369 @@
"""
Web API (wrapper around WSGI)
(from web.py)
"""
__all__ = [
"config",
"badrequest", "notfound", "gone", "internalerror",
"header", "output", "flush", "debug",
"input", "data",
"setcookie", "cookies",
"ctx",
"loadhooks", "load", "unloadhooks", "unload", "_loadhooks",
"wsgifunc"
]
import sys, os, cgi, threading, Cookie, pprint, traceback
try: import itertools
except ImportError: pass
from utils import storage, storify, threadeddict, dictadd, intget, lstrips, utf8
config = storage()
config.__doc__ = """
A configuration object for various aspects of web.py.
`db_parameters`
: A dictionary containing the parameters to be passed to `connect`
when `load()` is called.
`db_printing`
: Set to `True` if you would like SQL queries and timings to be
printed to the debug output.
"""
def badrequest():
"""Return a `400 Bad Request` error."""
ctx.status = '400 Bad Request'
header('Content-Type', 'text/html')
return output('bad request')
def notfound():
"""Returns a `404 Not Found` error."""
ctx.status = '404 Not Found'
header('Content-Type', 'text/html')
return output('not found')
def gone():
"""Returns a `410 Gone` error."""
ctx.status = '410 Gone'
header('Content-Type', 'text/html')
return output("gone")
def internalerror():
"""Returns a `500 Internal Server` error."""
ctx.status = "500 Internal Server Error"
ctx.headers = [('Content-Type', 'text/html')]
ctx.output = "internal server error"
def header(hdr, value, unique=False):
"""
Adds the header `hdr: value` with the response.
If `unique` is True and a header with that name already exists,
it doesn't add a new one.
"""
hdr, value = utf8(hdr), utf8(value)
# protection against HTTP response splitting attack
if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value:
raise ValueError, 'invalid characters in header'
if unique is True:
for h, v in ctx.headers:
if h.lower() == hdr.lower(): return
ctx.headers.append((hdr, value))
def output(string_):
"""Appends `string_` to the response."""
if isinstance(string_, unicode): string_ = string_.encode('utf8')
if ctx.get('flush'):
ctx._write(string_)
else:
ctx.output += str(string_)
def flush():
ctx.flush = True
return flush
def input(*requireds, **defaults):
"""
Returns a `storage` object with the GET and POST arguments.
See `storify` for how `requireds` and `defaults` work.
"""
from cStringIO import StringIO
def dictify(fs): return dict([(k, fs[k]) for k in fs.keys()])
_method = defaults.pop('_method', 'both')
e = ctx.env.copy()
a = b = {}
if _method.lower() in ['both', 'post']:
if e['REQUEST_METHOD'] == 'POST':
a = cgi.FieldStorage(fp = StringIO(data()), environ=e,
keep_blank_values=1)
a = dictify(a)
if _method.lower() in ['both', 'get']:
e['REQUEST_METHOD'] = 'GET'
b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1))
out = dictadd(b, a)
try:
return storify(out, *requireds, **defaults)
except KeyError:
badrequest()
raise StopIteration
def data():
"""Returns the data sent with the request."""
if 'data' not in ctx:
cl = intget(ctx.env.get('CONTENT_LENGTH'), 0)
ctx.data = ctx.env['wsgi.input'].read(cl)
return ctx.data
def setcookie(name, value, expires="", domain=None):
"""Sets a cookie."""
if expires < 0:
expires = -1000000000
kargs = {'expires': expires, 'path':'/'}
if domain:
kargs['domain'] = domain
# @@ should we limit cookies to a different path?
cookie = Cookie.SimpleCookie()
cookie[name] = value
for key, val in kargs.iteritems():
cookie[name][key] = val
header('Set-Cookie', cookie.items()[0][1].OutputString())
def cookies(*requireds, **defaults):
"""
Returns a `storage` object with all the cookies in it.
See `storify` for how `requireds` and `defaults` work.
"""
cookie = Cookie.SimpleCookie()
cookie.load(ctx.env.get('HTTP_COOKIE', ''))
try:
return storify(cookie, *requireds, **defaults)
except KeyError:
badrequest()
raise StopIteration
def debug(*args):
"""
Prints a prettyprinted version of `args` to stderr.
"""
try:
out = ctx.environ['wsgi.errors']
except:
out = sys.stderr
for arg in args:
print >> out, pprint.pformat(arg)
return ''
def _debugwrite(x):
try:
out = ctx.environ['wsgi.errors']
except:
out = sys.stderr
out.write(x)
debug.write = _debugwrite
class _outputter:
"""Wraps `sys.stdout` so that print statements go into the response."""
def __init__(self, file): self.file = file
def write(self, string_):
if hasattr(ctx, 'output'):
return output(string_)
else:
self.file.write(string_)
def __getattr__(self, attr): return getattr(self.file, attr)
def __getitem__(self, item): return self.file[item]
def _capturedstdout():
sysstd = sys.stdout
while hasattr(sysstd, 'file'):
if isinstance(sys.stdout, _outputter): return True
sysstd = sysstd.file
if isinstance(sys.stdout, _outputter): return True
return False
if not _capturedstdout():
sys.stdout = _outputter(sys.stdout)
_context = {threading.currentThread(): storage()}
ctx = context = threadeddict(_context)
ctx.__doc__ = """
A `storage` object containing various information about the request:
`environ` (aka `env`)
: A dictionary containing the standard WSGI environment variables.
`host`
: The domain (`Host` header) requested by the user.
`home`
: The base path for the application.
`ip`
: The IP address of the requester.
`method`
: The HTTP method used.
`path`
: The path request.
`query`
: If there are no query arguments, the empty string. Otherwise, a `?` followed
by the query string.
`fullpath`
: The full path requested, including query arguments (`== path + query`).
### Response Data
`status` (default: "200 OK")
: The status code to be used in the response.
`headers`
: A list of 2-tuples to be used in the response.
`output`
: A string to be used as the response.
"""
loadhooks = {}
_loadhooks = {}
def load():
"""
Loads a new context for the thread.
You can ask for a function to be run at loadtime by
adding it to the dictionary `loadhooks`.
"""
_context[threading.currentThread()] = storage()
ctx.status = '200 OK'
ctx.headers = []
if config.get('db_parameters'):
import db
db.connect(**config.db_parameters)
for x in loadhooks.values(): x()
def _load(env):
load()
ctx.output = ''
ctx.environ = ctx.env = env
ctx.host = env.get('HTTP_HOST')
ctx.homedomain = 'http://' + env.get('HTTP_HOST', '[unknown]')
ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', ''))
ctx.home = ctx.homedomain + ctx.homepath
ctx.ip = env.get('REMOTE_ADDR')
ctx.method = env.get('REQUEST_METHOD')
ctx.path = env.get('PATH_INFO')
# http://trac.lighttpd.net/trac/ticket/406 requires:
if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'):
ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0],
os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', '')))
if env.get('QUERY_STRING'):
ctx.query = '?' + env.get('QUERY_STRING', '')
else:
ctx.query = ''
ctx.fullpath = ctx.path + ctx.query
for x in _loadhooks.values(): x()
unloadhooks = {}
def unload():
"""
Unloads the context for the thread.
You can ask for a function to be run at loadtime by
adding it ot the dictionary `unloadhooks`.
"""
for x in unloadhooks.values(): x()
# ensures db cursors and such are GCed promptly
del _context[threading.currentThread()]
def _unload():
unload()
def wsgifunc(func, *middleware):
"""Returns a WSGI-compatible function from a webpy-function."""
middleware = list(middleware)
def wsgifunc(env, start_resp):
_load(env)
try:
result = func()
except StopIteration:
result = None
except:
print >> debug, traceback.format_exc()
result = internalerror()
is_generator = result and hasattr(result, 'next')
if is_generator:
# wsgi requires the headers first
# so we need to do an iteration
# and save the result for later
try:
firstchunk = result.next()
except StopIteration:
firstchunk = ''
status, headers, output = ctx.status, ctx.headers, ctx.output
ctx._write = start_resp(status, headers)
# and now, the fun:
def cleanup():
# we insert this little generator
# at the end of our itertools.chain
# so that it unloads the request
# when everything else is done
yield '' # force it to be a generator
_unload()
# result is the output of calling the webpy function
# it could be a generator...
if is_generator:
if firstchunk is flush:
# oh, it's just our special flush mode
# ctx._write is set up, so just continue execution
try:
result.next()
except StopIteration:
pass
_unload()
return []
else:
return itertools.chain([firstchunk], result, cleanup())
# ... but it's usually just None
#
# output is the stuff in ctx.output
# it's usually a string...
if isinstance(output, str): #@@ other stringlikes?
_unload()
return [output]
# it could be a generator...
elif hasattr(output, 'next'):
return itertools.chain(output, cleanup())
else:
_unload()
raise Exception, "Invalid ctx.output"
for mw_func in middleware:
wsgifunc = mw_func(wsgifunc)
return wsgifunc

View File

@ -0,0 +1,54 @@
"""
WSGI Utilities
(from web.py)
"""
import os, sys
import http
import webapi as web
from utils import listget
from net import validaddr, validip
import httpserver
def runfcgi(func, addr=('localhost', 8000)):
"""Runs a WSGI function as a FastCGI server."""
import flup.server.fcgi as flups
return flups.WSGIServer(func, multiplexed=True, bindAddress=addr).run()
def runscgi(func, addr=('localhost', 4000)):
"""Runs a WSGI function as an SCGI server."""
import flup.server.scgi as flups
return flups.WSGIServer(func, bindAddress=addr).run()
def runwsgi(func):
"""
Runs a WSGI-compatible `func` using FCGI, SCGI, or a simple web server,
as appropriate based on context and `sys.argv`.
"""
if os.environ.has_key('SERVER_SOFTWARE'): # cgi
os.environ['FCGI_FORCE_CGI'] = 'Y'
if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi
or os.environ.has_key('SERVER_SOFTWARE')):
return runfcgi(func, None)
if 'fcgi' in sys.argv or 'fastcgi' in sys.argv:
args = sys.argv[1:]
if 'fastcgi' in args: args.remove('fastcgi')
elif 'fcgi' in args: args.remove('fcgi')
if args:
return runfcgi(func, validaddr(args[0]))
else:
return runfcgi(func, None)
if 'scgi' in sys.argv:
args = sys.argv[1:]
args.remove('scgi')
if args:
return runscgi(func, validaddr(args[0]))
else:
return runscgi(func)
return httpserver.runsimple(func, validip(listget(sys.argv, 1, '')))

File diff suppressed because it is too large Load Diff

3
plugins/WebUi/run_webserver Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env python
import deluge_webserver
deluge_webserver.run()

View File

@ -0,0 +1,10 @@
#!/bin/bash
pwd=deluge
url=http://localhost:8112
for arg in "$@"
do
curl -F torrent=@"$arg" -F pwd=$pwd $url/remote/torrent/add
done

View File

@ -0,0 +1,207 @@
// ==UserScript==
// @name Add Torrents To Deluge
// @namespace http://blog.monstuff.com/archives/cat_greasemonkey.html
// @description Let's you add torrents to the deluge WebUi
// @include http://isohunt.com/torrent_details/*
// @include http://thepiratebay.org/details.php?*
// @include http://torrentreactor.net/view.php?*
// @include http://www.mininova.org/*
// @include http://www.torrentspy.com/*
// @include http://ts.searching.com/*
// @include *
// ==/UserScript==
//url-based submit and parsing based on : "Add Torrents To utorrent" by Julien Couvreur
//binary magic,contains from http://mgran.blogspot.com/2006/08/downloading-binary-streams-with.html
//these parameters need to be edited before using the script
// Server address
var host = "localhost";
// Server port
var port = "8112";
//open_page: "_blank" for a new window or "deluge_webui" for window re-use
//(not for private=1)
var open_page = "_blank"
//Private-trackers 0/1
//different behavior, gets torrent-data from (private) site and pops up a message.
var private_submit = 1;
//deluge_password, only needed if private_submit = 1.
var deluge_password = 'deluge';
//========================
if (host == "") { alert('You need to configure the "Add Torrents To Deluge" user script with your WebUI parameters before using it.'); }
function scanLinks() {
var links = getLinks();
for (var i=0; i < links.length; i++){
var link = links[i];
if (match(link.href)) {
if (private_submit) {
makeUTorrentLink_private(link,i);
}
else {
makeUTorrentLink(link);
}
}
}
}
function makeUTorrentLink(link) {
var uTorrentLink = document.createElement('a');
uTorrentLink.setAttribute("href", makeUTorrentUrl(link.href));
uTorrentLink.setAttribute("target", open_page);
uTorrentLink.style.paddingLeft = "5px";
uTorrentLink.innerHTML = "<img src=\"" + image + "\" style='border: 0px' />";
link.parentNode.insertBefore(uTorrentLink, link.nextSibling);
return uTorrentLink
}
function makeUTorrentUrl(url) {
var uTorrentUrl = "http://"+host+":"+port+"/torrent/add?redir_after_login=1";
return uTorrentUrl + "&url=" + escape(url);
}
function makeUTorrentLink_private(link,i) {
var id = 'deluge_link' + i;
var uTorrentLink = document.createElement('a');
uTorrentLink.setAttribute("href", '#');
uTorrentLink.setAttribute("id", id);
uTorrentLink.style.paddingLeft = "5px";
uTorrentLink.innerHTML = "<img src=\"" + image + "\" style='border: 0px' />";
link.parentNode.insertBefore(uTorrentLink, link.nextSibling);
ulink = document.getElementById(id)
ulink.addEventListener("click", evt_private_submit_factory(link.href),false);
return uTorrentLink
}
function evt_private_submit_factory(url) {
//can this be done without magic?
function evt_private_submit(evt) {
GM_xmlhttpRequest({ method: 'GET', url: url,
overrideMimeType: 'text/plain; charset=x-user-defined',
onload: function(xhr) {
var stream = translateToBinaryString(xhr.responseText);
var data_b64 = window.btoa(stream);
post_to_webui(url, data_b64);
},
onerror:function(xhr) {
alert('error fetching torrent file');
}
});
return false;
}
return evt_private_submit;
}
function post_to_webui(url,data_b64){
//alert('here1');
//data contains the content of the .torrent-file.
var POST_data = ('pwd=' + encodeURIComponent(deluge_password) +
'&torrent_name=' + encodeURIComponent(url) + '.torrent' + //+.torrent is a clutch!
'&data_b64=' + encodeURIComponent(data_b64) );
//alert(POST_data);
GM_xmlhttpRequest({ method: 'POST',
url: "http://"+host+":"+port+"/remote/torrent/add",
headers:{'Content-type':'application/x-www-form-urlencoded'},
data: POST_data,
onload: function(xhr) {
if (xhr.responseText == 'ok\n') {
alert('Added torrent to webui : \n' + url);
}
else {
alert('Error adding torrent to webui:\n"' + xhr.responseText + '"');
}
},
onerror:function(xhr) {
alert('error submitting torrent file');
}
});
}
function match(url) {
// isohunt format
if (url.match(/http:\/\/.*isohunt\.com\/download\//i)) {
return true;
}
if (url.match(/\.torrent$/)) {
return true;
}
if (url.match(/http:\/\/.*bt-chat\.com\/download\.php/)) {
return true;
}
// TorrentReactor
if (url.match(/http:\/\/dl\.torrentreactor\.net\/download.php\?/i)) {
return true;
}
// Mininova
if (url.match(/http:\/\/www\.mininova\.org\/get\//i)) {
return true;
}
// Mininova
if (url.match(/http:\/\/www\.mininova\.org\/get\//i)) {
return true;
}
// TorrentSpy
if (url.match(/http:\/\/ts\.searching\.com\/download\.asp\?/i)) {
return true;
}
if (url.match(/http:\/\/www\.torrentspy\.com\/download.asp\?/i)) {
return true;
}
// Seedler
if (url.match(/http:\/\/.*seedler\.org\/download\.x\?/i)) {
return true;
}
return false;
}
function getLinks() {
var doc_links = document.links;
var links = new Array();
for (var i=0; i < doc_links.length; i++){
links.push(doc_links[i]);
}
return links;
}
var image = "data:image/gif;base64,R0lGODlhEAAQAMZyAB1CdihAYx5CdiBEeCJGeSZJfChKfChLfSpPgTBRgThRdDRUgzRVhDVWhDZWhThYhjtbiD1ciD5diT5eiz9eikBeiUFeiT5fjT1gjkBfjERijkdjiUhljkVnlEdolUxokExqkk5qkU9rklBrklFtk1BullFulk5vmlZymFx3nE97rVZ5pUx8sl54nlt5oVl6pE5/tWJ6nVp9qFqArWOEq1uIuW6EpGCItl2Ku26Gp2KKuGuIrF+MvWaLtl+Nv3KJqG+KrGaOu2aQv2SRwnGOs2uQvGqSwICOpoCQqm6Ww3OVvHKWv3iWuoKWsn+XtnacxXaeynifyXigzICewn2gxnqizoqfunujzpWesX6l0IyivYijw4+jvpOiuoOp0puktY2x2I6y2Y+z2pG02pW43Ze42pa43Z/A4qjG56jH56nI6KzJ6a/M67nR67zW8sLa9cff+M/k+P///////////////////////////////////////////////////////yH+FUNyZWF0ZWQgd2l0aCBUaGUgR0lNUAAh+QQBCgB/ACwAAAAAEAAQAAAHkIB/goOEhYaCX1iHhkdIXU2LgzFARExbkYInCBcvRVSRHgQNEiYoPUmHGAkjO1FSSilBNYYQFTllY2BeSzJChg4iWmhpZ2JXOjgqhBMFH1xvbmtmWUMwM4QZBws/cXBsZFU+LCuFDwIhVm1qYVA8Nx2FEQQDHDZOU09GNIcWDAAGFEC0cBEpwAYNJUgowMQwEAA7";
scanLinks();
/*
binary magic,contains code taken from
http://mgran.blogspot.com/2006/08/downloading-binary-streams-with.html
*/
function translateToBinaryString(text){
var out;
out='';
for(i=0;i<text.length;i++){
//*bugfix* by Marcus Granado 2006 [http://mgran.blogspot.com] adapted by Thomas Belot
out+=String.fromCharCode(text.charCodeAt(i) & 0xff);
}
return out;
}

View File

@ -0,0 +1,7 @@
#!/bin/sh
cd ~/prj/WebUi
bzr revno > revno
bzr version-info > version
rm ~/prj/WebUi/WebUi.tgz
cd ~/prj
tar -zcvf ~/prj/WebUi/WebUi.tgz WebUi/ --exclude '.*' --exclude '*.pyc' --exclude '*.tgz' --exclude 'attic' --exclude 'xul' --exclude '*.sh' --exclude '*.*~'

View File

@ -0,0 +1 @@
curl -F torrent=@./test1.torrent -F pwd=deluge http://localhost:8112/remote/torrent/add

View File

@ -0,0 +1,29 @@
from __future__ import with_statement
import os
import re
template_dirs = ['~/prj/WebUi/templates/deluge',
'~/prj/WebUi/templates/advanced']
template_dirs = [os.path.expanduser(template_dir ) for template_dir in template_dirs]
files = []
for template_dir in template_dirs:
files += [os.path.join(template_dir,fname)
for fname in os.listdir(template_dir)
if fname.endswith('.html')]
all_strings = []
for filename in files:
with open(filename,'r') as f:
content = f.read()
all_strings += re.findall("_\(\"(.*?)\"\)",content)
all_strings += re.findall("_\(\'(.*?)\'\)",content)
all_strings = sorted(set(all_strings))
with open ('./template_strings.py','w') as f:
for value in all_strings:
f.write("_('%s')\n" % value )

View File

@ -0,0 +1,65 @@
_('# Of Files')
_('About')
_('Add')
_('Add Torrent')
_('Add torrent')
_('Apply')
_('Auto refresh:')
_('Ava')
_('Availability')
_('Config')
_('Connections')
_('Debug:Data Dump')
_('Delete .torrent file')
_('Delete downloaded files.')
_('Details')
_('Disable')
_('Down')
_('Down Speed')
_('Download')
_('Downloaded')
_('ETA')
_('Enable')
_('Error')
_('Eta')
_('Login')
_('Logout')
_('Name')
_('Next Announce')
_('Off')
_('Password')
_('Password is invalid,try again')
_('Pause')
_('Pause all')
_('Peers')
_('Pieces')
_('Progress')
_('Queue Down')
_('Queue Position')
_('Queue Up')
_('Ratio')
_('Reannounce')
_('Refresh page every:')
_('Remove')
_('Remove torrent')
_('Resume')
_('Resume all')
_('Seeders')
_('Set')
_('Set Timeout')
_('Share Ratio')
_('Size')
_('Speed')
_('Start')
_('Submit')
_('Torrent list')
_('Total Size')
_('Tracker')
_('Tracker Status')
_('Up')
_('Up Speed')
_('Upload')
_('Upload torrent')
_('Uploaded')
_('Url')
_('seconds')

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA1sPXr1O6l2J9NAEvEYQ/JFDSVcJHh9YxP7kPdjsu7k9Ih845
BHMX52A3Ypbe5MHe2bCj/8dRYCixRdF1KUTAKXdzc7mw9prgf3sS3RvmfcRsln6u
x7XRg7YprZJ46hFmcHiUPRgtTFLuFO2YWBnqxu/caTtAxx3PdoK6LDVnuVjHYofC
8uD4A9k6yL/jj3Yrkf8WYQqJ6pJcMAz/2c8ZXlBuiUCb9j5xKTzYoJaiUkKN2YrA
hoxRxfI7Zc7MH2yWw8/fTZJbGXo8nrfek7coSE7yQS1M6ciwkYk5VO2mBVJBJgAT
QUR/jGfLzEqNKXghQ564v9wmuFmUMd99a0tkVwIDAQABAoIBACID6sluLYOEqefu
uBHCLG4IDwheOQ4esrYxDW3gedJs5EP+ObGmuQaAisUmuC7rNeysuYzteMoOJ+Wz
AyeCKB1pOfP+WTT12tDWIWq73InW7ov3jJ89AO4nj/pZ1KTeFKeDsZbrmWEZUXQn
HZX2pOTVYMeaBuyCoDVZBzuxSbhlON4wS6ClMhem+eBOxg351CDTZa2cbq7Ffcos
VP7LY2ORQYNDTQSLguV/dJrFSotB8Eoz2xIpg5XR7msp6lzPzyAd+Aoz/T1lYxCY
IFZCJYKnIpgoYQvmtUlhQrdD8P0J4Kth7I8NgkWvXCKazQjhpUm+wojLKD0G7Kcz
9znIV+ECgYEA+qfp1C8jWbaAn1yAeORUA9aB6aGIURfOpZjnCvtMWM0Nu0nAJYDv
X7L5GRa1ulfKhfUG1Jv/ynMKXYuBUDhyccYLpP7BHpd29Arr7YAgb52KaD1PoKNa
Z45c61dj4sFoCmJEbDoL21UGb0LX3mc4XzPzwWs8AKfLW4aZh1NwCisCgYEA21gJ
Hy3egBgMT9+nVjqsgtIXgJOnzQRhvRwT7IFf392ZyFi8iM+pDUsx1yj0zSG4XNPw
NY8VtZuTBUlG73RKcrrz31jhCMfLCnoRkQeweZv0QWzbLU3V8DleUYdjFc/t0me5
4NBR9lBlwYHgyU3GQ814vum+m0IAH0Ng1UxAVIUCgYAFOHwZTEYLN07kgtO2MOND
FTOtfwzMy5clQdMGGofTjanMjdOvtEjIEH05tYxhbjSsp5bV1M32FIFRw3cVCafw
kLRrYlb5YSQ8HwIc9z81s+1PEH/ZE63tXDy5Nh/BeE/Hb5aHPopCrjmtFZJTcojt
CrL4A1jDlrsYk+wcsnMx8wKBgEhJJQhvd2pDgps4G8+hGoUqc7Bd+OjpzsQh4rcI
k+4U+7847zkvJolJBK3hw3tu53FAL2OXOhJVqQgO9B+p9XcGAaTTh6X7IgDb5bok
DJanPMHq+/hcNGssnNbFhXQEyF2U7X8XaEuCh2ZURR5SUUq7BlX0dmp4P84NyHXC
4Vh5AoGAZYWkXxQUGzVm+H3fPpmETWGRNFDTimzi+6N+/uHkqkiDa3LGSnabmKh+
voKm//DUjEVGlAZ3CGOjO/5SlZc/zjkgh1vg7KOU4x7DqVOuZjom5Tx3ZI4xVVVt
tVtvK0qjzUTVcwAQALN/PNak+gs9534e954rmA9kmc3xBe4ho9M=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDlzCCAn+gAwIBAgIJAPnW/GEzRy8xMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNV
BAYTAkFVMRUwEwYDVQQIEwxUaGUgSW50ZXJuZXQxFTATBgNVBAoTDERlbHVnZSBX
ZWJ1aTAeFw0wNzExMjQxMDAzNDRaFw0wODExMjMxMDAzNDRaMDsxCzAJBgNVBAYT
AkFVMRUwEwYDVQQIEwxUaGUgSW50ZXJuZXQxFTATBgNVBAoTDERlbHVnZSBXZWJ1
aTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANbD169TupdifTQBLxGE
PyRQ0lXCR4fWMT+5D3Y7Lu5PSIfOOQRzF+dgN2KW3uTB3tmwo//HUWAosUXRdSlE
wCl3c3O5sPaa4H97Et0b5n3EbJZ+rse10YO2Ka2SeOoRZnB4lD0YLUxS7hTtmFgZ
6sbv3Gk7QMcdz3aCuiw1Z7lYx2KHwvLg+APZOsi/4492K5H/FmEKieqSXDAM/9nP
GV5QbolAm/Y+cSk82KCWolJCjdmKwIaMUcXyO2XOzB9slsPP302SWxl6PJ633pO3
KEhO8kEtTOnIsJGJOVTtpgVSQSYAE0FEf4xny8xKjSl4IUOeuL/cJrhZlDHffWtL
ZFcCAwEAAaOBnTCBmjAdBgNVHQ4EFgQU1BbX1/4WtAKRKmWI1gqryIoj7BQwawYD
VR0jBGQwYoAU1BbX1/4WtAKRKmWI1gqryIoj7BShP6Q9MDsxCzAJBgNVBAYTAkFV
MRUwEwYDVQQIEwxUaGUgSW50ZXJuZXQxFTATBgNVBAoTDERlbHVnZSBXZWJ1aYIJ
APnW/GEzRy8xMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAEoiSz5x
hRCplxUG34g3F5yJe0QboqzJ/XmECfO80a980C/WVeivM2Kb1uafsKNp+WK7wD8g
mei+todYXG+fD8WmG41LG87Xi2Xe4SlAcemEpGcC5F1bpCdvqnVAWFnqoF88FOHx
NDlrq5H5lhMH9wVrX9qJvxL+StaDJ0sFk4kMGWEN+bdSYfFdBQzF903nPtm+PlvO
1Uo6gCuRTMYM5J1DC/GpNpo/Fzrkgm8mMf1MYy3rljiNgMt2rnxhtwi6jugwyMui
id6Of6gYAtvhi7kmaUpdI5PHO35dqRK7pHXH+YXaulosCPw/+bSRptFTykeEMrBj
CzotqJ+74MwXZyM=
-----END CERTIFICATE-----

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 588 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 498 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 912 B

View File

@ -0,0 +1,91 @@
/* ----------------------------------------------------------- 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*/ }
div.progress_bar_outer { /*used in table-view*/
width:150px;
}
td.progress_bar { white-space: nowrap; } td.info_label { font-weight: bold; } td { font-size: 10pt; color: #d1dae5; white-space: nowrap; } tr {
font-size: 10pt;
color: #d1dae5;
}
div.panel {
padding:10px;
width:750px;
background-color: #37506f;
-moz-border-radius:10px; /*ff-only!*/
margin-top:10px;
margin-bottom:10px;
}
/*New styles:*/
div.deluge_button {
display:inline;
}
form.deluge_button {
display:inline;
}
button.deluge_button {
background-color: #37506f;
border:1px solid #68a;
background: #99acc3;
color: #000;
vertical-align:middle;
-moz-border-radius:7px;
}
button.deluge_button:hover {
background-color:#68a;
}
div.error {
background-color:#FFFFFF;
color:#AA0000;
font-weight:bold;
-moz-border-radius:10px;
width:200px;
margin-bottom:20px;
padding:10px;
}
/*tr.torrent_table:hover {
background-color:#68a;
}*/
tr.torrent_table_selected {
background-color:#900;
}
img.button {
margin-bottom:0px;
padding:0px;
position:relative;
top:2px;
}
body.inner {
background:none;
}
form.pause_resume {
margin:0;
padding:0;
border:0;
}
th {
background: #1f3044;
font-size: 14px;
border: 0px;
white-space: nowrap;
}
#torrent_table {
border: #2a425c 1px solid;
}
/* Hides from IE-mac \*/ * html .clearfix {height: 1%;} .clearfix {display: block;} /* End hide from IE-mac */

View File

@ -0,0 +1,28 @@
$def with (title)
<html>
<head>
<title>Deluge:$title</title>
<link rel="icon" href="/static/images/deluge_icon.gif" type="image/gif" />
<link rel="shortcut icon" href="/static/images/deluge_icon.gif" type="image/gif" />
<link rel="stylesheet" type="text/css" href="/template/static/advanced.css" />
<script language="javascript" src="/template/static/deluge.js"></script>
<!--<link rel="stylesheet" type="text/css" href="/template/static/scrolling_table.css" />
-->
</head>
<body>
<div id="page">
<a href='/home'>
<div id="simple_logo">
<div class="title">Deluge</div>
<div class="info">$title</div>
</div>
</a>
<div id="main_content">
<div id="main">
<center>

View File

@ -0,0 +1,147 @@
$def with (torrent_list, all_torrents)
$:render.header(_('Torrent list'))
<div class="panel" id="toolbar">
<a class='toolbar_btn' href="#"
onclick='toolbar_get("/torrent/add/",0)'
title='$_("Add")'><img class='toolbar_btn'
src='/static/images/tango/list-add.png'></a>
<a class='toolbar_btn' href="#"
onclick='toolbar_get("/torrent/delete/",2)'><img class='toolbar_btn'
src='/static/images/tango/list-remove.png'
title='$_("Remove")'></a>
<a class='toolbar_btn' href="#"
onclick='toolbar_post("/torrent/stop/",2)'
title='$_("Pause")'><img class='toolbar_btn'
src='/static/images/tango/pause.png'
></a>
<a class='toolbar_btn' href="#"
onclick='toolbar_post("/torrent/start/",2)'
title='$_("Start")'><img class='toolbar_btn'
src='/static/images/tango/start.png'></a>
<a class='toolbar_btn' href="#"
onclick='toolbar_post("/torrent/queue/up/",2)'
title='$_("Up")'><img class='toolbar_btn'
src='/static/images/tango/queue-up.png'></a>
<a class='toolbar_btn' href="#"
onclick='toolbar_post("/torrent/queue/down/",2)'
title='$_("Down")'><img class='toolbar_btn'
src='/static/images/tango/queue-down.png'></a>
<a class='toolbar_btn' href="#"
onclick='toolbar_get("/torrent/info/",1)'
title='$_("Details")'><img class='toolbar_btn'
src='/static/images/tango/details.png'></a>
$:category_tabs(all_torrents)
</div>
<div id="tableContainer" class="tableContainer">
<table class="torrent_list" border=0 cellspacing=0 cellpadding=2 id="torrent_list">
<thead class="fixedHeader">
<tr>
$:(sort_head('calc_state_str', 'S'))
$:(sort_head('queue_pos', '#'))
$:(sort_head('name', _('Name')))
$:(sort_head('total_size', _('Size')))
$:(sort_head('progress', _('Progress')))
$if (not get('category')):
$:(sort_head('category', _('Tracker')))
$:(sort_head('num_seeds', _('Seeders')))
$:(sort_head('num_peers', _('Peers')))
$:(sort_head('download_rate', _('Download')))
$:(sort_head('upload_rate', _('Upload')))
$:(sort_head('eta', _('Eta')))
$:(sort_head('distributed_copies', _('Ava')))
$:(sort_head('ratio', _('Ratio')))
</tr>
</thead>
<tbody class="scrollContent">
$altrow(True)
$#4-space indentation is mandatory for for-loops in templetor!
$for torrent in torrent_list:
<tr class="$altrow()" onclick="on_click_row(event, '$torrent.id')" id="torrent_$torrent.id">
<td>
<form action="/torrent/$torrent.action/$torrent.id" method="POST"
class="pause_resume">
<input type="image"
src="/static/images/$(torrent.calc_state_str)16.png"
name="pauseresume" value="submit" />
</form>
</td>
<td>$torrent.queue_pos</td>
<td style="width:100px; overflow:hidden;white-space: nowrap">
$(crop(torrent.name, 40))</td>
<td>$fsize(torrent.total_size)</td>
<td class="progress_bar">
<div class="progress_bar_outer">
<div class="progress_bar" style="width:$(torrent.progress)%">
$torrent.message $int(torrent.progress) %
</div>
</div>
</td>
$if (not get('category')):
<td>$torrent.category</td>
<td>$torrent.num_seeds ($torrent.total_seeds)</td>
<td>$torrent.num_peers ($torrent.total_peers)</td>
<td>
$if (torrent.download_rate):
$fspeed(torrent.download_rate)
$else:
&nbsp;
</td>
<td>
$if (torrent.upload_rate):
$fspeed(torrent.upload_rate)
$else:
&nbsp;
</td>
<td>$torrent.eta</td>
<td>$("%.3f" % torrent.distributed_copies)</td>
<td>$("%.3f" % torrent.ratio)</td\>
</tr>
</tbody>
</table>
</div>
$:part_stats()
<div class="panel" id="info_panel_div">
<iframe
style="border-style:hidden;width:740px;height:200px;"
id="torrent_info">
</iframe>
</div>
<br />
<script language='javascript'>
/*on_click_action = open_details;*/
on_click_action = on_click_row_js;
reselect_rows();
var id = state.selected_rows[0];
if (id) {
open_inner_details(id);
}
</script>
<form id='toolbar_form' method="POST" action="changed by javascript">
</form>
$:render.footer()

View File

@ -0,0 +1,37 @@
$def with (filter_tabs, category_tabs)
<form method="GET" id="category_form">
<input type="hidden" name="sort" value="$get('sort')">
<input type="hidden" name="order" value="$get('order')">
<select name='filter' id='filter'
onchange="document.getElementById('category_form').submit()"
title="$_('Filter on state')">
$for tab in filter_tabs:
<option value="$tab.filter"
$if tab.filter == get('filter'):
selected
>
$tab.title
</option>
</select>
<select name='category' id='category'
onchange="document.getElementById('category_form').submit()"
title="$_('Filter on Tracker')">
$for tab in category_tabs:
<option value="$tab.category"
$if tab.category == get('category'):
selected
>
$tab.title
</option>
</select>
<input type="image" id='toolbar_refresh'
src='/static/images/tango/view-refresh.png'
title='$_('Refresh')'
>
</form>

View File

@ -0,0 +1,36 @@
$def with (stats)
<div class="panel" id='refresh_panel'>
$_('Auto refresh:')
$if getcookie('auto_refresh') == '1':
($getcookie('auto_refresh_secs')) $_('seconds') &nbsp;
$:render.part_button('GET', '/refresh/set', _('Set'), 'tango/preferences-system.png')
$:render.part_button('POST', '/refresh/off', _('Disable'), 'tango/process-stop.png')
$else:
$_('Off') &nbsp;
$:render.part_button('POST', '/refresh/on', _('Enable'), 'tango/view-refresh.png')
$#end
</div>
<div class="panel" id='stats_panel'>
<!--<a href='/config'>-->
$_('Connections') : $stats.num_connections ($stats.max_num_connections)
$_('Down Speed') : $stats.download_rate ($stats.max_download)
$_('Up Speed') : $stats.upload_rate ($stats.max_upload)
<!--</a>-->
</div>
<div id='about'>
<a href='/about'>$_('About')</a>
</div>

View File

@ -0,0 +1,35 @@
$def with (method, func, title, image='')
<div class="deluge_button">
<form method="$method" action="$url" class="deluge_button">
<input type="hidden" name="redir" value="$self_url()">
$if (get_config('button_style') == 0):
<button type="submit" class="deluge_button" alt="$title">
$title
$if image:
<image src="/static/images/$image" class="button" alt="$title"/>
</button>
$if (get_config('button_style') == 1):
$if image:
<input type="image" image src="/static/images/$image" class="img_button" alt="$title"/>
$else:
<button type="submit" class="deluge_button" alt="$title">
$title
</button>
$if (get_config('button_style') == 2):
<button type="submit" class="deluge_button" alt="$title">
$title
</button>
</form>
</div>
<!--
pause
start
up
down
-->

View File

@ -0,0 +1,263 @@
/* ----------------------------------------------------------- Theme Name: Simple Theme URI: http://deluge-torrent.org Description: Deluge Theme Version: 1.0 ----------------------------------------------------------- */ BODY { background: #304663 url(../../static/images/simple_bg.jpg) repeat-x; font-family: Bitstream Vera,Verdana; font-size: 10pt; margin: 0;
padding:0;
border:0; } /* GENERIC STYLES */ a img {border: 0px} hr {color: #627082; margin: 15px 0 15px 0;}
td {font-family: Bitstream Vera,Verdana;}
tr {font-family: Bitstream Vera,Verdana;}
table {font-family: Bitstream Vera,Verdana;} div {font-family: Bitstream Vera,Verdana;} /* STRUCTURE */ #page { min-width: 800px; margin-left: auto; margin-right: auto;
margin: 0;
padding:0;
font-family: Bitstream Vera,Verdana; } #main_content { background:url(../../static/images/simple_line.jpg) repeat-x;
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;
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{
background-color: #37506f;
border:1px solid #68a;
background: #99acc3;
color: #000;
/*vertical-align:middle;*/
-moz-border-radius:5px;
/*margin-top: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*/ }
div.progress_bar_outer { /*used in table-view*/
width:150px;
}
td.progress_bar { white-space: nowrap; } td.info_label { font-weight: bold; } td { font-size: 10pt; color: #d1dae5; white-space: nowrap; } tr { font-size: 10pt; color: #d1dae5; }
div.panel {
padding:10px;
width:750px;
background-color: #37506f;
-moz-border-radius:10px; /*ff-only!*/
margin-top:10px;
margin-bottom:10px;
}
/*New styles:*/
div.deluge_button {
display:inline;
}
form.deluge_button {
display:inline;
}
button.deluge_button {
background-color: #37506f;
border:1px solid #68a;
background: #99acc3;
color: #000;
vertical-align:middle;
-moz-border-radius:7px;
}
button.deluge_button:hover {
background-color:#68a;
}
div.error {
background-color:#FFFFFF;
color:#AA0000;
font-weight:bold;
-moz-border-radius:10px;
width:200px;
margin-bottom:20px;
padding:10px;
}
tr.torrent_table:hover {
background-color:#68a;
}
tr.altrow0:hover {
background-color:#68a;
}
tr.altrow1:hover {
background-color:#68a;
}
tr.altrow1{
background-color: #37506f;
}
tr.torrent_table_selected {
background-color:#900;
}
th.torrent_table:hover {
background-color:#68a;
}
th.torrent_table {
background-color: #37506f;
}
img.button {
margin-bottom:0px;
padding:0px;
position:relative;
top:2px;
}
body.inner {
background:none;
}
#stats_panel {
-moz-border-radius:0px;
width:100%;
position:fixed;
bottom:0px;
left:0px;
background-color:#304663;
margin: 0;
padding:0;
text-align:left;
height:20px;
background-color:#ddd;
color:#000;
border-style:solid;
border:0;
border-top:1px;
border-color:#000;
}
#about {
position:fixed;
bottom:0px;
right:10px;
}
#info_panel_div2 {
position:fixed;
bottom:10px;
right:0px;
width:100%;
background-color:#304663;
}
#refresh_panel {
-moz-border-radius:0px;
width:350px;
position:fixed;
bottom:0px;
right:0px;
background-color:#304663;
margin: 0;
padding:0;
text-align:right;
height:20px;
background-color:#ddd;
color:#000;
z-index:999;
}
#refresh_panel button {
background-color:#304663;
color:#FFFFFF;
border:0;
position:relative;
top:0px;
height:20px;
background-color:#ddd;
color:#000;
}
#refresh_panel button:hover {
text-decoration: underline;
}
#category_panel {
margin-bottom:0;
padding-bottom:0;
-moz-border-radius-bottomleft:0px;
-moz-border-radius-bottomright:0px;
padding-right:32px;
}
#toolbar {
text-align:left;
margin-top:0;
padding-top:0;
margin-bottom: 30px;
-moz-border-radius-topleft:0px;
-moz-border-radius-topright:0px;
padding-top:5px;
padding-bottom:5px;
margin-bottom: 15px;
padding-left:32px;
height:20px;
}
#toolbar select{
/*border:1px solid #68a;*/
border:0;
background-color: #37506f;
color: #FFF;
}
#toolbar select:hover{
background-color:#68a;
}
a.toolbar_btn {
width:20px;
height:20px;
padding-left:3px;
padding-top:7px;
padding-right:3px;
text-decoration: none;
margin-bottom:3px;
}
a.toolbar_btn:hover {
background-color:#68a;
-moz-border-radius:5px;
text-decoration: none;
}
#toolbar_refresh {
margin:0;
border:0;
background-color:none;
padding-left:2px;
padding-top:2px;
padding-right:2px;
text-decoration: none;
background-color: #37506f;
position:relative;
top:5px;
}
#toolbar_refresh:hover {
background-color:#68a;
-moz-border-radius:5px;
text-decoration: none;
}
#category_form{
display:inline;
position:relative;
top:-3px;
padding-left:20px;
}
form { /*all forms!*/
margin:0;
padding:0;
border:0;
}
#torrent_list {
-moz-border-radius:7px;
}
/* Hides from IE-mac \*/ * html .clearfix {height: 1%;} .clearfix {display: block;} /* End hide from IE-mac */

View File

@ -0,0 +1,148 @@
/*
all javascript is optional, everything should work web 1.0
but javascript may/will enhance the experience.
i'm not a full time web-dev so don't expect beautifull patterns.
There's so much crap out there,i can't find good examples.
so i'd rather start from scratch,
Probably broken in an unexpected way , but worksforme.
*/
state = {
'row_js_continue':true
,'selected_rows': new Array()
};
function $(el_id){
return document.getElementById(el_id)
}
function get_row(id){
return $('torrent_' + id);
}
function on_click_row(e,id) {
/*filter out web 1.0 events for detail-link and pause*/
if (state.row_js_continue) {
on_click_action(e,id);
}
state.row_js_continue = true;
}
function on_click_row_js(e, id) {
/*real onClick event*/
if (!e.ctrlKey) {
deselect_all_rows();
select_row(id);
open_inner_details(id);
}
else if (state.selected_rows.indexOf(id) != -1) {
deselect_row(id);
}
else{
select_row(id);
open_inner_details(id);
}
}
function select_row(id){
var row = get_row(id);
if (row) {
if (!(row.default_class_name)) {
row.default_class_name = row.className;
}
row.className = 'torrent_table_selected';
state.selected_rows[state.selected_rows.length] = id;
setCookie('selected_rows',state.selected_rows);
}
}
function deselect_row(id){
var row = get_row(id);
if (row) {
row.className = row.default_class_name
/*remove from state.selected_rows*/
var idx = state.selected_rows.indexOf(id);
state.selected_rows.splice(idx,1);
setCookie('selected_rows',state.selected_rows);
}
}
function deselect_all_rows(){
/*unbind state.selected_rows from for..in:
there must be a better way to do this*/
var a = new Array()
for (i in state.selected_rows) {
a[a.length] = state.selected_rows[i];
}
for (i in a){
deselect_row(a[i]);
}
}
function reselect_rows(){
var selected_rows = getCookie('selected_rows').split(',');
for (i in getCookie('selected_rows')) {
select_row(selected_rows[i]);
}
}
function open_details(e, id){
alert(id);
window.location.href = '/torrent/info/' + id;
}
function open_inner_details(id){
/*probably broken for IE, use FF!*/
$('torrent_info').src = '/torrent/info_inner/' + id;
}
function on_click_do_nothing(e, id){
}
on_click_action = on_click_do_nothing;
/*toobar buttons, */
function toolbar_post(url, selected) {
if ((!selected) || (state.selected_rows.length > 0)) {
var ids = state.selected_rows.join(',');
var form = $('toolbar_form');
form.action = url +ids;
form.submit();
}
return false;
}
function toolbar_get(url , selected) {
if (!selected) {
window.location.href = url
}
else if (state.selected_rows.length > 0) {
var ids = state.selected_rows.join(',');
window.location.href = url +ids;
}
return false;
}
/*stuff copied from various places:*/
/*http://www.w3schools.com/js/js_cookies.asp*/
function setCookie(c_name,value,expiredays)
{
var exdate=new Date()
exdate.setDate(exdate.getDate()+expiredays)
document.cookie=c_name+ "=" +escape(value)+
((expiredays==null) ? "" : ";expires="+exdate.toGMTString())
}
function getCookie(c_name)
{
if (document.cookie.length>0)
{
c_start=document.cookie.indexOf(c_name + "=")
if (c_start!=-1)
{
c_start=c_start + c_name.length+1
c_end=document.cookie.indexOf(";",c_start)
if (c_end==-1) c_end=document.cookie.length
return unescape(document.cookie.substring(c_start,c_end))
}
}
return ""
}

View File

@ -0,0 +1,106 @@
/*Taken from:
http://www.imaputz.com/cssStuff/bigFourVersion.html
*/
/* define height and width of scrollable area. Add 16px to width for scrollbar */
div.tableContainer {
clear: both;
/*border: 1px solid #963;*/
height: 285px;
overflow: auto;
width: 756px;
}
/* Reset overflow value to hidden for all non-IE browsers. */
html>body div.tableContainer {
overflow: hidden;
width: 756px
}
/* define width of table. IE browsers only */
div.tableContainer table {
float: left;
width: 740px;
}
/* define width of table. Add 16px to width for scrollbar. */
/* All other non-IE browsers. */
html>body div.tableContainer table {
width: 756px
}
/* set table header to a fixed position. WinIE 6.x only */
/* In WinIE 6.x, any element with a position property set to relative and is a child of */
/* an element that has an overflow property set, the relative value translates into fixed. */
/* Ex: parent element DIV with a class of tableContainer has an overflow property set to auto */
thead.fixedHeader tr {
position: relative
}
/* set THEAD element to have block level attributes. All other non-IE browsers */
/* this enables overflow to work on TBODY element. All other non-IE, non-Mozilla browsers */
html>body thead.fixedHeader tr {
display: block
}
/* define the table content to be scrollable */
/* set TBODY element to have block level attributes. All other non-IE browsers */
/* this enables overflow to work on TBODY element. All other non-IE, non-Mozilla browsers */
/* induced side effect is that child TDs no longer accept width: auto */
html>body tbody.scrollContent {
display: block;
height: 262px;
overflow: auto;
width: 100%
}
/* make TD elements pretty. Provide alternating classes for striping the table */
/* http://www.alistapart.com/articles/zebratables/ */
tbody.scrollContent td, tbody.scrollContent tr.normalRow td {
/*background: #FFF;*/
border-bottom: none;
border-left: none;
/*border-right: 1px solid #CCC;
border-top: 1px solid #DDD;*/
padding: 2px 3px 3px 4px
}
tbody.scrollContent tr.alternateRow td {
/*background: #EEE;*/
border-bottom: none;
border-left: none;
/*border-right: 1px solid #CCC;
border-top: 1px solid #DDD;*/
padding: 2px 3px 3px 4px
}
/* define width of TH elements: 1st, 2nd, and 3rd respectively. */
/* Add 16px to last TH for scrollbar padding. All other non-IE browsers. */
/* http://www.w3.org/TR/REC-CSS2/selector.html#adjacent-selectors */
html>body thead.fixedHeader th {
width: 200px
}
html>body thead.fixedHeader th + th {
width: 240px
}
html>body thead.fixedHeader th + th + th {
width: 316px
}
/* define width of TD elements: 1st, 2nd, and 3rd respectively. */
/* All other non-IE browsers. */
/* http://www.w3.org/TR/REC-CSS2/selector.html#adjacent-selectors */
html>body tbody.scrollContent td {
width: 200px
}
html>body tbody.scrollContent td + td {
width: 240px
}
html>body tbody.scrollContent td + td + td {
width: 300px
}

View File

@ -0,0 +1,15 @@
$def with (torrent)
<!--for iframe in javascript mode.-->
<html><head>
<html>
<head>
<title>Deluge:$torrent.name</title>
<link rel="icon" href="/static/images/deluge_icon.gif" type="image/gif" />
<link rel="shortcut icon" href="/static/images/deluge_icon.gif" type="image/gif" />
<link rel="stylesheet" type="text/css" href="/static/simple_site_style.css" />
</head>
<body class="inner">
<!--[Tab :Details] [Tab : Files] [Tab : Peers]-->
$:render.tab_meta(torrent)
$:render.footer()

View File

@ -0,0 +1,49 @@
$:render.header(_('About'))
<div class="panel" style="text-align:left">
<h2>Version</h2>
<pre>$version </pre>
<h2>Links</h2>
<ul>
<li><a href="http://deluge-torrent.org">Deluge</a></li>
<li><a href="http://forum.deluge-torrent.org/viewtopic.php?f=9&t=425">
WebUi forum Thread</a>
</li>
<li><a href="http://www.gnu.org/licenses/old-licenses/gpl-2.0.html">GPL v2
</a></li>
</ul>
<h2>Authors</h2>
<ul>
<h3>WebUi</h3>
<ul>
<li>Martijn Voncken</li>
</ul>
<h3>Template</h3>
<ul>
<li>Martijn Voncken</li>
<li>somedude</li>
</ul>
<h3>Deluge</h3>
<ul>
<li>Zach Tibbitts</li>
<li>Alon Zakai</li>
<li>Alon Zakai</li>
<li>Marcos Pinto</li>
<li>Andrew Resch</li>
<li>Alex Dedul</li>
</ul>
<h3>Windows Port</h3>
<ul>
<li>Slurdge</li>
</ul>
</ul>
*and all other authors/helpers/contributors I forgot to mention.
</div>
$:render.footer()

View File

@ -0,0 +1,5 @@
-first layout taken from deluge website
improved by:
-mvoncken
-somedude

View File

@ -0,0 +1,10 @@
$def with (form)
$:render.header(_('Config'))
<div class="error">Not Implemented!</div>
<form method="POST">
$:form.render()
<input type="submit" value="$_('Apply')"/>
</form>
$:render.footer()

View File

@ -0,0 +1,6 @@
$def with (error_msg)
$:render.header(_('Error'))
<pre class="error">
$error_msg
</pre>
$:render.footer()

View File

@ -0,0 +1,6 @@
</center>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,23 @@
$def with (title)
<html>
<head>
<title>Deluge:$title</title>
<link rel="icon" href="/static/images/deluge_icon.gif" type="image/gif" />
<link rel="shortcut icon" href="/static/images/deluge_icon.gif" type="image/gif" />
<link rel="stylesheet" type="text/css" href="/static/simple_site_style.css" />
</head>
<body>
<div id="page">
<a href='/home'>
<div id="simple_logo">
<div class="title">Deluge</div>
<div class="info">$title</div>
</div>
</a>
<div id="main_content">
<div id="main">
<center>

View File

@ -0,0 +1,61 @@
$def with (torrent_list, all_torrents)
$:render.header(_('Torrent list'))
<table class="torrent_list" border=0 id='torrent_table'>
<tr>
$:(sort_head('calc_state_str', 'S'))
$:(sort_head('queue_pos', '#'))
$:(sort_head('name', _('Name')))
$:(sort_head('total_size', _('Size')))
$:(sort_head('progress', _('Progress')))
$:(sort_head('num_seeds', _('Seeders')))
$:(sort_head('num_peers', _('Peers')))
$:(sort_head('download_rate', _('Download')))
$:(sort_head('upload_rate', _('Upload')))
$:(sort_head('eta', _('Eta')))
$:(sort_head('distributed_copies', _('Ava')))
$:(sort_head('ratio', _('Ratio')))
</tr>
$#4-space indentation is mandatory for for-loops in templetor!
$for torrent in torrent_list:
<tr class="torrent_table" id="torrent_$torrent.id">
<td>
<form action="/torrent/$torrent.action/$torrent.id" method="POST" class="pause_resume">
<input type="image"
src="/static/images/$(torrent.calc_state_str)16.png"
name="pauseresume" value="submit" /></form></td>
<td>$torrent.queue_pos</td>
<td style="width:100px; overflow:hidden;white-space: nowrap">
<a href="/torrent/info/$torrent.id" >
$(crop(torrent.name, 40))</a></td>
<td>$fsize(torrent.total_size)</td>
<td class="progress_bar">
<div class="progress_bar_outer">
<div class="progress_bar" style="width:$(torrent.progress)%">
$torrent.message $int(torrent.progress) %
</div>
</div>
</td>
<td>$torrent.num_seeds ($torrent.total_seeds)</td>
<td>$torrent.num_peers ($torrent.total_peers)</td>
<td>$fspeed(torrent.download_rate)</td>
<td>$fspeed(torrent.upload_rate)</td>
<td>$torrent.eta</td>
<td>$("%.3f" % torrent.distributed_copies)</td>
<td>$("%.3f" % torrent.ratio)</td\>
</tr>
</table>
<div class="panel">
$:render.part_button('GET', '/torrent/add', _('Add torrent'), 'tango/list-add.png')
$:render.part_button('POST', '/pause_all', _('Pause all'), 'tango/pause.png')
$:render.part_button('POST', '/resume_all', _('Resume all'), 'tango/start.png')
<!--$:render.part_button('POST', '/logout', _('Logout'), 'tango/system-log-out.png')-->
</div>
$:part_stats()
$:render.footer()

View File

@ -0,0 +1,25 @@
$def with (error)
$:render.header(_('Login'))
<div class="panel">
$if error > 0:
<div class="error">$_("Password is invalid,try again")</div>
<form method="POST" id="loginform" action='/login'>
<input type="hidden" name="redir" value="$get('redir')">
<div id="loginpanel">
<div class="form_row">
<span class="form_label">$_('Password')</span>
<input type="password" name="pwd" id="pwd" class="form_input">
</div>
<div class="form_row">
<span class="form_label"></span>
<input type="submit" name="submit"
id="submit" value="Submit" class="form_input">
</div>
<br />
<a href="/about">$_('About')</a>
</div>
</form>
</div>
$:render.footer()

View File

@ -0,0 +1,26 @@
$def with (method, url, title, image='')
<div class="deluge_button">
<form method="$method" action="$url" class="deluge_button">
<input type="hidden" name="redir" value="$self_url()">
$if (get_config('button_style') == 0):
<button type="submit" class="deluge_button" alt="$title">
$title
$if image:
<image src="/static/images/$image" class="button" alt="$title"/>
</button>
$if (get_config('button_style') == 1):
$if image:
<input type="image" image src="/static/images/$image" class="img_button" alt="$title"/>
$else:
<button type="submit" class="deluge_button" alt="$title">
$title
</button>
$if (get_config('button_style') == 2):
<button type="submit" class="deluge_button" alt="$title">
$title
</button>
</form>
</div>

View File

@ -0,0 +1,35 @@
$def with (stats)
<div class="panel" id='refresh_panel'>
$_('Auto refresh:')
$if getcookie('auto_refresh') == '1':
($getcookie('auto_refresh_secs')) $_('seconds') &nbsp;
$:render.part_button('GET', '/refresh/set', _('Set'), 'tango/preferences-system.png')
$:render.part_button('POST', '/refresh/off', _('Disable'), 'tango/process-stop.png')
$else:
$_('Off') &nbsp;
$:render.part_button('POST', '/refresh/on', _('Enable'), 'tango/view-refresh.png')
$#end
</div>
<div class="panel" id='stats_panel'>
<!--<a href='/config'>-->
$_('Connections') : $stats.num_connections ($stats.max_num_connections)
$_('Down Speed') : $stats.download_rate ($stats.max_download)
$_('Up Speed') : $stats.upload_rate ($stats.max_upload)
<!--</a>-->
<span id=#about>
(<a href='/about'>$_('About')</a>)
</span>
</div>

View File

@ -0,0 +1,11 @@
$:render.header(_('Set Timeout'))
<div class="panel">
<form action="/refresh/set" method="POST">
$_('Refresh page every:')
<input type="text" name="refresh" value="$getcookie('auto_refresh_secs')"
size="3">
$_('seconds')
<input type="submit" value="$_('Set')">
</form>
</div>
$:render.footer()

View File

@ -0,0 +1,12 @@
$def with (column_id, column_name, order, active_up, active_down)
<th class="torrent_table">
<a href="/index?sort=$column_id&order=$order&filter=$get('filter')&category=$get('category')">
$column_name\
$if active_up:
<img src="/static/images/tango/up.png" />
$if active_down:
<img src="/static/images/tango/down.png" />
</a>
</th>

View File

@ -0,0 +1,85 @@
$def with (torrent)
<table width="100%"><tr>
<td colspan=3 style="background-color:#999;-moz-border-radius:5px;">
<div class="progress_bar" style="width:$torrent.progress%;
text-align:center;font-weight:bold;">
$torrent.progress %</div>
</td>
</tr><td width=30%%>
<table>
<tr><td class="info_label">$_('Downloaded'):</td>
<td class="info_value">$torrent.calc_total_downloaded</td></tr>
<tr><td class="info_label">$_('Uploaded'):</td>
<td class="info_value">$torrent.calc_total_uploaded</td>
</tr>
<tr><td class="info_label">$_('Seeders'):</td>
<td class="info_value">$torrent.num_seeds ($torrent.total_seeds )</td></tr>
<tr><td class="info_label">$_('Share Ratio'):</td>
<td class="info_value">$("%.3f" % torrent.ratio)</td></tr>
<tr><td class="info_label">$_('Pieces'):</td>
<td class="info_value">$torrent.num_pieces x $fsize(torrent.piece_length) </td>
</tr>
<tr><td class="info_label">&nbsp;</td>
<td class="info_value">&nbsp; </td>
</table>
</td><td width=30%%>
<table>
<tr><td class="info_label">$_('Speed'):</td><td class="info_value">
$fspeed(torrent.download_rate)</td></td></tr>
<tr><td class="info_label">$_('Speed'):</td>
<td class="info_value">$fspeed(torrent.upload_rate)</td></tr>
<tr><td class="info_label">$_('Peers'):</td>
<td class="info_value">$torrent.num_peers ($torrent.total_peers )</td></tr>
<tr><td class="info_label">$_('ETA'):</td>
<td class="info_value">$torrent.eta </td></tr>
<tr><td class="info_label">$_('Availability'):</td>
<td class="info_value">$("%.3f" % torrent.distributed_copies)</td></td></tr>
<tr><td class="info_label">&nbsp;</td>
<td class="info_value">&nbsp; </td>
</tr>
</table>
</td><td width=30%%>
<table>
<tr><td class="info_label">$_('Total Size'):</td>
<td class="info_value">$fspeed(torrent.total_size)</td></tr>
<tr><td class="info_label">$_('# Of Files'):</td>
<td class="info_value">$torrent.num_files</td></tr>
<tr><td class="info_label">$_('Tracker'):</td>
<td class="info_value" title="$torrent.tracker">$(crop(torrent.tracker, 30))</td></tr>
<tr><td class="info_label">$_('Tracker Status'):</td>
<td class="info_value" title="$torrent.tracker_status">$(crop(torrent.tracker_status, 30)) </td></tr>
<tr><td class="info_label">$_('Next Announce'):</td>
<td class="info_value">$torrent.next_announce </td></tr>
<tr><td class="info_label">$_('Queue Position'):</td>
<td class="info_value">$torrent.queue_pos </td>
</tr>
</table>
</table>

View File

@ -0,0 +1,23 @@
$:render.header(_("Add Torrent"))
<div class="panel">
<form method="POST" action="/torrent/add" ENCTYPE="multipart/form-data">
<input type="hidden" name="redir" value="$get('redir')">
<div id="torrent_add">
<div class="form_row">
<span class="form_label">$_('Url')</span>
<input type="text" name="url" class="form_input" size=60
value="$get('url')" >
</div>
<div class="form_row">
<span class="form_label">$_('Upload torrent')</span>
<input type="file" name="torrent" class="form_input" size=50>
</div>
<div class="form_row">
<span class="form_label"></span>
<input type="submit" name="submit"
value="$_('Submit')" class="form_input">
</div>
</div>
</form>
</div>
$:render.footer()

View File

@ -0,0 +1,32 @@
$def with (torrent_ids, torrent_list)
$:render.header(_("Remove torrent"))
<div class="panel">
<form method="POST" action='/torrent/delete/$torrent_ids'>
<input type="hidden" name="redir" value="$get('redir')">
<div id="del_torrent">
<h2>$_("Remove torrent")</h2>
<ul>
$for torrent in torrent_list:
<li>$torrent.name</li>
</ul>
<div class="form_row2">
<span class="form_label2">
<input type="checkbox" name="torrent_also" class="form_input" checked
value="1">$_('Delete .torrent file')</span>
</div>
<div class="form_row2">
<span class="form_label2">
<input type="checkbox" name="data_also" class="form_input"
value="1">$_('Delete downloaded files.')</span>
</div>
<div class="form_row2">
<span class="form_label2"></span>
<input type="submit" name="submit"
value="$_('Remove')" class="form_input">
</div>
</div>
</form>
</div>
$:render.footer()

View File

@ -0,0 +1,50 @@
$def with (torrent)
$:(render.header(torrent.message + '/' + torrent.name))
<div class="panel">
<h3>$_('Details')</h3>
$:render.tab_meta(torrent)
$if (torrent.action == 'start'):
$:render.part_button('POST', '/torrent/start/' + str(torrent.id), _('Resume'), 'tango/start.png')
$else:
$:render.part_button('POST', '/torrent/stop/' + str(torrent.id), _('Pause'), 'tango/pause.png')
$:render.part_button('GET', '/torrent/delete/' + str(torrent.id), _('Remove'), 'tango/list-remove.png')
$:render.part_button('POST', '/torrent/reannounce/' + str(torrent.id), _('Reannounce'), 'tango/view-refresh.png')
$:render.part_button('POST', '/torrent/queue/up/' + str(torrent.id), _('Queue Up'), 'tango/queue-up.png')
$:render.part_button('POST', '/torrent/queue/down/' + str(torrent.id), _('Queue Down'), 'tango/queue-down.png')
<br>
<!--
[<a onclick="javascript:toggle_dump()">$_('Debug:Data Dump')</a>]
<pre style="background-color:white;color:black;display:none" id="data_dump">
$for key in sorted(torrent):
$key : $torrent[key]
</pre>
<script language="javascript">
function toggle_dump(){
el = document.getElementById("data_dump");
if (el.style.display == "block"){
el.style.display = "none";
}
else{
el.style.display = "block";
}
}
</script>
-->
</div>
$:part_stats()
$:render.footer()

View File

@ -0,0 +1,39 @@
Quickstart:
Just copy and rename an existing template.
-The settings panel will see all directory's in this folder ,and let you choose your new template.
-Clicking Ok in the settings panel will restart the webserver and reload your template.
Limited "Subclassing":
All templates are "subclassed" from the /deluge/ template.
If a html file is not found in the template dir, the file from /deluge/ will be used.
Notes:
Please configure your editor to use 4-space indents instead of tabs.
Or use scite and my config: http://mvoncken.sohosted.com/deluge/SciTEUser.properties.txt
template language: http://webpy.org/templetor
Exposed methods and variables (c&p from webserver_framework.py):
template.Template.globals.update({
'sort_head': template_sort_head,
'part_stats':template_part_stats,
'crop': template_crop,
'_': _ , #gettext/translations
'str': str, #because % in templetor is broken.
'sorted': sorted,
'get_config': get_config,
'self_url': self_url,
'fspeed': common.fspeed,
'fsize': common.fsize,
'render': ws.render, #for easy resuse of templates
'rev': 'rev.%s' % (REVNO, ),
'version': VERSION,
'getcookie':getcookie,
'get': lambda (var): getattr(web.input(**{var:None}), var) # unreadable :-(
})
I will update this file if there is interest in making templates.

View File

@ -0,0 +1,382 @@
"""
Testing the REST api, not the units.
unittest the right way feels so unpythonic :(
!! BIG FAT WARNING !!: this test deletes active torrents .
!! BIG FAT WARNING 2!!: this test hammers the tracker that is tested against.
"""
import unittest
import cookielib, urllib2 , urllib
import WebUi.webserver_common as ws
import operator
ws.init_05()
print 'test-env=',ws.ENV
#CONFIG:
BASE_URL = 'http://localhost:8112'
PWD = 'deluge'
def get_status(id):
return ws.proxy.get_torrent_status(id,ws.TORRENT_KEYS)
#BASE:
#303 = see other
#404 = not found
#500 = server error
#200 = OK, page exists.
class TestWebUiBase(unittest.TestCase):
def setUp(self):
#cookie aware-opener that DOES NOT use redirects.
opener = urllib2.OpenerDirector()
self.cj = cookielib.CookieJar()
for handler in [urllib2.HTTPHandler(),urllib2.HTTPDefaultErrorHandler(),
urllib2.FileHandler(),urllib2.HTTPErrorProcessor(),
urllib2.HTTPCookieProcessor(self.cj)]:
opener.add_handler(handler)
#/opener
self.opener = opener
def open_url(self, page, post=None):
url = BASE_URL + page
if post == 1:
post = {'Force_a_post' : 'spam'}
if post:
post = urllib.urlencode(post)
r = self.opener.open(url , data = post)
#BUG: error-page does not return status 500, but status 200
#workaround...
data = r.read()
if '<!--ERROR-MARKER-->' in data:
error = IOError()
error.code = 500
#print data
raise error
if r.code <> 200:
fail('no code 200, error-code=%s' % r.code)
return r
def get_cookies(self):
return dict((c.name,c.value) for c in self.cj)
cookies = property(get_cookies)
def assert_status(self,status, page, post):
try :
r = self.open_url(page, post)
except IOError,e:
self.assertEqual(e.code, status)
else:
self.fail('page was found "%s" (%s)' % (page, r.code ))
def assert_404(self, page, post = None):
self.assert_status(404, page, post)
def assert_500(self, page, post = None):
self.assert_status(500, page, post)
def assert_303(self, page, redirect_to, post=None):
try :
r = self.open_url(page, post)
except IOError,e:
self.assertEqual(e.code, 303)
self.assertEqual(e.headers['Location'], redirect_to)
else:
#print r
self.fail('No 303!')
def assert_exists(self, page, post = None):
try :
r = self.open_url(page, post)
except IOError,e:
self.fail('page was not found "%s" (%s)' % (page, e.code))
else:
pass
first_torrent_id = property(lambda self: ws.proxy.get_session_state()[0])
first_torrent = property(lambda self: get_status(self.first_torrent_id))
class TestNoAuth(TestWebUiBase):
def test303(self):
self.assert_303('/','/login')
self.assert_303('','/login')
self.assert_303('/index','/login')
#self.assert_303('/torrent/pause/','/login')
self.assert_303('/config','/login')
self.assert_303('/torrent/info/','/login')
def test404(self):
self.assert_404('/torrent/info')
self.assert_404('/garbage')
#self.assert_404('/static/garbage')
#self.assert_404('/template/static/garbage')
self.assert_404('/torrent/pause/', post=1)
def testOpen(self):
self.assert_exists('/login')
self.assert_exists('/about')
def testStatic(self):
self.assert_exists('/static/images/simple_line.jpg')
self.assert_exists('/static/images/tango/up.png')
#test 404
#test template-static
class TestSession(TestWebUiBase):
def testLogin(self):
self.assert_303('/home','/login')
#invalid pwd:
self.assert_303('/login','/login?error=1',{'pwd':'invalid'})
#login
self.assert_303('/login','/index',{'pwd':PWD})
#now i'm logged-in!
#there are no sort-coockies yet so the default page is /index.
self.assert_303('/home','/index')
self.assert_exists('/index')
self.assert_exists('/config')
self.assert_exists('/torrent/add')
self.assert_303('/','/index')
self.assert_303('','/index')
#logout
self.assert_303('/logout','/login', post=1)
#really logged out?
self.assert_303('/','/login')
self.assert_303('','/login')
self.assert_303('/index','/login')
self.assert_303('/torrent/add','/login')
self.assert_exists('/about')
def testRefresh(self):
#starting pos
self.assert_303('/login','/index',{'pwd':PWD})
r = self.open_url('/index')
assert not 'auto_refresh' in self.cookies
assert not 'auto_refresh_secs' in self.cookies
assert not r.headers.has_key('Refresh')
#on:
self.assert_303('/refresh/on','/index', post=1)
assert 'auto_refresh' in self.cookies
assert 'auto_refresh_secs' in self.cookies
self.assertEqual(self.cookies['auto_refresh'],'1')
self.assertEqual(self.cookies['auto_refresh_secs'],'10')
r = self.open_url('/index')
assert r.headers['Refresh'] == '10 ; url=/index'
#set:
self.assert_303('/refresh/set','/index',{'refresh':'5'})
self.assertEqual(self.cookies['auto_refresh_secs'],'5')
r = self.open_url('/index')
assert r.headers['Refresh'] == '5 ; url=/index'
self.assert_500('/refresh/set',{'refresh':'a string'})
#off:
self.assert_303('/refresh/off','/index', post=1)
self.assertEqual(self.cookies['auto_refresh'],'0')
self.assertEqual(self.cookies['auto_refresh_secs'],'5')
r = self.open_url('/index')
assert not 'Refresh' in r.headers
class TestIntegration(TestWebUiBase):
initialized = False
def setUp(self):
TestWebUiBase.setUp(self)
self.assert_303('/login','/index',{'pwd':PWD})
self.urls = sorted([
'http://torrents.aelitis.com:88/torrents/azplatform2_1.13.zip.torrent',
'http://torrents.aelitis.com:88/torrents/azplugins_2.1.4.jar.torrent',
'http://torrents.aelitis.com:88/torrents/azautoseeder_0.1.1.jar.torrent'
])
torrent_ids = ws.proxy.get_session_state()
#avoid hammering, investigate current torrent-list and do not re-add.
#correct means : 3 torrent's in list (for now)
if len(torrent_ids) <> 3:
#delete all, nice use case for refactoring delete..
torrent_ids = ws.proxy.get_session_state()
for torrent in torrent_ids:
ws.proxy.remove_torrent([torrent], False, False)
torrent_ids = ws.proxy.get_session_state()
self.assertEqual(torrent_ids, [])
#add 3 using url.
for url in self.urls:
self.assert_303('/torrent/add','/index',{'url':url,'torrent':None})
#added?
self.torrent_ids = ws.proxy.get_session_state()
self.assertEqual(len(self.torrent_ids), 3)
else:
#test correctness of existing-list
#The setup makes 0.6 fail everything, added an else..
for url in self.urls:
if ws.ENV.startswith('0.5'):
self.assert_500('/torrent/add',{'url':url,'torrent':None})
else:
self.assert_303('/torrent/add','/index',{'url':url,'torrent':None})
def testPauseResume(self):
#pause all
self.assert_303('/pause_all','/index', post=1)
#pause worked?
pause_status = [get_status(id)["user_paused"] for id in ws.proxy.get_session_state()]
for paused in pause_status:
self.assertEqual(paused, True)
#resume all
self.assert_303('/resume_all','/index', post=1)
#resume worked?
pause_status = [get_status(id)["user_paused"] for id in ws.proxy.get_session_state()]
for paused in pause_status:
self.assertEqual(paused,False)
#pause again.
self.assert_303('/pause_all','/index', post=1)
torrent_id = self.first_torrent_id
#single resume.
self.assert_303('/torrent/start/%s' % torrent_id ,'/index', post=1)
self.assertEqual(get_status(torrent_id)["user_paused"] ,False)
#single pause
self.assert_303('/torrent/stop/%s' % torrent_id,'/index', post=1)
self.assertEqual(get_status(torrent_id)["user_paused"] , True)
def testQueue(self):
#find last:
torrent_id = [id for id in ws.proxy.get_session_state()
if (get_status(id)['queue_pos'] ==3 )][0]
#queue
torrent = get_status(torrent_id)
self.assertEqual(torrent['queue_pos'], 3)
#up:
self.assert_303('/torrent/queue/up/%s' % torrent_id,'/index', post=1)
torrent = get_status(torrent_id)
self.assertEqual(torrent['queue_pos'], 2)
self.assert_303('/torrent/queue/up/%s' % torrent_id,'/index', post=1)
torrent = get_status(torrent_id)
self.assertEqual(torrent['queue_pos'], 1)
self.assert_303('/torrent/queue/up/%s' % torrent_id,'/index', post=1)
#upper limit
torrent = get_status(torrent_id)
self.assertEqual(torrent['queue_pos'], 1)
#down:
self.assert_303('/torrent/queue/down/%s' % torrent_id,'/index', post=1)
torrent = get_status(torrent_id)
self.assertEqual(torrent['queue_pos'], 2)
self.assert_303('/torrent/queue/down/%s' % torrent_id,'/index', post=1)
torrent = get_status(torrent_id)
self.assertEqual(torrent['queue_pos'], 3)
self.assert_303('/torrent/queue/down/%s' % torrent_id,'/index', post=1)
#down limit
torrent = get_status(torrent_id)
self.assertEqual(torrent['queue_pos'], 3)
def testMeta(self):
#info available?
for torrent_id in ws.proxy.get_session_state():
self.assert_exists('/torrent/info/%s' % torrent_id)
self.assert_exists('/torrent/delete/%s' % torrent_id)
#no info:
self.assert_500('/torrent/info/99999999')
self.assert_500('/torrent/delete/99999999')
def testAddRemove(self):
#add a duplicate:
self.assert_500('/torrent/add', post={'url':self.urls[0],'torrent':None})
#add a 4th using url
#delete
#add torrrent-file
#./test01.torrent
def test_do_redirect(self):
self.assert_303('/home','/index')
#1
self.assert_exists('/index?sort=download_rate&order=down')
self.assert_303('/home','/index?sort=download_rate&order=down')
assert self.cookies['sort'] == 'download_rate'
assert self.cookies['order'] == 'down'
#2
self.assert_exists('/index?sort=progress&order=up')
self.assert_303('/home','/index?sort=progress&order=up')
assert self.cookies['sort'] == 'progress'
assert self.cookies['order'] == 'up'
#redir after pause-POST? in /index.
self.assert_exists('/index?sort=name&order=down')
torrent_id = self.first_torrent_id
self.assert_303('/torrent/stop/%s' % torrent_id,
'/index?sort=name&order=down', post=1)
#redir in details 1
self.assert_303('/torrent/stop/%s?redir=/torrent/info/%s' %(torrent_id,torrent_id)
,'/torrent/info/' + torrent_id, post = 1)
#redir in details 2
self.assert_303('/torrent/stop/%s' % torrent_id
,'/torrent/info/' + torrent_id ,
post={'redir': '/torrent/info/' + torrent_id})
def testRemote(self):
pass
def test_redir_after_login(self):
pass
def testReannounce(self):
torrent_id = self.first_torrent_id
self.assert_303(
'/torrent/reannounce/%(id)s?redir=/torrent/info/%(id)s'
% {'id':torrent_id}
,'/torrent/info/' + torrent_id, post = 1)
def testRecheck(self):
#add test before writing code..
#RELEASE-->disable
"""
torrent_id = self.first_torrent_id
self.assert_303(
'/torrent/recheck/%(id)s?redir=/torrent/info/%(id)s'
% {'id':torrent_id}
,'/torrent/info/' + torrent_id, post = 1)
"""
#
if False:
suiteFew = unittest.TestSuite()
suiteFew.addTest(TestSession("testRefresh"))
unittest.TextTestRunner(verbosity=2).run(suiteFew)
elif False:
suiteFew = unittest.TestSuite()
suiteFew.addTest(TestIntegration("testDoRedirect"))
unittest.TextTestRunner(verbosity=2).run(suiteFew)
else:
unittest.main()

View File

@ -0,0 +1,225 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) Martijn Voncken 2007 <mvoncken@gmail.com>
#
# 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.
"""
initializes config,render and proxy.
All hacks go here, so this is a really ugly source-file..
Support running in process0.5 ,run inside-gtk0.5 and run in process0.6
"""
import os
import deluge
import random
import pickle
import sys
import base64
from lib.webpy022 import template
random.seed()
webui_path = os.path.dirname(__file__)
ENV = 'UNKNOWN'
config_defaults = {
"port":8112,
"button_style":2,
"auto_refresh":False,
"auto_refresh_secs": 10,
"template":"advanced",
"pwd_salt":"2540626806573060601127357001536142078273646936492343724296134859793541603059837926595027859394922651189016967573954758097008242073480355104215558310954",
"pwd_md5":"\xea\x8d\x90\x98^\x9f\xa9\xe2\x19l\x7f\x1a\xca\x82u%",
"cache_templates":False,
"use_https":False
}
try:
_('translate something')
except:
import gettext
gettext.install('~/')
#log.error('no translations :(')
try:
config_dir = deluge.common.CONFIG_DIR
except:
config_dir = os.path.expanduser("~/.config/deluge")
config_file = os.path.join(config_dir,'webui.conf')
session_file = os.path.join(config_dir,'webui.sessions')
class subclassed_render(object):
"""
try to use the html template in configured dir.
not available : use template in /deluge/
"""
def __init__(self, template_dirname, cache=False):
self.base_template = template.render(
os.path.join(webui_path, 'templates/deluge/'),
cache=cache)
self.sub_template = template.render(
os.path.join(webui_path, 'templates/%s/' % template_dirname),
cache=cache)
def __getattr__(self, attr):
if hasattr(self.sub_template, attr):
return getattr(self.sub_template, attr)
else:
return getattr(self.base_template, attr)
def init_process():
globals()['config'] = pickle.load(open(config_file))
globals()['render'] = subclassed_render(config.get('template'),
config.get('cache_templates'))
def init_06():
import deluge.ui.client as proxy
from deluge.log import LOG as log
globals()['log'] = log
proxy.set_core_uri('http://localhost:58846') #How to configure this?
def add_torrent_filecontent(name , data_b64):
log.debug('monkeypatched add_torrent_filecontent:%s,len(data:%s))' %
(name , len(data_b64)))
name = name.replace('\\','/')
name = 'deluge06_' + str(random.random()) + '_' + name.split('/')[-1]
filename = os.path.join('/tmp', name)
log.debug('write: %s' % filename)
f = open(filename,"wb")
f.write(base64.b64decode(data_b64))
f.close()
proxy.add_torrent_file([filename])
proxy.add_torrent_filecontent = add_torrent_filecontent
log.debug('cfg-file %s' % config_file)
if not os.path.exists(config_file):
log.debug('create cfg file %s' % config_file)
#load&save defaults.
f = file(config_file,'wb')
pickle.dump(config_defaults,f)
f.close()
init_process()
globals()['proxy'] = proxy
globals()['ENV'] = '0.6'
def init_05():
import dbus
init_process()
bus = dbus.SessionBus()
proxy = bus.get_object("org.deluge_torrent.dbusplugin"
, "/org/deluge_torrent/DelugeDbusPlugin")
globals()['proxy'] = proxy
globals()['ENV'] = '0.5_process'
init_logger()
def init_gtk_05():
#appy possibly changed config-vars, only called in when runing inside gtk.
from dbus_interface import get_dbus_manager
globals()['proxy'] = get_dbus_manager()
globals()['config'] = deluge.pref.Preferences(config_file, False)
globals()['render'] = subclassed_render(config.get('template'),
config.get('cache_templates'))
globals()['ENV'] = '0.5_gtk'
init_logger()
def init_logger():
#only for 0.5..
import logging
logging.basicConfig(level=logging.DEBUG,
format="[%(levelname)s] %(message)s")
globals()['log'] = logging
#hacks to determine environment, TODO: clean up.
if 'env=0.5' in sys.argv:
init_05()
elif 'env=0.6' in sys.argv:
init_06()
elif hasattr(deluge, 'ui'):
init_06()
elif not hasattr(deluge,'pref'):
init_05()
#constants
REVNO = open(os.path.join(os.path.dirname(__file__),'revno')).read()
VERSION = open(os.path.join(os.path.dirname(__file__),'version')).read()
TORRENT_KEYS = ['distributed_copies', 'download_payload_rate',
'eta', 'is_seed', 'name', 'next_announce',
'num_files', 'num_peers', 'num_pieces', 'num_seeds', 'paused',
'piece_length','progress', 'ratio', 'total_done', 'total_download',
'total_payload_download', 'total_payload_upload', 'total_peers',
'total_seeds', 'total_size', 'total_upload', 'total_wanted',
'tracker_status', 'upload_payload_rate',
'uploaded_memory','tracker','state','queue_pos','user_paused']
STATE_MESSAGES = (_("Queued"),
_("Checking"),
_("Connecting"),
_("Downloading Metadata"),
_("Downloading"),
_("Finished"),
_("Seeding"),
_("Allocating"))
SPEED_VALUES = [
(-1, 'Unlimited'),
(5, '5.0 Kib/sec'),
(10, '10.0 Kib/sec'),
(15, '15.0 Kib/sec'),
(25, '25.0 Kib/sec'),
(30, '30.0 Kib/sec'),
(50, '50.0 Kib/sec'),
(80, '80.0 Kib/sec'),
(300, '300.0 Kib/sec'),
(500, '500.0 Kib/sec')
]
#try:
# SESSIONS = pickle.load(open(session_file))
#except:
SESSIONS = []

View File

@ -0,0 +1,424 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# webserver_framework.py
#
# Copyright (C) Martijn Voncken 2007 <mvoncken@gmail.com>
#
# 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.
"""
Todo's before stable:
-__init__:kill->restart is not waiting for kill to be finished.
--later/features:---
-alternating rows?
-set prio
-clear finished?
-torrent files.
"""
import lib.webpy022 as web
from lib.webpy022.webapi import cookies, setcookie as w_setcookie
from lib.webpy022.http import seeother, url
from lib.webpy022 import template,changequery as self_url
from lib.webpy022.utils import Storage
from lib.static_handler import static_handler
from deluge.common import fsize,fspeed
import traceback
import random
from operator import attrgetter
import datetime
import pickle
from md5 import md5
from urlparse import urlparse
from deluge import common
from webserver_common import REVNO, VERSION, log
import webserver_common as ws
from debugerror import deluge_debugerror
#init:
web.webapi.internalerror = deluge_debugerror
#/init
debug_unicode = False
#methods:
def setcookie(key, val):
"""add 30 days expires header for persistent cookies"""
return w_setcookie(key, val , expires=2592000)
#really simple sessions, to bad i had to implement them myself.
def start_session():
log.debug('start session')
session_id = str(random.random())
ws.SESSIONS.append(session_id)
#if len(ws.SESSIONS) > 20: #save max 20 sessions?
# ws.SESSIONS = ws.SESSIONS[-20:]
#not thread safe! , but a verry rare bug.
#f = open(ws.session_file,'wb')
#pickle.dump(ws.SESSIONS, f)
#f.close()
setcookie("session_id", session_id)
def end_session():
session_id = getcookie("session_id")
#if session_id in ws.SESSIONS:
# ws.SESSIONS.remove(session_id)
#not thread safe! , but a verry rare bug.
#f = open(ws.session_file,'wb')
#pickle.dump(ws.SESSIONS, f)
#f.close()
setcookie("session_id","")
def do_redirect():
"""for redirects after a POST"""
vars = web.input(redir = None)
ck = cookies()
url_vars = {}
if vars.redir:
seeother(vars.redir)
return
#todo:cleanup
if ("order" in ck and "sort" in ck):
url_vars.update({'sort':ck['sort'] ,'order':ck['order'] })
if ("filter" in ck) and ck['filter']:
url_vars['filter'] = ck['filter']
if ("category" in ck) and ck['category']:
url_vars['category'] = ck['category']
seeother(url("/index", **url_vars))
def error_page(error):
web.header("Content-Type", "text/html; charset=utf-8")
web.header("Cache-Control", "no-cache, must-revalidate")
print ws.render.error(error)
def getcookie(key, default = None):
key = str(key).strip()
ck = cookies()
return ck.get(key, default)
#deco's:
def deluge_page_noauth(func):
"""
add http headers
print result of func
"""
def deco(self, name = None):
web.header("Content-Type", "text/html; charset=utf-8")
web.header("Cache-Control", "no-cache, must-revalidate")
res = func(self, name)
print res
deco.__name__ = func.__name__
return deco
def check_session(func):
"""
a decorator
return func if session is valid, else redirect to login page.
"""
def deco(self, name = None):
log.debug('%s.%s(name=%s)' % (self.__class__.__name__, func.__name__,
name))
vars = web.input(redir_after_login = None)
ck = cookies()
if ck.has_key("session_id") and ck["session_id"] in ws.SESSIONS:
return func(self, name) #ok, continue..
elif vars.redir_after_login:
seeother(url("/login",redir=self_url()))
else:
seeother("/login") #do not continue, and redirect to login page
return deco
def deluge_page(func):
return check_session(deluge_page_noauth(func))
#combi-deco's:
def auto_refreshed(func):
"decorator:adds a refresh header"
def deco(self, name = None):
if getcookie('auto_refresh') == '1':
web.header("Refresh", "%i ; url=%s" %
(int(getcookie('auto_refresh_secs',10)),self_url()))
return func(self, name)
deco.__name__ = func.__name__
return deco
def remote(func):
"decorator for remote api's"
def deco(self, name = None):
try:
log.debug('%s.%s(%s)' ,self.__class__.__name__, func.__name__,name )
print func(self, name)
except Exception, e:
print 'error:%s' % e.message
print '-'*20
print traceback.format_exc()
deco.__name__ = func.__name__
return deco
#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({
'download_rate':fspeed(ws.proxy.get_download_rate()),
'upload_rate':fspeed(ws.proxy.get_upload_rate()),
'max_download':ws.proxy.get_config_value('max_download_speed_bps'),
'max_upload':ws.proxy.get_config_value('max_upload_speed_bps'),
'num_connections':ws.proxy.get_num_connections(),
'max_num_connections':ws.proxy.get_config_value('max_connections_global')
})
if stats.max_upload < 0:
stats.max_upload = _("Unlimited")
else:
stats.max_upload = fspeed(stats.max_upload)
if stats.max_download < 0:
stats.max_download = _("Unlimited")
else:
stats.max_download = fspeed(stats.max_download)
return stats
def get_torrent_status(torrent_id):
"""
helper method.
enhance ws.proxy.get_torrent_status with some extra data
"""
status = Storage(ws.proxy.get_torrent_status(torrent_id,ws.TORRENT_KEYS))
#add missing values for deluge 0.6:
for key in ws.TORRENT_KEYS:
if not key in status:
status[key] = 0
status["id"] = torrent_id
url = urlparse(status.tracker)
if hasattr(url,'hostname'):
status.category = url.hostname or 'unknown'
else:
status.category = 'No-tracker'
#0.5-->0.6
status.download_rate = status.download_payload_rate
status.upload_rate = status.upload_payload_rate
#for naming the status-images
status.calc_state_str = "downloading"
if status.paused:
status.calc_state_str= "inactive"
elif status.is_seed:
status.calc_state_str = "seeding"
#action for torrent_pause
if status.user_paused:
status.action = "start"
else:
status.action = "stop"
if status.user_paused:
status.message = _("Paused")
elif status.paused:
status.message = _("Queued")
else:
status.message = (ws.STATE_MESSAGES[status.state])
#add some pre-calculated values
status.update({
"calc_total_downloaded" : (fsize(status.total_done)
+ " (" + fsize(status.total_download) + ")"),
"calc_total_uploaded": (fsize(status.uploaded_memory
+ status.total_payload_upload) + " ("
+ fsize(status.total_upload) + ")"),
})
#no non-unicode string may enter the templates.
#FIXED,l was a translation bug..
if debug_unicode:
for k, v in status.iteritems():
if (not isinstance(v, unicode)) and isinstance(v, str):
try:
status[k] = unicode(v)
except:
raise Exception('Non Unicode for key:%s' % (k, ))
return status
def get_categories(torrent_list):
trackers = [(torrent['category'] or 'unknown') for torrent in torrent_list]
categories = {}
for tracker in trackers:
categories[tracker] = categories.get(tracker,0) + 1
return categories
def filter_torrent_state(torrent_list,filter_name):
filters = {
'downloading': lambda t: (not t.paused and not t.is_seed)
,'queued':lambda t: (t.paused and not t.user_paused)
,'paused':lambda t: (t.user_paused)
,'seeding':lambda t:(t.is_seed and not t.paused )
,'traffic':lambda t: (t.download_rate > 0 or t.upload_rate > 0)
}
filter_func = filters[filter_name]
return [t for t in torrent_list if filter_func(t)]
#/utils
#template-defs:
def get_category_choosers(torrent_list):
"""
todo: split into 2 parts...
"""
categories = get_categories(torrent_list)
filter_tabs = [Storage(title='All (%s)' % len(torrent_list),
filter='', category=None)]
#static filters
for title, filter_name in [
(_('Downloading'),'downloading') ,
(_('Queued'),'queued') ,
(_('Paused'),'paused') ,
(_('Seeding'),'seeding'),
(_('Traffic'),'traffic')
]:
title += ' (%s)' % (
len(filter_torrent_state(torrent_list, filter_name)), )
filter_tabs.append(Storage(title=title, filter=filter_name))
categories = [x for x in get_categories(torrent_list).iteritems()]
categories.sort()
#trackers:
category_tabs = []
category_tabs.append(
Storage(title=_('Trackers'),category=''))
for title,count in categories:
category = title
title += ' (%s)' % (count, )
category_tabs.append(Storage(title=title, category=category))
return filter_tabs, category_tabs
def category_tabs(torrent_list):
filter_tabs, category_tabs = get_category_choosers(torrent_list)
return ws.render.part_categories(filter_tabs, category_tabs)
def template_crop(text, end):
if len(text) > end:
return text[0:end - 3] + '...'
return text
def template_sort_head(id,name):
#got tired of doing these complex things inside templetor..
vars = web.input(sort = None, order = None)
active_up = False
active_down = False
order = 'down'
if vars.sort == id:
if vars.order == 'down':
order = 'up'
active_down = True
else:
active_up = True
return ws.render.sort_column_head(id, name, order, active_up, active_down)
def template_part_stats():
return ws.render.part_stats(get_stats())
def get_config(var):
return ws.config.get(var)
irow = 0
def altrow(reset = False):
global irow
if reset:
irow = 1
return
irow +=1
irow = irow % 2
return "altrow%s" % irow
template.Template.globals.update({
'sort_head': template_sort_head,
'part_stats':template_part_stats,
'category_tabs':category_tabs,
'crop': template_crop,
'_': _ , #gettext/translations
'str': str, #because % in templetor is broken.
'int':int,
'sorted': sorted,
'altrow':altrow,
'get_config': get_config,
'self_url': self_url,
'fspeed': common.fspeed,
'fsize': common.fsize,
'render': ws.render, #for easy resuse of templates
'rev': 'rev.%s' % (REVNO, ),
'version': VERSION,
'getcookie':getcookie,
'get': lambda (var): getattr(web.input(**{var:None}), var) # unreadable :-(
})
#/template-defs
def create_webserver(urls, methods):
from lib.webpy022.request import webpyfunc
from lib.webpy022 import webapi
from lib.gtk_cherrypy_wsgiserver import CherryPyWSGIServer
import os
func = webapi.wsgifunc(webpyfunc(urls, methods, False))
server_address=("0.0.0.0", int(ws.config.get('port')))
server = CherryPyWSGIServer(server_address, func, server_name="localhost")
if ws.config.get('use_https'):
server.ssl_certificate = os.path.join(ws.webui_path,'ssl/deluge.pem')
server.ssl_private_key = os.path.join(ws.webui_path,'ssl/deluge.key')
print "http://%s:%d/" % server_address
return server
#------
__all__ = ['deluge_page_noauth', 'deluge_page', 'remote',
'auto_refreshed', 'check_session',
'do_redirect', 'error_page','start_session','getcookie'
,'setcookie','create_webserver','end_session',
'get_torrent_status', 'check_pwd','static_handler','get_categories'
,'template','filter_torrent_state','log']