sphinx / sphinx / web / application.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
# -*- coding: utf-8 -*-
"""
    sphinx.web.application
    ~~~~~~~~~~~~~~~~~~~~~~

    A simple WSGI application that serves an interactive version
    of the python documentation.

    :copyright: 2007 by Georg Brandl, Armin Ronacher.
    :license: Python license.
"""
from __future__ import with_statement

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 collections import defaultdict

from .feed import Feed
from .mail import Email
from .util import render_template, render_simple_template, get_target_uri, \
     blackhole_dict, striptags
from .admin import AdminPanel
from .userdb import UserDatabase
from .oldurls import handle_html_url
from .antispam import AntiSpam
from .database import connect, set_connection, Comment
from .wsgiutil import Request, Response, RedirectResponse, \
     JSONResponse, SharedDataMiddleware, NotFound, get_base_uri

from ..util import relative_uri, shorten_result
from ..search import SearchFrontend
from ..writer import HTMLWriter
from ..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 ''


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):
        self.cache = blackhole_dict() if config['debug'] else {}
        self.freqmodules = defaultdict(int)
        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..."
            with file(path.join(self.data_root, ENV_PICKLE_FILENAME)) as f:
                self.env = pickle.load(f)
            with file(path.join(self.data_root, 'globalcontext.pickle')) as f:
                self.globalcontext = pickle.load(f)
            with file(path.join(self.data_root, 'searchindex.pickle')) as f:
                self.search_frontend = SearchFrontend(pickle.load(f))
            self.buildmtime = path.getmtime(self.buildfile)
            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)
        if page_id is None:
            raise NotFound()
        filename = path.join(self.data_root, 'sources', page_id)[:-3] + 'txt'
        with file(filename) as f:
            return page_id, f.read()


    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) 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')
        writer = HTMLWriter(env2.config)
        doctree = env2.read_file(page_id, pathname, save_parsed=False)
        doctree = env2.get_and_resolve_doctree(page_id, MockBuilder(), doctree)
        doctree.settings = OptionParser(defaults=env2.settings,
                                        components=(writer,)).get_default_values()
        doctree.reporter = Reporter(page_id, 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.rpartition('?')[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[0][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')
        with open(filename, 'rb') as f:
            context = pickle.load(f)
        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)
        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)
        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)
        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, ()):
            self.freqmodules[modname] += 1
        # comments enabled?
        comments = self.env.metadata[page_id].get('nocomments', False)

        # how does the user want to view comments?
        commentmode = req.session.get('comments', 'inline') if comments else ''

        # 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[:-3] + 'fpickle')
        with open(filename, 'rb') as f:
            context = pickle.load(f)

        # 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')
        with open(filename, 'rb') as f:
            context = pickle.load(f)
        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)
            doctitle = striptags(self.globalcontext['titles'].get(page_id, 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))


    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 if is_error_page else 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]:
                with file(path.join(self.data_root, 'style', filename)) as f:
                    stylesheet.append(f.read())
            stylesheet = '\n'.join(stylesheet)
            if not self.config.get('debug'):
                self.generated_stylesheets[style] = stylesheet

        if req.args.get('admin') == 'yes':
            with file(path.join(self.data_root, 'style', 'admin.css')) as f:
                stylesheet += '\n' + f.read()

        # 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 = self.get_error_404()
            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
            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, {
        '/style':   path.join(config['data_root_path'], 'style')
    })
    return app
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.