Coverage for postrfp/model/questionnaire/weightings.py: 100%
36 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
1from decimal import Decimal
2from typing import Optional, TYPE_CHECKING
4from sqlalchemy import ForeignKey, Index, Integer, Unicode, text
5from sqlalchemy.orm import DynamicMapped, Mapped, mapped_column, relationship
6import sqlalchemy.types as types
7from sqlalchemy.types import DECIMAL
9from postrfp.model.meta import Base
11if TYPE_CHECKING:
12 from postrfp.model.questionnaire.nodes import QuestionInstance, Section
13 from postrfp.model.project import Project
16class WeightingSet(Base):
17 __tablename__ = "weighting_sets"
19 public_attrs = ("id", "name")
20 name: Mapped[Optional[str]] = mapped_column(Unicode(255), default=None)
21 project_id: Mapped[int] = mapped_column(
22 Integer, ForeignKey("projects.id", ondelete="CASCADE"), nullable=False
23 )
25 project: Mapped["Project"] = relationship(
26 "Project", back_populates="weighting_sets", foreign_keys=[project_id]
27 )
29 default_for_projects: Mapped[list["Project"]] = relationship(
30 "Project",
31 back_populates="default_weighting_set",
32 foreign_keys="Project.default_weighting_set_id",
33 viewonly=True,
34 )
36 weightings: DynamicMapped["Weighting"] = relationship(
37 "Weighting",
38 back_populates="weighting_set",
39 lazy="dynamic",
40 cascade="all,delete",
41 passive_deletes=True,
42 )
44 def __repr__(self):
45 return f'Weighting Set ID: {self.id} "{self.name}"'
48WEIGHTING_COL_TYPE: types.TypeEngine = DECIMAL(precision=15, scale=4)
51class Weighting(Base):
52 __tablename__ = "weightings"
54 weighting_set_id: Mapped[int] = mapped_column(
55 ForeignKey("weighting_sets.id", ondelete="CASCADE"),
56 nullable=False,
57 )
58 question_instance_id: Mapped[Optional[int]] = mapped_column(
59 ForeignKey("question_instances.id", ondelete="CASCADE")
60 )
61 section_id: Mapped[Optional[int]] = mapped_column(
62 ForeignKey("sections.id", ondelete="CASCADE")
63 )
64 value: Mapped[Decimal] = mapped_column(
65 WEIGHTING_COL_TYPE, server_default=text("'1.0000'"), nullable=False
66 )
68 question: Mapped[Optional["QuestionInstance"]] = relationship("QuestionInstance")
69 section: Mapped[Optional["Section"]] = relationship("Section")
70 weighting_set: Mapped["WeightingSet"] = relationship(
71 WeightingSet, back_populates="weightings"
72 )
74 def __repr__(self):
75 return f"<Weighting {self.id} - {self.value}>"
78class TotalWeighting(Base):
79 """
80 The total_weightings table is essentially a cache. It saves the absolute, normalised weight
81 values for question instances and sections for a given weighting_set_id. Null weighting_set_id
82 indicates the default weighting set.
84 Absolute weights are derived hierachically - the entire questionnaire needs to be loaded
85 as a tree structure in order to figure out these weights. e.g. question weight depends
86 on the weights of parent sections. It is therefore expense to derive on the fly - hence
87 the requirement for this table.
89 The cache is recalculated when weightings are saved
90 """
92 __tablename__ = "total_weightings"
93 __table_args__ = (
94 Index(
95 "idx_total_weightings_proj_weightset_qid_secid",
96 "project_id",
97 "weighting_set_id",
98 "question_instance_id",
99 "section_id",
100 unique=True,
101 ),
102 )
104 project_id: Mapped[int] = mapped_column(
105 ForeignKey(
106 "projects.id", ondelete="CASCADE"
107 ), # Removed name="total_weightings_wset"
108 nullable=False,
109 )
110 weighting_set_id: Mapped[Optional[int]] = mapped_column(
111 Integer, nullable=True, default=None
112 )
113 question_instance_id: Mapped[int] = mapped_column(
114 Integer, nullable=False, server_default=text("'0'")
115 )
116 section_id: Mapped[int] = mapped_column(
117 Integer, nullable=False, server_default=text("'0'")
118 )
119 # Used by scoring, reporting, and other systems throughout the application
120 weight: Mapped[Optional[Decimal]] = mapped_column(WEIGHTING_COL_TYPE, nullable=True)
122 # Additional columns for weight analysis and debugging
123 normalised_weight: Mapped[Optional[Decimal]] = mapped_column(
124 WEIGHTING_COL_TYPE, nullable=True
125 )
126 absolute_weight: Mapped[Optional[Decimal]] = mapped_column(
127 WEIGHTING_COL_TYPE, nullable=True
128 )
130 project: Mapped["Project"] = relationship(
131 "Project",
132 back_populates="total_weightings",
133 )
135 def __repr__(self):
136 return f"<TotalWeighting {self.id} - {self.weight}>"