Passed
Pull Request — develop (#268)
by Jace
02:01
created

strip_path()   A

Complexity

Conditions 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
c 2
b 0
f 0
dl 0
loc 4
ccs 1
cts 1
cp 1
crap 1
rs 10
1
#!/usr/bin/env python
2
3 1
"""REST server to display content and reserve item numbers."""
4
5 1
import os
6 1
from collections import defaultdict
7 1
import webbrowser
8 1
import argparse
9 1
import logging
10
11 1
import bottle
12 1
from bottle import get, post, request, hook, response, template
13
14 1
from doorstop import common, build, publisher
15 1
from doorstop.common import HelpFormatter
16 1
from doorstop.server import utilities
17 1
from doorstop import settings
18 1
from doorstop.core import vcs
19
20 1
log = common.logger(__name__)
21
22 1
app = utilities.StripPathMiddleware(bottle.app())
23 1
tree = None  # set in `run`, read in the route functions
24 1
numbers = defaultdict(int)  # cache of next document numbers
25
26
27 1
def main(args=None):
28
    """Process command-line arguments and run the program."""
29 1
    from doorstop import SERVER, VERSION
30
31
    # Shared options
32 1
    debug = argparse.ArgumentParser(add_help=False)
33 1
    debug.add_argument('-V', '--version', action='version', version=VERSION)
34 1
    debug.add_argument('--debug', action='store_true', help=argparse.SUPPRESS)
35 1
    debug.add_argument('--launch', action='store_true', help=argparse.SUPPRESS)
36 1
    shared = {'formatter_class': HelpFormatter, 'parents': [debug]}
37
38
    # Build main parser
39 1
    parser = argparse.ArgumentParser(prog=SERVER, description=__doc__,
40
                                     **shared)
41 1
    cwd = os.getcwd()
42 1
43
    parser.add_argument('-j', '--project', default=None,
44 1
                        help="path to the root of the project")
45
    parser.add_argument('-P', '--port', metavar='NUM', type=int,
46 1
                        default=settings.SERVER_PORT,
47
                        help="use a custom port for the server")
48
    parser.add_argument('-H', '--host', default='127.0.0.1',
49 1
                        help="IP address to listen")
50
    parser.add_argument('-w', '--wsgi', action='store_true',
51
                        help="Run as a WSGI process")
52
    parser.add_argument('-b', '--baseurl', default='',
53 1
                        help="Base URL this is served at (Usually only necessary for WSGI)")
54
55
    # Parse arguments
56 1
    args = parser.parse_args(args=args)
57
58
    if args.project is None:
59
        args.project = vcs.find_root(cwd)
60 1
61
    # Configure logging
62
    logging.basicConfig(format=settings.VERBOSE_LOGGING_FORMAT,
63 1
                        level=settings.VERBOSE_LOGGING_LEVEL)
64
65
    # Run the program
66
    run(args, os.getcwd(), parser.error)
67
68
69
def run(args, cwd, _):
70
    """Start the server.
71
72 1
    :param args: Namespace of CLI arguments (from this module or the CLI)
73 1
    :param cwd: current working directory
74 1
    :param error: function to call for CLI errors
75 1
76 1
    """
77 1
    global tree  # pylint: disable=W0603
78 1
    tree = build(cwd=cwd, root=args.project)
79 1
    tree.load()
80
    host = args.host
81
    port = args.port or settings.SERVER_PORT
82
    bottle.TEMPLATE_PATH.insert(0, os.path.join(os.path.dirname(__file__),
83 1
                                                '..', '..', 'views'))
84
85
    # If you started without WSGI, the base will be '/'.
86
    if args.baseurl == '' and not args.wsgi:
87
        args.baseurl = '/'
88
89 1
    # If you specified a base URL, make sure it ends with '/'.
90
    if args.baseurl != '' and not args.baseurl.endswith('/'):
91
        args.baseurl += '/'
92 1
93 1
    bottle.SimpleTemplate.defaults['baseurl'] = args.baseurl
94 1
    bottle.SimpleTemplate.defaults['navigation'] = True
95
96
    if args.launch:
97 1
        url = utilities.build_url(host=host, port=port)
98
        webbrowser.open(url)
99
    if not args.wsgi:
100 1
        bottle.run(app=app, host=host, port=port,
101 1
                   debug=args.debug, reloader=args.debug)
102 1
103 1
104
@hook('before_request')
105 1
def strip_path():
106
    request.environ['PATH_INFO'] = request.environ['PATH_INFO'].rstrip('/')
107
    request.environ['PATH_INFO'] = request.environ['PATH_INFO'].rstrip('.html')
108 1
109
110
@hook('after_request')
111 1
def enable_cors():  # pragma: no cover (manual test)
112 1
    """Allow a webserver running on the same machine to access data."""
113 1
    response.headers['Access-Control-Allow-Origin'] = '*'
114
115 1
116 1
@get('/')
117
def index():
118
    """Read the tree."""
119 1
    yield template('index', tree_code=tree.draw(html_links=True))
120
121
122 1
@get('/documents')
123 1
def get_documents():
124 1
    """Read the tree's documents."""
125 1
    prefixes = [str(document.prefix) for document in tree]
126
    if utilities.json_response(request):
127 1
        data = {'prefixes': prefixes}
128
        return data
129
    else:
130 1
        return template('document_list', prefixes=prefixes)
131
132
133 1
@get('/documents/all')
134 1
def get_all_documents():
135 1
    """Read the tree's documents."""
136 1
    if utilities.json_response(request):
137 1
        data = {str(d.prefix): {str(i.uid): i.data for i in d} for d in tree}
138
        return data
139 1
    else:
140
        prefixes = [str(document.prefix) for document in tree]
141
        return template('document_list', prefixes=prefixes)
142 1
143
144
@get('/documents/<prefix>')
145 1
def get_document(prefix):
146 1
    """Read a tree's document."""
147 1
    document = tree.find_document(prefix)
148 1
    if utilities.json_response(request):
149
        data = {str(item.uid): item.data for item in document}
150 1
        return data
151
    else:
152
        return publisher.publish_lines(document, ext='.html', linkify=True)
153 1
154
155
@get('/documents/<prefix>/items')
156 1
def get_items(prefix):
157 1
    """Read a document's items."""
158 1
    document = tree.find_document(prefix)
159 1
    uids = [str(item.uid) for item in document]
160 1
    if utilities.json_response(request):
161 1
        data = {'uids': uids}
162
        return data
163 1
    else:
164
        return template('item_list', prefix=prefix, items=uids)
165
166 1
167
@get('/documents/<prefix>/items/<uid>')
168
def get_item(prefix, uid):
169 1
    """Read a document's item."""
170 1
    document = tree.find_document(prefix)
171 1
    item = document.find_item(uid)
172 1
    if utilities.json_response(request):
173 1
        return {'data': item.data}
174 1
    else:
175
        return publisher.publish_lines(item, ext='.html')
176 1
177 1
178 1
@get('/documents/<prefix>/items/<uid>/attrs')
179 1
def get_attrs(prefix, uid):
180 1
    """Read an item's attributes."""
181 1
    document = tree.find_document(prefix)
182
    item = document.find_item(uid)
183
    attrs = sorted(item.data.keys())
184 1
    if utilities.json_response(request):
185
        data = {'attrs': attrs}
186
        return data
187 1
    else:
188 1
        return '<br>'.join(attrs)
189 1
190 1
191 1
@get('/documents/<prefix>/items/<uid>/attrs/<name>')
192 1
def get_attr(prefix, uid, name):
193
    """Read an item's attribute value."""
194 1
    document = tree.find_document(prefix)
195
    item = document.find_item(uid)
196
    value = item.data.get(name, None)
197
    if utilities.json_response(request):
198
        data = {'value': value}
199
        return data
200
    else:
201
        if isinstance(value, str):
202
            return value
203
        try:
204
            return '<br>'.join(str(e) for e in value)
205
        except TypeError:
206
            return str(value)
207
208
209
@get('/assets/doorstop/<filename>')
210
def get_assets(filename):
211
    """Serve static files. Mainly used to serve CSS files and javascript."""
212
    public_dir = os.path.join(os.path.dirname(__file__),
213
                              '..', 'core', 'files', 'assets', 'doorstop')
214
    return bottle.static_file(filename, root=public_dir)
215
216
217
@post('/documents/<prefix>/numbers')
218
def post_numbers(prefix):
219
    """Create the next number in a document."""
220
    document = tree.find_document(prefix)
221
    number = max(document.next_number, numbers[prefix])
222
    numbers[prefix] = number + 1
223
    if utilities.json_response(request):
224
        data = {'next': number}
225
        return data
226
    else:
227
        return str(number)
228
229
230
if __name__ == '__main__':  # pragma: no cover (manual test)
231
    main()
232