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"""Helpers to factor our massively redundant test code. 

2""" 

3 

4import re 

5from datetime import timedelta 

6 

7from flask import json 

8from pymongo import MongoClient 

9from bson.objectid import ObjectId 

10from control.utils import pick as G, serverprint, now 

11from conftest import USER_LIST 

12from example import ( 

13 _ID, 

14 CRITERIA, 

15 CRITERIA_ENTRY, 

16 EXPERT, 

17 FINAL, 

18 LEVEL, 

19 REVIEW, 

20 REVIEW_ENTRY, 

21 SCORE, 

22 UNDEF_VALUE, 

23) 

24 

25 

26materialRe = re.compile( 

27 r"""<div id=['"]material['"]>(.*?)</div>\s*</div>\s*<script""", re.S 

28) 

29fieldRe = re.compile("""<!-- ([a-zA-Z0-9]+)=(.*?) -->""", re.S) 

30mainNRe = re.compile("""<!-- mainN~([0-9]+)~(.*?) -->""", re.S) 

31stageRe = re.compile("""<!-- stage:(.*?) -->""", re.S) 

32taskRe = re.compile("""<!-- task!([a-zA-Z0-9]+):([a-f0-9]+) -->""", re.S) 

33captionRe = re.compile( 

34 r"""<!-- caption\^([^>]*?) --><a [^>]*href=['"]([^'"]*)['"]""", re.S 

35) 

36msgRe = re.compile("""<div class="msgitem.*?>(.*?)</div>""", re.S) 

37eidRe = re.compile("""<details itemkey=['"][a-zA-Z0-9_]+/([^/'"]*)['"]""", re.S) 

38userRe = re.compile( 

39 """<details itemkey=['"]user/([^'"]*)['"].*?<summary>.*?<span.*?>([^<]*)</span>""", 

40 re.S, 

41) 

42valueRe = re.compile("""eid=['"](.*?)['"][^>]*>(.*?)(?:&#xa;)?<""", re.S) 

43reviewRe = re.compile( 

44 """<!-- begin reviewer ([A-Za-z0-9]+) -->""" 

45 """(.*?)""" 

46 r"""<!-- end reviewer \1 -->""", 

47 re.S, 

48) 

49reviewEntryIdRe = re.compile( 

50 f"""<span [^>]*?table=['"]{REVIEW_ENTRY}['"][^>]*>""", re.S 

51) 

52idRe = re.compile("""eid=['"]([^'"]*)['"]""", re.S) 

53 

54 

55def accessUrl(client, url, redirect=False): 

56 """Get the response on accessing a url.""" 

57 

58 response = client.get(url, follow_redirects=redirect) 

59 text = response.get_data(as_text=True) 

60 status = response.status_code 

61 msgs = findMsg(text) 

62 return (text, status, msgs) 

63 

64 

65def checkWarning(text, label): 

66 """Whether there is a warned item with `label`. 

67 

68 See `reWarning`. 

69 """ 

70 

71 return not not reWarning(label).search(text) 

72 

73 

74def findCaptions(text): 

75 """Get the captions from a response. 

76 

77 !!! hint 

78 They are neatly packaged in comment lines! 

79 

80 Parameters 

81 ---------- 

82 text: string 

83 The response text. 

84 

85 Returns 

86 ------- 

87 list of string 

88 """ 

89 

90 return captionRe.findall(text) 

91 

92 

93def findDetails(text, dtable): 

94 """Get the details from a response, but only those in a specific table. 

95 

96 Parameters 

97 ---------- 

98 text: string 

99 The response text. 

100 dtail: string 

101 The detail table 

102 

103 Returns 

104 ------- 

105 list of tuple of (string(id), string(html)) 

106 The HTML for the details, chunked per detail record. 

107 Each chunk consists of the following parts: 

108 

109 * the entity id of that detail, 

110 * the piece of HTML representing the title of the detail. 

111 """ 

112 

113 result = [] 

114 for (eid, mat) in reDetail(dtable).findall(text): 

115 result.append((eid, mat)) 

116 return result 

117 

118 

119def findEid(text, multiple=False): 

120 """Get the entity id(s) from a response. 

121 

122 If the response shows one or more records, dig out its entity id(s). 

123 

124 Otherwise, return `None` 

125 

126 Parameters 

127 ---------- 

128 text: string 

129 The response text. 

130 multiple: boolean 

131 Whether we should return the list of all found ids or only the last one. 

132 

133 Returns 

134 ------- 

135 list of string(ObjectId) | string(ObjectId) | `None` 

136 """ 

137 

138 results = eidRe.findall(text) 

139 return results if multiple else results[-1] if results else None 

140 

141 

142def findFields(text): 

143 """Get the fields from a response. 

144 

145 If the response shows a record, dig out its fields and values. 

146 

147 !!! hint 

148 They are neatly packaged in comment lines! 

149 

150 Parameters 

151 ---------- 

152 text: string 

153 The response text. 

154 

155 Returns 

156 ------- 

157 dict 

158 Keyed by field names, valued by field values. 

159 """ 

160 

161 return {field: value for (field, value) in fieldRe.findall(text)} 

162 

163 

164def findMainN(text): 

165 """Get the number of main records from a response. 

166 

167 !!! hint 

168 They are neatly packaged in comment lines! 

169 

170 Parameters 

171 ---------- 

172 text: string 

173 The response text. 

174 

175 Returns 

176 ------- 

177 list of string 

178 """ 

179 

180 return mainNRe.findall(text) 

181 

182 

183def findMsg(text): 

184 """Get flashed messages from a response. 

185 

186 Parameters 

187 ---------- 

188 text: string 

189 The response text. 

190 

191 Returns 

192 ------- 

193 set 

194 All text messages found in the flash bar. 

195 """ 

196 

197 return set(msgRe.findall(text)) 

198 

199 

200def findReviewEntries(text): 

201 """Get review entries from a criteria entry record. 

202 

203 Parameters 

204 ---------- 

205 text: string 

206 The response text of a request for an item view on a criteriaEntry record. 

207 

208 Returns 

209 ------- 

210 dict 

211 Keyed by `expert` or `final`. The values are the review comments of that 

212 reviewer on this criteria entry. 

213 """ 

214 

215 result = {} 

216 for (kind, material) in reviewRe.findall(text): 

217 spans = reviewEntryIdRe.findall(material) 

218 if spans: 

219 reIds = idRe.findall(spans[0]) 

220 if reIds: 220 ↛ 216line 220 didn't jump to line 216, because the condition on line 220 was never false

221 reId = reIds[0] 

222 fields = findFields(material) 

223 result[kind] = (reId, fields) 

224 return result 

225 

226 

227def findStages(text): 

228 """Get the workflow stages from a response. 

229 

230 !!! hint 

231 They are neatly packaged in comment lines! 

232 

233 Parameters 

234 ---------- 

235 text: string 

236 The response text. 

237 

238 Returns 

239 ------- 

240 list of string 

241 """ 

242 

243 return stageRe.findall(text) 

244 

245 

246def findTasks(text): 

247 """Get the workflow tasks from a response. 

248 

249 !!! hint 

250 They are neatly packaged in comment lines! 

251 

252 Parameters 

253 ---------- 

254 text: string 

255 The response text. 

256 

257 Returns 

258 ------- 

259 list of string 

260 """ 

261 

262 return taskRe.findall(text) 

263 

264 

265def findValues(table, text): 

266 """Get the values from the response of a list view on that table. 

267 

268 Parameters 

269 ---------- 

270 table: string 

271 text: string 

272 The response text. 

273 

274 Returns 

275 ------- 

276 dict 

277 keyed by the titles of the records and valued by their ids. 

278 """ 

279 

280 return {name: eid for (eid, name) in reValueList(table).findall(text)} 

281 

282 

283def forall(cls, expect, assertFunc, *args): 

284 """Executes an assert function for a subset of all clients. 

285 

286 The subset is determined by `expect`, which holds expected outcomes 

287 for the clients. 

288 

289 Parameters 

290 ---------- 

291 cls: fixture 

292 Contains a dict of all clients: `conftest.clients` 

293 assertFunc: function 

294 The function to be applied for each client. 

295 It will be passed all the `args` and a relevant part of `expect` 

296 expect: dict 

297 Keyed by user (eppn), contains the expected value for that user. 

298 """ 

299 

300 for user in USER_LIST: 

301 if user not in expect: 

302 continue 

303 exp = expect[user] 

304 serverprint(f"USER {user} EXPECTS {exp}") 

305 assertFunc(cls[user], *args, exp) 

306 

307 

308def getEid(client, table, multiple=False): 

309 """Gets the id(s) of the records(s) in the mylist view. 

310 

311 !!! caution 

312 Not all tables have a `mylist` view. Only contributions, assessments 

313 and reviews. 

314 

315 Parameters 

316 ---------- 

317 table: string 

318 multiple: boolean 

319 Whether we should return the list of all found ids or only the last one. 

320 """ 

321 

322 url = f"/{table}/list?action=my" 

323 (text, status, msgs) = accessUrl(client, url, redirect=True) 

324 return findEid(text, multiple=multiple) 

325 

326 

327def getItem(client, table, eid): 

328 """Looks up an item directly. 

329 

330 The response texts will be analysed into messages and fields 

331 

332 Parameters 

333 ---------- 

334 client: fixture 

335 table: string 

336 eid: string(ObjectId) 

337 

338 Returns 

339 ------- 

340 text: string 

341 The complete response text 

342 fields: dict 

343 All fields and their values 

344 msgs: list 

345 All entries that have been flashed (and arrived in the flash bar) 

346 """ 

347 

348 url = f"/{table}/item/{eid}" 

349 response = client.get(url) 

350 text = response.get_data(as_text=True) 

351 fields = {field: value for (field, value) in fieldRe.findall(text)} 

352 msgs = findMsg(text) 

353 return dict(text=text, fields=fields, msgs=msgs) 

354 

355 

356def getReviewEntryId(clients, cId, rEId, rFId): 

357 """Get the review entries associated with a criteria entry. 

358 

359 We use a MongoDB query to get the corresponding review entries. 

360 

361 Parameters 

362 ---------- 

363 clients: dict 

364 Keyed by user eppns, values by the corresponding client fixtures 

365 cId: string(ObjectId) 

366 The id of the criteria entry in question 

367 rEId: string(ObjectId) 

368 The id of the expert review 

369 rFId: string(ObjectId) 

370 The id of the final review 

371 

372 Returns 

373 ------- 

374 dict 

375 Keyed by reviewer (`expert` or `final`), the values are the ids of the 

376 corresponding review entries. 

377 """ 

378 

379 client = MongoClient() 

380 db = client.dariah_test 

381 return { 

382 reviewer: G( 

383 list( 

384 db[REVIEW_ENTRY].find( 

385 {REVIEW: ObjectId(reviewId), CRITERIA_ENTRY: ObjectId(cId)} 

386 ) 

387 )[0], 

388 _ID, 

389 ) 

390 for (reviewer, reviewId) in zip((EXPERT, FINAL), (rEId, rFId)) 

391 } 

392 

393 

394def getRelatedValues(client, table, eid, field): 

395 """Get an editable view on a field that represents a related value."" 

396 

397 We check the contents. 

398 """ 

399 url = f"/api/{table}/item/{eid}/field/{field}?action=edit" 

400 response = client.get(url) 

401 text = response.get_data(as_text=True) 

402 thisRe = reEditField(eid, field) 

403 valueStr = thisRe.findall(text) 

404 values = valueRe.findall(valueStr[0]) 

405 valueDict = {value: eid for (eid, value) in values} 

406 return valueDict 

407 

408 

409def getScores(cId): 

410 """Get relevant scores directly from Mongo DB. 

411 

412 Parameters 

413 ---------- 

414 cId: string(ObjectId) 

415 The id of a criteria entry record whose set of possible scores we 

416 want to retrieve. 

417 

418 Returns 

419 ------- 

420 dict 

421 Keyed by the title of the score, values are their ids. 

422 """ 

423 client = MongoClient() 

424 db = client.dariah_test 

425 crId = G(list(db.criteriaEntry.find(dict(_id=ObjectId(cId))))[0], CRITERIA) 

426 scores = db.score.find(dict(criteria=ObjectId(crId))) 

427 result = {} 

428 for record in scores: 

429 score = G(record, SCORE) 

430 if score is None: 430 ↛ 431line 430 didn't jump to line 431, because the condition on line 430 was never true

431 return UNDEF_VALUE 

432 level = G(record, LEVEL) or UNDEF_VALUE 

433 title = f"""{score} - {level}""" 

434 result[title] = str(G(record, _ID)) 

435 return result 

436 

437 

438def modifyField(client, table, eid, field, newValue): 

439 """Post data to update a field and analyse the response for the effect.""" 

440 

441 url = f"/api/{table}/item/{eid}/field/{field}?action=view" 

442 text = postJson(client, url, newValue) 

443 fields = findFields(text) 

444 return (text, fields) 

445 

446 

447def postJson(client, url, value): 

448 """Post data to a url and retrieve the response text. 

449 

450 Parameters 

451 ---------- 

452 client: function 

453 url: string(url) 

454 value: mixed 

455 The value to post. 

456 Will be wrapped into JSON with a proper header. 

457 

458 Returns 

459 ------- 

460 string 

461 The response text 

462 """ 

463 

464 response = client.post( 

465 url, data=json.dumps(dict(save=value)), content_type="application/json", 

466 ) 

467 text = response.get_data(as_text=True) 

468 

469 return text 

470 

471 

472def reDetail(dtable): 

473 """Given a detail table name, return a `re` that looks for the detail records. 

474 

475 Parameters 

476 ---------- 

477 dtable: string 

478 """ 

479 

480 return re.compile( 

481 r"""<details itemkey=['"]{dtable}/([^'"]+)['"][^>]*>(.*?)</details>""".format( 

482 dtable=dtable 

483 ), 

484 re.S, 

485 ) 

486 

487 

488def reEditField(eid, field): 

489 """Given a field name, return a `re` that looks for the value of that field. 

490 

491 Parameters 

492 ---------- 

493 eid: string(ObjectId) 

494 The id of the record whose field data we are searching 

495 field: string 

496 """ 

497 

498 return re.compile( 

499 r""" 

500 <span\ [^>]*?eid=['"]{eid}['"]\s+field=['"]{field}['"].*? 

501 <div\ wtype=['"]related['"]\ .*? 

502 <div\ class=['"]wvalue['"]>(.*?)</div> 

503 """.format( 

504 eid=eid, field=field 

505 ), 

506 re.S | re.X, 

507 ) 

508 

509 

510def reValueList(table): 

511 """Given a table name, return a `re` that finds pairs of id and title strings. 

512 

513 The text is a list display of value records, and we peel the ids from the 

514 `<detail>` elements and the titles from the `<summary>` within them. 

515 

516 Parameters 

517 ---------- 

518 table: string 

519 """ 

520 

521 return re.compile( 

522 f"""<details itemkey=['"]{table}/([^'"]*)['"].*?<summary>.*?<span.*?>([^<]*)</span>""", 

523 re.S, 

524 ) 

525 

526 

527def reWarning(label): 

528 """Given a label, return a `re` that looks for warned items with that label. 

529 

530 A warned item is an item with a CSS class `warning` in it. 

531 

532 Parameters 

533 ---------- 

534 label: string 

535 Usually the title of an item 

536 """ 

537 

538 return re.compile( 

539 r"""\bclass=['"][^'"]*\bwarning\b[^'"]*['"][^>]*>{label}<""".format( 

540 label=label 

541 ), 

542 re.S, 

543 ) 

544 

545 

546def shiftDate(table, eid, field, amount): 

547 """Shifts the date in a field with a certain amount. 

548 

549 If the field in question is currently blank, it is assumed to 

550 represent `now`. 

551 

552 !!! caution "Recompute the workflow table" 

553 We have changed a field in the database on which the workflow status depends. 

554 Tests that need to see updated workflow data should perform a recomputation 

555 of the workflow data. 

556 

557 Parameters 

558 ---------- 

559 table: string 

560 The table that contains the date field 

561 eid: string(objectId) 

562 The id of the record that contains the ddate field 

563 field: string 

564 The name of the date field 

565 amount: timedelta 

566 The amount of hours to shift the date field. Can be negative or positive. 

567 """ 

568 

569 client = MongoClient() 

570 db = client.dariah_test 

571 eid = ObjectId(eid) 

572 justNow = now() 

573 currentDate = G(db[table].find_one({_ID: eid}), field) 

574 if currentDate is None: 574 ↛ 575line 574 didn't jump to line 575, because the condition on line 574 was never true

575 currentDate = justNow 

576 

577 shiftedDate = currentDate + timedelta(hours=amount) 

578 db[table].update_one({_ID: eid}, {"$set": {field: shiftedDate}}) 

579 

580 

581def viewField(client, table, eid, field): 

582 """Get the response for showing a field.""" 

583 

584 url = f"/api/{table}/item/{eid}/field/{field}?action=view" 

585 response = client.get(url) 

586 text = response.get_data(as_text=True) 

587 fields = findFields(text) 

588 return (text, fields)