Passed
Push — develop ( f34c45...4326c3 )
by
unknown
05:23 queued 15s
created

doorstop.server.main.get_traceability()   A

Complexity

Conditions 4

Size

Total Lines 29
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 25
dl 0
loc 29
rs 9.28
c 0
b 0
f 0
cc 4
nop 0
1
#!/usr/bin/env python
2
# SPDX-License-Identifier: LGPL-3.0-only
3
# pylint: disable=import-outside-toplevel
4
5
"""REST server to display content and reserve item numbers."""
6
7
import argparse
8
import logging
9
import os
10
import webbrowser
11
from collections import defaultdict
12
from typing import Dict
13
14
import bottle
15
from bottle import get, hook, post, request, response, template
16
17
from doorstop import Tree, build, common, settings
18
from doorstop.common import HelpFormatter
19
from doorstop.core import vcs
20
from doorstop.core.publishers.html import HtmlPublisher
21
from doorstop.server import utilities
22
23
log = common.logger(__name__)
24
25
app = utilities.StripPathMiddleware(bottle.app())
26
config = {}
27
tree: Tree = None  # type: ignore
28
html_publisher: HtmlPublisher = None  # type: ignore
29
numbers: Dict[str, int] = defaultdict(int)  # cache of next document numbers
30
31
32
def main(args=None):
33
    """Process command-line arguments and run the program."""
34
    from doorstop import SERVER, VERSION
35
36
    # Shared options
37
    debug = argparse.ArgumentParser(add_help=False)
38
    debug.add_argument("-V", "--version", action="version", version=VERSION)
39
    debug.add_argument(
40
        "--debug", action="store_true", help="run the server in debug mode"
41
    )
42
    debug.add_argument(
43
        "--launch", action="store_true", help="open the server UI in a browser"
44
    )
45
    shared = {"formatter_class": HelpFormatter, "parents": [debug]}
46
47
    # Build main parser
48
    parser = argparse.ArgumentParser(prog=SERVER, description=__doc__, **shared)  # type: ignore
49
    cwd = os.getcwd()
50
51
    parser.add_argument(
52
        "-j", "--project", default=None, help="path to the root of the project"
53
    )
54
    parser.add_argument(
55
        "-P",
56
        "--port",
57
        metavar="NUM",
58
        type=int,
59
        default=settings.SERVER_PORT,
60
        help="use a custom port for the server",
61
    )
62
    parser.add_argument(
63
        "-H", "--host", default="127.0.0.1", help="IP address to listen"
64
    )
65
    parser.add_argument(
66
        "-w", "--wsgi", action="store_true", help="Run as a WSGI process"
67
    )
68
    parser.add_argument(
69
        "-b",
70
        "--baseurl",
71
        default="",
72
        help="Base URL this is served at (Usually only necessary for WSGI)",
73
    )
74
75
    # Parse arguments
76
    args = parser.parse_args(args=args)
77
78
    if args.project is None:
79
        args.project = vcs.find_root(cwd)
80
81
    # Configure logging
82
    logging.basicConfig(
83
        format=settings.VERBOSE_LOGGING_FORMAT, level=settings.VERBOSE_LOGGING_LEVEL
84
    )
85
86
    # Run the program
87
    setup(args, os.getcwd(), parser.error)
88
    run(args)
89
90
91
def setup(args, cwd, _):
92
    """Handle the setup of the server.
93
94
    :param args: Namespace of CLI arguments (from this module or the CLI)
95
    :param cwd: current working directory
96
    :param error: function to call for CLI errors
97
98
    """
99
    global tree, html_publisher
100
    tree = build(cwd=cwd, root=args.project)
101
    tree.load()
102
    html_publisher = HtmlPublisher(tree, ext=".html")
103
    # Force html_publisher to set index and matrix to True.
104
    html_publisher.setup(True, True, True)
105
    host = args.host
106
    port = args.port or settings.SERVER_PORT
107
    bottle.TEMPLATE_PATH.insert(
108
        0, os.path.join(os.path.dirname(__file__), "..", "views")
109
    )
110
111
    # If you started without WSGI, the base will be '/'.
112
    if args.baseurl == "" and not args.wsgi:
113
        args.baseurl = "/"
114
115
    # If you specified a base URL, make sure it ends with '/'.
116
    if args.baseurl != "" and not args.baseurl.endswith("/"):
117
        args.baseurl += "/"
118
119
    bottle.SimpleTemplate.defaults["baseurl"] = args.baseurl
120
    # Remove navigation since we handle it ourselves.
121
    bottle.SimpleTemplate.defaults["navigation"] = False
122
123
    if args.launch:
124
        url = utilities.build_url(host=host, port=port)
125
        webbrowser.open(url)
126
127
    # Configure the app.
128
    config["host"] = host
129
    config["port"] = port
130
    config["args"] = args.debug
131
132
133
def run(args):
134
    if not args.wsgi:
135
        bottle.run(
136
            app=app,
137
            host=config["host"],
138
            port=config["port"],
139
            debug=config["args"],
140
            reloader=config["args"],
141
        )
142
143
144
@hook("before_request")
145
def strip_path():
146
    request.environ["PATH_INFO"] = request.environ["PATH_INFO"].rstrip("/")
147
    if (
148
        len(request.environ["PATH_INFO"]) > 0
149
        and request.environ["PATH_INFO"][-5:] == ".html"
150
    ):
151
        request.environ["PATH_INFO"] = request.environ["PATH_INFO"][:-5]
152
153
154
@hook("after_request")
155
def enable_cors():
156
    """Allow a webserver running on the same machine to access data."""
157
    response.headers["Access-Control-Allow-Origin"] = "*"
158
159
160
@get("/")
161
@get("/index")
162
def index():
163
    """Read the tree."""
164
    prefixes = [str(document.prefix) for document in tree]
165
    lines = html_publisher.lines_index(prefixes, tree=tree)
166
    yield template(
167
        "doorstop",
168
        body="\n".join(lines),
169
        toc=None,
170
        doc_attributes={
171
            "name": "Index",
172
            "ref": "-",
173
            "title": "Doorstop index",
174
            "by": "-",
175
            "major": "-",
176
            "minor": "",
177
        },
178
        is_doc=False,
179
    )
180
181
182
@get("/traceability")
183
def get_traceability():
184
    """Read the traceability matrix."""
185
    if utilities.json_response(request):
186
        trace_list = tree.get_traceability()
187
        # Convert the Items in the list to strings only.
188
        traces = []
189
        for row in trace_list:
190
            trace_row = []
191
            for col in row:
192
                trace_row.append(str(col))
193
            traces.append(trace_row)
194
        data = {"traceability": traces}
195
        return data
196
    else:
197
        lines = html_publisher.lines_matrix()
198
        return template(
199
            "doorstop",
200
            body="\n".join(lines),
201
            toc=None,
202
            doc_attributes={
203
                "name": "Traceability",
204
                "ref": "-",
205
                "title": "Doorstop traceability matrix",
206
                "by": "-",
207
                "major": "-",
208
                "minor": "",
209
            },
210
            is_doc=False,
211
        )
212
213
214
@get("/documents")
215
def get_documents():
216
    """Read the tree's documents."""
217
    prefixes = [str(document.prefix) for document in tree]
218
    if utilities.json_response(request):
219
        data = {"prefixes": prefixes}
220
        return data
221
    else:
222
        return template(
223
            "document_list",
224
            prefixes=prefixes,
225
            doc_attributes={
226
                "name": "Documents",
227
                "ref": "-",
228
                "title": "Doorstop document list",
229
                "by": "-",
230
                "major": "-",
231
                "minor": "",
232
            },
233
            is_doc=False,
234
        )
235
236
237
@get("/documents/all")
238
def get_all_documents():
239
    """Read the tree's documents."""
240
    if utilities.json_response(request):
241
        data = {str(d.prefix): {str(i.uid): i.data for i in d} for d in tree}
242
        return data
243
    else:
244
        prefixes = [str(document.prefix) for document in tree]
245
        return template(
246
            "document_list",
247
            prefixes=prefixes,
248
            doc_attributes={
249
                "name": "Documents",
250
                "ref": "-",
251
                "title": "Doorstop document list",
252
                "by": "-",
253
                "major": "-",
254
                "minor": "",
255
            },
256
            is_doc=False,
257
        )
258
259
260
@get("/documents/<prefix>")
261
def get_document(prefix):
262
    """Read a tree's document."""
263
    document = tree.find_document(prefix)
264
    if utilities.json_response(request):
265
        data = {str(item.uid): item.data for item in document}
266
        return data
267
    else:
268
        return html_publisher.lines(document, ext=".html", linkify=True, toc=True)
269
270
271
@get("/documents/<prefix>/items")
272
def get_items(prefix):
273
    """Read a document's items."""
274
    document = tree.find_document(prefix)
275
    uids = [str(item.uid) for item in document]
276
    if utilities.json_response(request):
277
        data = {"uids": uids}
278
        return data
279
    else:
280
        return template(
281
            "item_list",
282
            prefix=prefix,
283
            items=uids,
284
            doc_attributes={
285
                "name": "Items",
286
                "ref": "-",
287
                "title": "Doorstop item list",
288
                "by": "-",
289
                "major": "-",
290
                "minor": "",
291
            },
292
            is_doc=False,
293
        )
294
295
296
@get("/documents/<prefix>/items/<uid>")
297
def get_item(prefix, uid):
298
    """Read a document's item."""
299
    document = tree.find_document(prefix)
300
    item = document.find_item(uid)
301
    lines = html_publisher.lines(item, ext=".html")
302
    if utilities.json_response(request):
303
        return {"data": item.data}
304
    else:
305
        return "<br>".join(lines)
306
307
308
@get("/documents/<prefix>/items/<uid>/attrs")
309
def get_attrs(prefix, uid):
310
    """Read an item's attributes."""
311
    document = tree.find_document(prefix)
312
    item = document.find_item(uid)
313
    attrs = sorted(item.data.keys())
314
    if utilities.json_response(request):
315
        data = {"attrs": attrs}
316
        return data
317
    else:
318
        return "<br>".join(attrs)
319
320
321
@get("/documents/<prefix>/items/<uid>/attrs/<name>")
322
def get_attr(prefix, uid, name):
323
    """Read an item's attribute value."""
324
    document = tree.find_document(prefix)
325
    item = document.find_item(uid)
326
    value = item.data.get(name, None)
327
    if utilities.json_response(request):
328
        data = {"value": value}
329
        return data
330
    else:
331
        if isinstance(value, str):
332
            return value
333
        try:
334
            return "<br>".join(str(e) for e in value)
335
        except TypeError:
336
            return str(value)
337
338
339
@get("/template/<filename>")
340
def get_template(filename):
341
    """Serve static files. Mainly used to serve CSS files and javascript."""
342
    public_dir = os.path.join(
343
        os.path.dirname(__file__), "..", "core", "files", "templates", "html"
344
    )
345
    if os.path.isfile(os.path.join(public_dir, filename)):
346
        return bottle.static_file(filename, root=public_dir)
347
    return bottle.HTTPError(404, "File does not exist.")
348
349
350
@get("/documents/assets/<filename>")
351
def get_assets(filename):
352
    """Serve static files. Used to serve images and other assets."""
353
    # Since assets are stored in the document, we need to loop over all the
354
    # documents to find the requested asset.
355
    for document in tree:
356
        # Check if the asset exists in the document's assets folder.
357
        if document.assets:
358
            temporary_path = os.path.join(document.assets, filename)
359
            if os.path.exists(temporary_path):
360
                return bottle.static_file(filename, root=document.assets)
361
    # If the asset does not exist, return a 404.
362
    return bottle.HTTPError(404, "File does not exist.")
363
364
365
@post("/documents/<prefix>/numbers")
366
def post_numbers(prefix):
367
    """Create the next number in a document."""
368
    document = tree.find_document(prefix)
369
    number = max(document.next_number, numbers[prefix])
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable numbers does not seem to be defined.
Loading history...
370
    numbers[prefix] = number + 1
371
    if utilities.json_response(request):
372
        data = {"next": number}
373
        return data
374
    else:
375
        return str(number)
376
377
378
if __name__ == "__main__":
379
    main()
380