Coverage for postrfp/model/questionnaire/score_views.py: 84%

67 statements  

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

1from decimal import Decimal 

2from typing import Optional 

3 

4from sqlalchemy import Integer, String 

5from sqlalchemy.types import DECIMAL 

6from sqlalchemy.orm import Mapped, mapped_column 

7 

8from postrfp.model.meta import Base 

9 

10 

11class QuestionScoreComponent(Base): 

12 """ 

13 SQLAlchemy model for the question_score_components view. 

14 Provides all scoring components for questions. 

15 """ 

16 

17 __tablename__ = "question_score_components" 

18 __table_args__ = {"info": {"is_view": True}} 

19 

20 # Explicitly disable the default 'id' column 

21 __mapper_args__ = {"primary_key": ["question_id", "issue_id"]} 

22 

23 id = None # type: ignore 

24 

25 # Composite primary key since this is a view 

26 question_id: Mapped[int] = mapped_column(Integer, primary_key=True) 

27 issue_id: Mapped[int] = mapped_column(Integer, primary_key=True) 

28 

29 section_id: Mapped[int] = mapped_column(Integer, nullable=False) 

30 scoreset_id: Mapped[Optional[str]] = mapped_column(String(50)) 

31 raw_score: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(10, 4)) 

32 direct_weight: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(15, 4)) 

33 absolute_weight: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(15, 4)) 

34 weighting_set_id: Mapped[Optional[int]] = mapped_column(Integer) 

35 

36 def get_calculated_score(self, scoring_model: str) -> Decimal: 

37 """Calculate score based on scoring model with consistent precision.""" 

38 if not self.raw_score: 

39 return Decimal("0.0000") 

40 

41 # Ensure all values are Decimal with 4 decimal places 

42 raw_score = Decimal(str(self.raw_score)).quantize(Decimal("0.0001")) 

43 

44 if scoring_model == "Unweighted": 

45 return raw_score 

46 elif scoring_model == "Direct": 

47 if self.direct_weight is not None: 

48 weight = Decimal(str(self.direct_weight)).quantize(Decimal("0.0001")) 

49 return (raw_score * weight).quantize(Decimal("0.0001")) 

50 else: 

51 return raw_score 

52 elif scoring_model == "Absolute": 

53 if self.absolute_weight is not None: 

54 weight = Decimal(str(self.absolute_weight)).quantize(Decimal("0.0001")) 

55 return (raw_score * weight).quantize(Decimal("0.0001")) 

56 else: 

57 return raw_score 

58 else: 

59 return raw_score 

60 

61 

62class SectionScoreComponent(Base): 

63 """ 

64 SQLAlchemy model for the section_score_components view. 

65 Provides aggregated scoring components for sections. 

66 """ 

67 

68 __tablename__ = "section_score_components" 

69 __table_args__ = {"info": {"is_view": True}} 

70 

71 # Explicitly disable the default 'id' column 

72 __mapper_args__ = {"primary_key": ["section_id", "issue_id"]} 

73 

74 id = None # type: ignore 

75 

76 # Composite primary key 

77 section_id: Mapped[int] = mapped_column(Integer, primary_key=True) 

78 issue_id: Mapped[int] = mapped_column(Integer, primary_key=True) 

79 

80 parent_id: Mapped[Optional[int]] = mapped_column(Integer) 

81 scoreset_id: Mapped[Optional[str]] = mapped_column(String(50)) 

82 weighting_set_id: Mapped[Optional[int]] = mapped_column(Integer) 

83 question_count: Mapped[int] = mapped_column(Integer, default=0) 

84 questions_scored: Mapped[int] = mapped_column(Integer, default=0) 

85 raw_total: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(15, 4)) 

86 direct_weighted_total: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(15, 4)) 

87 absolute_weighted_total: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(15, 4)) 

88 

89 def get_calculated_score(self, scoring_model: str) -> Decimal: 

90 """Calculate total score based on scoring model with consistent precision.""" 

91 if scoring_model == "Unweighted": 

92 if self.raw_total is not None: 

93 return Decimal(str(self.raw_total)).quantize(Decimal("0.0001")) 

94 return Decimal("0.0000") 

95 elif scoring_model == "Direct": 

96 if self.direct_weighted_total is not None: 

97 return Decimal(str(self.direct_weighted_total)).quantize( 

98 Decimal("0.0001") 

99 ) 

100 return Decimal("0.0000") 

101 elif scoring_model == "Absolute": 

102 if self.absolute_weighted_total is not None: 

103 return Decimal(str(self.absolute_weighted_total)).quantize( 

104 Decimal("0.0001") 

105 ) 

106 return Decimal("0.0000") 

107 else: 

108 if self.raw_total is not None: 

109 return Decimal(str(self.raw_total)).quantize(Decimal("0.0001")) 

110 return Decimal("0.0000")