Coverage for postrfp/buyer/api/endpoints/network.py: 100%
101 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"""
2Define a network of relationships between organisations
3"""
5from enum import Enum
6from typing import List
8from sqlalchemy.orm import Session
9from sqlalchemy.orm.exc import NoResultFound
11from postrfp.buyer.api import authorise
12from postrfp.authorisation import perms
13from postrfp.shared import fetch, serial
14from postrfp.shared.decorators import http
15from postrfp.model import User, Edge, RelationshipType
16from postrfp.model.exc import BusinessRuleViolation
17from postrfp.model.humans import OrganisationType
20@http
21def get_network(session: Session, user: User) -> List[serial.NetworkRelationship]:
22 """
23 Get an array of all the Network Relationships described by your organisation
24 """
25 q = fetch.edges_for_org_query(session, user.org_id)
26 return [serial.NetworkRelationship.model_validate(r) for r in q]
29@http
30def delete_network(session: Session, user: User):
31 """
32 Delete all relationships defined for this network.
33 Relationship Types are not deleted.
34 """
35 authorise.check(user, perms.MANAGE_ORGANISATION, target_org=user.organisation)
36 rids = {rt.id for rt in user.organisation.relationship_types}
37 session.query(Edge).filter(Edge.relationship_id.in_(rids)).delete(
38 synchronize_session=False
39 )
42@http
43def get_reltypes(session: Session, user: User) -> List[serial.RelationshipType]:
44 """
45 Get an array of Relationship Types for your organisation
46 """
47 return [
48 serial.RelationshipType(id=rt.id, name=rt.name, description=rt.description)
49 for rt in user.organisation.relationship_types
50 ]
53@http
54def post_reltype(
55 session: Session, user: User, reltype_doc: serial.RelationshipType
56) -> serial.Id:
57 """
58 Create a new Relationship Type for your organisation
60 A RelationshipType must be defined for your organisation before a Relationship can
61 be defined between two 3rd party organisations.
62 Multiple RelationshipTypes can be defined for your organisation in order to model different
63 types of inter-organisation relationships - e.g. 'Partner', 'Downstream Supplier',
64 'Regulator', etc.
66 @permission MANAGE_ORGANISATION
67 """
68 authorise.check(user, perms.MANAGE_ORGANISATION, target_org=user.organisation)
69 rt = RelationshipType(**reltype_doc.model_dump())
70 user.organisation.relationship_types.append(rt)
71 session.flush()
72 return serial.Id(id=rt.id)
75@http
76def put_reltype(
77 session: Session, user: User, reltype_id: int, reltype_doc: serial.RelationshipType
78) -> serial.Id:
79 """
80 Update the name or description for the Relationship Type with the given ID
81 """
82 authorise.check(user, perms.MANAGE_ORGANISATION, target_org=user.organisation)
83 rt = user.organisation.relationship_types.filter(
84 RelationshipType.id == reltype_id
85 ).one()
86 rt.name = reltype_doc.name
87 rt.description = reltype_doc.description
89 return serial.Id(id=rt.id)
92@http
93def delete_reltype(session: Session, user: User, reltype_id: int) -> serial.Id:
94 """
95 Delete the Relationship Type with the given ID together with all
96 relationships of that type. Underlying Organisations are not deleted.
97 """
98 authorise.check(user, perms.MANAGE_ORGANISATION, target_org=user.organisation)
99 rt = user.organisation.relationship_types.filter(
100 RelationshipType.id == reltype_id
101 ).one()
102 session.delete(rt)
103 return serial.Id(id=rt.id)
106@http
107def post_relationship(
108 session: Session, user: User, relationship_doc: serial.Relationship
109) -> None:
110 """
111 Create a new Relationship between two organisations
112 """
113 authorise.check(user, perms.MANAGE_ORGANISATION, target_org=user.organisation)
114 reltype_id = relationship_doc.reltype_id
115 rt = user.organisation.relationship_types.filter(
116 RelationshipType.id == reltype_id
117 ).one()
118 from_org = fetch.organisation(session, relationship_doc.from_org_id)
119 to_org = fetch.organisation(session, relationship_doc.to_org_id)
120 edge = Edge(to_org=to_org, from_org=from_org, relationship_type=rt)
121 session.add(edge)
124@http
125def delete_relationship(
126 session: Session, user: User, relationship_doc: serial.Relationship
127) -> None:
128 """
129 Delete a new Relationship between two organisations
130 """
131 authorise.check(user, perms.MANAGE_ORGANISATION, target_org=user.organisation)
132 reltype_id = relationship_doc.reltype_id
133 rt = user.organisation.relationship_types.filter(
134 RelationshipType.id == reltype_id
135 ).one()
136 from_org = fetch.organisation(session, relationship_doc.from_org_id)
137 to_org = fetch.organisation(session, relationship_doc.to_org_id)
138 edge = (
139 session.query(Edge)
140 .filter_by(to_org_id=to_org.id, from_org_id=from_org.id, relationship_id=rt.id)
141 .one()
142 )
143 session.delete(edge)
146class StandardRelTypes(str, Enum):
147 CONSULTING_ENGAGEMENT = "Consults For"
148 CONSULTING_PARTNERSHIP = "Partners With"
149 SUPPLIES = "Supplies"
150 VENDOR_EVALUATION = "Evaluates"
153@http
154def post_network_project(
155 session: Session, user: User, project_id: int
156) -> List[serial.NetworkRelationship]:
157 """
158 Generate a network of relationships between vendors and participants for the project ID
159 provided. Creates default Relationship Types for standard RFP project relationships.
160 """
162 authorise.check(user, perms.MANAGE_ORGANISATION, target_org=user.organisation)
163 if user.organisation.type != OrganisationType.CONSULTANT:
164 raise BusinessRuleViolation(
165 "Action only permitted for Consultant Organisations"
166 )
167 project = fetch.project(session, project_id=project_id)
168 authorise.check(user, action=perms.PROJECT_ACCESS, project=project)
169 org = user.organisation
171 rt_lookup = dict()
172 for SRT in StandardRelTypes:
173 try:
174 rel = org.relationship_types.filter_by(name=SRT.value).one()
175 except NoResultFound:
176 rel = RelationshipType(org_id=org.id, name=SRT.value)
177 session.add(rel)
178 rt_lookup[SRT] = rel
180 # Populate Edge objects in the Session to facilitate merge() operations later
181 fetch.edges_for_org_query(session, user.org_id).all()
183 for participant in project.participants:
184 if participant.organisation is org:
185 continue
186 if participant.organisation.is_consultant:
187 rel = rt_lookup[StandardRelTypes.CONSULTING_PARTNERSHIP]
188 else:
189 rel = rt_lookup[StandardRelTypes.CONSULTING_ENGAGEMENT]
190 edge = Edge(
191 from_org_id=org.id, to_org_id=participant.org_id, relationship_id=rel.id
192 )
193 session.merge(edge)
195 respondent_id_set = {i.respondent_id for i in project.issues}
197 for respondent_id in respondent_id_set:
198 if respondent_id is None or respondent_id == org.id:
199 continue
201 buyer_id = project.org_id # Owner org assumed to be the buyer
203 rel = rt_lookup[StandardRelTypes.SUPPLIES]
204 session.merge(
205 Edge(from_org_id=respondent_id, to_org_id=buyer_id, relationship_id=rel.id)
206 )
208 evaluates_rel = rt_lookup[StandardRelTypes.VENDOR_EVALUATION]
209 session.merge(
210 Edge(
211 from_org_id=org.id,
212 to_org_id=respondent_id,
213 relationship_id=evaluates_rel.id,
214 )
215 )
217 return get_network(session, user)