Skip to content

Commit

Permalink
Age filter: use relative date expressions instead of absolute
Browse files Browse the repository at this point in the history
dates, interpret age as cut-off date rather than interval, allow
alternatives for set separation, support saved filters, allow extra
filter options (typically less than one year), remove redundant
0-option; use all of this in MRCMS
  • Loading branch information
nursix committed Jul 3, 2024
1 parent db9359e commit fcad33d
Show file tree
Hide file tree
Showing 9 changed files with 338 additions and 273 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
nursix-dev-5945-ga4a1f16d7 (2024-07-02 15:37:59)
nursix-dev-5946-gdb9359e15 (2024-07-03 22:28:37)
3 changes: 2 additions & 1 deletion languages/de.py
Original file line number Diff line number Diff line change
Expand Up @@ -4163,7 +4163,6 @@
'None (no such record)': 'Nichts (kein entsprechender Datensatz)',
'None of the above': 'Keine(r) der oben genannten',
'None': '-',
'Resource not found or not valid': 'Ressource nicht vorhanden oder ungültig',
'Noodles': 'Nudeln',
'Norfolk Island': 'Norfolkinsel',
'Normal Address': 'Normale Adresse',
Expand Down Expand Up @@ -5188,6 +5187,7 @@
'Resource Type': 'Ressourcentyp',
'Resource added': 'Ressource hinzugefügt',
'Resource deleted': 'Ressource gelöscht',
'Resource not found or not valid': 'Ressource nicht vorhanden oder ungültig',
'Resource updated': 'Ressource aktualisiert',
'Resource': 'Ressource',
'Resources': 'Ressourcen',
Expand Down Expand Up @@ -6927,6 +6927,7 @@
'unknown': 'unbekannt',
'unspecified': 'unspezifiziert',
'unverified': 'ungeprüft',
'up to': 'bis',
'updated': 'aktualisiert',
'updates only': 'nur Aktualisierungen',
'user account': 'Benutzerkonto',
Expand Down
132 changes: 96 additions & 36 deletions modules/core/filters/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,12 @@

import datetime

from dateutil.relativedelta import relativedelta

from gluon import current, DIV, INPUT, LABEL, OPTION, SELECT, TAG
from gluon.storage import Storage

from s3dal import Field

from ..tools import IS_UTC_DATE, S3DateTime, s3_decode_iso_datetime
from ..tools import IS_UTC_DATE, S3DateTime, s3_decode_iso_datetime, s3_relative_datetime
from ..ui import S3CalendarWidget
from ..resource import S3ResourceField

Expand Down Expand Up @@ -483,10 +481,34 @@ class AgeFilter(RangeFilter):

css_base = "age-filter"

operator = ["le", "gt"]
operator = ["le", "ge"]

# Untranslated labels for individual input boxes.
input_labels = {"le": "", "gt": "To"}
input_labels = {"le": "", "lt": "", "gt": "up to", "ge": "up to"}

# -------------------------------------------------------------------------
def __init__(self, field=None, **attr):
"""
Keyword Args:
exact - in which set to include exact age matches
* "from age to >age" (="from")
* "from <age to age" (="to")
* both (the default)
Note:
- with exact="from" or "to", filtering for "from age to age"
would result in an empty set
- with exact="both", filtering for "from age to age" will
return those records with exactly that age (to the day)
"""

super().__init__(field=field, **attr)

mode = self.opts.get("exact_age")
if mode == "from":
self.operator = ["lt", "ge"]
elif mode == "to":
self.operator = ["le", "gt"]

# -------------------------------------------------------------------------
def widget(self, resource, values):
Expand All @@ -508,54 +530,40 @@ def widget(self, resource, values):

input_class = "%s-%s" % (css_base, "input")
input_labels = self.input_labels
input_elements = DIV()
ie_append = input_elements.append

_id = attr["_id"]
_variable = self._variable
selector = self.selector

opts = self.opts
minimum = opts.get("minimum", 0)
maximum = opts.get("maximum", 120)

# Generate the input elements
input_elements = DIV()
ie_append = input_elements.append
for operator in self.operator:

input_id = "%s-%s" % (_id, operator)

variable = _variable(selector, operator)

# Populate with the value, if given
# if user has not set any of the limits, we get [] in values.
# The currently selected value
value = values.get(variable, None)
selected = None
if value not in [None, []]:
if type(value) is list:
value = value[0]
try:
then = s3_decode_iso_datetime(value).date()
except ValueError:
# Value may be an age => use as-is
selected = value
else:
# Value is an ISO date of birth => convert to age in years
now = S3DateTime.to_local(current.request.utcnow).date()
selected = relativedelta(now, then).years
if operator == "gt":
selected = max(0, selected - 1)

# Selectable options
input_opts = [OPTION("%s" % i, value=i) if selected != i else
OPTION("%s" % i, value=i, _selected="selected")
for i in range(minimum, maximum + 1)
]
input_opts.insert(0, OPTION("", value=""))
options = self.options(value)
input_opts = [OPTION("", _value="")]
selected_value = None
for l, v, _, selected in options:
if selected:
selected_value = v
option = OPTION(l, _value=v, _selected="selected" if selected else None)
input_opts.append(option)

# Input Element
input_box = SELECT(input_opts,
_id = input_id,
_class = input_class,
_value = selected,
_value = selected_value,
)

label = input_labels[operator]
Expand All @@ -573,10 +581,62 @@ def widget(self, resource, values):
_class = "range-filter-field",
))

ie_append(DIV(LABEL(T("Years")),
_class = "age-filter-unit",
))

return input_elements

# -------------------------------------------------------------------------
def options(self, selected):
"""
Returns the options for an age selector
Args:
operator: the operator for the selector
selected: the currently selected cutoff-date (datetime.date)
Returns:
A sorted list of options [(label, value, cutoff-date, is-selected)]
"""

opts = self.opts
minimum = max(1, opts.get("minimum", 0))
maximum = opts.get("maximum", 120)

T = current.T

cutoff = self.cutoff_date

options = []
append = options.append
for i in range(minimum, maximum + 1):

label = "1 %s" % T("year") if i == 1 else "%s %s" % (i, T("years"))
value = "-%sY" % i
append((label, value, cutoff(value), value == selected))

# Add other options
# - options format: (label, relative-date-expression)
extra = self.opts.get("extra_options")
if extra:
for label, value in extra:
append((label, value, cutoff(value), value == selected))

options = sorted(options, key=lambda i: i[2], reverse=True)

return options

# -------------------------------------------------------------------------
@staticmethod
def cutoff_date(value):
"""
Calculates the cutoff-date for a relative-date string
Args:
value: a relative-date string
Returns:
the cutoff-date (datetime.date), or None for invalid values
"""

dt = s3_relative_datetime(value)
return S3DateTime.to_local(dt).date() if dt else None

# END =========================================================================
4 changes: 2 additions & 2 deletions modules/core/tools/calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
ISOFORMAT = "%Y-%m-%dT%H:%M:%S" #: ISO 8601 Combined Date+Time format
OFFSET = re.compile(r"([+|-]{0,1})(\d{1,2}):(\d\d)")
RELATIVE = re.compile(r"([+-]{0,1})([0-9]*)([YMDhms])")
SECONDS = {"D": 86400, "h": 3600, "m": 60, "s": 1}
SECONDS = {"W": 604800, "D": 86400, "h": 3600, "m": 60, "s": 1}

# =============================================================================
class S3DateTime:
Expand Down Expand Up @@ -262,7 +262,7 @@ def get_utc_offset():
offset = request.post_vars.get("_utc_offset", None)
if offset:
offset = int(offset)
utcstr = offset < 0 and "+" or "-"
utcstr = "+" if offset < 0 else "-"
hours = abs(int(offset/60))
minutes = abs(int(offset % 60))
offset = "%s%02d%02d" % (utcstr, hours, minutes)
Expand Down
4 changes: 2 additions & 2 deletions modules/core/tools/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
from gluon import IS_TIME
from gluon.languages import lazyT

from .calendar import ISOFORMAT, s3_decode_iso_datetime, s3_relative_datetime
from .calendar import ISOFORMAT, s3_decode_iso_datetime, s3_relative_datetime, S3DateTime

# =============================================================================
class S3TypeConverter:
Expand Down Expand Up @@ -205,7 +205,7 @@ def _date(cls, b):
# Relative datime expression?
dt = s3_relative_datetime(b)
if dt:
value = dt.date()
value = S3DateTime.to_local(dt).date()
if value is None:
from .validators import IS_UTC_DATE
# Try ISO format first (e.g. DateFilter)
Expand Down
2 changes: 2 additions & 0 deletions modules/templates/MRCMS/customise/pr.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,6 +644,8 @@ def configure_case_filters(resource, organisation_id=None, privileged=False):
),
AgeFilter("date_of_birth",
label = T("Age"),
extra_options = [("6 %s" % T("months"), "-6M")],
#include = "upper",
hidden = True,
),
OptionsFilter("person_details.nationality",
Expand Down
Loading

0 comments on commit fcad33d

Please sign in to comment.