Coverage for rfpy/api/endpoints/search.py: 100%

39 statements  

« prev     ^ index     » next       coverage.py v7.0.1, created at 2022-12-31 16:00 +0000

1''' 

2Search for question, answer or note text within or across projects 

3''' 

4from typing import Sequence, List 

5 

6from sqlalchemy.orm import Session 

7 

8from rfpy.suxint import http 

9from rfpy.model import User 

10from rfpy.api import fetch, validate, update 

11from rfpy.web import serial 

12from rfpy.auth import perms 

13from rfpy.model.composite import update_qmeta_table 

14from rfpy.model.questionnaire import QuestionInstance, QuestionDefinition, to_b36 

15 

16 

17@http 

18def get_search( 

19 session: Session, 

20 user: User, 

21 search_term: str, 

22 search_options: Sequence[str], 

23 q_project_id: int, 

24 offset: int = None) -> List[serial.SearchResult]: 

25 ''' 

26 Text search for the various Object types given by 'options'. 

27 

28 Optionally, filter results by project ID (project_id parameter) 

29 

30 20 results are returned per query. Results can be paged using the offset parameter. 

31 

32 The following special characters can be used in searchTerm to refine the search: 

33 

34 * Prepend a + symbol to a word to only see results with that word 

35 * Prepend a - symbol to exlude any records containing that word 

36 * Append * to a word to act as a wildcard 

37 

38 For example "+green fingers find* -expensive" will find records that: 

39 

40 1. Must contain the work "green" 

41 2. Should contain "fingers" 

42 3. Should contain words starting with "find" e.g. "findings" or "finds" or "Findlay" 

43 4. Must not contain the work "expensive" 

44 

45 

46 ### Results 

47 A SearchResult record is returned for each hit. This has the following fields: 

48 

49 * __klass__ - identifies the type of search result, corresponding to the options parameter 

50 * __project_title__ - title of the associated Project 

51 * __object_id__ - for questions, answers, choices and scoreComment records this is in the form 

52 of a string containing section ID and question ID separated by a | character, e.g. 

53 239|121231, where 239 is the Section ID and 121231 is the Question ID. For note records 

54 the object_id is the ID of that note. 

55 * __object_ref__ - for questions, answers, choices and scoreComment this is the question number 

56 e.g. '4.2.18' 

57 * __snippet__ - this a block of text surrounding a containing the searchTerm 

58 * __search_score__ - a score indicating the closeness of match 

59 

60 ''' 

61 

62 if offset is None: 

63 offset = 0 

64 

65 user.check_is_standard() 

66 

67 if not search_options: 

68 return {'hits': []} 

69 

70 if q_project_id is not None: 

71 project = fetch.project(session, q_project_id) 

72 validate.check(user, perms.PROJECT_ACCESS, project=project) 

73 

74 return fetch.search(session, user.org_id, search_term, 

75 search_options, q_project_id, offset) 

76 

77 

78@http 

79def post_project_questions_replace(session, 

80 user, 

81 project_id, 

82 replace_doc) -> List[serial.ReplacedItem]: 

83 ''' 

84 Replace search_term with replace_term (from the Request's json body) in all questions in the 

85 project. Question titles, label field and multiple choice options are updated. 

86 

87 If the JSON body field 'dry_run' is true then no update is made, but the results returned 

88 show what changes would be made. 

89 

90 The Response is an array of ReplacedItem records, one for each question element or question 

91 title that has been changed, together with the old and new values. 

92 

93 This search is case sensitive 

94 ''' 

95 search_term = replace_doc['search_term'] 

96 replace_term = replace_doc['replace_term'] 

97 dry_run = replace_doc['dry_run'] 

98 

99 if len(search_term) < 3: 

100 raise ValueError((f"'{search_term}' is an invalid search term. " 

101 f" must be at least 3 characters wrong")) 

102 

103 project = fetch.project(session, project_id) 

104 validate.check(user, perms.PROJECT_VIEW_QUESTIONNAIRE, project=project) 

105 

106 changes = list(update.label_text(project, search_term, replace_term, dry_run=dry_run)) 

107 changes.extend(update.question_titles(project, search_term, replace_term, dry_run=dry_run)) 

108 changes.extend(update.choices_text(project, search_term, replace_term, dry_run=dry_run)) 

109 

110 if dry_run: 

111 return changes 

112 

113 qnums = {to_b36(c['question_number']) for c in changes} 

114 qidq = (session.query(QuestionDefinition.id) 

115 .join(QuestionInstance) 

116 .filter(QuestionInstance.project == project) 

117 .filter(QuestionInstance.number.in_(qnums))) 

118 qids = [r.id for r in qidq] 

119 update_qmeta_table(session, qids) 

120 

121 return changes