Coverage for control/app.py : 80%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Sets up the Flask app with all its routes.
2"""
4import os
5from itertools import chain
7from flask import (
8 Flask,
9 request,
10 render_template,
11 send_file,
12 redirect,
13 abort,
14 flash,
15)
17from config import Config as C, Names as N
18from control.utils import (
19 pick as G,
20 E,
21 serverprint,
22 isIdLike,
23 isEmailLike,
24 isEppnLike,
25 isNameLike,
26 isNamesLike,
27 isFileLike,
28 saveParam,
29 ZERO,
30 ONE,
31 MINONE,
32)
33from control.db import Db
34from control.workflow.compute import Workflow
35from control.workflow.apply import execute
36from control.perm import checkTable
37from control.auth import Auth
38from control.context import Context
39from control.sidebar import Sidebar
40from control.topbar import Topbar
41from control.overview import Overview
42from control.api import Api
43from control.cust.factory_table import make as mkTable
46CB = C.base
47CT = C.tables
48CW = C.web
49CF = C.workflow
51SECRET_FILE = CB.secretFile
53STATIC_ROOT = os.path.abspath(CW.staticRoot)
54"""The url to the directory from which static files are served."""
56ALL_TABLES = CT.all
57USER_TABLES_LIST = CT.userTables
58USER_TABLES = set(USER_TABLES_LIST)
59MASTERS = CT.masters
60DETAILS = CT.details
62TASKS = CF.tasks
64LIMITS = CW.limits
65LIMIT_DEFAULT = CW.limitDefault
66LIMIT_REQUEST = CW.limitRequest
67LIMIT_KEYS = CW.limitKeys
69URLS = CW.urls
70"""A dictionary of fixed fall-back urls."""
72MESSAGES = CW.messages
73"""A dictionary of fixed messages for display on the web interface."""
75INDEX = CW.indexPage
76LANDING = CW.landing
77BODY_METHODS = set(CW.bodyMethods)
78LIST_ACTIONS = set(CW.listActions)
79FIELD_ACTIONS = set(CW.fieldActions)
80OTHER_ACTIONS = set(CW.otherActions)
81OPTIONS = CW.options
82OPTION_SET = set(OPTIONS)
84START = URLS[N.home][N.url]
85OVERVIEW = URLS[N.info][N.url]
86DUMMY = URLS[N.dummy][N.url]
87LOGIN = URLS[N.login][N.url]
88LOGOUT = URLS[N.logout][N.url]
89SLOGOUT = URLS[N.slogout][N.url]
90REFRESH = URLS[N.refresh][N.url]
91WORKFLOW = URLS[N.workflow][N.url]
92SHIB_LOGOUT = URLS[N.shibLogout][N.url]
93NO_PAGE = MESSAGES[N.noPage]
94NO_TASK = MESSAGES[N.noTask]
95NO_TABLE = MESSAGES[N.noTable]
96NO_RECORD = MESSAGES[N.noRecord]
97NO_FIELD = MESSAGES[N.noField]
98NO_ACTION = MESSAGES[N.noAction]
101def redirectResult(url, good):
102 """Redirect.
104 Parameters
105 ----------
106 url: string
107 The url to redirect to
108 good:
109 Whether the redirection corresponds to a normal scenario or is the result of
110 an error
112 Returns
113 -------
114 response
115 A redirect response with either code 302 (good) or 303 (bad)
116 """
118 code = 302 if good else 303
119 return redirect(url, code=code)
122def checkBounds(**kwargs):
123 """Aggressive check on the arguments passed in an url and/or request.
125 First the total length of the request is counted.
126 If it is too much, the request will be aborted.
128 Each argument in request.args and `kwargs` must have a name that is allowed
129 and its value should have a length under an appropriate limit,
130 configured in `web.yaml`. There is always a fallback limit.
132 !!! caution "Security"
133 Before processing any request arg, whether from a form or from the url,
134 use this function to check whether the length is within limits.
136 If the length is exceeded, fail with a bad request,
137 without giving any useful feedback.
138 Because in this case we are dealing with a hacker.
140 Parameters
141 ----------
142 kwargs: dict
143 The key-values that need to be checked.
145 Raises
146 ------
147 HTTPException
148 If the length of any argument is out of bounds,
149 processing is aborted and a bad request response
150 is delivered
151 """
153 if request.content_length and request.content_length > LIMIT_REQUEST: 153 ↛ 154line 153 didn't jump to line 154, because the condition on line 153 was never true
154 abort(400)
156 n = len(request.args)
157 if len(kwargs) > LIMIT_KEYS: 157 ↛ 158line 157 didn't jump to line 158, because the condition on line 157 was never true
158 serverprint(f"""OUT-OF-BOUNDS: {n} > {LIMIT_KEYS} KEYS IN {kwargs}""")
159 abort(400)
161 for (k, v) in chain.from_iterable((kwargs.items(), request.args.items())):
162 if k not in LIMITS:
163 serverprint(f"""ILLEGAL PARAMETER NAME `{k}`: `{saveParam(v)}`""")
164 abort(400)
165 valN = G(LIMITS, k, default=LIMIT_DEFAULT)
166 if v is not None and len(v) > valN:
167 serverprint(
168 f"""OUT-OF-BOUNDS: LENGTH ARG "{k}" > {valN} ({saveParam(v)})"""
169 )
170 abort(400)
171 if not v:
172 # no value for v: no risk
173 return
175 # we taste the value of v and verify it conforms to expectations
176 if k == N.action:
177 if v not in LIST_ACTIONS | FIELD_ACTIONS | OTHER_ACTIONS:
178 serverprint(f"""ILLEGAL ACTION: `{v}`""")
179 abort(400)
180 elif k in OPTION_SET | {N.reverse}: # assessed reviewed
181 if v not in {ZERO, ONE, MINONE}: 181 ↛ 182line 181 didn't jump to line 182, because the condition on line 181 was never true
182 serverprint(f"""`{k}` with non-3-boolean value: `{v}`""")
183 abort(400)
184 elif k == N.bulk: 184 ↛ 185line 184 didn't jump to line 185, because the condition on line 184 was never true
185 if v not in {ZERO, ONE}:
186 serverprint(f"""`{k}` with non-boolean value: `{v}`""")
187 abort(400)
188 elif k == N.country: 188 ↛ 189line 188 didn't jump to line 189, because the condition on line 188 was never true
189 if v != "x" and (not v.isalpha() or not v == v.upper()):
190 serverprint(f"""`{k}` cannot be a country code: `{v}`""")
191 abort(400)
192 elif k in {N.deid, N.eid, N.masterId}:
193 if not isIdLike(v): 193 ↛ 194line 193 didn't jump to line 194, because the condition on line 193 was never true
194 serverprint(f"""`{k}` cannot be a mongo id: `{v}`""")
195 abort(400)
196 elif k == N.dtable:
197 if v not in MASTERS: 197 ↛ 198line 197 didn't jump to line 198, because the condition on line 197 was never true
198 serverprint(f"""`{k}` cannot be a details table: `{v}`""")
199 abort(400)
200 elif k == N.email:
201 if not isEmailLike(v): 201 ↛ 161line 201 didn't jump to line 161, because the condition on line 201 was never false
202 serverprint(f"""`{k}` cannot be an email address : `{v}`""")
203 abort(400)
204 elif k == N.eppn:
205 if not isEppnLike(v):
206 serverprint(f"""`{k}` cannot be an eppn: `{v}`""")
207 abort(400)
208 elif k == N.field:
209 if not isNameLike(v): 209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true
210 serverprint(f"""`{k}` cannot be an field name: `{v}`""")
211 abort(400)
212 elif k in {N.filepath, N.anything}:
213 if not isFileLike(v):
214 serverprint(f"""`{k}` cannot be an file path: `{v}`""")
215 abort(400)
216 elif k in {N.groups, N.sortcol}:
217 if not isNamesLike(v): 217 ↛ 220line 217 didn't jump to line 220, because the condition on line 217 was never false
218 serverprint(f"""`{k}` cannot be a list of names: `{v}`""")
219 abort(400)
220 pass
221 elif k == N.method:
222 if v not in BODY_METHODS: 222 ↛ 161line 222 didn't jump to line 161, because the condition on line 222 was never false
223 serverprint(f"""`{k}` is not a method: `{v}`""")
224 abort(400)
225 elif k == N.table:
226 if v not in ALL_TABLES: 226 ↛ 227line 226 didn't jump to line 227, because the condition on line 226 was never true
227 serverprint(f"""`{k}` is not a table name: `{v}`""")
228 abort(400)
229 elif k == N.task: 229 ↛ 234line 229 didn't jump to line 234, because the condition on line 229 was never false
230 if v not in TASKS: 230 ↛ 231line 230 didn't jump to line 231, because the condition on line 230 was never true
231 serverprint(f"""`{k}` is not a workflow task: `{v}`""")
232 abort(400)
233 else:
234 serverprint(f"""FORGOTTEN TO IMPLEMENT CHECK FOR `{k}`: `{v}`""")
235 abort(400)
238def appFactory(regime, test, debug, **kwargs):
239 """Creates the flask app that serves the website.
241 We read a secret key from the system which is stored in a file outside the app.
242 This information is needed to encrypt sessions.
244 !!! caution
245 We read and cache substantial information from MongoDb before
246 forking into workers.
247 Before we fork, we close the MongoDb connection, because PyMongo is not
248 [fork-safe](https://api.mongodb.com/python/current/faq.html#is-pymongo-fork-safe).
250 Parameters
251 ----------
252 regime: {development, production}
253 test: boolean
254 Whether the app is in test mode.
255 debug: boolean
256 Whether to generate debug messages for certain actions.
257 kwargs: dict
258 Additional parameters to tweak the behaviour of the Flask application.
259 They will be passed to the object initializer `Flask()`.
261 Returns
262 -------
263 object
264 The flask app.
265 """
267 kwargs["static_url_path"] = DUMMY
269 app = Flask(__name__, **kwargs)
270 if test: 270 ↛ 273line 270 didn't jump to line 273, because the condition on line 270 was never false
271 app.config.from_mapping(dict(TESTING=True))
273 with open(SECRET_FILE) as fh:
274 app.secret_key = fh.read()
276 GP = dict(methods=[N.GET, N.POST])
278 DB = Db(regime, test=test)
279 """*object* The `control.db.Db` singleton."""
281 WF = Workflow(DB)
282 """*object* The `control.workflow.compute.Workflow` singleton."""
284 WF.initWorkflow(drop=True)
286 auth = Auth(DB, regime)
288 DB.mongoClose()
290 def getContext():
291 return Context(DB, WF, auth)
293 def tablePerm(table, action=None):
294 return checkTable(auth, table) and (action is None or auth.authenticated())
296 if debug and auth.isDevel: 296 ↛ 297line 296 didn't jump to line 297, because the condition on line 296 was never true
297 CT.showReferences()
298 N.showNames()
300 @app.route("""/whoami""")
301 def serveWhoami():
302 checkBounds()
303 return G(auth.user, N.eppn) if auth.authenticated() else N.public
305 @app.route(f"""/{N.static}/<path:filepath>""")
306 def serveStatic(filepath):
307 checkBounds(filepath=filepath)
309 path = f"""{STATIC_ROOT}/{filepath}"""
310 if os.path.isfile(path):
311 return send_file(path)
312 flash(f"file not found: {filepath}", "error")
313 return redirectResult(START, False)
315 @app.route(f"""/{N.favicons}/<path:filepath>""")
316 def serveFavicons(filepath):
317 checkBounds(filepath=filepath)
319 path = f"""{STATIC_ROOT}/{N.favicons}/{filepath}"""
320 if os.path.isfile(path):
321 return send_file(path)
322 flash(f"icon not found: {filepath}", "error")
323 return redirectResult(START, False)
325 @app.route(START)
326 @app.route(f"""/{N.index}""")
327 @app.route(f"""/{INDEX}""")
328 def serveIndex():
329 checkBounds()
330 path = START
331 context = getContext()
332 auth.authenticate()
333 topbar = Topbar(context).wrap()
334 sidebar = Sidebar(context, path).wrap()
335 return render_template(INDEX, topbar=topbar, sidebar=sidebar, material=LANDING)
337 # OVERVIEW PAGE
339 @app.route(f"""{OVERVIEW}""")
340 def serveOverview():
341 checkBounds()
342 path = START
343 context = getContext()
344 auth.authenticate()
345 topbar = Topbar(context).wrap()
346 sidebar = Sidebar(context, path).wrap()
347 overview = Overview(context).wrap()
348 return render_template(INDEX, topbar=topbar, sidebar=sidebar, material=overview)
350 @app.route(f"""{OVERVIEW}.tsv""")
351 def serveOverviewTsv():
352 checkBounds()
353 context = getContext()
354 auth.authenticate()
355 return Overview(context).wrap(asTsv=True)
357 # LOGIN / LOGOUT
359 @app.route(f"""{SLOGOUT}""")
360 def serveSlogout():
361 checkBounds()
362 if auth.authenticated():
363 auth.deauthenticate()
364 flash("logged out from DARIAH")
365 return redirectResult(SHIB_LOGOUT, True)
366 flash("you were not logged in", "error")
367 return redirectResult(START, False)
369 @app.route(f"""{LOGIN}""")
370 def serveLogin():
371 checkBounds()
372 if auth.authenticated():
373 flash("you are already logged in")
374 good = True
375 if auth.authenticate(login=True):
376 flash("log in successful")
377 else:
378 good = False
379 flash("log in unsuccessful", "error")
380 return redirectResult(START, good)
382 @app.route(f"""{LOGOUT}""")
383 def serveLogout():
384 checkBounds()
385 if auth.authenticated():
386 auth.deauthenticate()
387 flash("logged out")
388 return redirectResult(START, True)
389 flash("you were not logged in", "error")
390 return redirectResult(START, False)
392 # SYSADMIN
394 @app.route(f"""{REFRESH}""")
395 def serveRefresh():
396 checkBounds()
397 context = getContext()
398 auth.authenticate()
399 done = context.refreshCache()
400 if done: 400 ↛ 403line 400 didn't jump to line 403, because the condition on line 400 was never false
401 flash("Cache refreshed")
402 else:
403 flash("Cache not refreshed", "error")
404 return redirectResult(START, done)
406 @app.route(f"""{WORKFLOW}""")
407 def serveWorkflow():
408 checkBounds()
409 context = getContext()
410 auth.authenticate()
411 nWf = context.resetWorkflow()
412 if nWf >= 0:
413 flash(f"{nWf} workflow records recomputed and stored")
414 else:
415 flash("workflow not recomputed", "error")
416 return redirectResult(START, nWf >= 0)
418 # API CALLS
420 @app.route("/api/db/<string:table>/<string:eid>", methods=["GET", "POST"])
421 def serveApiDbView(table, eid):
422 checkBounds(table=table, eid=eid)
423 context = getContext()
424 auth.authenticate()
425 return Api(context).view(table, eid)
427 @app.route("/api/db/<string:table>", methods=["GET", "POST"])
428 def serveApiDbList(table):
429 checkBounds(table=table)
430 context = getContext()
431 auth.authenticate()
432 return Api(context).list(table)
434 @app.route("/api/db/<path:verb>", methods=["GET", "POST"])
435 def serveApiDb(verb):
436 checkBounds()
437 context = getContext()
438 auth.authenticate()
439 return Api(context).notimplemented(verb)
441 # WORKFLOW TASKS
443 @app.route("""/api/task/<string:task>/<string:eid>""")
444 def serveTask(task, eid):
445 checkBounds(task=task, eid=eid)
447 context = getContext()
448 auth.authenticate()
449 (good, newPath) = execute(context, task, eid)
450 if not good and newPath is None: 450 ↛ 451line 450 didn't jump to line 451, because the condition on line 450 was never true
451 newPath = START
452 return redirectResult(newPath, good)
454 # INSERT RECORD IN TABLE
456 @app.route(f"""/api/<string:table>/{N.insert}""")
457 def serveTableInsert(table):
458 checkBounds(table=table)
460 newPath = f"""/{table}/{N.list}"""
461 if table in ALL_TABLES and table not in MASTERS: 461 ↛ 471line 461 didn't jump to line 471, because the condition on line 461 was never false
462 context = getContext()
463 auth.authenticate()
464 eid = None
465 if tablePerm(table): 465 ↛ 467line 465 didn't jump to line 467, because the condition on line 465 was never false
466 eid = mkTable(context, table).insert()
467 if eid:
468 newPath = f"""/{table}/{N.item}/{eid}"""
469 flash("item added")
470 else:
471 flash(f"Cannot add items to {table}", "error")
472 return redirectResult(newPath, eid is not None)
474 # INSERT RECORD IN DETAIL TABLE
476 @app.route(f"""/api/<string:table>/<string:eid>/<string:dtable>/{N.insert}""")
477 def serveTableInsertDetail(table, eid, dtable):
478 checkBounds(table=table, eid=eid, dtable=dtable)
480 newPath = f"""/{table}/{N.item}/{eid}"""
481 dEid = None
482 if ( 482 ↛ 493line 482 didn't jump to line 493
483 table in USER_TABLES_LIST[0:2]
484 and table in DETAILS
485 and dtable in DETAILS[table]
486 ):
487 context = getContext()
488 auth.authenticate()
489 if tablePerm(table): 489 ↛ 491line 489 didn't jump to line 491, because the condition on line 489 was never false
490 dEid = mkTable(context, dtable).insert(masterTable=table, masterId=eid)
491 if dEid: 491 ↛ 492line 491 didn't jump to line 492, because the condition on line 491 was never true
492 newPath = f"""/{table}/{N.item}/{eid}/{N.open}/{dtable}/{dEid}"""
493 if dEid: 493 ↛ 494line 493 didn't jump to line 494, because the condition on line 493 was never true
494 flash(f"{dtable} item added")
495 else:
496 flash(f"Cannot add a {dtable} here", "error")
497 return redirectResult(newPath, dEid is not None)
499 # LIST VIEWS ON TABLE
501 @app.route(f"""/<string:table>/{N.list}/<string:eid>""")
502 def serveTableListOpen(table, eid):
503 checkBounds(table=table, eid=eid)
505 return serveTable(table, eid)
507 @app.route(f"""/<string:table>/{N.list}""")
508 def serveTableList(table):
509 checkBounds(table=table)
511 return serveTable(table, None)
513 def serveTable(table, eid):
514 checkBounds()
515 action = G(request.args, N.action)
516 actionRep = f"?action={action}" if action else E
517 eidRep = f"""/{eid}""" if eid else E
518 path = f"""/{table}/{N.list}{eidRep}{actionRep}"""
519 if not action or action in LIST_ACTIONS: 519 ↛ 538line 519 didn't jump to line 538, because the condition on line 519 was never false
520 if table in ALL_TABLES: 520 ↛ 537line 520 didn't jump to line 537, because the condition on line 520 was never false
521 context = getContext()
522 auth.authenticate()
523 topbar = Topbar(context).wrap()
524 sidebar = Sidebar(context, path).wrap()
525 tableList = None
526 if tablePerm(table, action=action): 526 ↛ 528line 526 didn't jump to line 528, because the condition on line 526 was never false
527 tableList = mkTable(context, table).wrap(eid, action=action)
528 if tableList is None: 528 ↛ 529line 528 didn't jump to line 529, because the condition on line 528 was never true
529 flash(f"{action or E} view on {table} not allowed", "error")
530 return redirectResult(START, False)
531 return render_template(
532 INDEX,
533 topbar=topbar,
534 sidebar=sidebar,
535 material=tableList,
536 )
537 flash(f"Unknown table {table}", "error")
538 if action:
539 flash(f"Unknown view {action}", "error")
540 else:
541 flash("Missing view", "error")
542 return redirectResult(START, False)
544 # RECORD DELETE
546 @app.route(f"""/api/<string:table>/{N.delete}/<string:eid>""")
547 def serveRecordDelete(table, eid):
548 checkBounds(table=table, eid=eid)
550 if table in ALL_TABLES: 550 ↛ 563line 550 didn't jump to line 563, because the condition on line 550 was never false
551 context = getContext()
552 auth.authenticate()
553 good = False
554 if tablePerm(table): 554 ↛ 558line 554 didn't jump to line 558, because the condition on line 554 was never false
555 good = mkTable(context, table).record(eid=eid).delete()
556 newUrlPart = f"?{N.action}={N.my}" if table in USER_TABLES else E
557 newPath = f"""/{table}/{N.list}{newUrlPart}"""
558 if good:
559 flash("item deleted")
560 else:
561 flash("item not deleted", "error")
562 return redirectResult(newPath, good)
563 flash(f"Unknown table {table}", "error")
564 return redirectResult(START, False)
566 # RECORD DELETE DETAIL
568 @app.route(
569 f"""/api/<string:table>/<string:masterId>/"""
570 f"""<string:dtable>/{N.delete}/<string:eid>"""
571 )
572 def serveRecordDeleteDetail(table, masterId, dtable, eid):
573 checkBounds(table=table, masterId=masterId, dtable=dtable, eid=eid)
575 newPath = f"""/{table}/{N.item}/{masterId}"""
576 good = False
577 if ( 577 ↛ 591line 577 didn't jump to line 591
578 table in USER_TABLES_LIST[0:2]
579 and table in DETAILS
580 and dtable in DETAILS[table]
581 ):
582 context = getContext()
583 auth.authenticate()
584 if tablePerm(table): 584 ↛ 591line 584 didn't jump to line 591, because the condition on line 584 was never false
585 recordObj = mkTable(context, dtable).record(eid=eid)
587 wfitem = recordObj.wfitem
588 if wfitem: 588 ↛ 589line 588 didn't jump to line 589, because the condition on line 588 was never true
589 good = recordObj.delete()
591 if good: 591 ↛ 592line 591 didn't jump to line 592, because the condition on line 591 was never true
592 flash(f"{dtable} detail deleted")
593 else:
594 flash(f"{dtable} detail not deleted", "error")
595 return redirectResult(newPath, good)
597 # RECORD VIEW
599 @app.route(f"""/api/<string:table>/{N.item}/<string:eid>""")
600 def serveRecord(table, eid):
601 checkBounds(table=table, eid=eid)
603 if table in ALL_TABLES: 603 ↛ 612line 603 didn't jump to line 612, because the condition on line 603 was never false
604 context = getContext()
605 auth.authenticate()
606 if tablePerm(table): 606 ↛ 612line 606 didn't jump to line 612, because the condition on line 606 was never false
607 recordObj = mkTable(context, table).record(
608 eid=eid, withDetails=True, **method()
609 )
610 if recordObj.mayRead is not False: 610 ↛ 612line 610 didn't jump to line 612, because the condition on line 610 was never false
611 return recordObj.wrap()
612 return noRecord(table)
614 @app.route(f"""/api/<string:table>/{N.item}/<string:eid>/{N.title}""")
615 def serveRecordTitle(table, eid):
616 checkBounds(table=table, eid=eid)
618 if table in ALL_TABLES: 618 ↛ 627line 618 didn't jump to line 627, because the condition on line 618 was never false
619 context = getContext()
620 auth.authenticate()
621 if tablePerm(table): 621 ↛ 627line 621 didn't jump to line 627, because the condition on line 621 was never false
622 recordObj = mkTable(context, table).record(
623 eid=eid, withDetails=False, **method()
624 )
625 if recordObj.mayRead is not False: 625 ↛ 627line 625 didn't jump to line 627, because the condition on line 625 was never false
626 return recordObj.wrap(expanded=-1)
627 return noRecord(table)
629 # with specific detail opened
631 @app.route(
632 f"""/<string:table>/{N.item}/<string:eid>/"""
633 f"""{N.open}/<string:dtable>/<string:deid>"""
634 )
635 def serveRecordPageDetail(table, eid, dtable, deid):
636 checkBounds(table=table, eid=eid, dtable=dtable, deid=deid)
638 path = f"""/{table}/{N.item}/{eid}/{N.open}/{dtable}/{deid}"""
639 if table in ALL_TABLES: 639 ↛ 658line 639 didn't jump to line 658, because the condition on line 639 was never false
640 context = getContext()
641 auth.authenticate()
642 topbar = Topbar(context).wrap()
643 sidebar = Sidebar(context, path).wrap()
644 if tablePerm(table): 644 ↛ 658line 644 didn't jump to line 658, because the condition on line 644 was never false
645 recordObj = mkTable(context, table).record(
646 eid=eid, withDetails=True, **method()
647 )
648 if recordObj.mayRead is not False: 648 ↛ 656line 648 didn't jump to line 656, because the condition on line 648 was never false
649 record = recordObj.wrap(showTable=dtable, showEid=deid)
650 return render_template(
651 INDEX,
652 topbar=topbar,
653 sidebar=sidebar,
654 material=record,
655 )
656 flash(f"Unknown record in table {table}", "error")
657 return redirectResult(f"""/{table}/{N.list}""", False)
658 flash(f"Unknown table {table}", "error")
659 return redirectResult(START, False)
661 @app.route(f"""/<string:table>/{N.item}/<string:eid>""")
662 def serveRecordPageDet(table, eid):
663 checkBounds(table=table, eid=eid)
665 path = f"""/{table}/{N.item}/{eid}"""
666 if table in ALL_TABLES: 666 ↛ 685line 666 didn't jump to line 685, because the condition on line 666 was never false
667 context = getContext()
668 auth.authenticate()
669 topbar = Topbar(context).wrap()
670 sidebar = Sidebar(context, path).wrap()
671 if tablePerm(table): 671 ↛ 685line 671 didn't jump to line 685, because the condition on line 671 was never false
672 recordObj = mkTable(context, table).record(
673 eid=eid, withDetails=True, **method()
674 )
675 if recordObj.mayRead is not False:
676 record = recordObj.wrap()
677 return render_template(
678 INDEX,
679 topbar=topbar,
680 sidebar=sidebar,
681 material=record,
682 )
683 flash(f"Unknown record in table {table}", "error")
684 return redirectResult(f"""/{table}/{N.list}""", False)
685 flash(f"Unknown table {table}", "error")
686 return redirectResult(START, False)
688 def method():
689 method = G(request.args, N.method)
690 if method not in BODY_METHODS: 690 ↛ 692line 690 didn't jump to line 692, because the condition on line 690 was never false
691 return {}
692 return dict(bodyMethod=method)
694 # FIELD VIEWS AND EDITS
696 @app.route(
697 f"""/api/<string:table>/{N.item}/<string:eid>/{N.field}/<string:field>""", **GP
698 )
699 def serveField(table, eid, field):
700 checkBounds(table=table, eid=eid, field=field)
702 action = G(request.args, N.action)
703 if action in FIELD_ACTIONS:
704 context = getContext()
705 auth.authenticate()
706 if table in ALL_TABLES and tablePerm(table):
707 recordObj = mkTable(context, table).record(eid=eid)
708 if recordObj.mayRead is not False:
709 fieldObj = mkTable(context, table).record(eid=eid).field(field)
710 if fieldObj:
711 return fieldObj.wrap(action=action)
712 return noField(table, field)
713 return noRecord(table)
714 return noTable(table)
715 return noAction(action)
717 # FALL-BACK
719 @app.route("""/<path:anything>""")
720 def serveNotFound(anything=None):
721 checkBounds(anything=anything)
723 flash(f"Cannot find {anything}", "error")
724 return redirectResult(START, False)
726 def noTable(table):
727 return f"""{NO_TABLE} {table}"""
729 def noRecord(table):
730 return f"""{NO_RECORD} {table}"""
732 def noField(table, field):
733 return f"""{NO_FIELD} {table}:{field}"""
735 def noAction(action):
736 return f"""{NO_ACTION} {action}"""
738 return app