Coverage for postrfp/buyer/api/endpoints/notes.py: 100%

61 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-22 21:34 +0000

1""" 

2Manage Project Notes - messages between participants and respondents in a project. 

3""" 

4 

5from sqlalchemy.orm import Session 

6 

7from postrfp.model import ProjectNote, AuditEvent 

8from postrfp.model.helpers import audited_patch 

9from postrfp.model.humans import User 

10from postrfp.shared import fetch 

11from postrfp.shared.decorators import http 

12from postrfp.shared import serial 

13from postrfp.buyer.api import authorise 

14from postrfp.authorisation import perms 

15 

16 

17@http 

18def get_project_notes( 

19 session: Session, user: User, project_id: int 

20) -> serial.ReadNotes: 

21 """ 

22 Get a list of ProjectNotes for the given project_id. 

23 

24 @raise AuthorisationFailue - if the user is not a standard user from the buying organisation 

25 @raise NoResultFound - if the specified project cannot be loaded 

26 """ 

27 project = fetch.project(session, project_id) 

28 authorise.check(user, perms.PROJECT_ACCESS, project=project) 

29 return serial.ReadNotes( 

30 [ 

31 serial.ReadNote.model_validate(pn) 

32 for pn in fetch.participant_notes_query(project) 

33 ] 

34 ) 

35 

36 

37def issue_if_target_set(project, note): 

38 if not note.target_org_id: 

39 return None 

40 

41 issue = project._issues.filter_by(respondent_id=note.target_org_id).first() 

42 if issue is None: 

43 m = f"Org '{note.target_org_id}'' is not a respondent in project {project.id}" 

44 raise ValueError(m) 

45 return issue 

46 

47 

48@http 

49def post_project_note( 

50 session: Session, user: User, project_id: int, note_doc: serial.ProjectNote 

51) -> serial.Id: 

52 """ 

53 Add a Note to the given Project 

54 

55 The visibility of Notes to different organisation is given by the table below - the last three 

56 columns indicate the visiblity of the message to each group. 

57 

58 |private|target_org_id defined|all respondents? |target respondent?|participants? | 

59 |-------|---------------------|-----------------|------------------|----------------| 

60 |false | true | no | yes | yes | 

61 |false | false | yes | not applicable | yes | 

62 |true | false | yes | not applicable | yes | 

63 |true | true | no | no | yes | 

64 

65 

66 Field 'target_org_id' is the ID of the Organisation to whom a Note is addressed. The 

67 Organisation must be a respondent for the current Project 

68 (i.e assigned as Respondent for an Issue). 

69 

70 Setting 'target_org_id' has no effect if the message is private (in which case only users in 

71 Particpant organisations can view the message) 

72 

73 @permissions PROJECT_ADD_NOTE 

74 """ 

75 project = fetch.project(session, project_id) 

76 authorise.check(user, perms.PROJECT_ADD_NOTE, project=project) 

77 note = ProjectNote( 

78 kind="IssuerNote", 

79 project_id=project_id, 

80 note_text=note_doc.note_text, 

81 private=note_doc.private, 

82 user_id=user.id, 

83 org_id=user.org_id, 

84 ) 

85 issue = None 

86 target_id = note_doc.target_org_id 

87 if target_id is not None and target_id.strip() != "": 

88 note.target_org_id = target_id 

89 issue = issue_if_target_set(project, note) 

90 

91 session.add(note) 

92 session.flush() 

93 evt = AuditEvent.create( 

94 session, 

95 "PROJECT_NOTE_ADDED", 

96 project=project, 

97 object_id=note.id, 

98 user_id=user.id, 

99 org_id=user.org_id, 

100 issue=issue, 

101 ) 

102 evt.add_change("note_text", "", note.note_text) 

103 evt.add_change("private", "", note.private) 

104 evt.add_change("target_org_id", "", note.target_org_id) 

105 session.add(evt) 

106 

107 return serial.Id(id=note.id) 

108 

109 

110@http 

111def put_project_note( 

112 session: Session, 

113 user: User, 

114 project_id: int, 

115 note_id: int, 

116 note_doc: serial.ProjectNote, 

117): 

118 """ 

119 Update a Project Note. See Post Project Note for details of privacy and visibility 

120 

121 @permissions PROJECT_ADD_NOTE 

122 """ 

123 project = fetch.project(session, project_id) 

124 authorise.check(user, perms.PROJECT_ADD_NOTE, project=project) 

125 note = fetch.note(project, note_id) 

126 issue = issue_if_target_set(project, note) 

127 

128 if issue is not None: 

129 evt = AuditEvent.create( 

130 session, 

131 "PROJECT_NOTE_UPDATED", 

132 project=project, 

133 object_id=note.id, 

134 user_id=user.id, 

135 org_id=user.org_id, 

136 issue=issue, 

137 ) 

138 else: 

139 evt = AuditEvent.create( 

140 session, 

141 "PROJECT_NOTE_UPDATED", 

142 project=project, 

143 object_id=note.id, 

144 user_id=user.id, 

145 org_id=user.org_id, 

146 ) 

147 

148 audited_patch(note, note_doc, evt, ["note_text", "private", "target_org_id"]) 

149 

150 session.add(evt) 

151 

152 

153@http 

154def delete_project_note(session: Session, user: User, project_id: int, note_id: int): 

155 """ 

156 Delete a Project Note 

157 

158 @permissions PROJECT_ADD_NOTE 

159 """ 

160 project = fetch.project(session, project_id) 

161 authorise.check(user, perms.PROJECT_ADD_NOTE, project=project) 

162 note = fetch.note(project, note_id) 

163 

164 issue = issue_if_target_set(project, note) 

165 evt = AuditEvent.create( 

166 session, 

167 "PROJECT_NOTE_DELETED", 

168 project=project, 

169 object_id=note.id, 

170 user_id=user.id, 

171 org_id=user.org_id, 

172 issue=issue, 

173 ) 

174 session.add(evt) 

175 session.flush() 

176 session.delete(note)