Coverage for rfpy/api/endpoints/issues.py: 98%
94 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 managing issues - invitations to and responses from vendors
3'''
4from datetime import datetime
5from typing import List
7from sqlalchemy.orm.session import Session
9from rfpy.suxint import http
10from rfpy.auth import perms, AuthorizationFailure
11from rfpy.api import fetch, validate
12from rfpy.model import (
13 Organisation, Issue, NotSentIssue, AuditEvent, Project, Participant
14)
15from rfpy.model.humans import User
16from rfpy.web import serial, Pager
19@http
20def get_vendor(session, user, q_org_id) -> serial.Supplier:
21 '''
22 Information about a vendor organisation:
23 - Name & ID
24 - Issues from this buyer/participant
25 - Users (only exposed to Consultant organisations)
26 '''
28 if not user.organisation.is_consultant:
29 raise AuthorizationFailure('Only Consultant users can view Vendor details')
31 # Will raise No Result Found if not in Suppliers list
32 vendor = user.organisation.suppliers.filter(Organisation.id == q_org_id).one()
34 issues = [i._asdict() for i in
35 fetch.issues_for_respondent(session, user, q_org_id)]
37 data = {
38 'issues': issues,
39 'organisation': vendor.as_dict(),
40 'users': [u.as_dict() for u in vendor.users]
41 }
43 return data
46@http
47def get_project_issue(session, user, project_id, issue_id) -> serial.Issue:
48 project = fetch.project(session, project_id)
49 validate.check(user, perms.PROJECT_ACCESS, project=project)
50 issue = project.get_issue(issue_id)
51 return serial.Issue.from_orm(issue)
54@http
55def get_project_issues(session, user, project_id) -> List[serial.Issue]:
56 '''Fetch a list of Issue objects for the given project'''
57 project = fetch.project(session, project_id)
58 validate.check(user, perms.PROJECT_ACCESS, project=project)
59 fo = serial.Issue.from_orm
60 return [fo(i) for i in project.issues]
63@http
64def put_project_issue(session, user, project_id, issue_id, issue_doc):
65 '''
66 Update the properties of an existing Issue
68 Status cannot be updated this way - use post_issue_status instead
69 '''
70 project: Project = fetch.project(session, project_id)
71 validate.check(user, perms.PROJECT_ACCESS, project=project)
72 issue = project.get_issue(issue_id)
73 validate.check(user, perms.ISSUE_UPDATE, issue=issue)
75 change_list = []
77 for field, value in issue_doc.items():
78 change_list.append((field, getattr(issue, field), value))
79 setattr(issue, field, value)
81 AuditEvent.create('ISSUE_UPDATED', project=issue.project, issue_id=issue.id,
82 user_id=user.id, org_id=user.organisation.id,
83 object_id=issue.id, timestamp=datetime.now(),
84 private=True, change_list=change_list)
87@http
88def get_issue(session, user, issue_id) -> serial.Issue:
89 issue = fetch.issue(session, issue_id)
90 validate.check(user, perms.PROJECT_ACCESS, project=issue.project)
91 return serial.Issue.from_orm(issue)
94@http
95def post_project_issue(
96 session: Session,
97 user: User,
98 project_id: int,
99 new_issue_doc: serial.NewIssue) -> serial.Id:
100 '''
101 Create a new Issue
103 Either respondent_id or respondent_email should be set, but not both
104 '''
105 project = fetch.project(session, project_id)
106 validate.check(user, perms.ISSUE_CREATE, project=project)
107 label = new_issue_doc.get('label', None)
108 respondent_id = new_issue_doc.get('respondent_id', None)
109 if respondent_id is None:
110 issue = NotSentIssue(
111 respondent_email=new_issue_doc['respondent_email'],
112 label=label
113 )
114 else:
115 org: Organisation = session.query(Organisation).filter_by(id=respondent_id).one()
116 issue = NotSentIssue(respondent=org, label=label)
118 project.add_issue(issue)
119 session.flush()
120 evt = AuditEvent.create('ISSUE_CREATED',
121 issue_id=issue.id,
122 project=project,
123 user=user)
124 for k, v in new_issue_doc.items():
125 if k not in ('respondent_id', 'respondent_email'):
126 evt.add_change(k, '', v)
127 session.add(evt)
129 return dict(id=issue.id)
132@http
133def delete_project_issue(session: Session, user, project_id, issue_id):
134 '''
135 Delete the Issue with the given id
137 Only permitted at status:
138 - Not Sent
139 - Declined
140 - Retracted
142 At other statuses the Issue belongs to the Respondent organisation
143 therefore cannot be deleted by the Buyer
144 '''
145 project: Project = fetch.project(session, project_id)
146 validate.check(user, perms.PROJECT_ACCESS, project=project)
147 issue = project.get_issue(issue_id)
148 validate.check(user, perms.ISSUE_DELETE, issue=issue)
149 session.delete(issue)
150 evt = AuditEvent.create('ISSUE_DELETED',
151 issue_id=issue_id,
152 project=project,
153 user=user)
154 session.add(evt)
157@http
158def post_issue_status(session, user, issue_id, issue_status_doc):
159 '''Change the status of the given issue'''
160 issue = fetch.issue(session, issue_id)
161 validate.check(user, perms.ISSUE_SUBMIT, project=issue.project)
162 issue.change_status(user, issue_status_doc['new_status'])
165@http
166def get_issues(
167 session: Session,
168 user: User,
169 issue_sort: str,
170 sort_order: str,
171 q_org_id: str = None,
172 issue_status: str = None,
173 pager: Pager = None) -> serial.IssuesList:
174 '''
175 Get an array of Issues from all projects visible to the current user.
177 '''
178 if pager is None:
179 pager = Pager(page=1, pagesize=100)
181 cols = (Issue.id.label('issue_id'), Issue.respondent_id, Issue.respondent_email,
182 Project.id.label('project_id'), Issue.status,
183 Issue.deadline, Issue.submitted_date, Issue.issue_date,
184 Project.title.label('project_title'), Issue.winloss_exposed,
185 Issue.winloss_expiry, Issue.label)
187 sort_cols = {
188 'deadline': Issue.deadline,
189 'submitted_date': Issue.submitted_date,
190 'issue_date': Issue.issue_date
191 }
193 ordering = sort_cols[issue_sort]
194 ordering = ordering.desc() if sort_order == 'desc' else ordering.asc()
196 iq = session.query(*cols)\
197 .join(Project, Participant)\
198 .filter(Participant.org_id == user.org_id)
200 if q_org_id:
201 iq = iq.filter(Issue.respondent_id == q_org_id)
203 if issue_status:
204 iq = iq.filter(Issue.status == issue_status)
206 total_records = iq.count()
208 iq = iq.order_by(ordering).slice(pager.startfrom, pager.goto)
209 res = [i._asdict() for i in iq]
210 return {
211 "data": res,
212 "pagination": pager.asdict(total_records, len(res))
213 }