Skip to content

Commit 03e4509

Browse files
authored
Merge pull request #120 from DataIntegrationGroup/jab-contact-updates
NO TICKET: further contact updates
2 parents 99e71ec + 8ba1f46 commit 03e4509

7 files changed

Lines changed: 164 additions & 76 deletions

File tree

api/contact.py

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
from services.contact_helper import (
4747
add_contact,
4848
)
49+
from services.lexicon_helper import get_terms_by_category
4950
from services.query_helper import (
5051
simple_get_by_id,
5152
paginated_all_getter,
@@ -107,6 +108,18 @@ def database_error_handler(
107108
"type": "value_error",
108109
"input": {"contact_id": payload.contact_id},
109110
}
111+
elif (
112+
error_message
113+
== 'insert or update on table "contact" violates foreign key constraint "contact_contact_type_fkey"'
114+
):
115+
valid_terms = get_terms_by_category("contact_type")
116+
valid_contact_types_for_msg = " | ".join(valid_terms)
117+
detail = {
118+
"loc": ["body", "contact_type"],
119+
"msg": f"Invalid contact_type. Valid terms are: {valid_contact_types_for_msg}",
120+
"type": "value_error",
121+
"input": {"contact_type": payload.contact_type},
122+
}
110123

111124
raise PydanticStyleException(status_code=status.HTTP_409_CONFLICT, detail=[detail])
112125

@@ -309,40 +322,54 @@ async def update_contact(
309322
"""
310323
contact = simple_get_by_id(session, Contact, contact_id)
311324

325+
"""
326+
if new name is set to None, new organization is unset, and existing organization is already None raise an error
327+
if new organization is set to None, new name is unset, and existing name is already None raise an error
328+
329+
both new name and new organization cannot be set to None - this is a schema restriction
330+
"""
331+
# exclude unsets so only intentional Nones are evaluated
332+
payload_excluding_unsets = contact_data.model_dump(exclude_unset=True)
333+
312334
if (
313-
contact.name is None
314-
and contact_data.name is None
315-
and contact_data.organization is None
335+
contact.organization is None
336+
and payload_excluding_unsets.get("name", "unset") is None
337+
and payload_excluding_unsets.get("organization", "unset") == "unset"
316338
):
317339
raise PydanticStyleException(
318340
status_code=status.HTTP_409_CONFLICT,
319341
detail=[
320342
{
321-
"loc": ["body", "organization"],
322-
"msg": "organization cannot be None if name is None.",
343+
"loc": ["body", "name"],
344+
"msg": "name cannot be set to None because organization is already None.",
323345
"type": "value_error",
324-
"input": {"organization": contact_data.organization},
346+
"input": {"name": payload_excluding_unsets.get("name")},
325347
}
326348
],
327349
)
328350
elif (
329-
contact.organization is None
330-
and contact_data.organization is None
331-
and contact_data.name is None
351+
contact.name is None
352+
and payload_excluding_unsets.get("organization", "unset") is None
353+
and payload_excluding_unsets.get("name", "unset") == "unset"
332354
):
333355
raise PydanticStyleException(
334356
status_code=status.HTTP_409_CONFLICT,
335357
detail=[
336358
{
337-
"loc": ["body", "name"],
338-
"msg": "name cannot be None if organization is None.",
359+
"loc": ["body", "organization"],
360+
"msg": "organization cannot be set to None because name is already None.",
339361
"type": "value_error",
340-
"input": {"name": contact_data.name},
362+
"input": {
363+
"organization": payload_excluding_unsets.get("organization")
364+
},
341365
}
342366
],
343367
)
344368

345-
return model_patcher(session, Contact, contact_id, contact_data, user=user)
369+
try:
370+
return model_patcher(session, Contact, contact_id, contact_data, user=user)
371+
except ProgrammingError as e:
372+
database_error_handler(contact_data, e)
346373

347374

348375
# ====== GET ===================================================================

core/lexicon.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,6 @@
105105
{"categories": [{"name": "county", "description": null}], "term": "Valencia", "definition": "Valencia"},
106106

107107
{"categories": [{"name": "role", "description": null}], "term": "Owner", "definition": "Owner"},
108-
{"categories": [{"name": "role", "description": null}], "term": "Primary", "definition": "Primary"},
109-
{"categories": [{"name": "role", "description": null}], "term": "Secondary", "definition": "Secondary"},
110108
{"categories": [{"name": "role", "description": null}], "term": "Manager", "definition": "Manager"},
111109
{"categories": [{"name": "role", "description": null}], "term": "Operator", "definition": "Operator"},
112110
{"categories": [{"name": "role", "description": null}], "term": "Driller", "definition": "Driller"},
@@ -119,7 +117,9 @@
119117

120118
{"categories": [{"name": "email_type", "description": null},
121119
{"name": "phone_type", "description": null},
122-
{"name": "address_type", "description": null}], "term": "Primary", "definition": "primary"},
120+
{"name": "address_type", "description": null},
121+
{"name": "contact_type", "description": null}], "term": "Primary", "definition": "primary"},
122+
{"categories": [{"name": "contact_type", "description": null}], "term": "Secondary", "definition": "secondary"},
123123

124124
{"categories": [{"name": "email_type", "description": null},
125125
{"name": "phone_type", "description": null},

db/contact.py

Lines changed: 57 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,47 +13,57 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
# ===============================================================================
16-
from sqlalchemy import Column, Integer, ForeignKey, String
16+
from sqlalchemy import Integer, ForeignKey, String
1717
from sqlalchemy.ext.associationproxy import association_proxy
18-
from sqlalchemy.orm import relationship
18+
from sqlalchemy.orm import relationship, Mapped, mapped_column
1919
from sqlalchemy_utils import TSVectorType
20+
from typing import List
2021

2122
from db.base import Base, AutoBaseMixin, ReleaseMixin, lexicon_term
2223

2324

2425
class ThingContactAssociation(Base, AutoBaseMixin):
25-
thing_id = Column(
26+
thing_id: Mapped[int] = mapped_column(
2627
Integer, ForeignKey("thing.id", ondelete="CASCADE"), nullable=False
2728
)
28-
contact_id = Column(
29+
contact_id: Mapped[int] = mapped_column(
2930
Integer, ForeignKey("contact.id", ondelete="CASCADE"), nullable=False
3031
)
3132

32-
contact = relationship("Contact")
33-
thing = relationship("Thing")
33+
contact: Mapped[List["Contact"]] = relationship("Contact")
34+
thing: Mapped[List["Thing"]] = relationship("Thing") # noqa: F821
3435

3536

3637
class Contact(Base, AutoBaseMixin, ReleaseMixin):
37-
name = Column(String(100), nullable=True)
38-
role = lexicon_term(nullable=False)
39-
organization = Column(String(100), nullable=True)
40-
nma_pk_owners = Column(String(100), nullable=True)
41-
42-
phones = relationship("Phone", back_populates="contact", passive_deletes=True)
43-
emails = relationship("Email", back_populates="contact", passive_deletes=True)
44-
addresses = relationship("Address", back_populates="contact", passive_deletes=True)
38+
name: Mapped[str | None] = mapped_column(String(100))
39+
organization: Mapped[str | None] = mapped_column(String(100))
40+
role: Mapped[str] = lexicon_term()
41+
contact_type: Mapped[str] = lexicon_term()
42+
nma_pk_owners: Mapped[str | None] = mapped_column(String(100))
43+
44+
phones: Mapped[List["Phone"]] = relationship(
45+
"Phone", back_populates="contact", passive_deletes=True
46+
)
47+
emails: Mapped[List["Email"]] = relationship(
48+
"Email", back_populates="contact", passive_deletes=True
49+
)
50+
addresses: Mapped[List["Address"]] = relationship(
51+
"Address", back_populates="contact", passive_deletes=True
52+
)
4553

46-
search_vector = Column(
54+
search_vector: Mapped[TSVectorType] = mapped_column(
4755
TSVectorType("name", "role", "organization", "nma_pk_owners")
4856
)
4957

50-
author_associations = relationship(
51-
"AuthorContactAssociation",
52-
back_populates="contact",
53-
cascade="all, delete-orphan",
58+
author_associations: Mapped[List["AuthorContactAssociation"]] = ( # noqa: F821
59+
relationship(
60+
"AuthorContactAssociation",
61+
back_populates="contact",
62+
cascade="all, delete-orphan",
63+
)
5464
)
5565
authors = association_proxy("author_associations", "author")
56-
thing_associations = relationship(
66+
thing_associations: Mapped[List["ThingContactAssociation"]] = relationship(
5767
"ThingContactAssociation",
5868
back_populates="contact",
5969
cascade="all, delete-orphan",
@@ -63,42 +73,48 @@ class Contact(Base, AutoBaseMixin, ReleaseMixin):
6373

6474

6575
class Phone(Base, AutoBaseMixin, ReleaseMixin):
66-
contact_id = Column(
76+
contact_id: Mapped[int] = mapped_column(
6777
Integer, ForeignKey("contact.id", ondelete="CASCADE"), nullable=False
6878
)
69-
phone_number = Column(String(20), nullable=False)
70-
phone_type = lexicon_term(nullable=False)
79+
phone_number: Mapped[str] = mapped_column(String(20), nullable=False)
80+
phone_type: Mapped[str] = lexicon_term(nullable=False)
7181

72-
contact = relationship("Contact", back_populates="phones", passive_deletes=True)
73-
search_vector = Column(TSVectorType("phone_number"))
82+
contact: Mapped["Contact"] = relationship(
83+
"Contact", back_populates="phones", passive_deletes=True
84+
)
85+
search_vector: Mapped[TSVectorType] = mapped_column(TSVectorType("phone_number"))
7486

7587

7688
class Email(Base, AutoBaseMixin, ReleaseMixin):
77-
contact_id = Column(
89+
contact_id: Mapped[int] = mapped_column(
7890
Integer, ForeignKey("contact.id", ondelete="CASCADE"), nullable=False
7991
)
80-
email = Column(String(100), nullable=False)
81-
email_type = lexicon_term(nullable=False)
92+
email: Mapped[str] = mapped_column(String(100), nullable=False)
93+
email_type: Mapped[str] = lexicon_term(nullable=False)
8294

83-
contact = relationship("Contact", back_populates="emails", passive_deletes=True)
95+
contact: Mapped["Contact"] = relationship(
96+
"Contact", back_populates="emails", passive_deletes=True
97+
)
8498

85-
search_vector = Column(TSVectorType("email"))
99+
search_vector: Mapped[TSVectorType] = mapped_column(TSVectorType("email"))
86100

87101

88102
class Address(Base, AutoBaseMixin, ReleaseMixin):
89-
contact_id = Column(
103+
contact_id: Mapped[int] = mapped_column(
90104
Integer, ForeignKey("contact.id", ondelete="CASCADE"), nullable=False
91105
)
92-
address_line_1 = Column(String(255), nullable=False)
93-
address_line_2 = Column(String(255), nullable=True)
94-
city = Column(String(100), nullable=False)
95-
state = Column(String(50), nullable=False)
96-
postal_code = Column(String(20), nullable=False)
97-
country = lexicon_term(nullable=False, default="United States")
98-
address_type = lexicon_term(nullable=False)
99-
100-
contact = relationship("Contact", back_populates="addresses", passive_deletes=True)
101-
search_vector = Column(
106+
address_line_1: Mapped[str] = mapped_column(String(255), nullable=False)
107+
address_line_2: Mapped[str | None] = mapped_column(String(255), nullable=True)
108+
city: Mapped[str] = mapped_column(String(100), nullable=False)
109+
state: Mapped[str] = mapped_column(String(50), nullable=False)
110+
postal_code: Mapped[str] = mapped_column(String(20), nullable=False)
111+
country: Mapped[str] = lexicon_term(nullable=False, default="United States")
112+
address_type: Mapped[str] = lexicon_term(nullable=False)
113+
114+
contact: Mapped["Contact"] = relationship(
115+
"Contact", back_populates="addresses", passive_deletes=True
116+
)
117+
search_vector: Mapped[TSVectorType] = mapped_column(
102118
TSVectorType(
103119
"address_line_1",
104120
"address_line_2",

schemas/contact.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@
2727
# -------- VALIDATORS ----------
2828

2929

30+
class ValidateContact(BaseModel):
31+
name: str | None = None
32+
organization: str | None = None
33+
34+
@model_validator(mode="before")
35+
def check_empty(data: dict) -> dict:
36+
if (
37+
data.get("name", "unset") is None
38+
and data.get("organization", "unset") is None
39+
):
40+
raise ValueError("Either name or organization must be provided.")
41+
return data
42+
43+
3044
class ValidateEmail(BaseModel):
3145

3246
email: str | None = None
@@ -111,15 +125,16 @@ class CreateAddress(BaseCreateModel):
111125
# thing_id: int
112126

113127

114-
class CreateContact(BaseCreateModel):
128+
class CreateContact(BaseCreateModel, ValidateContact):
115129
"""
116130
Schema for creating a contact.
117131
"""
118132

119133
thing_id: int
120134
name: str | None = None
121-
role: str
122135
organization: str | None = None
136+
role: str
137+
contact_type: str = "Primary"
123138
# description: str | None = None
124139
# email: str | None = None
125140
# phone: str | None = None
@@ -128,12 +143,6 @@ class CreateContact(BaseCreateModel):
128143
phones: list[CreatePhone] | None = None
129144
addresses: list[CreateAddress] | None = None
130145

131-
@model_validator(mode="before")
132-
def check_empty(data: dict) -> dict:
133-
if data.get("name", None) is None and data.get("organization", None) is None:
134-
raise ValueError("Either name or organization must be provided.")
135-
return data
136-
137146

138147
# -------- RESPONSE ----------
139148

@@ -179,8 +188,10 @@ class ContactResponse(BaseResponseModel):
179188
Response schema for contact details.
180189
"""
181190

182-
name: str
191+
name: str | None
192+
organization: str | None
183193
role: str
194+
contact_type: str
184195
emails: List[EmailResponse] = []
185196
phones: List[PhoneResponse] = []
186197
addresses: List[AddressResponse] = []
@@ -198,13 +209,14 @@ class ContactResponse(BaseResponseModel):
198209

199210

200211
# -------- UPDATE ----------
201-
class UpdateContact(BaseUpdateModel):
212+
class UpdateContact(BaseUpdateModel, ValidateContact):
202213
"""
203214
Schema for updating contact information.
204215
"""
205216

206217
name: str | None = None
207218
role: str | None = None
219+
contact_type: str | None = None
208220
thing_id: int | None = None
209221
organization: str | None = None
210222
# email: str | None = None

tests/conftest.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ def contact(water_well_thing):
262262
release_status="private",
263263
name="Test Contact",
264264
role="Owner",
265+
contact_type="Primary",
265266
organization="Test Organization",
266267
)
267268
session.add(contact)
@@ -335,6 +336,7 @@ def second_contact():
335336
release_status="private",
336337
name="Test Second Contact",
337338
role="Owner",
339+
contact_type="Primary",
338340
organization=None,
339341
)
340342
session.add(contact)
@@ -410,6 +412,7 @@ def third_contact():
410412
release_status="private",
411413
name=None,
412414
role="Owner",
415+
contact_type="Primary",
413416
organization="Third Organization",
414417
)
415418
session.add(contact)

0 commit comments

Comments
 (0)