Skip to content

Commit 243c8bb

Browse files
Adding part of scenario 11.
Added new classes relating to the person repository Adding new utils to be able to fetch a person matching a set of criteria updating investigation dataset utils and apps to reflect new fields
1 parent 3d9ebfa commit 243c8bb

File tree

12 files changed

+1805
-97
lines changed

12 files changed

+1805
-97
lines changed
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
from datetime import datetime, timedelta, date
2+
from typing import Optional
3+
import logging
4+
from classes.date.date_description import DateDescription
5+
6+
7+
class DateDescriptionUtils:
8+
"""
9+
Utility class for interpreting and converting date descriptions to Python date objects or formatted strings.
10+
"""
11+
12+
DATE_FORMAT_YYYY_MM_DD = "%Y-%m-%d"
13+
DATE_FORMAT_DD_MM_YYYY = "%d/%m/%Y"
14+
15+
@staticmethod
16+
def interpret_date(date_field_name: str, date_value: str) -> str:
17+
"""
18+
Interprets a date description and returns a formatted date string (dd/MM/yyyy).
19+
If the date cannot be interpreted, returns the original value.
20+
"""
21+
logging.debug(f"interpret_date: {date_field_name}, {date_value}")
22+
try:
23+
return DateDescriptionUtils.convert_description_to_string_date(
24+
date_field_name, date_value, DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY
25+
)
26+
except Exception as e:
27+
logging.error(f"Could not interpret date: {e}")
28+
return date_value
29+
30+
@staticmethod
31+
def convert_description_to_sql_date(
32+
which_date: str, date_description: str
33+
) -> Optional[str]:
34+
"""
35+
Converts a date description to an Oracle TO_DATE SQL string.
36+
Returns None if the date cannot be interpreted.
37+
"""
38+
logging.debug(
39+
f"convert_description_to_sql_date: {which_date}, {date_description}"
40+
)
41+
return_date_string = None
42+
return_date = None
43+
44+
date_description_words = date_description.split(" ")
45+
46+
# First handle actual dates
47+
if DateDescriptionUtils.is_valid_date(
48+
date_description, DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY
49+
):
50+
return_date_string = DateDescriptionUtils.oracle_to_date_function(
51+
date_description, "dd/mm/yyyy"
52+
)
53+
elif DateDescriptionUtils.is_valid_date(
54+
date_description, DateDescriptionUtils.DATE_FORMAT_YYYY_MM_DD
55+
):
56+
return_date_string = DateDescriptionUtils.oracle_to_date_function(
57+
date_description, "yyyy-mm-dd"
58+
)
59+
elif date_description.endswith(" ago") and len(date_description_words) == 3:
60+
return_date = DateDescriptionUtils.convert_description_to_local_date(
61+
which_date, date_description
62+
)
63+
else:
64+
# If the date description is in the enum, use the suggested suitable date, plus allow for NULL and NOT NULL
65+
enum_val = DateDescription.by_description_case_insensitive(date_description)
66+
if enum_val is not None:
67+
if enum_val.name == "NULL":
68+
return_date_string = "NULL"
69+
elif enum_val.name == "NOT_NULL":
70+
return_date_string = "NOT NULL"
71+
else:
72+
return_date = enum_val.suitable_date
73+
74+
if return_date is not None and return_date_string is None:
75+
return_date_string = DateDescriptionUtils.oracle_to_date_function(
76+
return_date.strftime(DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY),
77+
"dd/mm/yyyy",
78+
)
79+
80+
return return_date_string
81+
82+
@staticmethod
83+
def convert_description_to_local_date(
84+
which_date: str, date_description: str
85+
) -> date:
86+
"""
87+
Converts a date description to a Python date object.
88+
Raises ValueError if the description cannot be interpreted.
89+
"""
90+
logging.debug(
91+
f"convert_description_to_local_date: {which_date}, {date_description}"
92+
)
93+
date_description_words = date_description.split(" ")
94+
95+
# Handle actual dates
96+
if DateDescriptionUtils.is_valid_date(
97+
date_description, DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY
98+
):
99+
return datetime.strptime(
100+
date_description, DateDescriptionUtils.DATE_FORMAT_DD_MM_YYYY
101+
).date()
102+
if DateDescriptionUtils.is_valid_date(
103+
date_description, DateDescriptionUtils.DATE_FORMAT_YYYY_MM_DD
104+
):
105+
return datetime.strptime(
106+
date_description, DateDescriptionUtils.DATE_FORMAT_YYYY_MM_DD
107+
).date()
108+
109+
# Handle relative dates ("ago" or "ahead")
110+
if DateDescriptionUtils._is_relative_date(
111+
date_description, date_description_words, "ago"
112+
):
113+
return DateDescriptionUtils._calculate_relative_date(
114+
date_description_words, is_ago=True
115+
)
116+
if DateDescriptionUtils._is_relative_date(
117+
date_description, date_description_words, "ahead"
118+
):
119+
return DateDescriptionUtils._calculate_relative_date(
120+
date_description_words, is_ago=False
121+
)
122+
123+
# Handle enum-based descriptions
124+
enum_val = DateDescription.by_description_case_insensitive(date_description)
125+
if enum_val is not None:
126+
if enum_val.name in ["NULL", "NOT_NULL"]:
127+
raise ValueError(f"Cannot convert '{date_description}' to a date.")
128+
if enum_val.suitable_date is not None:
129+
return enum_val.suitable_date
130+
131+
raise ValueError(f"Cannot interpret date description '{date_description}'.")
132+
133+
@staticmethod
134+
def _is_relative_date(date_description, words, suffix):
135+
return date_description.endswith(f" {suffix}") and len(words) == 3
136+
137+
@staticmethod
138+
def _calculate_relative_date(words, is_ago):
139+
if not words[0].isdigit():
140+
raise ValueError(f"Invalid period number in '{' '.join(words)}'")
141+
number_of_periods = int(words[0])
142+
period_type = words[1]
143+
today = date.today()
144+
delta_days = DateDescriptionUtils._get_delta_days(
145+
number_of_periods, period_type
146+
)
147+
if is_ago:
148+
return today - timedelta(days=delta_days)
149+
else:
150+
return today + timedelta(days=delta_days)
151+
152+
@staticmethod
153+
def _get_delta_days(number_of_periods, period_type):
154+
if period_type in ["year", "years"]:
155+
return number_of_periods * 365
156+
if period_type in ["month", "months"]:
157+
return number_of_periods * 30
158+
if period_type in ["week", "weeks"]:
159+
return number_of_periods * 7
160+
if period_type in ["day", "days"]:
161+
return number_of_periods
162+
raise ValueError(f"Unknown period type '{period_type}'")
163+
164+
@staticmethod
165+
def convert_description_to_string_date(
166+
which_date: str, date_description: str, date_format: str
167+
) -> str:
168+
"""
169+
Converts a date description to a formatted date string.
170+
Raises ValueError if the description cannot be interpreted.
171+
"""
172+
logging.debug(
173+
f"convert_description_to_string_date: {which_date}, {date_description}, {date_format}"
174+
)
175+
local_date = DateDescriptionUtils.convert_description_to_local_date(
176+
which_date, date_description
177+
)
178+
return local_date.strftime(date_format)
179+
180+
@staticmethod
181+
def is_valid_date(date_str: str, date_format: str) -> bool:
182+
"""
183+
Checks if the given string is a valid date in the specified format.
184+
"""
185+
try:
186+
datetime.strptime(date_str, date_format)
187+
return True
188+
except ValueError:
189+
return False
190+
191+
@staticmethod
192+
def oracle_to_date_function(date_str: str, oracle_format: str) -> str:
193+
"""
194+
Constructs an Oracle TO_DATE function to convert a string to a date.
195+
This method formats the date string according to the specified format.
196+
Args:
197+
date_str (str): The date string to be converted.
198+
oracle_format (str): The format in which the date string is provided.
199+
Returns:
200+
str: The SQL expression for the Oracle TO_DATE function.
201+
"""
202+
return f" TO_DATE( '{date_str}', '{oracle_format}') "

classes/person/person.py

Lines changed: 34 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,49 @@
1-
import logging
1+
from dataclasses import dataclass, field
22
from typing import Optional
3-
from classes.subject.gender_type import GenderType
43

54

5+
@dataclass
66
class Person:
77
"""
8-
Represents a person with name, title, gender, and other attributes.
8+
Represents a person with identifying and role-related attributes.
9+
10+
Attributes:
11+
person_id (Optional[int]): Unique identifier for the person (PRS ID).
12+
surname (Optional[str]): The surname of the person.
13+
forenames (Optional[str]): The forenames of the person.
14+
registration_code (Optional[str]): The registration code of the person.
15+
role_id (Optional[int]): Role identifier (PIO ID). Used to hold one
16+
selected role for the person.
917
"""
1018

11-
def __init__(self) -> None:
12-
self.surname: str = ""
13-
self.forename: str = ""
14-
self.title: str = ""
15-
self.other_forenames: str = ""
16-
self.previous_surname: str = ""
17-
self.gender: Optional[GenderType] = GenderType.NOT_KNOWN
19+
person_id: Optional[int] = field(default=None)
20+
surname: Optional[str] = field(default=None)
21+
forenames: Optional[str] = field(default=None)
22+
registration_code: Optional[str] = field(default=None)
23+
role_id: Optional[int] = field(default=None)
1824

19-
def get_full_name(self) -> str:
25+
@property
26+
def full_name(self) -> str:
2027
"""
21-
Returns the full name of the person, including title, forename, and surname.
22-
"""
23-
return f"{self.title} {self.forename} {self.surname}"
24-
25-
def __str__(self) -> str:
26-
"""
27-
Returns a string representation of the person, including gender.
28-
"""
29-
return f"{self.title} {self.forename} {self.surname} (gender={self.gender})"
30-
31-
def get_surname(self) -> str:
32-
"""
33-
Returns the surname of the person.
34-
"""
35-
return self.surname
36-
37-
def set_surname(self, surname: str) -> None:
38-
"""
39-
Sets the surname of the person.
40-
"""
41-
logging.debug("set surname")
42-
self.surname = surname
43-
44-
def get_forename(self) -> str:
45-
"""
46-
Returns the forename of the person.
47-
"""
48-
return self.forename
49-
50-
def set_forename(self, forename: str) -> None:
51-
"""
52-
Sets the forename of the person.
53-
"""
54-
logging.debug("set forename")
55-
self.forename = forename
56-
57-
def get_title(self) -> str:
58-
"""
59-
Returns the title of the person.
60-
"""
61-
return self.title
62-
63-
def set_title(self, title: str) -> None:
64-
"""
65-
Sets the title of the person.
66-
"""
67-
logging.debug("set title")
68-
self.title = title
69-
70-
def get_other_forenames(self) -> str:
71-
"""
72-
Returns other forenames of the person.
73-
"""
74-
return self.other_forenames
75-
76-
def set_other_forenames(self, other_forenames: str) -> None:
77-
"""
78-
Sets other forenames of the person.
79-
"""
80-
logging.debug("set other_forenames")
81-
self.other_forenames = other_forenames
28+
Get the full name of the person.
8229
83-
def get_previous_surname(self) -> str:
84-
"""
85-
Returns the previous surname of the person.
30+
Returns:
31+
str: The concatenation of forenames and surname, or an empty string if missing.
8632
"""
87-
return self.previous_surname
33+
parts = [self.forenames or "", self.surname or ""]
34+
return " ".join(p for p in parts if p).strip()
8835

89-
def set_previous_surname(self, previous_surname: str) -> None:
90-
"""
91-
Sets the previous surname of the person.
92-
"""
93-
logging.debug("set previous_surname")
94-
self.previous_surname = previous_surname
95-
96-
def get_gender(self) -> Optional[GenderType]:
97-
"""
98-
Returns the gender of the person.
36+
def __str__(self) -> str:
9937
"""
100-
return self.gender
38+
Get a string representation of the person object.
10139
102-
def set_gender(self, gender: GenderType) -> None:
103-
"""
104-
Sets the gender of the person.
40+
Returns:
41+
str: String with field values, similar to the Java `toString` implementation.
10542
"""
106-
logging.debug("set gender")
107-
self.gender = gender
43+
return (
44+
f"User [personId={self.person_id}, "
45+
f"surname={self.surname}, "
46+
f"forenames={self.forenames}, "
47+
f"registrationCode={self.registration_code}, "
48+
f"roleId={self.role_id}]"
49+
)

0 commit comments

Comments
 (0)