Hide keyboard shortcuts

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""" 

3 

4import os 

5from itertools import chain 

6 

7from flask import ( 

8 Flask, 

9 request, 

10 render_template, 

11 send_file, 

12 redirect, 

13 abort, 

14 flash, 

15) 

16 

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 

44 

45 

46CB = C.base 

47CT = C.tables 

48CW = C.web 

49CF = C.workflow 

50 

51SECRET_FILE = CB.secretFile 

52 

53STATIC_ROOT = os.path.abspath(CW.staticRoot) 

54"""The url to the directory from which static files are served.""" 

55 

56ALL_TABLES = CT.all 

57USER_TABLES_LIST = CT.userTables 

58USER_TABLES = set(USER_TABLES_LIST) 

59MASTERS = CT.masters 

60DETAILS = CT.details 

61 

62TASKS = CF.tasks 

63 

64LIMITS = CW.limits 

65LIMIT_DEFAULT = CW.limitDefault 

66LIMIT_REQUEST = CW.limitRequest 

67LIMIT_KEYS = CW.limitKeys 

68 

69URLS = CW.urls 

70"""A dictionary of fixed fall-back urls.""" 

71 

72MESSAGES = CW.messages 

73"""A dictionary of fixed messages for display on the web interface.""" 

74 

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) 

83 

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] 

99 

100 

101def redirectResult(url, good): 

102 """Redirect. 

103 

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 

111 

112 Returns 

113 ------- 

114 response 

115 A redirect response with either code 302 (good) or 303 (bad) 

116 """ 

117 

118 code = 302 if good else 303 

119 return redirect(url, code=code) 

120 

121 

122def checkBounds(**kwargs): 

123 """Aggressive check on the arguments passed in an url and/or request. 

124 

125 First the total length of the request is counted. 

126 If it is too much, the request will be aborted. 

127 

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. 

131 

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. 

135 

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. 

139 

140 Parameters 

141 ---------- 

142 kwargs: dict 

143 The key-values that need to be checked. 

144 

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 """ 

152 

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) 

155 

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) 

160 

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 

174 

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) 

236 

237 

238def appFactory(regime, test, debug, **kwargs): 

239 """Creates the flask app that serves the website. 

240 

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. 

243 

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). 

249 

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()`. 

260 

261 Returns 

262 ------- 

263 object 

264 The flask app. 

265 """ 

266 

267 kwargs["static_url_path"] = DUMMY 

268 

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)) 

272 

273 with open(SECRET_FILE) as fh: 

274 app.secret_key = fh.read() 

275 

276 GP = dict(methods=[N.GET, N.POST]) 

277 

278 DB = Db(regime, test=test) 

279 """*object* The `control.db.Db` singleton.""" 

280 

281 WF = Workflow(DB) 

282 """*object* The `control.workflow.compute.Workflow` singleton.""" 

283 

284 WF.initWorkflow(drop=True) 

285 

286 auth = Auth(DB, regime) 

287 

288 DB.mongoClose() 

289 

290 def getContext(): 

291 return Context(DB, WF, auth) 

292 

293 def tablePerm(table, action=None): 

294 return checkTable(auth, table) and (action is None or auth.authenticated()) 

295 

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() 

299 

300 @app.route("""/whoami""") 

301 def serveWhoami(): 

302 checkBounds() 

303 return G(auth.user, N.eppn) if auth.authenticated() else N.public 

304 

305 @app.route(f"""/{N.static}/<path:filepath>""") 

306 def serveStatic(filepath): 

307 checkBounds(filepath=filepath) 

308 

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) 

314 

315 @app.route(f"""/{N.favicons}/<path:filepath>""") 

316 def serveFavicons(filepath): 

317 checkBounds(filepath=filepath) 

318 

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) 

324 

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) 

336 

337 # OVERVIEW PAGE 

338 

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) 

349 

350 @app.route(f"""{OVERVIEW}.tsv""") 

351 def serveOverviewTsv(): 

352 checkBounds() 

353 context = getContext() 

354 auth.authenticate() 

355 return Overview(context).wrap(asTsv=True) 

356 

357 # LOGIN / LOGOUT 

358 

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) 

368 

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) 

381 

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) 

391 

392 # SYSADMIN 

393 

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) 

405 

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) 

417 

418 # API CALLS 

419 

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) 

426 

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) 

433 

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) 

440 

441 # WORKFLOW TASKS 

442 

443 @app.route("""/api/task/<string:task>/<string:eid>""") 

444 def serveTask(task, eid): 

445 checkBounds(task=task, eid=eid) 

446 

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) 

453 

454 # INSERT RECORD IN TABLE 

455 

456 @app.route(f"""/api/<string:table>/{N.insert}""") 

457 def serveTableInsert(table): 

458 checkBounds(table=table) 

459 

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) 

473 

474 # INSERT RECORD IN DETAIL TABLE 

475 

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) 

479 

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) 

498 

499 # LIST VIEWS ON TABLE 

500 

501 @app.route(f"""/<string:table>/{N.list}/<string:eid>""") 

502 def serveTableListOpen(table, eid): 

503 checkBounds(table=table, eid=eid) 

504 

505 return serveTable(table, eid) 

506 

507 @app.route(f"""/<string:table>/{N.list}""") 

508 def serveTableList(table): 

509 checkBounds(table=table) 

510 

511 return serveTable(table, None) 

512 

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) 

543 

544 # RECORD DELETE 

545 

546 @app.route(f"""/api/<string:table>/{N.delete}/<string:eid>""") 

547 def serveRecordDelete(table, eid): 

548 checkBounds(table=table, eid=eid) 

549 

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) 

565 

566 # RECORD DELETE DETAIL 

567 

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) 

574 

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) 

586 

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() 

590 

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) 

596 

597 # RECORD VIEW 

598 

599 @app.route(f"""/api/<string:table>/{N.item}/<string:eid>""") 

600 def serveRecord(table, eid): 

601 checkBounds(table=table, eid=eid) 

602 

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) 

613 

614 @app.route(f"""/api/<string:table>/{N.item}/<string:eid>/{N.title}""") 

615 def serveRecordTitle(table, eid): 

616 checkBounds(table=table, eid=eid) 

617 

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) 

628 

629 # with specific detail opened 

630 

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) 

637 

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) 

660 

661 @app.route(f"""/<string:table>/{N.item}/<string:eid>""") 

662 def serveRecordPageDet(table, eid): 

663 checkBounds(table=table, eid=eid) 

664 

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) 

687 

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) 

693 

694 # FIELD VIEWS AND EDITS 

695 

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) 

701 

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) 

716 

717 # FALL-BACK 

718 

719 @app.route("""/<path:anything>""") 

720 def serveNotFound(anything=None): 

721 checkBounds(anything=anything) 

722 

723 flash(f"Cannot find {anything}", "error") 

724 return redirectResult(START, False) 

725 

726 def noTable(table): 

727 return f"""{NO_TABLE} {table}""" 

728 

729 def noRecord(table): 

730 return f"""{NO_RECORD} {table}""" 

731 

732 def noField(table, field): 

733 return f"""{NO_FIELD} {table}:{field}""" 

734 

735 def noAction(action): 

736 return f"""{NO_ACTION} {action}""" 

737 

738 return app