Coverage for tests/helpers.py : 97%

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"""
4import re
5from datetime import timedelta
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)
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=['"](.*?)['"][^>]*>(.*?)(?:
)?<""", 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)
55def accessUrl(client, url, redirect=False):
56 """Get the response on accessing a url."""
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)
65def checkWarning(text, label):
66 """Whether there is a warned item with `label`.
68 See `reWarning`.
69 """
71 return not not reWarning(label).search(text)
74def findCaptions(text):
75 """Get the captions from a response.
77 !!! hint
78 They are neatly packaged in comment lines!
80 Parameters
81 ----------
82 text: string
83 The response text.
85 Returns
86 -------
87 list of string
88 """
90 return captionRe.findall(text)
93def findDetails(text, dtable):
94 """Get the details from a response, but only those in a specific table.
96 Parameters
97 ----------
98 text: string
99 The response text.
100 dtail: string
101 The detail table
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:
109 * the entity id of that detail,
110 * the piece of HTML representing the title of the detail.
111 """
113 result = []
114 for (eid, mat) in reDetail(dtable).findall(text):
115 result.append((eid, mat))
116 return result
119def findEid(text, multiple=False):
120 """Get the entity id(s) from a response.
122 If the response shows one or more records, dig out its entity id(s).
124 Otherwise, return `None`
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.
133 Returns
134 -------
135 list of string(ObjectId) | string(ObjectId) | `None`
136 """
138 results = eidRe.findall(text)
139 return results if multiple else results[-1] if results else None
142def findFields(text):
143 """Get the fields from a response.
145 If the response shows a record, dig out its fields and values.
147 !!! hint
148 They are neatly packaged in comment lines!
150 Parameters
151 ----------
152 text: string
153 The response text.
155 Returns
156 -------
157 dict
158 Keyed by field names, valued by field values.
159 """
161 return {field: value for (field, value) in fieldRe.findall(text)}
164def findMainN(text):
165 """Get the number of main records from a response.
167 !!! hint
168 They are neatly packaged in comment lines!
170 Parameters
171 ----------
172 text: string
173 The response text.
175 Returns
176 -------
177 list of string
178 """
180 return mainNRe.findall(text)
183def findMsg(text):
184 """Get flashed messages from a response.
186 Parameters
187 ----------
188 text: string
189 The response text.
191 Returns
192 -------
193 set
194 All text messages found in the flash bar.
195 """
197 return set(msgRe.findall(text))
200def findReviewEntries(text):
201 """Get review entries from a criteria entry record.
203 Parameters
204 ----------
205 text: string
206 The response text of a request for an item view on a criteriaEntry record.
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 """
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
227def findStages(text):
228 """Get the workflow stages from a response.
230 !!! hint
231 They are neatly packaged in comment lines!
233 Parameters
234 ----------
235 text: string
236 The response text.
238 Returns
239 -------
240 list of string
241 """
243 return stageRe.findall(text)
246def findTasks(text):
247 """Get the workflow tasks from a response.
249 !!! hint
250 They are neatly packaged in comment lines!
252 Parameters
253 ----------
254 text: string
255 The response text.
257 Returns
258 -------
259 list of string
260 """
262 return taskRe.findall(text)
265def findValues(table, text):
266 """Get the values from the response of a list view on that table.
268 Parameters
269 ----------
270 table: string
271 text: string
272 The response text.
274 Returns
275 -------
276 dict
277 keyed by the titles of the records and valued by their ids.
278 """
280 return {name: eid for (eid, name) in reValueList(table).findall(text)}
283def forall(cls, expect, assertFunc, *args):
284 """Executes an assert function for a subset of all clients.
286 The subset is determined by `expect`, which holds expected outcomes
287 for the clients.
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 """
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)
308def getEid(client, table, multiple=False):
309 """Gets the id(s) of the records(s) in the mylist view.
311 !!! caution
312 Not all tables have a `mylist` view. Only contributions, assessments
313 and reviews.
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 """
322 url = f"/{table}/list?action=my"
323 (text, status, msgs) = accessUrl(client, url, redirect=True)
324 return findEid(text, multiple=multiple)
327def getItem(client, table, eid):
328 """Looks up an item directly.
330 The response texts will be analysed into messages and fields
332 Parameters
333 ----------
334 client: fixture
335 table: string
336 eid: string(ObjectId)
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 """
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)
356def getReviewEntryId(clients, cId, rEId, rFId):
357 """Get the review entries associated with a criteria entry.
359 We use a MongoDB query to get the corresponding review entries.
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
372 Returns
373 -------
374 dict
375 Keyed by reviewer (`expert` or `final`), the values are the ids of the
376 corresponding review entries.
377 """
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 }
394def getRelatedValues(client, table, eid, field):
395 """Get an editable view on a field that represents a related value.""
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
409def getScores(cId):
410 """Get relevant scores directly from Mongo DB.
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.
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
438def modifyField(client, table, eid, field, newValue):
439 """Post data to update a field and analyse the response for the effect."""
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)
447def postJson(client, url, value):
448 """Post data to a url and retrieve the response text.
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.
458 Returns
459 -------
460 string
461 The response text
462 """
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)
469 return text
472def reDetail(dtable):
473 """Given a detail table name, return a `re` that looks for the detail records.
475 Parameters
476 ----------
477 dtable: string
478 """
480 return re.compile(
481 r"""<details itemkey=['"]{dtable}/([^'"]+)['"][^>]*>(.*?)</details>""".format(
482 dtable=dtable
483 ),
484 re.S,
485 )
488def reEditField(eid, field):
489 """Given a field name, return a `re` that looks for the value of that field.
491 Parameters
492 ----------
493 eid: string(ObjectId)
494 The id of the record whose field data we are searching
495 field: string
496 """
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 )
510def reValueList(table):
511 """Given a table name, return a `re` that finds pairs of id and title strings.
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.
516 Parameters
517 ----------
518 table: string
519 """
521 return re.compile(
522 f"""<details itemkey=['"]{table}/([^'"]*)['"].*?<summary>.*?<span.*?>([^<]*)</span>""",
523 re.S,
524 )
527def reWarning(label):
528 """Given a label, return a `re` that looks for warned items with that label.
530 A warned item is an item with a CSS class `warning` in it.
532 Parameters
533 ----------
534 label: string
535 Usually the title of an item
536 """
538 return re.compile(
539 r"""\bclass=['"][^'"]*\bwarning\b[^'"]*['"][^>]*>{label}<""".format(
540 label=label
541 ),
542 re.S,
543 )
546def shiftDate(table, eid, field, amount):
547 """Shifts the date in a field with a certain amount.
549 If the field in question is currently blank, it is assumed to
550 represent `now`.
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.
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 """
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
577 shiftedDate = currentDate + timedelta(hours=amount)
578 db[table].update_one({_ID: eid}, {"$set": {field: shiftedDate}})
581def viewField(client, table, eid, field):
582 """Get the response for showing a field."""
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)