Coverage for postrfp/model/notes.py: 100%

49 statements  

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

1from enum import Enum 

2from datetime import datetime 

3from typing import Optional, TYPE_CHECKING 

4 

5from sqlalchemy import ( 

6 text, 

7 Integer, 

8 ForeignKey, 

9 DateTime, 

10 Boolean, 

11 Index, 

12 Enum as SQLEnum, 

13 func, 

14) 

15from sqlalchemy.orm import relationship, foreign, remote, Mapped, mapped_column 

16from sqlalchemy.types import VARCHAR, TEXT 

17 

18 

19from postrfp.model.meta import Base 

20from postrfp.model import Organisation 

21 

22if TYPE_CHECKING: 

23 from postrfp.model.project import Project 

24 

25 

26class NoteKindEnum(str, Enum): 

27 ISSUER_NOTE = "IssuerNote" 

28 RESPONDENT_NOTE = "RespondentNote" 

29 

30 

31class Distribution(str, Enum): 

32 BROADCAST_NOTICE = "BROADCAST_NOTICE" 

33 RESPONDENT_QUERY = "RESPONDENT_QUERY" 

34 RESPONDENT_INTERNAL_MEMO = "RESPONDENT_INTERNAL_MEMO" 

35 ISSUER_INTERNAL_MEMO = "ISSUER_INTERNAL_MEMO" 

36 TARGETED = "TARGETED" 

37 

38 

39class ProjectNote(Base): 

40 __tablename__ = "project_notes" 

41 

42 __table_args__ = ( 

43 Index( 

44 "ft_note_text", 

45 "note_text", 

46 mariadb_prefix="FULLTEXT", 

47 postgresql_using="gin", 

48 ), 

49 ) 

50 

51 public_attrs = ( 

52 "id,note_time,user_id,org_id,target_org_id,note_text,private,kind" 

53 ).split(",") 

54 project_id: Mapped[int] = mapped_column( 

55 Integer, 

56 ForeignKey( 

57 "projects.id", ondelete="CASCADE" 

58 ), # Removed name="project_notes_ibfk_3" 

59 nullable=False, 

60 ) 

61 

62 note_time: Mapped[datetime] = mapped_column( 

63 DateTime, 

64 nullable=False, 

65 server_default=func.utc_timestamp(), 

66 ) 

67 user_id: Mapped[str] = mapped_column( 

68 VARCHAR(length=150), nullable=False, server_default=text("''") 

69 ) 

70 org_id: Mapped[Optional[str]] = mapped_column( 

71 VARCHAR(length=150), 

72 ForeignKey("organisations.id", ondelete="SET NULL", onupdate="CASCADE"), 

73 nullable=True, 

74 server_default=text("''"), 

75 ) 

76 target_org_id: Mapped[Optional[str]] = mapped_column( 

77 VARCHAR(length=150), 

78 ForeignKey("organisations.id", ondelete="SET NULL", onupdate="CASCADE"), 

79 nullable=True, 

80 ) 

81 note_text: Mapped[str] = mapped_column(TEXT(), nullable=False) 

82 private: Mapped[bool] = mapped_column( 

83 Boolean, nullable=False, server_default=text("'0'") 

84 ) 

85 kind: Mapped[NoteKindEnum] = mapped_column( 

86 "type", 

87 SQLEnum(NoteKindEnum, name="note_kind_enum"), 

88 nullable=False, 

89 server_default=NoteKindEnum.ISSUER_NOTE.name, 

90 ) 

91 

92 organisation: Mapped[Optional[Organisation]] = relationship( 

93 "Organisation", 

94 primaryjoin=foreign(org_id) == remote(Organisation.id), 

95 uselist=False, 

96 ) 

97 

98 target_organisation: Mapped[Optional[Organisation]] = relationship( 

99 "Organisation", 

100 primaryjoin=foreign(target_org_id) == remote(Organisation.id), 

101 uselist=False, 

102 ) 

103 

104 project: Mapped["Project"] = relationship( 

105 "Project", 

106 back_populates="notes_query", 

107 ) 

108 

109 def __repr__(self): 

110 tm = "<Note[%s] kind: %s, private: %s, target_org_id: %s>" 

111 ms = tm % (self.id, self.kind, self.private, self.target_org_id) 

112 return ms 

113 

114 @property 

115 def distribution(self) -> Distribution: 

116 if self.private: 

117 if self.kind == NoteKindEnum.ISSUER_NOTE: 

118 return Distribution.ISSUER_INTERNAL_MEMO 

119 elif self.kind == NoteKindEnum.RESPONDENT_NOTE: 

120 return Distribution.RESPONDENT_INTERNAL_MEMO 

121 elif self.kind == NoteKindEnum.ISSUER_NOTE: 

122 if self.target_org_id is None: 

123 return Distribution.BROADCAST_NOTICE 

124 else: 

125 return Distribution.TARGETED 

126 elif self.kind == NoteKindEnum.RESPONDENT_NOTE: 

127 return ( 

128 Distribution.RESPONDENT_QUERY 

129 ) # Note isn't private: can be viewed by buyer 

130 

131 raise ValueError("ProjectNote misconfigured %s", self) 

132 

133 @property 

134 def pretty_distribution(self) -> str: 

135 return " ".join(a.title() for a in self.distribution.value.split("_"))