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
« 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"""
5from sqlalchemy.orm import Session
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
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.
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 )
37def issue_if_target_set(project, note):
38 if not note.target_org_id:
39 return None
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
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
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.
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 |
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).
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)
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)
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)
107 return serial.Id(id=note.id)
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
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)
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 )
148 audited_patch(note, note_doc, evt, ["note_text", "private", "target_org_id"])
150 session.add(evt)
153@http
154def delete_project_note(session: Session, user: User, project_id: int, note_id: int):
155 """
156 Delete a Project Note
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)
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)