Coverage for rfpy/api/endpoints/search.py: 100%
40 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-24 10:52 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-24 10:52 +0000
1"""
2Search for question, answer or note text within or across projects
3"""
5from typing import Optional
7from sqlalchemy.orm import Session
9from rfpy.suxint import http
10from rfpy.model import User
11from rfpy.api import fetch, validate, update
12from rfpy.web import serial
13from rfpy.auth import perms
14from rfpy.model.composite import update_qmeta_table
15from rfpy.model.questionnaire import QuestionInstance, QuestionDefinition, to_b36
18@http
19def get_search(
20 session: Session,
21 user: User,
22 search_term: str,
23 search_options: list[str],
24 q_project_id: int,
25 offset: Optional[int] = None,
26) -> list[serial.SearchResult]:
27 """
28 Text search for the various Object types given by 'options'.
30 Optionally, filter results by project ID (project_id parameter)
32 20 results are returned per query. Results can be paged using the offset parameter.
34 The following special characters can be used in searchTerm to refine the search:
36 * Prepend a + symbol to a word to only see results with that word
37 * Prepend a - symbol to exlude any records containing that word
38 * Append * to a word to act as a wildcard
40 For example "+green fingers find* -expensive" will find records that:
42 1. Must contain the work "green"
43 2. Should contain "fingers"
44 3. Should contain words starting with "find" e.g. "findings" or "finds" or "Findlay"
45 4. Must not contain the work "expensive"
48 ### Results
49 A SearchResult record is returned for each hit. This has the following fields:
51 * __klass__ - identifies the type of search result, corresponding to the options parameter
52 * __project_title__ - title of the associated Project
53 * __object_id__ - for questions, answers, choices and scoreComment records this is in the form
54 of a string containing section ID and question ID separated by a | character, e.g.
55 239|121231, where 239 is the Section ID and 121231 is the Question ID. For note records
56 the object_id is the ID of that note.
57 * __object_ref__ - for questions, answers, choices and scoreComment this is the question number
58 e.g. '4.2.18'
59 * __snippet__ - this a block of text surrounding a containing the searchTerm
60 * __search_score__ - a score indicating the closeness of match
62 """
64 if offset is None:
65 offset = 0
67 user.check_is_standard()
69 if not search_options:
70 return []
72 if q_project_id is not None:
73 project = fetch.project(session, q_project_id)
74 validate.check(user, perms.PROJECT_ACCESS, project=project)
76 hits = fetch.search(
77 session, user.org_id, search_term, search_options, q_project_id, offset
78 )
79 return [serial.SearchResult(**h) for h in hits]
82@http
83def post_project_questions_replace(
84 session, user, project_id, replace_doc: serial.TextReplace
85) -> list[serial.ReplacedItem]:
86 """
87 Replace search_term with replace_term (from the Request's json body) in all questions in the
88 project. Question titles, label field and multiple choice options are updated.
90 If the JSON body field 'dry_run' is true then no update is made, but the results returned
91 show what changes would be made.
93 The Response is an array of ReplacedItem records, one for each question element or question
94 title that has been changed, together with the old and new values.
96 This search is case sensitive
97 """
98 search_term = replace_doc.search_term
99 replace_term = replace_doc.replace_term
100 dry_run = replace_doc.dry_run
102 if len(search_term) < 3:
103 raise ValueError(
104 (
105 f"'{search_term}' is an invalid search term. "
106 f" must be at least 3 characters wrong"
107 )
108 )
110 project = fetch.project(session, project_id)
111 validate.check(user, perms.PROJECT_VIEW_QUESTIONNAIRE, project=project)
113 changes = list(
114 update.label_text(project, search_term, replace_term, dry_run=dry_run)
115 )
116 changes.extend(
117 update.question_titles(project, search_term, replace_term, dry_run=dry_run)
118 )
119 changes.extend(
120 update.choices_text(project, search_term, replace_term, dry_run=dry_run)
121 )
123 if dry_run:
124 return changes
126 qnums = {to_b36(c["question_number"]) for c in changes}
127 qidq = (
128 session.query(QuestionDefinition.id)
129 .join(QuestionInstance)
130 .filter(QuestionInstance.project == project)
131 .filter(QuestionInstance.number.in_(qnums))
132 )
133 qids = [r.id for r in qidq]
134 update_qmeta_table(session, qids)
136 return changes