mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Remove the old web package.
This commit is contained in:
parent
7e80f60412
commit
d4185e6f80
@ -1,62 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web
|
||||
~~~~~~~~~~
|
||||
|
||||
A web application to serve the Python docs interactively.
|
||||
|
||||
:copyright: 2007-2008 by Georg Brandl.
|
||||
:license: BSD.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import getopt
|
||||
|
||||
import sphinx
|
||||
from sphinx.web.application import setup_app
|
||||
from sphinx.web.serve import run_simple
|
||||
|
||||
try:
|
||||
from werkzeug.debug import DebuggedApplication
|
||||
except ImportError:
|
||||
DebuggedApplication = lambda x, y: x
|
||||
|
||||
|
||||
def main(argv=sys.argv):
|
||||
opts, args = getopt.getopt(argv[1:], "dhf:")
|
||||
opts = dict(opts)
|
||||
if len(args) != 1 or '-h' in opts:
|
||||
print 'usage: %s [-d] [-f cfg.py] <doc_root>' % argv[0]
|
||||
print ' -d: debug mode, use werkzeug debugger if installed'
|
||||
print ' -f: use "cfg.py" file instead of doc_root/webconf.py'
|
||||
return 2
|
||||
|
||||
conffile = opts.get('-f', os.path.join(args[0], 'webconf.py'))
|
||||
config = {}
|
||||
execfile(conffile, config)
|
||||
|
||||
port = config.get('listen_port', 3000)
|
||||
hostname = config.get('listen_addr', 'localhost')
|
||||
debug = ('-d' in opts) or (hostname == 'localhost')
|
||||
|
||||
config['data_root_path'] = args[0]
|
||||
config['debug'] = debug
|
||||
|
||||
def make_app():
|
||||
app = setup_app(config, check_superuser=True)
|
||||
if debug:
|
||||
app = DebuggedApplication(app, True)
|
||||
return app
|
||||
|
||||
if os.environ.get('RUN_MAIN') != 'true':
|
||||
print '* Sphinx %s- Python documentation web application' % \
|
||||
sphinx.__version__.replace('$', '').replace('Revision:', 'rev.')
|
||||
if debug:
|
||||
print '* Running in debug mode'
|
||||
|
||||
run_simple(hostname, port, make_app, use_reloader=debug)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main(sys.argv))
|
@ -1,258 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.admin
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Admin application parts.
|
||||
|
||||
:copyright: 2007-2008 by Georg Brandl, Armin Ronacher.
|
||||
:license: BSD.
|
||||
"""
|
||||
|
||||
from sphinx.web.util import render_template
|
||||
from sphinx.web.wsgiutil import Response, RedirectResponse, NotFound
|
||||
from sphinx.web.database import Comment
|
||||
|
||||
|
||||
class AdminPanel(object):
|
||||
"""
|
||||
Provide the admin functionallity.
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.env = app.env
|
||||
self.userdb = app.userdb
|
||||
|
||||
def dispatch(self, req, page):
|
||||
"""
|
||||
Dispatch the requests for the current user in the admin panel.
|
||||
"""
|
||||
is_logged_in = req.user is not None
|
||||
if is_logged_in:
|
||||
privileges = self.userdb.privileges[req.user]
|
||||
is_master_admin = 'master' in privileges
|
||||
can_change_password = 'frozenpassword' not in privileges
|
||||
else:
|
||||
privileges = set()
|
||||
can_change_password = is_master_admin = False
|
||||
|
||||
# login and logout
|
||||
if page == 'login':
|
||||
return self.do_login(req)
|
||||
elif not is_logged_in:
|
||||
return RedirectResponse('@admin/login/')
|
||||
elif page == 'logout':
|
||||
return self.do_logout(req)
|
||||
|
||||
# account maintance
|
||||
elif page == 'change_password' and can_change_password:
|
||||
return self.do_change_password(req)
|
||||
elif page == 'manage_users' and is_master_admin:
|
||||
return self.do_manage_users(req)
|
||||
|
||||
# moderate comments
|
||||
elif page.split('/')[0] == 'moderate_comments':
|
||||
return self.do_moderate_comments(req, page[18:])
|
||||
|
||||
# missing page
|
||||
elif page != '':
|
||||
raise NotFound()
|
||||
return Response(render_template(req, 'admin/index.html', {
|
||||
'is_master_admin': is_master_admin,
|
||||
'can_change_password': can_change_password
|
||||
}))
|
||||
|
||||
def do_login(self, req):
|
||||
"""
|
||||
Display login form and do the login procedure.
|
||||
"""
|
||||
if req.user is not None:
|
||||
return RedirectResponse('@admin/')
|
||||
login_failed = False
|
||||
if req.method == 'POST':
|
||||
if req.form.get('cancel'):
|
||||
return RedirectResponse('')
|
||||
username = req.form.get('username')
|
||||
password = req.form.get('password')
|
||||
if self.userdb.check_password(username, password):
|
||||
req.login(username)
|
||||
return RedirectResponse('@admin/')
|
||||
login_failed = True
|
||||
return Response(render_template(req, 'admin/login.html', {
|
||||
'login_failed': login_failed
|
||||
}))
|
||||
|
||||
def do_logout(self, req):
|
||||
"""
|
||||
Log the user out.
|
||||
"""
|
||||
req.logout()
|
||||
return RedirectResponse('@admin/login/')
|
||||
|
||||
def do_change_password(self, req):
|
||||
"""
|
||||
Allows the user to change his password.
|
||||
"""
|
||||
change_failed = change_successful = False
|
||||
if req.method == 'POST':
|
||||
if req.form.get('cancel'):
|
||||
return RedirectResponse('@admin/')
|
||||
pw = req.form.get('pw1')
|
||||
if pw and pw == req.form.get('pw2'):
|
||||
self.userdb.set_password(req.user, pw)
|
||||
self.userdb.save()
|
||||
change_successful = True
|
||||
else:
|
||||
change_failed = True
|
||||
return Response(render_template(req, 'admin/change_password.html', {
|
||||
'change_failed': change_failed,
|
||||
'change_successful': change_successful
|
||||
}))
|
||||
|
||||
def do_manage_users(self, req):
|
||||
"""
|
||||
Manage other user accounts. Requires master privileges.
|
||||
"""
|
||||
add_user_mode = False
|
||||
user_privileges = {}
|
||||
users = sorted((user, []) for user in self.userdb.users)
|
||||
to_delete = set()
|
||||
generated_user = generated_password = None
|
||||
user_exists = False
|
||||
|
||||
if req.method == 'POST':
|
||||
for item in req.form.getlist('delete'):
|
||||
try:
|
||||
to_delete.add(item)
|
||||
except ValueError:
|
||||
pass
|
||||
for name, item in req.form.iteritems():
|
||||
if name.startswith('privileges-'):
|
||||
user_privileges[name[11:]] = [x.strip() for x
|
||||
in item.split(',')]
|
||||
if req.form.get('cancel'):
|
||||
return RedirectResponse('@admin/')
|
||||
elif req.form.get('add_user'):
|
||||
username = req.form.get('username')
|
||||
if username:
|
||||
if username in self.userdb.users:
|
||||
user_exists = username
|
||||
else:
|
||||
generated_password = self.userdb.add_user(username)
|
||||
self.userdb.save()
|
||||
generated_user = username
|
||||
else:
|
||||
add_user_mode = True
|
||||
elif req.form.get('aborted'):
|
||||
return RedirectResponse('@admin/manage_users/')
|
||||
|
||||
users = {}
|
||||
for user in self.userdb.users:
|
||||
if user not in user_privileges:
|
||||
users[user] = sorted(self.userdb.privileges[user])
|
||||
else:
|
||||
users[user] = user_privileges[user]
|
||||
|
||||
new_users = users.copy()
|
||||
for user in to_delete:
|
||||
new_users.pop(user, None)
|
||||
|
||||
self_destruction = req.user not in new_users or \
|
||||
'master' not in new_users[req.user]
|
||||
|
||||
if req.method == 'POST' and (not to_delete or
|
||||
(to_delete and req.form.get('confirmed'))) and \
|
||||
req.form.get('update'):
|
||||
old_users = self.userdb.users.copy()
|
||||
for user in old_users:
|
||||
if user not in new_users:
|
||||
del self.userdb.users[user]
|
||||
else:
|
||||
self.userdb.privileges[user].clear()
|
||||
self.userdb.privileges[user].update(new_users[user])
|
||||
self.userdb.save()
|
||||
return RedirectResponse('@admin/manage_users/')
|
||||
|
||||
return Response(render_template(req, 'admin/manage_users.html', {
|
||||
'users': users,
|
||||
'add_user_mode': add_user_mode,
|
||||
'to_delete': to_delete,
|
||||
'ask_confirmation': req.method == 'POST' and to_delete \
|
||||
and not self_destruction,
|
||||
'generated_user': generated_user,
|
||||
'generated_password': generated_password,
|
||||
'self_destruction': self_destruction,
|
||||
'user_exists': user_exists
|
||||
}))
|
||||
|
||||
def do_moderate_comments(self, req, url):
|
||||
"""
|
||||
Comment moderation panel.
|
||||
"""
|
||||
if url == 'recent_comments':
|
||||
details_for = None
|
||||
recent_comments = Comment.get_recent(20)
|
||||
else:
|
||||
details_for = url and self.env.get_real_filename(url) or None
|
||||
recent_comments = None
|
||||
to_delete = set()
|
||||
edit_detail = None
|
||||
|
||||
if 'edit' in req.args:
|
||||
try:
|
||||
edit_detail = Comment.get(int(req.args['edit']))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if req.method == 'POST':
|
||||
for item in req.form.getlist('delete'):
|
||||
try:
|
||||
to_delete.add(int(item))
|
||||
except ValueError:
|
||||
pass
|
||||
if req.form.get('cancel'):
|
||||
return RedirectResponse('@admin/')
|
||||
elif req.form.get('confirmed'):
|
||||
for comment_id in to_delete:
|
||||
try:
|
||||
Comment.get(comment_id).delete()
|
||||
except ValueError:
|
||||
pass
|
||||
return RedirectResponse(req.path)
|
||||
elif req.form.get('aborted'):
|
||||
return RedirectResponse(req.path)
|
||||
elif req.form.get('edit') and not to_delete:
|
||||
if 'delete_this' in req.form:
|
||||
try:
|
||||
to_delete.add(req.form['delete_this'])
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
edit_detail = c = Comment.get(int(req.args['edit']))
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if req.form.get('view'):
|
||||
return RedirectResponse(c.url)
|
||||
c.author = req.form.get('author', '')
|
||||
c.author_mail = req.form.get('author_mail', '')
|
||||
c.title = req.form.get('title', '')
|
||||
c.comment_body = req.form.get('comment_body', '')
|
||||
c.save()
|
||||
self.app.cache.pop(edit_detail.associated_page, None)
|
||||
return RedirectResponse(req.path)
|
||||
|
||||
return Response(render_template(req, 'admin/moderate_comments.html', {
|
||||
'pages_with_comments': [{
|
||||
'page_id': page_id,
|
||||
'title': page_id, #XXX: get title somehow
|
||||
'has_details': details_for == page_id,
|
||||
'comments': comments
|
||||
} for page_id, comments in Comment.get_overview(details_for)],
|
||||
'recent_comments': recent_comments,
|
||||
'to_delete': to_delete,
|
||||
'ask_confirmation': req.method == 'POST' and to_delete,
|
||||
'edit_detail': edit_detail
|
||||
}))
|
@ -1,69 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.antispam
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Small module that performs anti spam tests based on the bad content
|
||||
regex list provided by moin moin.
|
||||
|
||||
:copyright: 2007-2008 by Armin Ronacher.
|
||||
:license: BSD.
|
||||
"""
|
||||
|
||||
import re
|
||||
import urllib
|
||||
import time
|
||||
from os import path
|
||||
|
||||
DOWNLOAD_URL = 'http://moinmaster.wikiwikiweb.de/BadContent?action=raw'
|
||||
UPDATE_INTERVAL = 60 * 60 * 24 * 7
|
||||
|
||||
|
||||
class AntiSpam(object):
|
||||
"""
|
||||
Class that reads a bad content database (flat file that is automatically
|
||||
updated from the moin moin server) and checks strings against it.
|
||||
"""
|
||||
|
||||
def __init__(self, bad_content_file):
|
||||
self.bad_content_file = bad_content_file
|
||||
lines = None
|
||||
|
||||
if not path.exists(self.bad_content_file):
|
||||
last_change = 0
|
||||
else:
|
||||
last_change = path.getmtime(self.bad_content_file)
|
||||
|
||||
if last_change + UPDATE_INTERVAL < time.time():
|
||||
try:
|
||||
f = urllib.urlopen(DOWNLOAD_URL)
|
||||
data = f.read()
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
lines = [l.strip() for l in data.splitlines()
|
||||
if not l.startswith('#')]
|
||||
f = open(bad_content_file, 'w')
|
||||
try:
|
||||
f.write('\n'.join(lines))
|
||||
finally:
|
||||
f.close()
|
||||
last_change = int(time.time())
|
||||
|
||||
if lines is None:
|
||||
try:
|
||||
f = open(bad_content_file)
|
||||
try:
|
||||
lines = [l.strip() for l in f]
|
||||
finally:
|
||||
f.close()
|
||||
except:
|
||||
lines = []
|
||||
self.rules = [re.compile(rule) for rule in lines if rule]
|
||||
|
||||
def is_spam(self, fields):
|
||||
for regex in self.rules:
|
||||
for field in fields:
|
||||
if regex.search(field) is not None:
|
||||
return True
|
||||
return False
|
@ -1,826 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.application
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A simple WSGI application that serves an interactive version
|
||||
of the python documentation.
|
||||
|
||||
:copyright: 2007-2008 by Georg Brandl, Armin Ronacher.
|
||||
:license: BSD.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import copy
|
||||
import time
|
||||
import heapq
|
||||
import math
|
||||
import difflib
|
||||
import tempfile
|
||||
import threading
|
||||
import cPickle as pickle
|
||||
import cStringIO as StringIO
|
||||
from os import path
|
||||
from itertools import groupby
|
||||
|
||||
from sphinx.web.feed import Feed
|
||||
from sphinx.web.mail import Email
|
||||
from sphinx.web.util import render_template, get_target_uri, blackhole_dict, striptags
|
||||
from sphinx.web.admin import AdminPanel
|
||||
from sphinx.web.userdb import UserDatabase
|
||||
from sphinx.web.robots import robots_txt
|
||||
from sphinx.web.oldurls import handle_html_url
|
||||
from sphinx.web.antispam import AntiSpam
|
||||
from sphinx.web.database import connect, set_connection, Comment
|
||||
from sphinx.web.wsgiutil import Request, Response, RedirectResponse, \
|
||||
JSONResponse, SharedDataMiddleware, NotFound, get_base_uri
|
||||
|
||||
from sphinx.util import relative_uri
|
||||
from sphinx.search import SearchFrontend
|
||||
from sphinx.htmlwriter import HTMLWriter
|
||||
from sphinx.builder import LAST_BUILD_FILENAME, ENV_PICKLE_FILENAME
|
||||
|
||||
from docutils.io import StringOutput
|
||||
from docutils.utils import Reporter
|
||||
from docutils.frontend import OptionParser
|
||||
|
||||
_mail_re = re.compile(r'^([a-zA-Z0-9_\.\-])+\@'
|
||||
r'(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,})+$')
|
||||
|
||||
env_lock = threading.Lock()
|
||||
|
||||
|
||||
PATCH_MESSAGE = '''\
|
||||
A new documentation patch has been submitted.
|
||||
Author: %(author)s <%(email)s>
|
||||
Date: %(asctime)s
|
||||
Page: %(page_id)s
|
||||
Summary: %(summary)s
|
||||
|
||||
'''
|
||||
|
||||
known_designs = {
|
||||
'default': (['default.css', 'pygments.css'],
|
||||
'The default design, with the sidebar on the left side.'),
|
||||
'rightsidebar': (['default.css', 'rightsidebar.css', 'pygments.css'],
|
||||
'Display the sidebar on the right side.'),
|
||||
'stickysidebar': (['default.css', 'stickysidebar.css', 'pygments.css'],
|
||||
'''\
|
||||
Display the sidebar on the left and don\'t scroll it
|
||||
with the content. This can cause parts of the content to
|
||||
become inaccessible when the table of contents is too long.'''),
|
||||
'traditional': (['traditional.css'],
|
||||
'''\
|
||||
A design similar to the old documentation style.'''),
|
||||
}
|
||||
|
||||
comments_methods = {
|
||||
'inline': 'Show all comments inline.',
|
||||
'bottom': 'Show all comments at the page bottom.',
|
||||
'none': 'Don\'t show comments at all.',
|
||||
}
|
||||
|
||||
|
||||
class MockBuilder(object):
|
||||
def get_relative_uri(self, from_, to):
|
||||
return ''
|
||||
name = 'web'
|
||||
|
||||
|
||||
NoCache = object()
|
||||
|
||||
def cached(inner):
|
||||
"""
|
||||
Response caching system.
|
||||
"""
|
||||
def caching_function(self, *args, **kwds):
|
||||
gen = inner(self, *args, **kwds)
|
||||
cache_id = gen.next()
|
||||
if cache_id is NoCache:
|
||||
response = gen.next()
|
||||
gen.close()
|
||||
# this could also return a RedirectResponse...
|
||||
if isinstance(response, Response):
|
||||
return response
|
||||
else:
|
||||
return Response(response)
|
||||
try:
|
||||
text = self.cache[cache_id]
|
||||
gen.close()
|
||||
except KeyError:
|
||||
text = gen.next()
|
||||
self.cache[cache_id] = text
|
||||
return Response(text)
|
||||
return caching_function
|
||||
|
||||
|
||||
class DocumentationApplication(object):
|
||||
"""
|
||||
Serves the documentation.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
if config['debug']:
|
||||
self.cache = blackhole_dict()
|
||||
else:
|
||||
self.cache = {}
|
||||
self.freqmodules = {}
|
||||
self.last_most_frequent = []
|
||||
self.generated_stylesheets = {}
|
||||
self.config = config
|
||||
self.data_root = config['data_root_path']
|
||||
self.buildfile = path.join(self.data_root, LAST_BUILD_FILENAME)
|
||||
self.buildmtime = -1
|
||||
self.load_env(0)
|
||||
self.db_con = connect(path.join(self.data_root, 'sphinx.db'))
|
||||
self.antispam = AntiSpam(path.join(self.data_root, 'bad_content'))
|
||||
self.userdb = UserDatabase(path.join(self.data_root, 'docusers'))
|
||||
self.admin_panel = AdminPanel(self)
|
||||
|
||||
|
||||
def load_env(self, new_mtime):
|
||||
env_lock.acquire()
|
||||
try:
|
||||
if self.buildmtime == new_mtime:
|
||||
# happens if another thread already reloaded the env
|
||||
return
|
||||
print "* Loading the environment..."
|
||||
f = open(path.join(self.data_root, ENV_PICKLE_FILENAME), 'rb')
|
||||
try:
|
||||
self.env = pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
f = open(path.join(self.data_root, 'globalcontext.pickle'), 'rb')
|
||||
try:
|
||||
self.globalcontext = pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
f = open(path.join(self.data_root, 'searchindex.pickle'), 'rb')
|
||||
try:
|
||||
self.search_frontend = SearchFrontend(pickle.load(f))
|
||||
finally:
|
||||
f.close()
|
||||
self.buildmtime = new_mtime
|
||||
self.cache.clear()
|
||||
finally:
|
||||
env_lock.release()
|
||||
|
||||
|
||||
def search(self, req):
|
||||
"""
|
||||
Search the database. Currently just a keyword based search.
|
||||
"""
|
||||
if not req.args.get('q'):
|
||||
return RedirectResponse('')
|
||||
return RedirectResponse('q/%s/' % req.args['q'])
|
||||
|
||||
|
||||
def get_page_source(self, page):
|
||||
"""
|
||||
Get the reST source of a page.
|
||||
"""
|
||||
page_id = self.env.get_real_filename(page)[:-4]
|
||||
if page_id is None:
|
||||
raise NotFound()
|
||||
filename = path.join(self.data_root, 'sources', page_id) + '.txt'
|
||||
f = open(filename)
|
||||
try:
|
||||
return page_id, f.read()
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
|
||||
def show_source(self, req, page):
|
||||
"""
|
||||
Show the highlighted source for a given page.
|
||||
"""
|
||||
return Response(self.get_page_source(page)[1], mimetype='text/plain')
|
||||
|
||||
|
||||
def suggest_changes(self, req, page):
|
||||
"""
|
||||
Show a "suggest changes" form.
|
||||
"""
|
||||
page_id, contents = self.get_page_source(page)
|
||||
|
||||
return Response(render_template(req, 'edit.html', self.globalcontext, dict(
|
||||
contents=contents,
|
||||
pagename=page,
|
||||
doctitle=self.globalcontext['titles'].get(page_id+'.rst') or 'this page',
|
||||
submiturl=relative_uri('/@edit/'+page+'/', '/@submit/'+page),
|
||||
)))
|
||||
|
||||
def _generate_preview(self, page_id, contents):
|
||||
"""
|
||||
Generate a preview for suggested changes.
|
||||
"""
|
||||
handle, pathname = tempfile.mkstemp()
|
||||
os.write(handle, contents.encode('utf-8'))
|
||||
os.close(handle)
|
||||
|
||||
warning_stream = StringIO.StringIO()
|
||||
env2 = copy.deepcopy(self.env)
|
||||
destination = StringOutput(encoding='utf-8')
|
||||
builder = MockBuilder()
|
||||
builder.config = env2.config
|
||||
writer = HTMLWriter(builder)
|
||||
doctree = env2.read_doc(page_id, pathname, save_parsed=False)
|
||||
doctree = env2.get_and_resolve_doctree(page_id+'.rst', builder, doctree)
|
||||
doctree.settings = OptionParser(defaults=env2.settings,
|
||||
components=(writer,)).get_default_values()
|
||||
doctree.reporter = Reporter(page_id+'.rst', 2, 4, stream=warning_stream)
|
||||
output = writer.write(doctree, destination)
|
||||
writer.assemble_parts()
|
||||
return writer.parts['fragment']
|
||||
|
||||
|
||||
def submit_changes(self, req, page):
|
||||
"""
|
||||
Submit the suggested changes as a patch.
|
||||
"""
|
||||
if req.method != 'POST':
|
||||
# only available via POST
|
||||
raise NotFound()
|
||||
if req.form.get('cancel'):
|
||||
# handle cancel requests directly
|
||||
return RedirectResponse(page)
|
||||
# raises NotFound if page doesn't exist
|
||||
page_id, orig_contents = self.get_page_source(page)
|
||||
author = req.form.get('name')
|
||||
email = req.form.get('email')
|
||||
summary = req.form.get('summary')
|
||||
contents = req.form.get('contents')
|
||||
fields = (author, email, summary, contents)
|
||||
|
||||
form_error = None
|
||||
rendered = None
|
||||
|
||||
if not all(fields):
|
||||
form_error = 'You have to fill out all fields.'
|
||||
elif not _mail_re.search(email):
|
||||
form_error = 'You have to provide a valid e-mail address.'
|
||||
elif req.form.get('homepage') or self.antispam.is_spam(fields):
|
||||
form_error = 'Your text contains blocked URLs or words.'
|
||||
else:
|
||||
if req.form.get('preview'):
|
||||
rendered = self._generate_preview(page_id, contents)
|
||||
|
||||
else:
|
||||
asctime = time.asctime()
|
||||
contents = contents.splitlines()
|
||||
orig_contents = orig_contents.splitlines()
|
||||
diffname = 'suggestion on %s by %s <%s>' % (asctime, author, email)
|
||||
diff = difflib.unified_diff(orig_contents, contents, n=3,
|
||||
fromfile=page_id, tofile=diffname,
|
||||
lineterm='')
|
||||
diff_text = '\n'.join(diff)
|
||||
try:
|
||||
mail = Email(
|
||||
self.config['patch_mail_from'], 'Python Documentation Patches',
|
||||
self.config['patch_mail_to'], '',
|
||||
'Patch for %s by %s' % (page_id, author),
|
||||
PATCH_MESSAGE % locals(),
|
||||
self.config['patch_mail_smtp'],
|
||||
)
|
||||
mail.attachments.add_string('patch.diff', diff_text, 'text/x-diff')
|
||||
mail.send()
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
# XXX: how to report?
|
||||
pass
|
||||
return Response(render_template(req, 'submitted.html',
|
||||
self.globalcontext, dict(
|
||||
backlink=relative_uri('/@submit/'+page+'/', page+'/')
|
||||
)))
|
||||
|
||||
return Response(render_template(req, 'edit.html', self.globalcontext, dict(
|
||||
contents=contents,
|
||||
author=author,
|
||||
email=email,
|
||||
summary=summary,
|
||||
pagename=page,
|
||||
form_error=form_error,
|
||||
rendered=rendered,
|
||||
submiturl=relative_uri('/@edit/'+page+'/', '/@submit/'+page),
|
||||
)))
|
||||
|
||||
|
||||
def get_settings_page(self, req):
|
||||
"""
|
||||
Handle the settings page.
|
||||
"""
|
||||
referer = req.environ.get('HTTP_REFERER') or ''
|
||||
if referer:
|
||||
base = get_base_uri(req.environ)
|
||||
if not referer.startswith(base):
|
||||
referer = ''
|
||||
else:
|
||||
referer = referer[len(base):]
|
||||
referer = referer.split('?')[0] or referer
|
||||
|
||||
if req.method == 'POST':
|
||||
if req.form.get('cancel'):
|
||||
if req.form.get('referer'):
|
||||
return RedirectResponse(req.form['referer'])
|
||||
return RedirectResponse('')
|
||||
new_style = req.form.get('design')
|
||||
if new_style and new_style in known_designs:
|
||||
req.session['design'] = new_style
|
||||
new_comments = req.form.get('comments')
|
||||
if new_comments and new_comments in comments_methods:
|
||||
req.session['comments'] = new_comments
|
||||
if req.form.get('goback') and req.form.get('referer'):
|
||||
return RedirectResponse(req.form['referer'])
|
||||
# else display the same page again
|
||||
referer = ''
|
||||
|
||||
context = {
|
||||
'known_designs': sorted(known_designs.iteritems()),
|
||||
'comments_methods': comments_methods.items(),
|
||||
'curdesign': req.session.get('design') or 'default',
|
||||
'curcomments': req.session.get('comments') or 'inline',
|
||||
'referer': referer,
|
||||
}
|
||||
|
||||
return Response(render_template(req, 'settings.html',
|
||||
self.globalcontext, context))
|
||||
|
||||
|
||||
@cached
|
||||
def get_module_index(self, req):
|
||||
"""
|
||||
Get the module index or redirect to a module from the module index.
|
||||
"""
|
||||
most_frequent = heapq.nlargest(30, self.freqmodules.iteritems(),
|
||||
lambda x: x[1])
|
||||
if most_frequent:
|
||||
base_count = most_frequent[-1][1]
|
||||
most_frequent = [{
|
||||
'name': x[0],
|
||||
'size': 100 + math.log((x[1] - base_count) + 1) * 20,
|
||||
'count': x[1]
|
||||
} for x in sorted(most_frequent)]
|
||||
|
||||
showpf = None
|
||||
newpf = req.args.get('newpf')
|
||||
sesspf = req.session.get('pf')
|
||||
if newpf or sesspf:
|
||||
yield NoCache
|
||||
if newpf:
|
||||
req.session['pf'] = showpf = req.args.getlist('pf')
|
||||
else:
|
||||
showpf = sesspf
|
||||
else:
|
||||
if most_frequent != self.last_most_frequent:
|
||||
self.cache.pop('@modindex', None)
|
||||
yield '@modindex'
|
||||
|
||||
filename = path.join(self.data_root, 'modindex.fpickle')
|
||||
f = open(filename, 'rb')
|
||||
try:
|
||||
context = pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
if showpf:
|
||||
entries = context['modindexentries']
|
||||
i = 0
|
||||
while i < len(entries):
|
||||
if entries[i][6]:
|
||||
for pform in entries[i][6]:
|
||||
if pform in showpf:
|
||||
break
|
||||
else:
|
||||
del entries[i]
|
||||
continue
|
||||
i += 1
|
||||
context['freqentries'] = most_frequent
|
||||
context['showpf'] = showpf or context['platforms']
|
||||
self.last_most_frequent = most_frequent
|
||||
yield render_template(req, 'modindex.html',
|
||||
self.globalcontext, context)
|
||||
|
||||
def show_comment_form(self, req, page):
|
||||
"""
|
||||
Show the "new comment" form.
|
||||
"""
|
||||
page_id = self.env.get_real_filename(page)[:-4]
|
||||
ajax_mode = req.args.get('mode') == 'ajax'
|
||||
target = req.args.get('target')
|
||||
page_comment_mode = not target
|
||||
|
||||
form_error = preview = None
|
||||
title = req.form.get('title', '').strip()
|
||||
if 'author' in req.form:
|
||||
author = req.form['author']
|
||||
else:
|
||||
author = req.session.get('author', '')
|
||||
if 'author_mail' in req.form:
|
||||
author_mail = req.form['author_mail']
|
||||
else:
|
||||
author_mail = req.session.get('author_mail', '')
|
||||
comment_body = req.form.get('comment_body', '')
|
||||
fields = (title, author, author_mail, comment_body)
|
||||
|
||||
if req.method == 'POST':
|
||||
if req.form.get('preview'):
|
||||
preview = Comment(page_id, target, title, author, author_mail,
|
||||
comment_body)
|
||||
# 'homepage' is a forbidden field to thwart bots
|
||||
elif req.form.get('homepage') or self.antispam.is_spam(fields):
|
||||
form_error = 'Your text contains blocked URLs or words.'
|
||||
else:
|
||||
if not all(fields):
|
||||
form_error = 'You have to fill out all fields.'
|
||||
elif _mail_re.search(author_mail) is None:
|
||||
form_error = 'You have to provide a valid e-mail address.'
|
||||
elif len(comment_body) < 20:
|
||||
form_error = 'You comment is too short ' \
|
||||
'(must have at least 20 characters).'
|
||||
else:
|
||||
# '|none' can stay since it doesn't include comments
|
||||
self.cache.pop(page_id + '|inline', None)
|
||||
self.cache.pop(page_id + '|bottom', None)
|
||||
comment = Comment(page_id, target,
|
||||
title, author, author_mail,
|
||||
comment_body)
|
||||
comment.save()
|
||||
req.session['author'] = author
|
||||
req.session['author_mail'] = author_mail
|
||||
if ajax_mode:
|
||||
return JSONResponse({'posted': True, 'error': False,
|
||||
'commentID': comment.comment_id})
|
||||
return RedirectResponse(comment.url)
|
||||
|
||||
output = render_template(req, '_commentform.html', {
|
||||
'ajax_mode': ajax_mode,
|
||||
'preview': preview,
|
||||
'suggest_url': '@edit/%s/' % page,
|
||||
'comments_form': {
|
||||
'target': target,
|
||||
'title': title,
|
||||
'author': author,
|
||||
'author_mail': author_mail,
|
||||
'comment_body': comment_body,
|
||||
'error': form_error
|
||||
}
|
||||
})
|
||||
|
||||
if ajax_mode:
|
||||
return JSONResponse({
|
||||
'body': output,
|
||||
'error': bool(form_error),
|
||||
'posted': False
|
||||
})
|
||||
return Response(render_template(req, 'commentform.html', {
|
||||
'form': output
|
||||
}))
|
||||
|
||||
def _insert_comments(self, req, url, context, mode):
|
||||
"""
|
||||
Insert inline comments into a page context.
|
||||
"""
|
||||
if 'body' not in context:
|
||||
return
|
||||
|
||||
comment_url = '@comments/%s/' % url
|
||||
page_id = self.env.get_real_filename(url)[:-4]
|
||||
tx = context['body']
|
||||
all_comments = Comment.get_for_page(page_id)
|
||||
global_comments = []
|
||||
for name, comments in groupby(all_comments, lambda x: x.associated_name):
|
||||
if not name:
|
||||
global_comments.extend(comments)
|
||||
continue
|
||||
comments = list(comments)
|
||||
if not comments:
|
||||
continue
|
||||
tx = re.sub('<!--#%s#-->' % name,
|
||||
render_template(req, 'inlinecomments.html', {
|
||||
'comments': comments,
|
||||
'id': name,
|
||||
'comment_url': comment_url,
|
||||
'mode': mode}),
|
||||
tx)
|
||||
if mode == 'bottom':
|
||||
global_comments.extend(comments)
|
||||
if mode == 'inline':
|
||||
# replace all markers for items without comments
|
||||
tx = re.sub('<!--#([^#]*)#-->',
|
||||
(lambda match:
|
||||
render_template(req, 'inlinecomments.html', {
|
||||
'id': match.group(1),
|
||||
'mode': 'inline',
|
||||
'comment_url': comment_url
|
||||
},)),
|
||||
tx)
|
||||
tx += render_template(req, 'comments.html', {
|
||||
'comments': global_comments,
|
||||
'comment_url': comment_url
|
||||
})
|
||||
context['body'] = tx
|
||||
|
||||
|
||||
@cached
|
||||
def get_page(self, req, url):
|
||||
"""
|
||||
Show the requested documentation page or raise an
|
||||
`NotFound` exception to display a page with close matches.
|
||||
"""
|
||||
page_id = self.env.get_real_filename(url)[:-4]
|
||||
if page_id is None:
|
||||
raise NotFound(show_keyword_matches=True)
|
||||
# increment view count of all modules on that page
|
||||
for modname in self.env.filemodules.get(page_id+'.rst', ()):
|
||||
self.freqmodules[modname] = self.freqmodules.get(modname, 0) + 1
|
||||
# comments enabled?
|
||||
comments = self.env.metadata[page_id+'.rst'].get('nocomments', False)
|
||||
|
||||
# how does the user want to view comments?
|
||||
commentmode = comments and req.session.get('comments', 'inline') or ''
|
||||
|
||||
# show "old URL" message? -> no caching possible
|
||||
oldurl = req.args.get('oldurl')
|
||||
if oldurl:
|
||||
yield NoCache
|
||||
else:
|
||||
# there must be different cache entries per comment mode
|
||||
yield page_id + '|' + commentmode
|
||||
|
||||
# cache miss; load the page and render it
|
||||
filename = path.join(self.data_root, page_id + '.fpickle')
|
||||
f = open(filename, 'rb')
|
||||
try:
|
||||
context = pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
# add comments to paqe text
|
||||
if commentmode != 'none':
|
||||
self._insert_comments(req, url, context, commentmode)
|
||||
|
||||
yield render_template(req, 'page.html', self.globalcontext, context,
|
||||
{'oldurl': oldurl})
|
||||
|
||||
|
||||
@cached
|
||||
def get_special_page(self, req, name):
|
||||
yield '@'+name
|
||||
filename = path.join(self.data_root, name + '.fpickle')
|
||||
f = open(filename, 'rb')
|
||||
try:
|
||||
context = pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
yield render_template(req, name+'.html',
|
||||
self.globalcontext, context)
|
||||
|
||||
|
||||
def comments_feed(self, req, url):
|
||||
if url == 'recent':
|
||||
feed = Feed(req, 'Recent Comments', 'Recent Comments', '')
|
||||
for comment in Comment.get_recent():
|
||||
feed.add_item(comment.title, comment.author, comment.url,
|
||||
comment.parsed_comment_body, comment.pub_date)
|
||||
else:
|
||||
page_id = self.env.get_real_filename(url)[:-4]
|
||||
doctitle = striptags(self.globalcontext['titles'].get(page_id+'.rst', url))
|
||||
feed = Feed(req, 'Comments for "%s"' % doctitle,
|
||||
'List of comments for the topic "%s"' % doctitle, url)
|
||||
for comment in Comment.get_for_page(page_id):
|
||||
feed.add_item(comment.title, comment.author, comment.url,
|
||||
comment.parsed_comment_body, comment.pub_date)
|
||||
return Response(feed.generate(), mimetype='application/rss+xml')
|
||||
|
||||
|
||||
def get_error_404(self, req):
|
||||
"""
|
||||
Show a simple error 404 page.
|
||||
"""
|
||||
return Response(render_template(req, 'not_found.html', self.globalcontext),
|
||||
status=404)
|
||||
|
||||
|
||||
pretty_type = {
|
||||
'data': 'module data',
|
||||
'cfunction': 'C function',
|
||||
'cmember': 'C member',
|
||||
'cmacro': 'C macro',
|
||||
'ctype': 'C type',
|
||||
'cvar': 'C variable',
|
||||
}
|
||||
|
||||
def get_keyword_matches(self, req, term=None, avoid_fuzzy=False,
|
||||
is_error_page=False):
|
||||
"""
|
||||
Find keyword matches. If there is an exact match, just redirect:
|
||||
http://docs.python.org/os.path.exists would automatically
|
||||
redirect to http://docs.python.org/library/os.path/#os.path.exists.
|
||||
Else, show a page with close matches.
|
||||
|
||||
Module references are processed first so that "os.path" is handled as
|
||||
a module and not as member of os.
|
||||
"""
|
||||
if term is None:
|
||||
term = req.path.strip('/')
|
||||
|
||||
matches = self.env.find_keyword(term, avoid_fuzzy)
|
||||
|
||||
# if avoid_fuzzy is False matches can be None
|
||||
if matches is None:
|
||||
return
|
||||
|
||||
if isinstance(matches, tuple):
|
||||
url = get_target_uri(matches[1])
|
||||
if matches[0] != 'module':
|
||||
url += '#' + matches[2]
|
||||
return RedirectResponse(url)
|
||||
else:
|
||||
# get some close matches
|
||||
close_matches = []
|
||||
good_matches = 0
|
||||
for ratio, type, filename, anchorname, desc in matches:
|
||||
link = get_target_uri(filename)
|
||||
if type != 'module':
|
||||
link += '#' + anchorname
|
||||
good_match = ratio > 0.75
|
||||
good_matches += good_match
|
||||
close_matches.append({
|
||||
'href': relative_uri(req.path, link),
|
||||
'title': anchorname,
|
||||
'good_match': good_match,
|
||||
'type': self.pretty_type.get(type, type),
|
||||
'description': desc,
|
||||
})
|
||||
return Response(render_template(req, 'keyword_not_found.html', {
|
||||
'close_matches': close_matches,
|
||||
'good_matches_count': good_matches,
|
||||
'keyword': term
|
||||
}, self.globalcontext), status=404)
|
||||
|
||||
|
||||
def get_user_stylesheet(self, req):
|
||||
"""
|
||||
Stylesheets are exchangeable. Handle them here and
|
||||
cache them on the server side until server shuts down
|
||||
and on the client side for 1 hour (not in debug mode).
|
||||
"""
|
||||
style = req.session.get('design')
|
||||
if style not in known_designs:
|
||||
style = 'default'
|
||||
|
||||
if style in self.generated_stylesheets:
|
||||
stylesheet = self.generated_stylesheets[style]
|
||||
else:
|
||||
stylesheet = []
|
||||
for filename in known_designs[style][0]:
|
||||
f = open(path.join(self.data_root, 'style', filename))
|
||||
try:
|
||||
stylesheet.append(f.read())
|
||||
finally:
|
||||
f.close()
|
||||
stylesheet = '\n'.join(stylesheet)
|
||||
if not self.config.get('debug'):
|
||||
self.generated_stylesheets[style] = stylesheet
|
||||
|
||||
if req.args.get('admin') == 'yes':
|
||||
f = open(path.join(self.data_root, 'style', 'admin.css'))
|
||||
try:
|
||||
stylesheet += '\n' + f.read()
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
# XXX: add timestamp based http caching
|
||||
return Response(stylesheet, mimetype='text/css')
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""
|
||||
Dispatch requests.
|
||||
"""
|
||||
set_connection(self.db_con)
|
||||
req = Request(environ)
|
||||
url = req.path.strip('/') or 'index'
|
||||
|
||||
# check if the environment was updated
|
||||
new_mtime = path.getmtime(self.buildfile)
|
||||
if self.buildmtime != new_mtime:
|
||||
self.load_env(new_mtime)
|
||||
|
||||
try:
|
||||
if req.path == '/favicon.ico':
|
||||
# TODO: change this to real favicon?
|
||||
resp = Response('404 Not Found', status=404)
|
||||
elif req.path == '/robots.txt':
|
||||
resp = Response(robots_txt, mimetype='text/plain')
|
||||
elif not req.path.endswith('/') and req.method == 'GET':
|
||||
# may be an old URL
|
||||
if url.endswith('.html'):
|
||||
resp = handle_html_url(self, url)
|
||||
else:
|
||||
# else, require a trailing slash on GET requests
|
||||
# this ensures nice looking urls and working relative
|
||||
# links for cached resources.
|
||||
query = req.environ.get('QUERY_STRING', '')
|
||||
resp = RedirectResponse(req.path + '/' + (query and '?'+query))
|
||||
# index page is special
|
||||
elif url == 'index':
|
||||
# presets for settings
|
||||
if req.args.get('design') and req.args['design'] in known_designs:
|
||||
req.session['design'] = req.args['design']
|
||||
if req.args.get('comments') and req.args['comments'] in comments_methods:
|
||||
req.session['comments'] = req.args['comments']
|
||||
# alias for fuzzy search
|
||||
if 'q' in req.args:
|
||||
resp = RedirectResponse('q/%s/' % req.args['q'])
|
||||
# stylesheet
|
||||
elif req.args.get('do') == 'stylesheet':
|
||||
resp = self.get_user_stylesheet(req)
|
||||
else:
|
||||
resp = self.get_special_page(req, 'index')
|
||||
# go to the search page
|
||||
# XXX: this is currently just a redirect to /q/ which is handled below
|
||||
elif url == 'search':
|
||||
resp = self.search(req)
|
||||
# settings page cannot be cached
|
||||
elif url == 'settings':
|
||||
resp = self.get_settings_page(req)
|
||||
# module index page is special
|
||||
elif url == 'modindex':
|
||||
resp = self.get_module_index(req)
|
||||
# genindex page is special too
|
||||
elif url == 'genindex':
|
||||
resp = self.get_special_page(req, 'genindex')
|
||||
# start the fuzzy search
|
||||
elif url[:2] == 'q/':
|
||||
resp = self.get_keyword_matches(req, url[2:])
|
||||
# special URLs -- don't forget to add them to robots.py
|
||||
elif url[0] == '@':
|
||||
# source view
|
||||
if url[:8] == '@source/':
|
||||
resp = self.show_source(req, url[8:])
|
||||
# suggest changes view
|
||||
elif url[:6] == '@edit/':
|
||||
resp = self.suggest_changes(req, url[6:])
|
||||
# suggest changes submit
|
||||
elif url[:8] == '@submit/':
|
||||
resp = self.submit_changes(req, url[8:])
|
||||
# show that comment form
|
||||
elif url[:10] == '@comments/':
|
||||
resp = self.show_comment_form(req, url[10:])
|
||||
# comments RSS feed
|
||||
elif url[:5] == '@rss/':
|
||||
resp = self.comments_feed(req, url[5:])
|
||||
# dispatch requests to the admin panel
|
||||
elif url == '@admin' or url[:7] == '@admin/':
|
||||
resp = self.admin_panel.dispatch(req, url[7:])
|
||||
else:
|
||||
raise NotFound()
|
||||
# everything else is handled as page or fuzzy search
|
||||
# if a page does not exist.
|
||||
else:
|
||||
resp = self.get_page(req, url)
|
||||
# views can raise a NotFound exception to show an error page.
|
||||
# Either a real not found page or a similar matches page.
|
||||
except NotFound, e:
|
||||
if e.show_keyword_matches:
|
||||
resp = self.get_keyword_matches(req, is_error_page=True)
|
||||
else:
|
||||
resp = self.get_error_404(req)
|
||||
return resp(environ, start_response)
|
||||
|
||||
|
||||
def _check_superuser(app):
|
||||
"""Check if there is a superuser and create one if necessary."""
|
||||
if not app.userdb.users:
|
||||
print 'Warning: you have no user database or no master "admin" account.'
|
||||
create = raw_input('Do you want to create an admin account now? [y/n] ')
|
||||
if not create or create.lower().startswith('y'):
|
||||
import getpass
|
||||
print 'Creating "admin" user.'
|
||||
pw1 = getpass.getpass('Enter password: ')
|
||||
pw2 = getpass.getpass('Enter password again: ')
|
||||
if pw1 != pw2:
|
||||
print 'Error: Passwords don\'t match.'
|
||||
raise SystemExit(1)
|
||||
app.userdb.set_password('admin', pw1)
|
||||
app.userdb.privileges['admin'].add('master')
|
||||
app.userdb.save()
|
||||
|
||||
|
||||
def setup_app(config, check_superuser=False):
|
||||
"""
|
||||
Create the WSGI application based on a configuration dict.
|
||||
Handled configuration values so far:
|
||||
|
||||
`data_root_path`
|
||||
the folder containing the documentation data as generated
|
||||
by sphinx with the web builder.
|
||||
"""
|
||||
app = DocumentationApplication(config)
|
||||
if check_superuser:
|
||||
_check_superuser(app)
|
||||
app = SharedDataMiddleware(app, {
|
||||
'/static': path.join(config['data_root_path'], 'static')
|
||||
})
|
||||
return app
|
@ -1,194 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.database
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The database connections are thread local. To set the connection
|
||||
for a thread use the `set_connection` function provided. The
|
||||
`connect` method automatically sets up new tables and returns a
|
||||
usable connection which is also set as the connection for the
|
||||
thread that called that function.
|
||||
|
||||
:copyright: 2007-2008 by Georg Brandl, Armin Ronacher.
|
||||
:license: BSD.
|
||||
"""
|
||||
import time
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from threading import local
|
||||
|
||||
from sphinx.web.markup import markup
|
||||
|
||||
|
||||
_thread_local = local()
|
||||
|
||||
|
||||
def connect(path):
|
||||
"""Connect and create tables if required. Also assigns
|
||||
the connection for the current thread."""
|
||||
con = sqlite3.connect(path, detect_types=sqlite3.PARSE_DECLTYPES)
|
||||
con.isolation_level = None
|
||||
|
||||
# create tables that do not exist.
|
||||
for table in tables:
|
||||
try:
|
||||
con.execute('select * from %s limit 1;' % table)
|
||||
except sqlite3.OperationalError:
|
||||
con.execute(tables[table])
|
||||
|
||||
set_connection(con)
|
||||
return con
|
||||
|
||||
|
||||
def get_cursor():
|
||||
"""Return a new cursor."""
|
||||
return _thread_local.connection.cursor()
|
||||
|
||||
|
||||
def set_connection(con):
|
||||
"""Call this after thread creation to make this connection
|
||||
the connection for this thread."""
|
||||
_thread_local.connection = con
|
||||
|
||||
|
||||
#: tables that we use
|
||||
tables = {
|
||||
'comments': '''
|
||||
create table comments (
|
||||
comment_id integer primary key,
|
||||
associated_page varchar(200),
|
||||
associated_name varchar(200),
|
||||
title varchar(120),
|
||||
author varchar(200),
|
||||
author_mail varchar(250),
|
||||
comment_body text,
|
||||
pub_date timestamp
|
||||
);'''
|
||||
}
|
||||
|
||||
|
||||
class Comment(object):
|
||||
"""
|
||||
Represents one comment.
|
||||
"""
|
||||
|
||||
def __init__(self, associated_page, associated_name, title, author,
|
||||
author_mail, comment_body, pub_date=None):
|
||||
self.comment_id = None
|
||||
self.associated_page = associated_page
|
||||
self.associated_name = associated_name
|
||||
self.title = title
|
||||
if pub_date is None:
|
||||
pub_date = datetime.utcnow()
|
||||
self.pub_date = pub_date
|
||||
self.author = author
|
||||
self.author_mail = author_mail
|
||||
self.comment_body = comment_body
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return '%s#comment-%s' % (
|
||||
self.associated_page,
|
||||
self.comment_id
|
||||
)
|
||||
|
||||
@property
|
||||
def parsed_comment_body(self):
|
||||
from sphinx.web.util import get_target_uri
|
||||
from sphinx.util import relative_uri
|
||||
uri = get_target_uri(self.associated_page)
|
||||
def make_rel_link(keyword):
|
||||
return relative_uri(uri, 'q/%s/' % keyword)
|
||||
return markup(self.comment_body, make_rel_link)
|
||||
|
||||
def save(self):
|
||||
"""
|
||||
Save the comment and use the cursor provided.
|
||||
"""
|
||||
cur = get_cursor()
|
||||
args = (self.associated_page, self.associated_name, self.title,
|
||||
self.author, self.author_mail, self.comment_body, self.pub_date)
|
||||
if self.comment_id is None:
|
||||
cur.execute('''insert into comments (associated_page, associated_name,
|
||||
title,
|
||||
author, author_mail,
|
||||
comment_body, pub_date)
|
||||
values (?, ?, ?, ?, ?, ?, ?)''', args)
|
||||
self.comment_id = cur.lastrowid
|
||||
else:
|
||||
args += (self.comment_id,)
|
||||
cur.execute('''update comments set associated_page=?,
|
||||
associated_name=?,
|
||||
title=?, author=?,
|
||||
author_mail=?, comment_body=?,
|
||||
pub_date=? where comment_id = ?''', args)
|
||||
cur.close()
|
||||
|
||||
def delete(self):
|
||||
cur = get_cursor()
|
||||
cur.execute('delete from comments where comment_id = ?',
|
||||
(self.comment_id,))
|
||||
cur.close()
|
||||
|
||||
@staticmethod
|
||||
def _make_comment(row):
|
||||
rv = Comment(*row[1:])
|
||||
rv.comment_id = row[0]
|
||||
return rv
|
||||
|
||||
@staticmethod
|
||||
def get(comment_id):
|
||||
cur = get_cursor()
|
||||
cur.execute('select * from comments where comment_id = ?', (comment_id,))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
raise ValueError('comment not found')
|
||||
try:
|
||||
return Comment._make_comment(row)
|
||||
finally:
|
||||
cur.close()
|
||||
|
||||
@staticmethod
|
||||
def get_for_page(associated_page, reverse=False):
|
||||
cur = get_cursor()
|
||||
cur.execute('''select * from comments where associated_page = ?
|
||||
order by associated_name, comment_id %s''' %
|
||||
(reverse and 'desc' or 'asc'),
|
||||
(associated_page,))
|
||||
try:
|
||||
return [Comment._make_comment(row) for row in cur]
|
||||
finally:
|
||||
cur.close()
|
||||
|
||||
@staticmethod
|
||||
def get_recent(n=10):
|
||||
cur = get_cursor()
|
||||
cur.execute('select * from comments order by comment_id desc limit ?',
|
||||
(n,))
|
||||
try:
|
||||
return [Comment._make_comment(row) for row in cur]
|
||||
finally:
|
||||
cur.close()
|
||||
|
||||
@staticmethod
|
||||
def get_overview(detail_for=None):
|
||||
cur = get_cursor()
|
||||
cur.execute('''select distinct associated_page from comments
|
||||
order by associated_page asc''')
|
||||
pages = []
|
||||
for row in cur:
|
||||
page_id = row[0]
|
||||
if page_id == detail_for:
|
||||
pages.append((page_id, Comment.get_for_page(page_id, True)))
|
||||
else:
|
||||
pages.append((page_id, []))
|
||||
cur.close()
|
||||
return pages
|
||||
|
||||
def __repr__(self):
|
||||
return '<Comment by %r on %r:%r (%s)>' % (
|
||||
self.author,
|
||||
self.associated_page,
|
||||
self.associated_name,
|
||||
self.comment_id or 'not saved'
|
||||
)
|
@ -1,78 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.feed
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Nifty module that generates RSS feeds.
|
||||
|
||||
:copyright: 2007-2008 by Armin Ronacher.
|
||||
:license: BSD.
|
||||
"""
|
||||
import time
|
||||
from datetime import datetime
|
||||
from xml.dom.minidom import Document
|
||||
from email.Utils import formatdate
|
||||
|
||||
|
||||
def format_rss_date(date):
|
||||
"""
|
||||
Pass it a datetime object to receive the string representation
|
||||
for RSS date fields.
|
||||
"""
|
||||
return formatdate(time.mktime(date.timetuple()) + date.microsecond / 1e6)
|
||||
|
||||
|
||||
class Feed(object):
|
||||
"""
|
||||
Abstract feed creation class. To generate feeds use one of
|
||||
the subclasses `RssFeed` or `AtomFeed`.
|
||||
"""
|
||||
|
||||
def __init__(self, req, title, description, link):
|
||||
self.req = req
|
||||
self.title = title
|
||||
self.description = description
|
||||
self.link = req.make_external_url(link)
|
||||
self.items = []
|
||||
self._last_update = None
|
||||
|
||||
def add_item(self, title, author, link, description, pub_date):
|
||||
if self._last_update is None or pub_date > self._last_update:
|
||||
self._last_update = pub_date
|
||||
date = pub_date or datetime.utcnow()
|
||||
self.items.append({
|
||||
'title': title,
|
||||
'author': author,
|
||||
'link': self.req.make_external_url(link),
|
||||
'description': description,
|
||||
'pub_date': date
|
||||
})
|
||||
|
||||
def generate(self):
|
||||
return self.generate_document().toxml('utf-8')
|
||||
|
||||
def generate_document(self):
|
||||
doc = Document()
|
||||
Element = doc.createElement
|
||||
Text = doc.createTextNode
|
||||
|
||||
rss = doc.appendChild(Element('rss'))
|
||||
rss.setAttribute('version', '2.0')
|
||||
|
||||
channel = rss.appendChild(Element('channel'))
|
||||
for key in ('title', 'description', 'link'):
|
||||
value = getattr(self, key)
|
||||
channel.appendChild(Element(key)).appendChild(Text(value))
|
||||
date = format_rss_date(self._last_update or datetime.utcnow())
|
||||
channel.appendChild(Element('pubDate')).appendChild(Text(date))
|
||||
|
||||
for item in self.items:
|
||||
d = Element('item')
|
||||
for key in ('title', 'author', 'link', 'description'):
|
||||
d.appendChild(Element(key)).appendChild(Text(item[key]))
|
||||
pub_date = format_rss_date(item['pub_date'])
|
||||
d.appendChild(Element('pubDate')).appendChild(Text(pub_date))
|
||||
d.appendChild(Element('guid')).appendChild(Text(item['link']))
|
||||
channel.appendChild(d)
|
||||
|
||||
return doc
|
@ -1,278 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.mail
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
A simple module for sending e-mails, based on simplemail.py.
|
||||
|
||||
:copyright: 2004-2007 by Gerold Penz.
|
||||
:copyright: 2007-2008 by Georg Brandl.
|
||||
:license: BSD.
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
import time
|
||||
import smtplib
|
||||
import mimetypes
|
||||
|
||||
from email import Encoders
|
||||
from email.Header import Header
|
||||
from email.MIMEText import MIMEText
|
||||
from email.MIMEMultipart import MIMEMultipart
|
||||
from email.Utils import formataddr
|
||||
from email.Utils import formatdate
|
||||
from email.Message import Message
|
||||
from email.MIMEAudio import MIMEAudio
|
||||
from email.MIMEBase import MIMEBase
|
||||
from email.MIMEImage import MIMEImage
|
||||
|
||||
|
||||
|
||||
# Exceptions
|
||||
#----------------------------------------------------------------------
|
||||
class SimpleMail_Exception(Exception):
|
||||
def __str__(self):
|
||||
return self.__doc__
|
||||
|
||||
class NoFromAddress_Exception(SimpleMail_Exception):
|
||||
pass
|
||||
|
||||
class NoToAddress_Exception(SimpleMail_Exception):
|
||||
pass
|
||||
|
||||
class NoSubject_Exception(SimpleMail_Exception):
|
||||
pass
|
||||
|
||||
class AttachmentNotFound_Exception(SimpleMail_Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Attachments(object):
|
||||
def __init__(self):
|
||||
self._attachments = []
|
||||
|
||||
def add_filename(self, filename = ''):
|
||||
self._attachments.append(('file', filename))
|
||||
|
||||
def add_string(self, filename, text, mimetype):
|
||||
self._attachments.append(('string', (filename, text, mimetype)))
|
||||
|
||||
def count(self):
|
||||
return len(self._attachments)
|
||||
|
||||
def get_list(self):
|
||||
return self._attachments
|
||||
|
||||
|
||||
class Recipients(object):
|
||||
def __init__(self):
|
||||
self._recipients = []
|
||||
|
||||
def add(self, address, caption = ''):
|
||||
self._recipients.append(formataddr((caption, address)))
|
||||
|
||||
def count(self):
|
||||
return len(self._recipients)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self._recipients)
|
||||
|
||||
def get_list(self):
|
||||
return self._recipients
|
||||
|
||||
|
||||
class CCRecipients(Recipients):
|
||||
pass
|
||||
|
||||
|
||||
class BCCRecipients(Recipients):
|
||||
pass
|
||||
|
||||
|
||||
class Email(object):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
from_address = "",
|
||||
from_caption = "",
|
||||
to_address = "",
|
||||
to_caption = "",
|
||||
subject = "",
|
||||
message = "",
|
||||
smtp_server = "localhost",
|
||||
smtp_user = "",
|
||||
smtp_password = "",
|
||||
user_agent = "",
|
||||
reply_to_address = "",
|
||||
reply_to_caption = "",
|
||||
use_tls = False,
|
||||
):
|
||||
"""
|
||||
Initialize the email object
|
||||
from_address = the email address of the sender
|
||||
from_caption = the caption (name) of the sender
|
||||
to_address = the email address of the recipient
|
||||
to_caption = the caption (name) of the recipient
|
||||
subject = the subject of the email message
|
||||
message = the body text of the email message
|
||||
smtp_server = the ip-address or the name of the SMTP-server
|
||||
smtp_user = (optional) Login name for the SMTP-Server
|
||||
smtp_password = (optional) Password for the SMTP-Server
|
||||
user_agent = (optional) program identification
|
||||
reply_to_address = (optional) Reply-to email address
|
||||
reply_to_caption = (optional) Reply-to caption (name)
|
||||
use_tls = (optional) True, if the connection should use TLS
|
||||
to encrypt.
|
||||
"""
|
||||
|
||||
self.from_address = from_address
|
||||
self.from_caption = from_caption
|
||||
self.recipients = Recipients()
|
||||
self.cc_recipients = CCRecipients()
|
||||
self.bcc_recipients = BCCRecipients()
|
||||
if to_address:
|
||||
self.recipients.add(to_address, to_caption)
|
||||
self.subject = subject
|
||||
self.message = message
|
||||
self.smtp_server = smtp_server
|
||||
self.smtp_user = smtp_user
|
||||
self.smtp_password = smtp_password
|
||||
self.attachments = Attachments()
|
||||
self.content_subtype = "plain"
|
||||
self.content_charset = "iso-8859-1"
|
||||
self.header_charset = "us-ascii"
|
||||
self.statusdict = None
|
||||
self.user_agent = user_agent
|
||||
self.reply_to_address = reply_to_address
|
||||
self.reply_to_caption = reply_to_caption
|
||||
self.use_tls = use_tls
|
||||
|
||||
|
||||
def send(self):
|
||||
"""
|
||||
Send the mail. Returns True if successfully sent to at least one
|
||||
recipient.
|
||||
"""
|
||||
|
||||
# validation
|
||||
if len(self.from_address.strip()) == 0:
|
||||
raise NoFromAddress_Exception
|
||||
if self.recipients.count() == 0:
|
||||
if (
|
||||
(self.cc_recipients.count() == 0) and
|
||||
(self.bcc_recipients.count() == 0)
|
||||
):
|
||||
raise NoToAddress_Exception
|
||||
if len(self.subject.strip()) == 0:
|
||||
raise NoSubject_Exception
|
||||
|
||||
# assemble
|
||||
if self.attachments.count() == 0:
|
||||
msg = MIMEText(
|
||||
_text = self.message,
|
||||
_subtype = self.content_subtype,
|
||||
_charset = self.content_charset
|
||||
)
|
||||
else:
|
||||
msg = MIMEMultipart()
|
||||
if self.message:
|
||||
att = MIMEText(
|
||||
_text = self.message,
|
||||
_subtype = self.content_subtype,
|
||||
_charset = self.content_charset
|
||||
)
|
||||
msg.attach(att)
|
||||
|
||||
# add headers
|
||||
from_str = formataddr((self.from_caption, self.from_address))
|
||||
msg["From"] = from_str
|
||||
if self.reply_to_address:
|
||||
reply_to_str = formataddr((self.reply_to_caption, self.reply_to_address))
|
||||
msg["Reply-To"] = reply_to_str
|
||||
if self.recipients.count() > 0:
|
||||
msg["To"] = ", ".join(self.recipients.get_list())
|
||||
if self.cc_recipients.count() > 0:
|
||||
msg["Cc"] = ", ".join(self.cc_recipients.get_list())
|
||||
msg["Date"] = formatdate(time.time())
|
||||
msg["User-Agent"] = self.user_agent
|
||||
try:
|
||||
msg["Subject"] = Header(
|
||||
self.subject, self.header_charset
|
||||
)
|
||||
except(UnicodeDecodeError):
|
||||
msg["Subject"] = Header(
|
||||
self.subject, self.content_charset
|
||||
)
|
||||
msg.preamble = "You will not see this in a MIME-aware mail reader.\n"
|
||||
msg.epilogue = ""
|
||||
|
||||
# assemble multipart
|
||||
if self.attachments.count() > 0:
|
||||
for typ, info in self.attachments.get_list():
|
||||
if typ == 'file':
|
||||
filename = info
|
||||
if not os.path.isfile(filename):
|
||||
raise AttachmentNotFound_Exception, filename
|
||||
mimetype, encoding = mimetypes.guess_type(filename)
|
||||
if mimetype is None or encoding is not None:
|
||||
mimetype = 'application/octet-stream'
|
||||
if mimetype.startswith('text/'):
|
||||
fp = file(filename)
|
||||
else:
|
||||
fp = file(filename, 'rb')
|
||||
text = fp.read()
|
||||
fp.close()
|
||||
else:
|
||||
filename, text, mimetype = info
|
||||
maintype, subtype = mimetype.split('/', 1)
|
||||
if maintype == 'text':
|
||||
# Note: we should handle calculating the charset
|
||||
att = MIMEText(text, _subtype=subtype)
|
||||
elif maintype == 'image':
|
||||
att = MIMEImage(text, _subtype=subtype)
|
||||
elif maintype == 'audio':
|
||||
att = MIMEAudio(text, _subtype=subtype)
|
||||
else:
|
||||
att = MIMEBase(maintype, subtype)
|
||||
att.set_payload(text)
|
||||
# Encode the payload using Base64
|
||||
Encoders.encode_base64(att)
|
||||
# Set the filename parameter
|
||||
att.add_header(
|
||||
'Content-Disposition',
|
||||
'attachment',
|
||||
filename = os.path.basename(filename).strip()
|
||||
)
|
||||
msg.attach(att)
|
||||
|
||||
# connect to server
|
||||
smtp = smtplib.SMTP()
|
||||
if self.smtp_server:
|
||||
smtp.connect(self.smtp_server)
|
||||
else:
|
||||
smtp.connect()
|
||||
|
||||
# TLS?
|
||||
if self.use_tls:
|
||||
smtp.ehlo()
|
||||
smtp.starttls()
|
||||
smtp.ehlo()
|
||||
|
||||
# authenticate
|
||||
if self.smtp_user:
|
||||
smtp.login(user = self.smtp_user, password = self.smtp_password)
|
||||
|
||||
# send
|
||||
self.statusdict = smtp.sendmail(
|
||||
from_str,
|
||||
(
|
||||
self.recipients.get_list() +
|
||||
self.cc_recipients.get_list() +
|
||||
self.bcc_recipients.get_list()
|
||||
),
|
||||
msg.as_string()
|
||||
)
|
||||
smtp.close()
|
||||
|
||||
return True
|
@ -1,239 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.markup
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Awfully simple markup used in comments. Syntax:
|
||||
|
||||
`this is some <code>`
|
||||
like <tt> in HTML
|
||||
|
||||
``this is like ` just that i can contain backticks``
|
||||
like <tt> in HTML
|
||||
|
||||
*emphasized*
|
||||
translates to <em class="important">
|
||||
|
||||
**strong**
|
||||
translates to <strong>
|
||||
|
||||
!!!very important message!!!
|
||||
use this to mark important or dangerous things.
|
||||
Translates to <em class="dangerous">
|
||||
|
||||
[[http://www.google.com/]]
|
||||
Simple link with the link target as caption. If the
|
||||
URL is relative the provided callback is called to get
|
||||
the full URL.
|
||||
|
||||
[[http://www.google.com/ go to google]]
|
||||
Link with "go to google" as caption.
|
||||
|
||||
<code>preformatted code that could by python code</code>
|
||||
Python code (most of the time), otherwise preformatted.
|
||||
|
||||
<quote>cite someone</quote>
|
||||
Like <blockquote> in HTML.
|
||||
|
||||
:copyright: 2007-2008 by Armin Ronacher.
|
||||
:license: BSD.
|
||||
"""
|
||||
import cgi
|
||||
import re
|
||||
from urlparse import urlparse
|
||||
|
||||
from sphinx.highlighting import highlight_block
|
||||
|
||||
|
||||
inline_formatting = {
|
||||
'escaped_code': ('``', '``'),
|
||||
'code': ('`', '`'),
|
||||
'strong': ('**', '**'),
|
||||
'emphasized': ('*', '*'),
|
||||
'important': ('!!!', '!!!'),
|
||||
'link': ('[[', ']]'),
|
||||
'quote': ('<quote>', '</quote>'),
|
||||
'code_block': ('<code>', '</code>'),
|
||||
'paragraph': (r'\n{2,}', None),
|
||||
'newline': (r'\\$', None)
|
||||
}
|
||||
|
||||
simple_formattings = {
|
||||
'strong_begin': '<strong>',
|
||||
'strong_end': '</strong>',
|
||||
'emphasized_begin': '<em>',
|
||||
'emphasized_end': '</em>',
|
||||
'important_begin': '<em class="important">',
|
||||
'important_end': '</em>',
|
||||
'quote_begin': '<blockquote>',
|
||||
'quote_end': '</blockquote>'
|
||||
}
|
||||
|
||||
raw_formatting = set(['link', 'code', 'escaped_code', 'code_block'])
|
||||
|
||||
formatting_start_re = re.compile('|'.join(
|
||||
'(?P<%s>%s)' % (name, end is not None and re.escape(start) or start)
|
||||
for name, (start, end)
|
||||
in sorted(inline_formatting.items(), key=lambda x: -len(x[1][0]))
|
||||
), re.S | re.M)
|
||||
|
||||
formatting_end_res = dict(
|
||||
(name, re.compile(re.escape(end))) for name, (start, end)
|
||||
in inline_formatting.iteritems() if end is not None
|
||||
)
|
||||
|
||||
without_end_tag = set(name for name, (_, end) in inline_formatting.iteritems()
|
||||
if end is None)
|
||||
|
||||
|
||||
|
||||
class StreamProcessor(object):
|
||||
|
||||
def __init__(self, stream):
|
||||
self._pushed = []
|
||||
self._stream = stream
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
if self._pushed:
|
||||
return self._pushed.pop()
|
||||
return self._stream.next()
|
||||
|
||||
def push(self, token, data):
|
||||
self._pushed.append((token, data))
|
||||
|
||||
def get_data(self, drop_needle=False):
|
||||
result = []
|
||||
try:
|
||||
while True:
|
||||
token, data = self.next()
|
||||
if token != 'text':
|
||||
if not drop_needle:
|
||||
self.push(token, data)
|
||||
break
|
||||
result.append(data)
|
||||
except StopIteration:
|
||||
pass
|
||||
return ''.join(result)
|
||||
|
||||
|
||||
class MarkupParser(object):
|
||||
|
||||
def __init__(self, make_rel_url):
|
||||
self.make_rel_url = make_rel_url
|
||||
|
||||
def tokenize(self, text):
|
||||
text = '\n'.join(text.splitlines())
|
||||
last_pos = 0
|
||||
pos = 0
|
||||
end = len(text)
|
||||
stack = []
|
||||
text_buffer = []
|
||||
|
||||
while pos < end:
|
||||
if stack:
|
||||
m = formatting_end_res[stack[-1]].match(text, pos)
|
||||
if m is not None:
|
||||
if text_buffer:
|
||||
yield 'text', ''.join(text_buffer)
|
||||
del text_buffer[:]
|
||||
yield stack[-1] + '_end', None
|
||||
stack.pop()
|
||||
pos = m.end()
|
||||
continue
|
||||
|
||||
m = formatting_start_re.match(text, pos)
|
||||
if m is not None:
|
||||
if text_buffer:
|
||||
yield 'text', ''.join(text_buffer)
|
||||
del text_buffer[:]
|
||||
|
||||
for key, value in m.groupdict().iteritems():
|
||||
if value is not None:
|
||||
if key in without_end_tag:
|
||||
yield key, None
|
||||
else:
|
||||
if key in raw_formatting:
|
||||
regex = formatting_end_res[key]
|
||||
m2 = regex.search(text, m.end())
|
||||
if m2 is None:
|
||||
yield key, text[m.end():]
|
||||
else:
|
||||
yield key, text[m.end():m2.start()]
|
||||
m = m2
|
||||
else:
|
||||
yield key + '_begin', None
|
||||
stack.append(key)
|
||||
break
|
||||
|
||||
if m is None:
|
||||
break
|
||||
else:
|
||||
pos = m.end()
|
||||
continue
|
||||
|
||||
text_buffer.append(text[pos])
|
||||
pos += 1
|
||||
|
||||
yield 'text', ''.join(text_buffer)
|
||||
for token in reversed(stack):
|
||||
yield token + '_end', None
|
||||
|
||||
def stream_to_html(self, text):
|
||||
stream = StreamProcessor(self.tokenize(text))
|
||||
paragraph = []
|
||||
result = []
|
||||
|
||||
def new_paragraph():
|
||||
result.append(paragraph[:])
|
||||
del paragraph[:]
|
||||
|
||||
for token, data in stream:
|
||||
if token in simple_formattings:
|
||||
paragraph.append(simple_formattings[token])
|
||||
elif token in ('text', 'escaped_code', 'code'):
|
||||
if data:
|
||||
data = cgi.escape(data)
|
||||
if token in ('escaped_code', 'code'):
|
||||
data = '<tt>%s</tt>' % data
|
||||
paragraph.append(data)
|
||||
elif token == 'link':
|
||||
if ' ' in data:
|
||||
href, caption = data.split(' ', 1)
|
||||
else:
|
||||
href = caption = data
|
||||
protocol = urlparse(href)[0]
|
||||
nofollow = True
|
||||
if not protocol:
|
||||
href = self.make_rel_url(href)
|
||||
nofollow = False
|
||||
elif protocol == 'javascript':
|
||||
href = href[11:]
|
||||
paragraph.append('<a href="%s"%s>%s</a>' % (cgi.escape(href),
|
||||
nofollow and ' rel="nofollow"' or '',
|
||||
cgi.escape(caption)))
|
||||
elif token == 'code_block':
|
||||
result.append(highlight_block(data, 'python'))
|
||||
new_paragraph()
|
||||
elif token == 'paragraph':
|
||||
new_paragraph()
|
||||
elif token == 'newline':
|
||||
paragraph.append('<br>')
|
||||
|
||||
if paragraph:
|
||||
result.append(paragraph)
|
||||
for item in result:
|
||||
if isinstance(item, list):
|
||||
if item:
|
||||
yield '<p>%s</p>' % ''.join(item)
|
||||
else:
|
||||
yield item
|
||||
|
||||
def to_html(self, text):
|
||||
return ''.join(self.stream_to_html(text))
|
||||
|
||||
|
||||
def markup(text, make_rel_url=lambda x: './' + x):
|
||||
return MarkupParser(make_rel_url).to_html(text)
|
@ -1,93 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.oldurls
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Handle old URLs gracefully.
|
||||
|
||||
:copyright: 2007-2008 by Georg Brandl.
|
||||
:license: BSD.
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from sphinx.web.wsgiutil import RedirectResponse, NotFound
|
||||
|
||||
|
||||
_module_re = re.compile(r'module-(.*)\.html')
|
||||
_modobj_re = re.compile(r'(.*)-objects\.html')
|
||||
_modsub_re = re.compile(r'(.*?)-(.*)\.html')
|
||||
|
||||
|
||||
special_module_names = {
|
||||
'main': '__main__',
|
||||
'builtin': '__builtin__',
|
||||
'future': '__future__',
|
||||
'pycompile': 'py_compile',
|
||||
}
|
||||
|
||||
tutorial_nodes = [
|
||||
'', '', '',
|
||||
'appetite',
|
||||
'interpreter',
|
||||
'introduction',
|
||||
'controlflow',
|
||||
'datastructures',
|
||||
'modules',
|
||||
'inputoutput',
|
||||
'errors',
|
||||
'classes',
|
||||
'stdlib',
|
||||
'stdlib2',
|
||||
'whatnow',
|
||||
'interactive',
|
||||
'floatingpoint',
|
||||
'',
|
||||
'glossary',
|
||||
]
|
||||
|
||||
|
||||
def handle_html_url(req, url):
|
||||
def inner():
|
||||
# global special pages
|
||||
if url.endswith('/contents.html'):
|
||||
return 'contents/'
|
||||
if url.endswith('/genindex.html'):
|
||||
return 'genindex/'
|
||||
if url.endswith('/about.html'):
|
||||
return 'about/'
|
||||
if url.endswith('/reporting-bugs.html'):
|
||||
return 'bugs/'
|
||||
if url == 'modindex.html' or url.endswith('/modindex.html'):
|
||||
return 'modindex/'
|
||||
if url == 'mac/using.html':
|
||||
return 'howto/pythonmac/'
|
||||
# library
|
||||
if url[:4] in ('lib/', 'mac/'):
|
||||
p = 'library/'
|
||||
m = _module_re.match(url[4:])
|
||||
if m:
|
||||
mn = m.group(1)
|
||||
return p + special_module_names.get(mn, mn)
|
||||
# module sub-pages
|
||||
m = _modsub_re.match(url[4:])
|
||||
if m and not _modobj_re.match(url[4:]):
|
||||
mn = m.group(1)
|
||||
return p + special_module_names.get(mn, mn)
|
||||
# XXX: handle all others
|
||||
# tutorial
|
||||
elif url[:4] == 'tut/':
|
||||
try:
|
||||
node = int(url[8:].split('.html')[0])
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if tutorial_nodes[node]:
|
||||
return 'tutorial/' + tutorial_nodes[node]
|
||||
# installing: all in one (ATM)
|
||||
elif url[:5] == 'inst/':
|
||||
return 'install/'
|
||||
# no mapping for "documenting Python..."
|
||||
# nothing found
|
||||
raise NotFound()
|
||||
return RedirectResponse('%s?oldurl=1' % inner())
|
@ -1,28 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.robots
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
robots.txt
|
||||
|
||||
:copyright: 2007-2008 by Georg Brandl.
|
||||
:license: BSD.
|
||||
"""
|
||||
|
||||
robots_txt = """\
|
||||
User-agent: *
|
||||
Disallow: /@source/
|
||||
Disallow: /@edit/
|
||||
Disallow: /@submit/
|
||||
Disallow: /@comments/
|
||||
Disallow: /@rss/
|
||||
Disallow: /@admin
|
||||
|
||||
User-agent: Googlebot
|
||||
Disallow: /@source/
|
||||
Disallow: /@edit/
|
||||
Disallow: /@submit/
|
||||
Disallow: /@comments/
|
||||
Disallow: /@rss/
|
||||
Disallow: /@admin
|
||||
"""
|
@ -1,99 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.serve
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This module optionally wraps the `wsgiref` module so that it reloads code
|
||||
automatically. Works with any WSGI application but it won't help in non
|
||||
`wsgiref` environments. Use it only for development.
|
||||
|
||||
:copyright: 2007-2008 by Armin Ronacher, Georg Brandl.
|
||||
:license: BSD.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import thread
|
||||
|
||||
|
||||
def reloader_loop(extra_files):
|
||||
"""When this function is run from the main thread, it will force other
|
||||
threads to exit when any modules currently loaded change.
|
||||
|
||||
:param extra_files: a list of additional files it should watch.
|
||||
"""
|
||||
mtimes = {}
|
||||
while True:
|
||||
for filename in filter(None, [getattr(module, '__file__', None)
|
||||
for module in sys.modules.values()] +
|
||||
extra_files):
|
||||
while not os.path.isfile(filename):
|
||||
filename = os.path.dirname(filename)
|
||||
if not filename:
|
||||
break
|
||||
if not filename:
|
||||
continue
|
||||
|
||||
if filename[-4:] in ('.pyc', '.pyo'):
|
||||
filename = filename[:-1]
|
||||
|
||||
mtime = os.stat(filename).st_mtime
|
||||
if filename not in mtimes:
|
||||
mtimes[filename] = mtime
|
||||
continue
|
||||
if mtime > mtimes[filename]:
|
||||
sys.exit(3)
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def restart_with_reloader():
|
||||
"""Spawn a new Python interpreter with the same arguments as this one,
|
||||
but running the reloader thread."""
|
||||
while True:
|
||||
print '* Restarting with reloader...'
|
||||
args = [sys.executable] + sys.argv
|
||||
if sys.platform == 'win32':
|
||||
args = ['"%s"' % arg for arg in args]
|
||||
new_environ = os.environ.copy()
|
||||
new_environ['RUN_MAIN'] = 'true'
|
||||
exit_code = os.spawnve(os.P_WAIT, sys.executable, args, new_environ)
|
||||
if exit_code != 3:
|
||||
return exit_code
|
||||
|
||||
|
||||
def run_with_reloader(main_func, extra_watch):
|
||||
"""
|
||||
Run the given function in an independent Python interpreter.
|
||||
"""
|
||||
if os.environ.get('RUN_MAIN') == 'true':
|
||||
thread.start_new_thread(main_func, ())
|
||||
try:
|
||||
reloader_loop(extra_watch)
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
try:
|
||||
sys.exit(restart_with_reloader())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
def run_simple(hostname, port, make_app, use_reloader=False,
|
||||
extra_files=None):
|
||||
"""
|
||||
Start an application using wsgiref and with an optional reloader.
|
||||
"""
|
||||
from wsgiref.simple_server import make_server
|
||||
def inner():
|
||||
application = make_app()
|
||||
print '* Startup complete.'
|
||||
srv = make_server(hostname, port, application)
|
||||
try:
|
||||
srv.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
if os.environ.get('RUN_MAIN') != 'true':
|
||||
print '* Running on http://%s:%d/' % (hostname, port)
|
||||
if use_reloader:
|
||||
run_with_reloader(inner, extra_files or [])
|
||||
else:
|
||||
inner()
|
@ -1,162 +0,0 @@
|
||||
/**
|
||||
* Sphinx Admin Panel
|
||||
*/
|
||||
|
||||
div.admin {
|
||||
margin: 0 -20px -30px -20px;
|
||||
padding: 0 20px 10px 20px;
|
||||
background-color: #f2f2f2;
|
||||
color: black;
|
||||
}
|
||||
|
||||
div.admin a {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.admin a:hover {
|
||||
color: black;
|
||||
}
|
||||
|
||||
div.admin h1,
|
||||
div.admin h2 {
|
||||
background-color: #555;
|
||||
border-bottom: 1px solid #222;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.admin form form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.admin input, div.admin textarea {
|
||||
font-family: 'Bitstream Vera Sans', 'Arial', sans-serif;
|
||||
font-size: 13px;
|
||||
color: #333;
|
||||
padding: 2px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #aaa;
|
||||
}
|
||||
|
||||
div.admin input[type="reset"],
|
||||
div.admin input[type="submit"] {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
div.admin input[type="reset"]:hover,
|
||||
div.admin input[type="submit"]:hover {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
div.admin div.actions {
|
||||
margin: 10px 0 0 0;
|
||||
padding: 5px;
|
||||
background-color: #aaa;
|
||||
border: 1px solid #777;
|
||||
}
|
||||
|
||||
div.admin div.error {
|
||||
margin: 10px 0 0 0;
|
||||
padding: 5px;
|
||||
border: 2px solid #222;
|
||||
background-color: #ccc;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.admin div.dialog {
|
||||
background-color: #ccc;
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
div.admin div.dialog h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
padding: 4px 10px 4px 10px;
|
||||
}
|
||||
|
||||
div.admin div.dialog div.text {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
div.admin div.dialog div.buttons {
|
||||
padding: 5px 10px 5px 10px;
|
||||
}
|
||||
|
||||
div.admin table.mapping {
|
||||
width: 100%;
|
||||
border: 1px solid #999;
|
||||
border-collapse: collapse;
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
div.admin table.mapping th {
|
||||
background-color: #ddd;
|
||||
border-bottom: 1px solid #888;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
div.admin table.mapping th.recent_comments {
|
||||
background-color: #c5cba4;
|
||||
}
|
||||
|
||||
div.admin table.mapping,
|
||||
div.admin table.mapping a {
|
||||
color: black;
|
||||
}
|
||||
|
||||
div.admin table.mapping td {
|
||||
border: 1px solid #888;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
text-align: left;
|
||||
line-height: 24px;
|
||||
padding: 0 5px 0 5px;
|
||||
}
|
||||
|
||||
div.admin table.mapping tr:hover {
|
||||
background-color: #888;
|
||||
}
|
||||
|
||||
div.admin table.mapping td.username {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
div.admin table.mapping td.pub_date {
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
div.admin table.mapping td.groups input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.admin table.mapping td.actions input {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.admin table.mapping .actions {
|
||||
text-align: right;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
div.admin table.mapping span.meta {
|
||||
font-size: 11px;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
div.admin table.mapping span.meta a {
|
||||
color: #222;
|
||||
}
|
||||
|
||||
div.admin div.detail_form dt {
|
||||
clear: both;
|
||||
float: left;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
div.admin div.detail_form textarea {
|
||||
width: 98%;
|
||||
height: 160px;
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 401 B |
Binary file not shown.
Before Width: | Height: | Size: 522 B |
Binary file not shown.
Before Width: | Height: | Size: 415 B |
Binary file not shown.
Before Width: | Height: | Size: 37 KiB |
@ -1,94 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.userdb
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
A module that provides pythonic access to the `docusers` file
|
||||
that stores users and their passwords so that they can gain access
|
||||
to the administration system.
|
||||
|
||||
:copyright: 2007-2008 by Armin Ronacher.
|
||||
:license: BSD.
|
||||
"""
|
||||
|
||||
from os import path
|
||||
from hashlib import sha1
|
||||
from random import choice, randrange
|
||||
|
||||
|
||||
def gen_password(length=8, add_numbers=True, mix_case=True,
|
||||
add_special_char=True):
|
||||
"""
|
||||
Generate a pronounceable password.
|
||||
"""
|
||||
if length <= 0:
|
||||
raise ValueError('requested password of length <= 0')
|
||||
consonants = 'bcdfghjklmnprstvwz'
|
||||
vowels = 'aeiou'
|
||||
if mix_case:
|
||||
consonants = consonants * 2 + consonants.upper()
|
||||
vowels = vowels * 2 + vowels.upper()
|
||||
pw = ''.join([choice(consonants) +
|
||||
choice(vowels) +
|
||||
choice(consonants + vowels) for _
|
||||
in xrange(length // 3 + 1)])[:length]
|
||||
if add_numbers:
|
||||
n = length // 3
|
||||
if n > 0:
|
||||
pw = pw[:-n]
|
||||
for _ in xrange(n):
|
||||
pw += choice('0123456789')
|
||||
if add_special_char:
|
||||
tmp = randrange(0, len(pw))
|
||||
l1 = pw[:tmp]
|
||||
l2 = pw[tmp:]
|
||||
if max(len(l1), len(l2)) == len(l1):
|
||||
l1 = l1[:-1]
|
||||
else:
|
||||
l2 = l2[:-1]
|
||||
return l1 + choice('#$&%?!') + l2
|
||||
return pw
|
||||
|
||||
|
||||
class UserDatabase(object):
|
||||
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
self.users = {}
|
||||
self.privileges = {}
|
||||
if path.exists(filename):
|
||||
f = open(filename)
|
||||
try:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and line[0] != '#':
|
||||
parts = line.split(':')
|
||||
self.users[parts[0]] = parts[1]
|
||||
self.privileges.setdefault(parts[0], set()).update(
|
||||
x for x in parts[2].split(',') if x)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def set_password(self, user, password):
|
||||
"""Encode the password for a user (also adds users)."""
|
||||
self.users[user] = sha1('%s|%s' % (user, password)).hexdigest()
|
||||
|
||||
def add_user(self, user):
|
||||
"""Add a new user and return the generated password."""
|
||||
pw = gen_password(8, add_special_char=False)
|
||||
self.set_password(user, pw)
|
||||
self.privileges[user].clear()
|
||||
return pw
|
||||
|
||||
def check_password(self, user, password):
|
||||
return user in self.users and \
|
||||
self.users[user] == sha1('%s|%s' % (user, password)).hexdigest()
|
||||
|
||||
def save(self):
|
||||
f = open(self.filename, 'w')
|
||||
try:
|
||||
for username, password in self.users.iteritems():
|
||||
privileges = ','.join(self.privileges.get(username, ()))
|
||||
f.write('%s:%s:%s\n' % (username, password, privileges))
|
||||
finally:
|
||||
f.close()
|
@ -1,94 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.util
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Miscellaneous utilities.
|
||||
|
||||
:copyright: 2007-2008 by Georg Brandl.
|
||||
:license: BSD.
|
||||
"""
|
||||
|
||||
import re
|
||||
from os import path
|
||||
|
||||
from sphinx.util import relative_uri
|
||||
from sphinx._jinja import Environment, FileSystemLoader
|
||||
|
||||
|
||||
def get_target_uri(source_filename):
|
||||
"""Get the web-URI for a given reST file name (without extension)."""
|
||||
if source_filename == 'index':
|
||||
return ''
|
||||
if source_filename.endswith('/index'):
|
||||
return source_filename[:-5] # up to /
|
||||
return source_filename + '/'
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Setup the templating environment
|
||||
|
||||
templates_path = path.join(path.dirname(__file__), '..', 'templates')
|
||||
jinja_env = Environment(loader=FileSystemLoader(templates_path,
|
||||
use_memcache=True),
|
||||
friendly_traceback=True)
|
||||
|
||||
def do_datetime_format():
|
||||
def wrapped(env, ctx, value):
|
||||
return value.strftime('%a, %d %b %Y %H:%M')
|
||||
return wrapped
|
||||
|
||||
jinja_env.filters['datetimeformat'] = do_datetime_format
|
||||
|
||||
|
||||
_striptags_re = re.compile(r'(<!--.*?-->|<[^>]+>)')
|
||||
|
||||
def striptags(text):
|
||||
return ' '.join(_striptags_re.sub('', text).split())
|
||||
|
||||
|
||||
def render_template(req, template_name, *contexts):
|
||||
context = {}
|
||||
for ctx in contexts:
|
||||
context.update(ctx)
|
||||
tmpl = jinja_env.get_template(template_name)
|
||||
|
||||
path = req.path.lstrip('/')
|
||||
if not path[-1:] == '/':
|
||||
path += '/'
|
||||
def relative_path_to(otheruri, resource=False):
|
||||
if not resource:
|
||||
otheruri = get_target_uri(otheruri)
|
||||
return relative_uri(path, otheruri)
|
||||
context['pathto'] = relative_path_to
|
||||
|
||||
# add it here a second time for templates that don't
|
||||
# get the builder information from the environment (such as search)
|
||||
context['builder'] = 'web'
|
||||
context['req'] = req
|
||||
|
||||
return tmpl.render(context)
|
||||
|
||||
|
||||
class lazy_property(object):
|
||||
"""
|
||||
Descriptor implementing a "lazy property", i.e. the function
|
||||
calculating the property value is called only once.
|
||||
"""
|
||||
|
||||
def __init__(self, func, name=None, doc=None):
|
||||
self._func = func
|
||||
self._name = name or func.func_name
|
||||
self.__doc__ = doc or func.__doc__
|
||||
|
||||
def __get__(self, obj, objtype=None):
|
||||
if obj is None:
|
||||
return self
|
||||
value = self._func(obj)
|
||||
setattr(obj, self._name, value)
|
||||
return value
|
||||
|
||||
|
||||
class blackhole_dict(dict):
|
||||
def __setitem__(self, key, value):
|
||||
pass
|
@ -1,13 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Sphinx documentation web application configuration file
|
||||
#
|
||||
|
||||
# Where the server listens.
|
||||
listen_addr = 'localhost'
|
||||
listen_port = 3000
|
||||
|
||||
# How patch mails are sent.
|
||||
patch_mail_from = 'patches@localhost'
|
||||
patch_mail_to = 'docs@localhost'
|
||||
patch_mail_smtp = 'localhost'
|
@ -1,705 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.web.wsgiutil
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To avoid further dependencies this module collects some of the
|
||||
classes werkzeug provides and use in other views.
|
||||
|
||||
:copyright: 2007-2008 by Armin Ronacher.
|
||||
:license: BSD.
|
||||
"""
|
||||
|
||||
import cgi
|
||||
import urllib
|
||||
import cPickle as pickle
|
||||
import tempfile
|
||||
from os import path
|
||||
from time import gmtime, time, asctime
|
||||
from random import random
|
||||
from Cookie import SimpleCookie
|
||||
from hashlib import sha1
|
||||
from datetime import datetime
|
||||
from cStringIO import StringIO
|
||||
|
||||
from sphinx.web.util import lazy_property
|
||||
from sphinx.util.json import dump_json
|
||||
|
||||
|
||||
HTTP_STATUS_CODES = {
|
||||
100: 'CONTINUE',
|
||||
101: 'SWITCHING PROTOCOLS',
|
||||
102: 'PROCESSING',
|
||||
200: 'OK',
|
||||
201: 'CREATED',
|
||||
202: 'ACCEPTED',
|
||||
203: 'NON-AUTHORITATIVE INFORMATION',
|
||||
204: 'NO CONTENT',
|
||||
205: 'RESET CONTENT',
|
||||
206: 'PARTIAL CONTENT',
|
||||
207: 'MULTI STATUS',
|
||||
300: 'MULTIPLE CHOICES',
|
||||
301: 'MOVED PERMANENTLY',
|
||||
302: 'FOUND',
|
||||
303: 'SEE OTHER',
|
||||
304: 'NOT MODIFIED',
|
||||
305: 'USE PROXY',
|
||||
306: 'RESERVED',
|
||||
307: 'TEMPORARY REDIRECT',
|
||||
400: 'BAD REQUEST',
|
||||
401: 'UNAUTHORIZED',
|
||||
402: 'PAYMENT REQUIRED',
|
||||
403: 'FORBIDDEN',
|
||||
404: 'NOT FOUND',
|
||||
405: 'METHOD NOT ALLOWED',
|
||||
406: 'NOT ACCEPTABLE',
|
||||
407: 'PROXY AUTHENTICATION REQUIRED',
|
||||
408: 'REQUEST TIMEOUT',
|
||||
409: 'CONFLICT',
|
||||
410: 'GONE',
|
||||
411: 'LENGTH REQUIRED',
|
||||
412: 'PRECONDITION FAILED',
|
||||
413: 'REQUEST ENTITY TOO LARGE',
|
||||
414: 'REQUEST-URI TOO LONG',
|
||||
415: 'UNSUPPORTED MEDIA TYPE',
|
||||
416: 'REQUESTED RANGE NOT SATISFIABLE',
|
||||
417: 'EXPECTATION FAILED',
|
||||
500: 'INTERNAL SERVER ERROR',
|
||||
501: 'NOT IMPLEMENTED',
|
||||
502: 'BAD GATEWAY',
|
||||
503: 'SERVICE UNAVAILABLE',
|
||||
504: 'GATEWAY TIMEOUT',
|
||||
505: 'HTTP VERSION NOT SUPPORTED',
|
||||
506: 'VARIANT ALSO VARIES',
|
||||
507: 'INSUFFICIENT STORAGE',
|
||||
510: 'NOT EXTENDED'
|
||||
}
|
||||
|
||||
SID_COOKIE_NAME = 'python_doc_sid'
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Support for HTTP parameter parsing, requests and responses
|
||||
|
||||
|
||||
class _StorageHelper(cgi.FieldStorage):
|
||||
"""
|
||||
Helper class used by `Request` to parse submitted file and
|
||||
form data. Don't use this class directly.
|
||||
"""
|
||||
|
||||
FieldStorageClass = cgi.FieldStorage
|
||||
|
||||
def __init__(self, environ, get_stream):
|
||||
cgi.FieldStorage.__init__(self,
|
||||
fp=environ['wsgi.input'],
|
||||
environ={
|
||||
'REQUEST_METHOD': environ['REQUEST_METHOD'],
|
||||
'CONTENT_TYPE': environ['CONTENT_TYPE'],
|
||||
'CONTENT_LENGTH': environ['CONTENT_LENGTH']
|
||||
},
|
||||
keep_blank_values=True
|
||||
)
|
||||
self.get_stream = get_stream
|
||||
|
||||
def make_file(self, binary=None):
|
||||
return self.get_stream()
|
||||
|
||||
|
||||
class MultiDict(dict):
|
||||
"""
|
||||
A dict that takes a list of multiple values as only argument
|
||||
in order to store multiple values per key.
|
||||
"""
|
||||
|
||||
def __init__(self, mapping=()):
|
||||
if isinstance(mapping, MultiDict):
|
||||
dict.__init__(self, mapping.lists())
|
||||
elif isinstance(mapping, dict):
|
||||
tmp = {}
|
||||
for key, value in mapping:
|
||||
tmp[key] = [value]
|
||||
dict.__init__(self, tmp)
|
||||
else:
|
||||
tmp = {}
|
||||
for key, value in mapping:
|
||||
tmp.setdefault(key, []).append(value)
|
||||
dict.__init__(self, tmp)
|
||||
|
||||
def __getitem__(self, key):
|
||||
"""
|
||||
Return the first data value for this key;
|
||||
raises KeyError if not found.
|
||||
"""
|
||||
return dict.__getitem__(self, key)[0]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set an item as list."""
|
||||
dict.__setitem__(self, key, [value])
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""Return the default value if the requested data doesn't exist"""
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def getlist(self, key):
|
||||
"""Return an empty list if the requested data doesn't exist"""
|
||||
try:
|
||||
return dict.__getitem__(self, key)
|
||||
except KeyError:
|
||||
return []
|
||||
|
||||
def setlist(self, key, new_list):
|
||||
"""Set new values for an key."""
|
||||
dict.__setitem__(self, key, list(new_list))
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
if key not in self:
|
||||
self[key] = default
|
||||
else:
|
||||
default = self[key]
|
||||
return default
|
||||
|
||||
def setlistdefault(self, key, default_list=()):
|
||||
if key not in self:
|
||||
default_list = list(default_list)
|
||||
dict.__setitem__(self, key, default_list)
|
||||
else:
|
||||
default_list = self.getlist(key)
|
||||
return default_list
|
||||
|
||||
def items(self):
|
||||
"""
|
||||
Return a list of (key, value) pairs, where value is the last item in
|
||||
the list associated with the key.
|
||||
"""
|
||||
return [(key, self[key]) for key in self.iterkeys()]
|
||||
|
||||
lists = dict.items
|
||||
|
||||
def values(self):
|
||||
"""Returns a list of the last value on every key list."""
|
||||
return [self[key] for key in self.iterkeys()]
|
||||
|
||||
listvalues = dict.values
|
||||
|
||||
def iteritems(self):
|
||||
for key, values in dict.iteritems(self):
|
||||
yield key, values[0]
|
||||
|
||||
iterlists = dict.iteritems
|
||||
|
||||
def itervalues(self):
|
||||
for values in dict.itervalues(self):
|
||||
yield values[0]
|
||||
|
||||
iterlistvalues = dict.itervalues
|
||||
|
||||
def copy(self):
|
||||
"""Return a shallow copy of this object."""
|
||||
return self.__class__(self)
|
||||
|
||||
def update(self, other_dict):
|
||||
"""update() extends rather than replaces existing key lists."""
|
||||
if isinstance(other_dict, MultiDict):
|
||||
for key, value_list in other_dict.iterlists():
|
||||
self.setlistdefault(key, []).extend(value_list)
|
||||
elif isinstance(other_dict, dict):
|
||||
for key, value in other_dict.items():
|
||||
self.setlistdefault(key, []).append(value)
|
||||
else:
|
||||
for key, value in other_dict:
|
||||
self.setlistdefault(key, []).append(value)
|
||||
|
||||
def pop(self, *args):
|
||||
"""Pop the first item for a list on the dict."""
|
||||
return dict.pop(self, *args)[0]
|
||||
|
||||
def popitem(self):
|
||||
"""Pop an item from the dict."""
|
||||
item = dict.popitem(self)
|
||||
return (item[0], item[1][0])
|
||||
|
||||
poplist = dict.pop
|
||||
popitemlist = dict.popitem
|
||||
|
||||
def __repr__(self):
|
||||
tmp = []
|
||||
for key, values in self.iterlists():
|
||||
for value in values:
|
||||
tmp.append((key, value))
|
||||
return '%s(%r)' % (self.__class__.__name__, tmp)
|
||||
|
||||
|
||||
class Headers(object):
|
||||
"""
|
||||
An object that stores some headers.
|
||||
"""
|
||||
|
||||
def __init__(self, defaults=None):
|
||||
self._list = []
|
||||
if isinstance(defaults, dict):
|
||||
for key, value in defaults.iteritems():
|
||||
if isinstance(value, (tuple, list)):
|
||||
for v in value:
|
||||
self._list.append((key, v))
|
||||
else:
|
||||
self._list.append((key, value))
|
||||
elif defaults is not None:
|
||||
for key, value in defaults:
|
||||
self._list.append((key, value))
|
||||
|
||||
def __getitem__(self, key):
|
||||
ikey = key.lower()
|
||||
for k, v in self._list:
|
||||
if k.lower() == ikey:
|
||||
return v
|
||||
raise KeyError(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def getlist(self, key):
|
||||
ikey = key.lower()
|
||||
result = []
|
||||
for k, v in self._list:
|
||||
if k.lower() == ikey:
|
||||
result.append((k, v))
|
||||
return result
|
||||
|
||||
def setlist(self, key, values):
|
||||
del self[key]
|
||||
self.addlist(key, values)
|
||||
|
||||
def addlist(self, key, values):
|
||||
self._list.extend(values)
|
||||
|
||||
def lists(self, lowercased=False):
|
||||
if not lowercased:
|
||||
return self._list[:]
|
||||
return [(x.lower(), y) for x, y in self._list]
|
||||
|
||||
def iterlists(self, lowercased=False):
|
||||
for key, value in self._list:
|
||||
if lowercased:
|
||||
key = key.lower()
|
||||
yield key, value
|
||||
|
||||
def iterkeys(self):
|
||||
for key, _ in self.iterlists():
|
||||
yield key
|
||||
|
||||
def itervalues(self):
|
||||
for _, value in self.iterlists():
|
||||
yield value
|
||||
|
||||
def keys(self):
|
||||
return list(self.iterkeys())
|
||||
|
||||
def values(self):
|
||||
return list(self.itervalues())
|
||||
|
||||
def __delitem__(self, key):
|
||||
key = key.lower()
|
||||
new = []
|
||||
for k, v in self._list:
|
||||
if k != key:
|
||||
new.append((k, v))
|
||||
self._list[:] = new
|
||||
|
||||
remove = __delitem__
|
||||
|
||||
def __contains__(self, key):
|
||||
key = key.lower()
|
||||
for k, v in self._list:
|
||||
if k.lower() == key:
|
||||
return True
|
||||
return False
|
||||
|
||||
has_key = __contains__
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._list)
|
||||
|
||||
def add(self, key, value):
|
||||
"""add a new header tuple to the list"""
|
||||
self._list.append((key, value))
|
||||
|
||||
def clear(self):
|
||||
"""clears all headers"""
|
||||
del self._list[:]
|
||||
|
||||
def set(self, key, value):
|
||||
"""remove all header tuples for key and add
|
||||
a new one
|
||||
"""
|
||||
del self[key]
|
||||
self.add(key, value)
|
||||
|
||||
__setitem__ = set
|
||||
|
||||
def to_list(self, charset):
|
||||
"""Create a str only list of the headers."""
|
||||
result = []
|
||||
for k, v in self:
|
||||
if isinstance(v, unicode):
|
||||
v = v.encode(charset)
|
||||
else:
|
||||
v = str(v)
|
||||
result.append((k, v))
|
||||
return result
|
||||
|
||||
def copy(self):
|
||||
return self.__class__(self._list)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (
|
||||
self.__class__.__name__,
|
||||
self._list
|
||||
)
|
||||
|
||||
|
||||
class Session(dict):
|
||||
|
||||
def __init__(self, sid):
|
||||
self.sid = sid
|
||||
if sid is not None:
|
||||
if path.exists(self.filename):
|
||||
f = open(self.filename, 'rb')
|
||||
try:
|
||||
self.update(pickle.load(f))
|
||||
finally:
|
||||
f.close()
|
||||
self._orig = dict(self)
|
||||
|
||||
@property
|
||||
def filename(self):
|
||||
if self.sid is not None:
|
||||
return path.join(tempfile.gettempdir(), '__pydoc_sess' + self.sid)
|
||||
|
||||
@property
|
||||
def worth_saving(self):
|
||||
return self != self._orig
|
||||
|
||||
def save(self):
|
||||
if self.sid is None:
|
||||
self.sid = sha1('%s|%s' % (time(), random())).hexdigest()
|
||||
f = open(self.filename, 'wb')
|
||||
try:
|
||||
pickle.dump(dict(self), f, pickle.HIGHEST_PROTOCOL)
|
||||
finally:
|
||||
f.close()
|
||||
self._orig = dict(self)
|
||||
|
||||
|
||||
class Request(object):
|
||||
charset = 'utf-8'
|
||||
|
||||
def __init__(self, environ):
|
||||
self.environ = environ
|
||||
self.environ['werkzeug.request'] = self
|
||||
self.session = Session(self.cookies.get(SID_COOKIE_NAME))
|
||||
self.user = self.session.get('user')
|
||||
|
||||
def login(self, user):
|
||||
self.user = self.session['user'] = user
|
||||
|
||||
def logout(self):
|
||||
self.user = None
|
||||
self.session.pop('user', None)
|
||||
|
||||
def _get_file_stream(self):
|
||||
"""Called to get a stream for the file upload.
|
||||
|
||||
This must provide a file-like class with `read()`, `readline()`
|
||||
and `seek()` methods that is both writeable and readable."""
|
||||
return tempfile.TemporaryFile('w+b')
|
||||
|
||||
def _load_post_data(self):
|
||||
"""Method used internally to retrieve submitted data."""
|
||||
self._data = ''
|
||||
post = []
|
||||
files = []
|
||||
if self.environ['REQUEST_METHOD'] in ('POST', 'PUT'):
|
||||
storage = _StorageHelper(self.environ, self._get_file_stream)
|
||||
for key in storage.keys():
|
||||
values = storage[key]
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
for item in values:
|
||||
if getattr(item, 'filename', None) is not None:
|
||||
fn = item.filename.decode(self.charset, 'ignore')
|
||||
# fix stupid IE bug
|
||||
if len(fn) > 1 and fn[1] == ':' and '\\' in fn:
|
||||
fn = fn[fn.index('\\') + 1:]
|
||||
files.append((key, FileStorage(key, fn, item.type,
|
||||
item.length, item.file)))
|
||||
else:
|
||||
post.append((key, item.value.decode(self.charset,
|
||||
'ignore')))
|
||||
self._form = MultiDict(post)
|
||||
self._files = MultiDict(files)
|
||||
|
||||
def read(self, *args):
|
||||
if not hasattr(self, '_buffered_stream'):
|
||||
self._buffered_stream = StringIO(self.data)
|
||||
return self._buffered_stream.read(*args)
|
||||
|
||||
def readline(self, *args):
|
||||
if not hasattr(self, '_buffered_stream'):
|
||||
self._buffered_stream = StringIO(self.data)
|
||||
return self._buffered_stream.readline(*args)
|
||||
|
||||
def make_external_url(self, path):
|
||||
url = self.environ['wsgi.url_scheme'] + '://'
|
||||
if 'HTTP_HOST' in self.environ:
|
||||
url += self.environ['HTTP_HOST']
|
||||
else:
|
||||
url += self.environ['SERVER_NAME']
|
||||
if (self.environ['wsgi.url_scheme'], self.environ['SERVER_PORT']) not \
|
||||
in (('https', '443'), ('http', '80')):
|
||||
url += ':' + self.environ['SERVER_PORT']
|
||||
|
||||
url += urllib.quote(self.environ.get('SCRIPT_INFO', '').rstrip('/'))
|
||||
if not path.startswith('/'):
|
||||
path = '/' + path
|
||||
return url + path
|
||||
|
||||
def args(self):
|
||||
"""URL parameters"""
|
||||
items = []
|
||||
qs = self.environ.get('QUERY_STRING', '')
|
||||
for key, values in cgi.parse_qs(qs, True).iteritems():
|
||||
for value in values:
|
||||
value = value.decode(self.charset, 'ignore')
|
||||
items.append((key, value))
|
||||
return MultiDict(items)
|
||||
args = lazy_property(args)
|
||||
|
||||
def data(self):
|
||||
"""raw value of input stream."""
|
||||
if not hasattr(self, '_data'):
|
||||
self._load_post_data()
|
||||
return self._data
|
||||
data = lazy_property(data)
|
||||
|
||||
def form(self):
|
||||
"""form parameters."""
|
||||
if not hasattr(self, '_form'):
|
||||
self._load_post_data()
|
||||
return self._form
|
||||
form = lazy_property(form)
|
||||
|
||||
def files(self):
|
||||
"""File uploads."""
|
||||
if not hasattr(self, '_files'):
|
||||
self._load_post_data()
|
||||
return self._files
|
||||
files = lazy_property(files)
|
||||
|
||||
def cookies(self):
|
||||
"""Stored Cookies."""
|
||||
cookie = SimpleCookie()
|
||||
cookie.load(self.environ.get('HTTP_COOKIE', ''))
|
||||
result = {}
|
||||
for key, value in cookie.iteritems():
|
||||
result[key] = value.value.decode(self.charset, 'ignore')
|
||||
return result
|
||||
cookies = lazy_property(cookies)
|
||||
|
||||
def method(self):
|
||||
"""Request method."""
|
||||
return self.environ['REQUEST_METHOD']
|
||||
method = property(method, doc=method.__doc__)
|
||||
|
||||
def path(self):
|
||||
"""Requested path."""
|
||||
path = '/' + (self.environ.get('PATH_INFO') or '').lstrip('/')
|
||||
path = path.decode(self.charset, self.charset)
|
||||
parts = path.replace('+', ' ').split('/')
|
||||
return u'/'.join(p for p in parts if p != '..')
|
||||
path = lazy_property(path)
|
||||
|
||||
|
||||
class Response(object):
|
||||
charset = 'utf-8'
|
||||
default_mimetype = 'text/html'
|
||||
|
||||
def __init__(self, response=None, headers=None, status=200, mimetype=None):
|
||||
if response is None:
|
||||
self.response = []
|
||||
elif isinstance(response, basestring):
|
||||
self.response = [response]
|
||||
else:
|
||||
self.response = iter(response)
|
||||
if not headers:
|
||||
self.headers = Headers()
|
||||
elif isinstance(headers, Headers):
|
||||
self.headers = headers
|
||||
else:
|
||||
self.headers = Headers(headers)
|
||||
if mimetype is None and 'Content-Type' not in self.headers:
|
||||
mimetype = self.default_mimetype
|
||||
if mimetype is not None:
|
||||
if 'charset=' not in mimetype and mimetype.startswith('text/'):
|
||||
mimetype += '; charset=' + self.charset
|
||||
self.headers['Content-Type'] = mimetype
|
||||
self.status = status
|
||||
self._cookies = None
|
||||
|
||||
def write(self, value):
|
||||
if not isinstance(self.response, list):
|
||||
raise RuntimeError('cannot write to streaming response')
|
||||
self.write = self.response.append
|
||||
self.response.append(value)
|
||||
|
||||
def set_cookie(self, key, value='', max_age=None, expires=None,
|
||||
path='/', domain=None, secure=None):
|
||||
if self._cookies is None:
|
||||
self._cookies = SimpleCookie()
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode(self.charset)
|
||||
self._cookies[key] = value
|
||||
if max_age is not None:
|
||||
self._cookies[key]['max-age'] = max_age
|
||||
if expires is not None:
|
||||
if isinstance(expires, basestring):
|
||||
self._cookies[key]['expires'] = expires
|
||||
expires = None
|
||||
elif isinstance(expires, datetime):
|
||||
expires = expires.utctimetuple()
|
||||
elif not isinstance(expires, (int, long)):
|
||||
expires = gmtime(expires)
|
||||
else:
|
||||
raise ValueError('datetime or integer required')
|
||||
month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',
|
||||
'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][expires.tm_mon - 1]
|
||||
day = ['Monday', 'Tuesday', 'Wednesday', 'Thursday',
|
||||
'Friday', 'Saturday', 'Sunday'][expires.tm_wday]
|
||||
date = '%02d-%s-%s' % (
|
||||
expires.tm_mday, month, str(expires.tm_year)[-2:]
|
||||
)
|
||||
d = '%s, %s %02d:%02d:%02d GMT' % (day, date, expires.tm_hour,
|
||||
expires.tm_min, expires.tm_sec)
|
||||
self._cookies[key]['expires'] = d
|
||||
if path is not None:
|
||||
self._cookies[key]['path'] = path
|
||||
if domain is not None:
|
||||
self._cookies[key]['domain'] = domain
|
||||
if secure is not None:
|
||||
self._cookies[key]['secure'] = secure
|
||||
|
||||
def delete_cookie(self, key):
|
||||
if self._cookies is None:
|
||||
self._cookies = SimpleCookie()
|
||||
if key not in self._cookies:
|
||||
self._cookies[key] = ''
|
||||
self._cookies[key]['max-age'] = 0
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
req = environ['werkzeug.request']
|
||||
if req.session.worth_saving:
|
||||
req.session.save()
|
||||
self.set_cookie(SID_COOKIE_NAME, req.session.sid)
|
||||
|
||||
headers = self.headers.to_list(self.charset)
|
||||
if self._cookies is not None:
|
||||
for morsel in self._cookies.values():
|
||||
headers.append(('Set-Cookie', morsel.output(header='')))
|
||||
status = '%d %s' % (self.status, HTTP_STATUS_CODES[self.status])
|
||||
|
||||
charset = self.charset or 'ascii'
|
||||
start_response(status, headers)
|
||||
for item in self.response:
|
||||
if isinstance(item, unicode):
|
||||
yield item.encode(charset)
|
||||
else:
|
||||
yield str(item)
|
||||
|
||||
def get_base_uri(environ):
|
||||
url = environ['wsgi.url_scheme'] + '://'
|
||||
if 'HTTP_HOST' in environ:
|
||||
url += environ['HTTP_HOST']
|
||||
else:
|
||||
url += environ['SERVER_NAME']
|
||||
if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \
|
||||
in (('https', '443'), ('http', '80')):
|
||||
url += ':' + environ['SERVER_PORT']
|
||||
url += urllib.quote(environ.get('SCRIPT_INFO', '').rstrip('/'))
|
||||
return url
|
||||
|
||||
|
||||
class RedirectResponse(Response):
|
||||
|
||||
def __init__(self, target_url, code=302):
|
||||
if not target_url.startswith('/'):
|
||||
target_url = '/' + target_url
|
||||
self.target_url = target_url
|
||||
super(RedirectResponse, self).__init__('Moved...', status=code)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
url = get_base_uri(environ) + self.target_url
|
||||
self.headers['Location'] = url
|
||||
return super(RedirectResponse, self).__call__(environ, start_response)
|
||||
|
||||
|
||||
class JSONResponse(Response):
|
||||
|
||||
def __init__(self, data):
|
||||
assert not isinstance(data, list), 'list unsafe for json dumping'
|
||||
super(JSONResponse, self).__init__(dump_json(data), mimetype='text/javascript')
|
||||
|
||||
|
||||
class SharedDataMiddleware(object):
|
||||
"""
|
||||
Redirects calls to an folder with static data.
|
||||
"""
|
||||
|
||||
def __init__(self, app, exports):
|
||||
self.app = app
|
||||
self.exports = exports
|
||||
self.cache = {}
|
||||
|
||||
def serve_file(self, filename, start_response):
|
||||
from mimetypes import guess_type
|
||||
guessed_type = guess_type(filename)
|
||||
mime_type = guessed_type[0] or 'text/plain'
|
||||
expiry = time() + 3600 # one hour
|
||||
expiry = asctime(gmtime(expiry))
|
||||
start_response('200 OK', [('Content-Type', mime_type),
|
||||
('Cache-Control', 'public'),
|
||||
('Expires', expiry)])
|
||||
f = open(filename, 'rb')
|
||||
try:
|
||||
return [f.read()]
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
p = environ.get('PATH_INFO', '')
|
||||
if p in self.cache:
|
||||
return self.serve_file(self.cache[p], start_response)
|
||||
for search_path, file_path in self.exports.iteritems():
|
||||
if not search_path.endswith('/'):
|
||||
search_path += '/'
|
||||
if p.startswith(search_path):
|
||||
real_path = path.join(file_path, p[len(search_path):])
|
||||
if path.exists(real_path) and path.isfile(real_path):
|
||||
self.cache[p] = real_path
|
||||
return self.serve_file(real_path, start_response)
|
||||
return self.app(environ, start_response)
|
||||
|
||||
|
||||
class NotFound(Exception):
|
||||
"""
|
||||
Raise to display the 404 error page.
|
||||
"""
|
||||
|
||||
def __init__(self, show_keyword_matches=False):
|
||||
self.show_keyword_matches = show_keyword_matches
|
||||
Exception.__init__(self, show_keyword_matches)
|
Loading…
Reference in New Issue
Block a user