Coverage for postrfp / web / adaptors / core.py: 99%
164 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-03 01:35 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2025-12-03 01:35 +0000
1"""
2Functions that accept http request objects (webob)
3and return values to be consumed by api functions
5Purpose is to abstract api functions from http and
6to perform common validation and marshalling jobs
8Request is expected to be HttpRequest, webob subclass defined in rfy.web.request
10ORM model instances returned by functions here should be assumed to be validated as
11being visible to the calling user. Primitives (e.g. ID values like section_id) are not
12validated.
13"""
15import re
17from postrfp.shared import fetch
18from postrfp.model.questionnaire.b36 import to_b36
19from postrfp.shared import serial
20from postrfp.shared.pager import Pager
21from postrfp.web.suxint.extractors import SchemaDocArg
22from postrfp.model.audit import evt_types
25from postrfp.web.suxint import (
26 ArgExtractor,
27 PathArg,
28 GetArg,
29 GetArgSet,
30 PostFileArg,
31 PostArg,
32 HeaderArg,
33)
36class PagerArg(GetArg):
37 page_number = GetArg(
38 "page",
39 default=1,
40 validator=lambda x: x > 0,
41 doc="Page number of records to show, beginning from 1",
42 )
43 page_size = GetArg(
44 "page_size",
45 default=20,
46 validator=lambda x: 0 < x < 201,
47 doc="The number of records to return in a single page, maximum 200, minimum 1",
48 )
50 def extract(self, request):
51 _pagenum = PagerArg.page_number.extract(request)
52 PagerArg.page_number.validate(_pagenum)
54 _pagesize = PagerArg.page_size.extract(request)
55 PagerArg.page_size.validate(_pagesize)
57 if (_pagenum * _pagesize) > 2**64:
58 # MySQL Limit is a 64 bit integer
59 raise ValueError("Either page number or page size is too large")
61 return Pager(page=_pagenum, page_size=_pagesize)
63 def update_openapi_path_object(self, path_object: dict):
64 ArgExtractor.update_openapi_path_object(PagerArg.page_size, path_object)
65 ArgExtractor.update_openapi_path_object(PagerArg.page_number, path_object)
68def check_node_number(val):
69 bare = val.strip(". ")
70 if not bare:
71 # root is an empty string
72 return True
73 if re.match(r"^(\d+\.?)+$", bare) is None:
74 return False
75 for el in bare.split("."):
76 # base 36 maximum for converting into two letters
77 if int(el) >= 1295:
78 return False
80 return True
83# PATH ARGUMENTS
84# These are used to extract values from the URL path
85# the string value passed to the constructor is the name of the
86# preceding component of the url path
87# e.g. /api/v1/projects/{project_id}/ is defined by project_id = PathArg("project")
88# the name of variable set here is the name of the variable
89# expected by the receiving handler function
91section_id = PathArg("section")
93issue_id = PathArg("issue")
95question_id = PathArg("question")
97question_number = PathArg("question", converter=to_b36)
99score_id = PathArg("score")
101project_id = PathArg("project", arg_type="int")
103attachment_id = PathArg("attachment")
105answer_id = PathArg("answer")
107element_id = PathArg("element")
109event_id = PathArg("event")
111category_id = PathArg("category")
113reltype_id = PathArg("reltype")
115tag_id = PathArg("tag")
117note_id = PathArg("note")
119weightset_path_id = PathArg("weightset", arg_type="int")
121workflow_id = PathArg("workflow")
123entity_id = PathArg("entity")
125approval_id = PathArg("approval", arg_type="int")
127webhook_id = PathArg("webhook", arg_type="int")
129feed_id = PathArg("feed", arg_type="str")
132# HEADER ARGUMENTS
134if_match = HeaderArg(
135 "If-Match", doc="ETag of the content to be updated", required=True, arg_type="str"
136)
139# GET ARGUMENTS
141role_id = GetArg("roleId", arg_type="str")
143q_org_id = GetArg("orgId", arg_type="str", default=None)
145q_participant_id = GetArg("participantId", arg_type="str")
147with_ancestors = GetArg("ancestors", arg_type="boolean", default=False)
149search_doc = "See Path documentation for details of search special characters"
151org_type = GetArg(
152 "orgType", arg_type="str", default="vendors", enum_values=("clients", "vendors")
153)
156def search_val(val):
157 if val is None or len(val) < 2:
158 raise ValueError("Search term must be at least 2 characters long")
159 return True
162search_term = GetArg(
163 "searchTerm", arg_type="str", validator=search_val, doc=search_doc, required=True
164)
166search_options = GetArgSet(
167 "options",
168 arg_type="str",
169 enum_values=("answers", "choices", "notes", "questions", "scoreComments"),
170 doc="Defines which object types should be searched",
171)
173project_statuses = GetArgSet(
174 "projectStatus",
175 required=False,
176 arg_type="str",
177 doc="Filter by provided Project Status values",
178)
180project_sort = GetArg(
181 "projectSort",
182 arg_type="str",
183 enum_values=("date_created", "date_published", "deadline", "title"),
184 doc="Sort projects by dates or title",
185 default="date_created",
186)
188q_title = GetArg("qTitle", arg_type="str", doc="Search for string inside a title")
190q_name = GetArg("qName", arg_type="str", doc="Search for string inside a name")
192issue_sort = GetArg(
193 "issueSort",
194 arg_type="str",
195 enum_values=("deadline", "submitted_date", "issue_date"),
196 doc="Issue list sort order",
197 default="issue_date",
198)
200issue_status = GetArg(
201 "projectStatus",
202 arg_type="str",
203 enum_values=(
204 "Not Sent",
205 "Opportunity",
206 "Accepted",
207 "Updateable",
208 "Declined",
209 "Submitted",
210 "Retracted",
211 ),
212 doc="Filter by provided Project Status values",
213)
215sort_order = GetArg("sort", arg_type="str", enum_values=("asc", "desc"), default="desc")
218offset = GetArg("offset", arg_type="int", doc="For paging results")
220q_section_id = GetArg("sectionId")
222scoreset_docs = (
223 "Set to the user ID of a scoring user if Multiple Score Sets are "
224 "enabled for this project"
225)
227"""
228scoreset_id is the ID of the user that created a score set. __default__ is used when returning
229data to indicate the default user. For db queries an empty string is used.
230"""
233def default_scoreset(v):
234 if v in (None, "__default__"):
235 return ""
236 return v
239scoreset_id = GetArg(
240 "scoresetId",
241 arg_type="str",
242 default="__default__",
243 doc=scoreset_docs,
244 converter=default_scoreset,
245)
248weightset_id = GetArg("weightsetId", arg_type="int")
250q_project_id = GetArg("projectId", arg_type="int", doc="ID of the Project")
252q_category_id = GetArg("categoryId", arg_type="int", doc="ID of the Project")
254q_issue_id = GetArg("issueId")
256q_workflow_id = GetArg("workflowId", arg_type="int")
258user_id = GetArg("userId", arg_type="str", required=True)
260node_doc = (
261 "Dotted String number e.g. 3.12.5 giving position of the question"
262 " or section within the questionnaire"
263)
265node_number = GetArg(
266 "nodeNumber",
267 arg_type="str",
268 converter=to_b36,
269 validator=check_node_number,
270 doc=node_doc,
271 default="",
272)
274q_question_number = GetArg(
275 "questionNumber",
276 arg_type="str",
277 converter=to_b36,
278 doc='e.g. "5.2.9"',
279 required=True,
280)
283def val_evt_type(val):
284 if val is not None and not hasattr(evt_types, val):
285 raise ValueError(f"{val} is not a valid event type")
286 return True
289event_type = GetArg("eventType", arg_type="str", validator=val_evt_type)
290entity = GetArg("entity", arg_type="str")
292restricted_users = GetArg("restricted", arg_type="boolean", default=False)
294pager = PagerArg("page", default=1)
296q_with_questions = GetArg("withQuestions", arg_type="boolean", default=True)
298issue_ids = GetArgSet(
299 "issueIds",
300 array_items_type="int",
301 min_items=1,
302 max_items=10,
303 doc="ID values of Issues to query against",
304)
306project_ids = GetArgSet(
307 "projectIds",
308 array_items_type="int",
309 min_items=0,
310 max_items=100,
311 doc="ID values of Projects to query against",
312)
314scoring_model_docs = (
315 "Type of scoring model to return. One of: "
316 "Unweighted - no weighting applied, "
317 "Direct - score multiplied by the score of the question or section, "
318 "Absolute - score multiplied by absolute weight of the question or section"
319)
321scoring_model = GetArg(
322 "scoringModel",
323 arg_type="str",
324 enum_values=[v for v in serial.ScoringModel],
325 default="Unweighted",
326 doc=scoring_model_docs,
327)
329# POST Args
331attachment_description = PostArg("description", arg_type="str")
332el_id = PostArg("el_id", arg_type="int")
334# POST File Uploads
335attachment_upload = PostFileArg("attachment_upload")
337data_upload = PostFileArg("data_upload", required=True)
339# VALIDATED JSON REQUEST BODY ARGUMENTS
341allocation_doc = SchemaDocArg(serial.AllocatedToList, as_dict=False)
343answers_doc = SchemaDocArg(serial.ElementAnswerList, as_dict=False)
345category_doc = SchemaDocArg(serial.NewCategory, as_dict=False)
347element_doc = SchemaDocArg(serial.qmodels.QElement)
349feed_doc = SchemaDocArg(serial.Datafeed, as_dict=False)
351ids_doc = SchemaDocArg(serial.IdList, as_dict=False)
353name_doc = SchemaDocArg(serial.ShortName, as_dict=False)
355import_answers_doc = SchemaDocArg(serial.ImportAnswers, as_dict=False)
357import_section_doc = SchemaDocArg(serial.SectionImportDoc, as_dict=False)
359issue_doc = SchemaDocArg(serial.UpdateableIssue, exclude_unset=True)
361issue_status_doc = SchemaDocArg(serial.IssueStatus, as_dict=False)
363issue_workflow_doc = SchemaDocArg(serial.IssueUseWorkflow, as_dict=False)
365move_section_doc = SchemaDocArg(serial.MoveSection, as_dict=False)
367new_client_doc = SchemaDocArg(serial.NewClient, as_dict=False)
369new_issue_doc = SchemaDocArg(serial.NewIssue, as_dict=False)
371new_project_doc = SchemaDocArg(serial.NewProject)
373note_doc = SchemaDocArg(serial.ProjectNote, as_dict=False)
375org_doc = SchemaDocArg(serial.Organisation, as_dict=False)
377participants_list = SchemaDocArg(serial.UpdateParticipantList)
379perm_doc = SchemaDocArg(serial.ProjectPermission, as_dict=False)
381publish_doc = SchemaDocArg(serial.PublishProject)
383qdef_doc = SchemaDocArg(serial.QuestionDef, as_dict=False)
385relationship_doc = SchemaDocArg(serial.Relationship, as_dict=False)
387reltype_doc = SchemaDocArg(serial.RelationshipType, as_dict=False)
389replace_doc = SchemaDocArg(serial.TextReplace, as_dict=False)
391respondent_note_doc = SchemaDocArg(serial.RespondentNote, as_dict=False)
393score_doc = SchemaDocArg(serial.Score, as_dict=False)
395section_score_docs = SchemaDocArg(serial.SectionScoreDocs, as_dict=False)
397edit_sec_doc = SchemaDocArg(serial.EditableSection, as_dict=False)
399tag_assigns_doc = SchemaDocArg(serial.TagAssigns, as_dict=False)
401tag_doc = SchemaDocArg(serial.NewTag, as_dict=False)
403update_project_doc = SchemaDocArg(serial.UpdateableProject)
405user_doc = SchemaDocArg(serial.EditableUser, as_dict=False)
407user_id_doc = SchemaDocArg(serial.UserId, as_dict=False)
409watch_doc = SchemaDocArg(serial.TargetUser, as_dict=False)
411watchers_doc = SchemaDocArg(serial.TargetUserList)
413webhook_doc = SchemaDocArg(serial.Webhook, as_dict=False)
415weights_doc = SchemaDocArg(serial.WeightingsDoc, as_dict=False)
417weightset_doc = SchemaDocArg(serial.NewWeightSet, as_dict=False)
419child_nodes_doc = SchemaDocArg(serial.SectionChildNodes, as_dict=False)
421transition_doc = SchemaDocArg(serial.TransitionRequest, as_dict=False)
423workflow_doc = SchemaDocArg(serial.WorkflowSchema, as_dict=False)
426class UserObject(ArgExtractor):
427 swagger_in = "query"
428 swagger_type = "string"
430 def extract(self, request):
431 user_id = request.GET.get("targetUser", None)
432 if not user_id:
433 return None
434 return fetch.user(request.session, user_id)
437target_user = UserObject("targetUser")
440# NON USER DEFINED VALUES ASSOCIATED WITH THE REQUEST
443def session(request):
444 return request.session
447def user(request):
448 return request.user
451def effective_user(request):
452 return request.user