-
Notifications
You must be signed in to change notification settings - Fork 13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate sample's DateOfBirth field to AgeDateOfBirthField #74
Changes from 14 commits
9a8da93
3c14565
d9d3a92
ec3ea35
4916a3b
83bcd9b
fb3002d
cd6d305
5ab48a7
3c933f2
3134074
8027573
62ced84
4fa47da
c06ae2b
11b8c1e
628aa0f
499002f
0f4ec7d
e77df12
19b9aae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,11 +19,13 @@ | |
# Some rights reserved, see README and LICENSE. | ||
|
||
import re | ||
from bika.lims import deprecated | ||
from datetime import datetime | ||
|
||
from bika.lims import api | ||
from bika.lims.utils import tmpID | ||
from dateutil.relativedelta import relativedelta | ||
from senaite.core.api import dtime | ||
from senaite.patient.config import PATIENT_CATALOG | ||
from senaite.patient.permissions import AddPatient | ||
from zope.component import getUtility | ||
|
@@ -186,6 +188,7 @@ def update_patient(patient, **values): | |
patient.reindexObject() | ||
|
||
|
||
@deprecated("Us senaite.core.api.dtime.to_dt instead") | ||
def to_datetime(date_value, default=None, tzinfo=None): | ||
if isinstance(date_value, datetime): | ||
return date_value | ||
|
@@ -202,67 +205,80 @@ def to_datetime(date_value, default=None, tzinfo=None): | |
return date_value.replace(tzinfo=tzinfo) | ||
|
||
|
||
def to_ymd(delta): | ||
def to_ymd(val, default=_marker): | ||
"""Returns a representation of a relative delta in ymd format | ||
""" | ||
if not isinstance(delta, relativedelta): | ||
raise TypeError("delta parameter must be a relative_delta") | ||
if not isinstance(val, relativedelta): | ||
if is_ymd(val): | ||
return val | ||
if default is _marker: | ||
raise TypeError("delta parameter must be a relative_delta or ymd") | ||
return default | ||
|
||
ymd = list("ymd") | ||
diff = map(str, (delta.years, delta.months, delta.days)) | ||
diff = map(str, (val.years, val.months, val.days)) | ||
age = filter(lambda it: int(it[0]), zip(diff, ymd)) | ||
return " ".join(map("".join, age)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Man, you drive me nuts with constructs like this 🙈 Just to understand: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I've seen it in the doctest now, it generates something like |
||
|
||
|
||
def is_ymd(ymd): | ||
"""Returns whether the string represents a period in ymd format | ||
""" | ||
valid = map(lambda p: p in ymd, "ymd") | ||
return any(valid) | ||
values = get_years_months_days(ymd, default=None) | ||
return values is not None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drop the def is_ymd(ymd):
try:
get_years_months_days(ymd)
except TypeError:
return False
return True |
||
|
||
|
||
def get_birth_date(age_ymd, on_date=None): | ||
"""Returns the birth date given an age in ymd format and the date when age | ||
was recorded or current datetime if None | ||
def get_years_months_days(ymd, default=_marker): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed |
||
"""Returns a tuple of (years, month, days) | ||
""" | ||
on_date = to_datetime(on_date, default=datetime.now()) | ||
|
||
def extract_period(val, period): | ||
num = re.findall(r'(\d{1,2})'+period, val) or [0] | ||
num = re.findall(r'(\d{1,2})'+period, str(val)) or [0] | ||
return api.to_int(num[0], default=0) | ||
|
||
# Extract the periods | ||
years = extract_period(age_ymd, "y") | ||
months = extract_period(age_ymd, "m") | ||
days = extract_period(age_ymd, "d") | ||
years = extract_period(ymd, "y") | ||
months = extract_period(ymd, "m") | ||
days = extract_period(ymd, "d") | ||
if not any([years, months, days]): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why can't we just return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, with latest changes it always returns |
||
raise AttributeError("No valid ymd: {}".format(age_ymd)) | ||
if default is _marker: | ||
raise AttributeError("No valid ymd: {}".format(ymd)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better use a |
||
return default | ||
|
||
return years, months, days | ||
|
||
|
||
def get_birth_date(age_ymd, on_date=None, default=_marker): | ||
"""Returns the birth date given an age in ymd format and the date when age | ||
was recorded or current datetime if None | ||
""" | ||
try: | ||
ymd = to_ymd(age_ymd) | ||
years, months, days = get_years_months_days(ymd) | ||
except AttributeError: | ||
if default is _marker: | ||
raise AttributeError("No valid ymd: {}".format(age_ymd)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Phew, catch an I would maybe completely skip the handling here and leave it to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I simplified the whole thing and now it raises |
||
return default | ||
|
||
on_date = dtime.to_dt(on_date) or datetime.now() | ||
dob = on_date - relativedelta(years=years, months=months, days=days) | ||
return dob | ||
|
||
|
||
def get_age_ymd(birth_date, on_date=None): | ||
"""Returns the age at on_date if not None. Otherwise, current age | ||
""" | ||
delta = get_relative_delta(birth_date, on_date) | ||
return to_ymd(delta) | ||
try: | ||
delta = dtime.get_relative_delta(birth_date, on_date) | ||
return to_ymd(delta) | ||
except (ValueError, TypeError): | ||
return None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You do everywhere the Maybe you can skip here the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
|
||
|
||
@deprecated("Use senaite.core.api.dtime.get_relative_delta instead") | ||
def get_relative_delta(from_date, to_date=None): | ||
"""Returns the relative delta between two dates. If to_date is None, | ||
compares the from_date with now | ||
""" | ||
from_date = to_datetime(from_date) | ||
if not from_date: | ||
raise TypeError("Type not supported: from_date") | ||
|
||
to_date = to_date or datetime.now() | ||
to_date = to_datetime(to_date) | ||
if not to_date: | ||
raise TypeError("Type not supported: to_date") | ||
|
||
return relativedelta(to_date, from_date) | ||
return dtime.get_relative_delta(from_date, to_date) | ||
|
||
|
||
def tuplify_identifiers(identifiers): | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
from AccessControl import ClassSecurityInfo | ||
from bika.lims import api | ||
from senaite.core.api import dtime | ||
from Products.Archetypes.Registry import registerWidget | ||
from senaite.core.browser.widgets import DateTimeWidget | ||
from senaite.patient import api as patient_api | ||
|
@@ -19,113 +19,78 @@ class AgeDoBWidget(DateTimeWidget): | |
"macro": "senaite_patient_widgets/agedobwidget", | ||
}) | ||
|
||
def get_age_selected(self, context): | ||
name = self.getName() | ||
attr = "_%s_age_selected" % name | ||
return getattr(context, attr, False) | ||
|
||
def set_age_selected(self, context, value): | ||
name = self.getName() | ||
attr = "_%s_age_selected" % name | ||
setattr(context, attr, bool(value)) | ||
|
||
def get_dob_estimated(self, context): | ||
name = self.getName() | ||
attr = "_%s_dob_estimated" % name | ||
return getattr(context, attr, self.get_age_selected(context)) | ||
|
||
def set_dob_estimated(self, context, value): | ||
name = self.getName() | ||
attr = "_%s_dob_estimated" % name | ||
setattr(context, attr, bool(value)) | ||
|
||
def get_current_age(self, dob): | ||
"""Returns a dict with keys "years", "months", "days" | ||
""" | ||
if not api.is_date(dob): | ||
return {} | ||
|
||
delta = patient_api.get_relative_delta(dob) | ||
return { | ||
"years": delta.years, | ||
"months": delta.months, | ||
"days": delta.days, | ||
} | ||
|
||
def is_age_supported(self, context): | ||
def is_age_supported(self): | ||
"""Returns whether the introduction of age is supported or not | ||
""" | ||
return patient_api.is_age_supported() | ||
|
||
def is_years_only(self, dob): | ||
def is_years_only(self): | ||
"""Returns whether months and days are not displayed when the age is | ||
greater than one year | ||
""" | ||
if not patient_api.is_age_in_years(): | ||
return False | ||
dob = self.get_current_age(dob) | ||
years = dob.get("years", 0) | ||
return years >= 1 | ||
return patient_api.is_age_in_years() | ||
|
||
def process_form(self, instance, field, form, empty_marker=None, | ||
emptyReturnsMarker=False, validating=True): | ||
value = form.get(field.getName()) | ||
|
||
# Not interested in the hidden field, but in the age + dob specific | ||
if isinstance(value, (list, tuple)): | ||
value = value[0] or None | ||
|
||
# Allow non-required fields | ||
if not value: | ||
return None, {} | ||
|
||
# handle DateTime object when creating partitions | ||
if api.is_date(value): | ||
self.set_age_selected(instance, False) | ||
return value, {} | ||
# We always return a dict suitable for the field | ||
output = dict.fromkeys(["dob", "from_age", "estimated"], False) | ||
|
||
# Grab the input for DoB first | ||
dob = value.get("dob", "") | ||
dob = patient_api.to_datetime(dob) | ||
age_selected = value.get("selector") == "age" | ||
if dtime.is_date(value): | ||
# handle date-like objects directly | ||
output["dob"] = dtime.to_dt(value) | ||
return output, {} | ||
|
||
# remember what was selected | ||
self.set_age_selected(instance, age_selected) | ||
elif patient_api.is_ymd(value): | ||
# handle age-like inputs directly | ||
output["dob"] = patient_api.get_birth_date(value) | ||
output["from_age"] = True | ||
return output, {} | ||
|
||
# Maybe user entered age instead of DoB | ||
if age_selected: | ||
# Validate the age entered | ||
if value.get("selector") == "age": | ||
# Age entered | ||
ymd = map(lambda p: value.get(p), ["years", "months", "days"]) | ||
if not any(ymd): | ||
# No values set | ||
return None | ||
return None, {} | ||
|
||
# Age in ymd format | ||
ymd = filter(lambda p: p[0], zip(ymd, 'ymd')) | ||
ymd = "".join(map(lambda p: "".join(p), ymd)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, once again this dance. If it is really required, I would somehow encapsulate it in a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done with e77df12 . Is now using to |
||
|
||
# Calculate the DoB | ||
dob = patient_api.get_birth_date(ymd) | ||
output["dob"] = dob | ||
output["from_age"] = True | ||
|
||
# Consider DoB as estimated? | ||
orig_dob = value.get("original") | ||
orig_dob = patient_api.to_datetime(orig_dob) | ||
orig_dob = dtime.to_dt(orig_dob) | ||
if not orig_dob: | ||
# First time age is set, assume dob is estimated | ||
self.set_dob_estimated(instance, True) | ||
output["estimated"] = True | ||
else: | ||
# Do not update estimated unless value changed. Maybe the user | ||
# set the DoB at the beginning and now is just viewing the | ||
# Age value in edit mode. We do not want the property | ||
# "estimated" to change if he/she presses the Save button | ||
# without the dob value being changed | ||
if orig_dob.strftime("%y%m%d") != dob.strftime("%y%m%d"): | ||
self.set_dob_estimated(instance, True) | ||
orig_ansi = dtime.to_ansi(orig_dob, show_time=False) | ||
dob_ansi = dtime.to_ansi(dob, show_time=False) | ||
output["estimated"] = orig_ansi != dob_ansi | ||
|
||
else: | ||
# User entered date of birth, so is not estimated | ||
self.set_dob_estimated(instance, False) | ||
dob = value.get("dob", "") | ||
output["dob"] = dtime.to_dt(dob) | ||
output["from_age"] = value.get("from_age", False) | ||
output["estimated"] = value.get("estimated", False) | ||
|
||
return dob, {} | ||
return output, {} | ||
|
||
|
||
registerWidget(AgeDoBWidget, title="AgeDoBWidget") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small typo:
Us
->Use
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
resolved with c06ae2b