Coverage for postrfp/buyer/api/endpoints/weighting.py: 97%
79 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"""
2Assign weightings to Sections and Questions. Manage named weighting sets.
3"""
5from typing import cast
6from decimal import Decimal
8from sqlalchemy.orm.session import Session
9from sqlalchemy.orm.exc import NoResultFound
11from postrfp.shared import fetch, update
12from postrfp.shared.serial.models import WeightSet
13from postrfp.model.humans import User
14from postrfp.shared.decorators import http
15from postrfp.model.questionnaire.weightings import WeightingSet
16from postrfp.buyer.api import authorise
17from postrfp.authorisation import perms
18from postrfp.shared import serial
21@http
22def get_project_weightings(
23 session: Session, user: User, project_id: int, weightset_id: int | None = None
24) -> serial.ProjectWeightings:
25 """
26 Get total and instance(local) weightings for questions and sections in the current project.
28 Total weightings are the normalised, hierarchical weights reflecting the weight of the parent
29 section and all ancestor sections.
31 Instance weighings are the weightings that the user assigns to a question or section.
32 """
34 project = fetch.project(session, project_id)
35 authorise.check(user, perms.PROJECT_VIEW_WEIGHTING, project=project)
37 # Resolve None weightset_id to the actual default weighting set ID
38 if weightset_id is None:
39 weightset_id = project.get_or_create_default_weighting_set_id()
40 name = "Default"
41 else:
42 name = (
43 project.weighting_sets.filter(WeightingSet.id == weightset_id)
44 .with_entities(WeightingSet.name)
45 .scalar()
46 )
48 tw = fetch.total_weightings_dict(project, weightset_id)
49 inst = fetch.weightings_dict(project, weightset_id)
51 # Use cast to ensure the types match the expected serial models
52 # this only affects type checking, not runtime behavior
53 return serial.ProjectWeightings(
54 weightset=serial.WeightSet(name=name, id=weightset_id),
55 total=serial.Weightings(**cast(dict, tw)),
56 instance=serial.Weightings(**cast(dict, inst)),
57 )
60@http
61def post_project_weightings(
62 session: Session, user: User, project_id: int, weights_doc: serial.WeightingsDoc
63):
64 """
65 Batch saving/updating of Weightings
67 To save default weightings set the value of weightset_id to null.
68 """
69 project = fetch.project(session, project_id)
70 authorise.check(user, perms.PROJECT_EDIT_WEIGHTING, project=project)
71 weightset_id = weights_doc.weightset_id
72 if weightset_id:
73 try:
74 weightset = project.weighting_sets.filter(
75 WeightingSet.id == weightset_id
76 ).one()
77 except NoResultFound:
78 raise ValueError(
79 f"Weighting Set with ID {weightset_id} does not exist in project {project_id}."
80 )
81 update.save_weightset_weightings(session, weightset, weights_doc)
82 else:
83 update.save_default_weightings(session, project, weights_doc)
84 session.flush()
85 project.delete_total_weights(weighting_set_id=weightset_id)
86 # Use the new CTE-based calculation by default
87 project.save_total_weights(weighting_set_id=weightset_id)
90@http
91def get_project_weightsets(
92 session: Session, user: User, project_id: int
93) -> list[serial.WeightSet]:
94 """
95 Get an array of Weighting Set objects for the given Project
96 """
97 project = fetch.project(session, project_id)
98 authorise.check(user, perms.PROJECT_VIEW_WEIGHTING, project=project)
99 return [ws.as_dict() for ws in project.weighting_sets]
102@http
103def post_project_weightset(
104 session: Session, user: User, project_id: int, weightset_doc: serial.NewWeightSet
105) -> serial.WeightSet:
106 """
107 Create a new Weighting Set for the given Project
109 Weightings will be created for each question and section in the project. The value of
110 these weightings will be set to the initial_value field in the json body, if given,
111 or will be copied from the weightset whose ID is given by source_weightset_id.
112 """
113 project = fetch.project(session, project_id)
114 authorise.check(user, perms.PROJECT_EDIT_WEIGHTING, project=project)
115 ws = WeightingSet(project=project, name=weightset_doc.name)
116 session.add(ws)
117 session.flush()
118 initial_value = weightset_doc.initial_value
119 if initial_value is not None:
120 update.set_initial_weightings(ws, Decimal(initial_value))
121 else:
122 source_ws_id = weightset_doc.source_weightset_id
123 source_ws = session.get_one(WeightingSet, source_ws_id)
124 update.copy_weightings(source_ws, ws)
126 project.save_total_weights(weighting_set_id=ws.id)
127 wn = ws.name or ""
128 return WeightSet(name=wn, id=ws.id)
131@http
132def put_project_weightset(
133 session: Session,
134 user: User,
135 project_id: int,
136 weightset_path_id: int,
137 name_doc: serial.ShortName,
138):
139 """
140 Change the name of an existing Weighting Set
141 """
142 project = fetch.project(session, project_id)
143 authorise.check(user, perms.PROJECT_EDIT_WEIGHTING, project=project)
144 ws = project.weighting_sets.filter(WeightingSet.id == weightset_path_id).one()
145 ws.name = name_doc.name
148@http
149def delete_project_weightset(
150 session: Session, user: User, project_id: int, weightset_path_id: int
151):
152 """
153 Delete the given weighting set from the Project
154 """
155 project = fetch.project(session, project_id)
156 authorise.check(user, perms.PROJECT_EDIT_WEIGHTING, project=project)
157 ws = project.weighting_sets.filter(WeightingSet.id == weightset_path_id).one()
158 session.delete(ws)
161@http
162def get_project_section_weightings(
163 session: Session, user: User, project_id: int, section_id: int, weightset_id: int
164) -> serial.ParentedWeighting:
165 """
166 Get weightings for the Sections or Questions in the given section, together with the
167 absolute weighting of parent section (allowing absolute weights to be calculated for
168 subsections or questions)
169 """
170 section = fetch.section_by_id(session, section_id)
171 project = section.project
173 authorise.check(
174 user, perms.PROJECT_VIEW_WEIGHTING, project=project, section_id=section.id
175 )
177 wdict = fetch.weightings_dict(
178 project, weightset_id=weightset_id, parent_section_id=section.id
179 )
181 # Need to create a new dict to add parent_absolute_weight
182 result_dict = {
183 "questions": wdict["questions"],
184 "sections": wdict["sections"],
185 "parent_absolute_weight": fetch.sec_total_weighting(section, weightset_id),
186 }
188 return serial.ParentedWeighting(**cast(dict, result_dict))