Coverage for rfpy/api/endpoints/answers.py: 87%
62 statements
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-31 16:00 +0000
« prev ^ index » next coverage.py v7.0.1, created at 2022-12-31 16:00 +0000
1'''
2Operations for fetching vendor's answers
3'''
4from collections import defaultdict
5from typing import List
7from rfpy.suxint import http
8from rfpy.model import (
9 Issue, QuestionInstance, Answer
10)
11from rfpy.api import fetch, validate
12from rfpy.auth import perms
13from rfpy.web import serial
16# Reusable chunk of documentation for openapi spec documentation of endpoint functions
17lookup_docs = '''
18 Results are provided as a nested mapping object allowing easy reference when
19 assigning answers to questions for specific Issues:
20 ```
21 {
22 // ID of the Issue
23 "923": {
25 // Element ID to Answer Value
26 "2342742": "Yes, we can deliver to Jupiter",
27 "7234232": "No, we cannot deliver to Baltimore"
29 }
30 }
31 ```
32 So to reference an answer for Issue 923 to Element 7234232, the notation is
33 data['923']['7234232']
35 (bear in mind that numerical IDs are delivered as Strings when used as Object keys in JSON)
36'''
39@http
40def get_question_answers(session, user, question_id) -> serial.AnswerLookup:
41 """
42 Fetch answers for question `question_id` from project structures as nested object
43 mapping issue ID to question element ID to answer.
44 """
45 # all standard users get the same copy
46 question = fetch.question(session, question_id)
47 validate.check(user, perms.ISSUE_VIEW_ANSWERS,
48 project=question.project,
49 section_id=question.section_id)
51 def get_answers():
52 qelements = defaultdict(dict)
53 for answer in question.all_answers():
54 qelements[str(answer.issue_id)][str(answer.element_id)] = answer.as_dict()
56 return qelements
58 return get_answers()
61get_question_answers.__doc__ += lookup_docs
64@http
65def get_project_issue_question(session, user,
66 project_id, issue_id,
67 q_question_number) -> serial.SingleRespondentQuestion:
68 '''
69 Get the answered question for the given project and issue.
70 '''
71 project = fetch.project(session, project_id)
72 question = project.question_by_number(q_question_number)
73 issue = project.issue_by_id(issue_id)
74 validate.check(user, perms.ISSUE_VIEW_ANSWERS, issue=issue,
75 project=project, section_id=question.section_id)
76 return question.single_vendor_dict(issue)
79@http
80def get_project_section_issue_answers(session,
81 user,
82 project_id,
83 section_id,
84 issue_id) -> List[serial.Answer]:
85 '''
86 Fetch an array of Answer objects for the given Project, Issue and Section
87 '''
88 project = fetch.project(session, project_id)
89 issue = fetch.issue(session, issue_id)
91 validate.check(user, perms.ISSUE_VIEW_ANSWERS,
92 project=project, section_id=section_id,
93 issue=issue)
95 cols = (
96 Answer.element_id,
97 Answer.answer,
98 Answer.issue_id,
99 Answer.question_instance_id.label('question_id')
100 )
101 answers = session.query(*cols)\
102 .join(Issue)\
103 .join(QuestionInstance)\
104 .filter(
105 Issue.project == project,
106 Answer.issue == issue,
107 QuestionInstance.section_id == section_id)
109 return [a._asdict() for a in answers]
111@http
112def get_question_issue_answers(session, user, question_id, issue_id) -> List[serial.ElementAnswerList]:
113 '''
114 Fetch an array of Answer & Element ID for the given Question, Issue
115 '''
116 question = fetch.question(session, question_id)
117 issue = question.project.get_issue(issue_id)
119 validate.check(user, perms.ISSUE_VIEW_ANSWERS,
120 project=question.project, section_id=question.section_id,
121 issue=issue)
123 result = []
124 answers = question.answers_for_issue(issue.id).all()
125 for answer in answers:
126 result.append({
127 "element_id": answer.element_id,
128 "answer": answer.answer
129 })
131 return result
133@http
134def get_element_answers(session, user, element_id) -> serial.RespondentAnswers:
135 '''
136 An array of answers from all projects and all issues for the provided element_id.
138 Project ID, Title and Date Published are provided to be used as a reference when reviewing
139 previous answers.
140 '''
141 validate.check(user, perms.ISSUE_VIEW_ANSWERS, multiproject=True)
142 return [a._asdict() for a in fetch.element_answers(session, user, element_id)]
145@http
146def get_project_answers(session, user, project_id, issue_ids) -> serial.AnswerLookup:
147 '''
148 Fetch Answers to all questions in the current project for the Issue IDs provided.
150 __N.B.__ This operation is inefficient for projects with very many questions or Issues. A
151 maximum of 2,000 element answers will be returned. An HTTP 400 error will be returned if more
152 than 2,000 answers are found. This total does not include unanswered Question Element / Issue
153 combinations. Operation ID get_project_qstats can be used to check how many
154 answerable elements are in the project.
155 '''
156 project = fetch.project(session, project_id)
157 validate.check(user, perms.ISSUE_VIEW_ANSWERS,
158 project=project, deny_restricted=True)
160 issue_id_set = {i.id for i in project.scoreable_issues} & issue_ids
162 aq = fetch.answers_in_issues_query(session, project_id, issue_id_set)
164 if aq.count() > 2000:
165 m = 'More than 2,000 results found: submit fewer issueIds or use a different operation'
166 raise ValueError(m)
168 adict = defaultdict(dict)
169 for issue_id, element_id, answer in aq.with_entities(Answer.issue_id,
170 Answer.element_id,
171 Answer.answer):
172 adict[str(issue_id)][str(element_id)] = answer
174 return adict
177get_project_answers.__doc__ += lookup_docs