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
« 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
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
19from postrfp.model.meta import Base
20from postrfp.model import Organisation
22if TYPE_CHECKING:
23 from postrfp.model.project import Project
26class NoteKindEnum(str, Enum):
27 ISSUER_NOTE = "IssuerNote"
28 RESPONDENT_NOTE = "RespondentNote"
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"
39class ProjectNote(Base):
40 __tablename__ = "project_notes"
42 __table_args__ = (
43 Index(
44 "ft_note_text",
45 "note_text",
46 mariadb_prefix="FULLTEXT",
47 postgresql_using="gin",
48 ),
49 )
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 )
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 )
92 organisation: Mapped[Optional[Organisation]] = relationship(
93 "Organisation",
94 primaryjoin=foreign(org_id) == remote(Organisation.id),
95 uselist=False,
96 )
98 target_organisation: Mapped[Optional[Organisation]] = relationship(
99 "Organisation",
100 primaryjoin=foreign(target_org_id) == remote(Organisation.id),
101 uselist=False,
102 )
104 project: Mapped["Project"] = relationship(
105 "Project",
106 back_populates="notes_query",
107 )
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
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
131 raise ValueError("ProjectNote misconfigured %s", self)
133 @property
134 def pretty_distribution(self) -> str:
135 return " ".join(a.title() for a in self.distribution.value.split("_"))