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

1''' 

2Operations for managing issues - invitations to and responses from vendors 

3''' 

4from datetime import datetime 

5from typing import List 

6 

7from sqlalchemy.orm.session import Session 

8 

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 

17 

18 

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 ''' 

27 

28 if not user.organisation.is_consultant: 

29 raise AuthorizationFailure('Only Consultant users can view Vendor details') 

30 

31 # Will raise No Result Found if not in Suppliers list 

32 vendor = user.organisation.suppliers.filter(Organisation.id == q_org_id).one() 

33 

34 issues = [i._asdict() for i in 

35 fetch.issues_for_respondent(session, user, q_org_id)] 

36 

37 data = { 

38 'issues': issues, 

39 'organisation': vendor.as_dict(), 

40 'users': [u.as_dict() for u in vendor.users] 

41 } 

42 

43 return data 

44 

45 

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) 

52 

53 

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] 

61 

62 

63@http 

64def put_project_issue(session, user, project_id, issue_id, issue_doc): 

65 ''' 

66 Update the properties of an existing Issue 

67 

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) 

74 

75 change_list = [] 

76 

77 for field, value in issue_doc.items(): 

78 change_list.append((field, getattr(issue, field), value)) 

79 setattr(issue, field, value) 

80 

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) 

85 

86 

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) 

92 

93 

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 

102 

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) 

117 

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) 

128 

129 return dict(id=issue.id) 

130 

131 

132@http 

133def delete_project_issue(session: Session, user, project_id, issue_id): 

134 ''' 

135 Delete the Issue with the given id 

136 

137 Only permitted at status: 

138 - Not Sent 

139 - Declined 

140 - Retracted 

141 

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) 

155 

156 

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']) 

163 

164 

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. 

176 

177 ''' 

178 if pager is None: 

179 pager = Pager(page=1, pagesize=100) 

180 

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) 

186 

187 sort_cols = { 

188 'deadline': Issue.deadline, 

189 'submitted_date': Issue.submitted_date, 

190 'issue_date': Issue.issue_date 

191 } 

192 

193 ordering = sort_cols[issue_sort] 

194 ordering = ordering.desc() if sort_order == 'desc' else ordering.asc() 

195 

196 iq = session.query(*cols)\ 

197 .join(Project, Participant)\ 

198 .filter(Participant.org_id == user.org_id) 

199 

200 if q_org_id: 

201 iq = iq.filter(Issue.respondent_id == q_org_id) 

202 

203 if issue_status: 

204 iq = iq.filter(Issue.status == issue_status) 

205 

206 total_records = iq.count() 

207 

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 }