Skip to content
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

Refactor date/time/day/hour test expressions as 'transform expressions' #185

Closed
119 changes: 119 additions & 0 deletions modelcluster/datetime_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import datetime

from django import forms


TIMEFIELD_TRANSFORM_EXPRESSIONS = {"hour", "minute", "second"}
DATEFIELD_TRANSFORM_EXPRESSIONS = {
"year",
"iso_year",
"month",
"day",
"week",
"week_day",
"iso_week_day",
"quarter",
}
DATETIMEFIELD_TRANSFORM_EXPRESSIONS = (
{"date", "time"}
| TIMEFIELD_TRANSFORM_EXPRESSIONS
| DATEFIELD_TRANSFORM_EXPRESSIONS
)
TRANSFORM_FIELD_TYPES = {
"year": forms.IntegerField,
"iso_year": forms.IntegerField,
"month": forms.IntegerField,
"hour": forms.IntegerField,
"minute": forms.IntegerField,
"second": forms.IntegerField,
"day": forms.IntegerField,
"week": forms.IntegerField,
"week_day": forms.IntegerField,
"iso_week_day": forms.IntegerField,
"quarter": forms.IntegerField,
"date": forms.DateField,
"time": forms.TimeField,
}


def derive_from_value(value, expr):
if isinstance(value, datetime.datetime):
return derive_from_datetime(value, expr)
if isinstance(value, datetime.date):
return derive_from_date(value, expr)
if isinstance(value, datetime.time):
return derive_from_time(value, expr)
return None


def derive_from_time(value, expr):
"""
Mimics the behaviour of the ``hour``, ``minute`` and ``second`` lookup
expressions that Django querysets support for ``TimeField`` and
``DateTimeField``, by extracting the relevant value from an in-memory
``time`` or ``datetime`` value.
"""
if expr == "hour":
return value.hour
if expr == "minute":
return value.minute
if expr == "second":
return value.second
raise ValueError(
"Expression '{expression}' is not supported for {value}".format(
expression=expr, value=repr(value)
)
)


def derive_from_date(value, expr):
"""
Mimics the behaviour of the ``year``, ``iso_year`` ``month``, ``day``,
``week``, ``week_day``, ``iso_week_day`` and ``quarter`` lookup
expressions that Django querysets support for ``DateField`` and
``DateTimeField`` columns, by extracting the relevant value from an
in-memory ``date`` or ``datetime`` value.
"""
if expr == "year":
return value.year
if expr == "iso_year":
return value.isocalendar()[0]
if expr == "month":
return value.month
if expr == "day":
return value.day
if expr == "week":
return value.isocalendar()[1]
if expr == "week_day":
v = value.isoweekday()
return 1 if v == 7 else v + 1
if expr == "iso_week_day":
return value.isoweekday()
if expr == "quarter":
return (value.month - 1) // 3 + 1
raise ValueError(
"Expression '{expression}' is not supported for {value}".format(
expression=expr, value=repr(value)
)
)


def derive_from_datetime(value, expr):
"""
Mimics the behaviour of the ``date``, ``time`` and other lookup
expressions that Django querysets support for ``DateTimeField`` columns,
by extracting the relevant value from an in-memory ``datetime`` value.
"""
if expr == "date":
return value.date()
if expr == "time":
return value.time()
if expr in TIMEFIELD_TRANSFORM_EXPRESSIONS:
return derive_from_time(value, expr)
if expr in DATEFIELD_TRANSFORM_EXPRESSIONS:
return derive_from_date(value, expr)
raise ValueError(
"Expression '{expression}' is not supported for {value}".format(
expression=expr, value=repr(value)
)
)
157 changes: 0 additions & 157 deletions modelcluster/queryset.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import unicode_literals

import datetime
import re

from django.core.exceptions import FieldDoesNotExist
Expand Down Expand Up @@ -248,151 +247,6 @@ def _test(obj):
return _test


def test_date(model, attribute_name, match_value):
def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
if isinstance(val, datetime.datetime):
return val.date() == match_value
else:
return val == match_value

return _test


def test_year(model, attribute_name, match_value):
match_value = int(match_value)

def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
return val is not None and val.year == match_value

return _test


def test_month(model, attribute_name, match_value):
match_value = int(match_value)

def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
return val is not None and val.month == match_value

return _test


def test_day(model, attribute_name, match_value):
match_value = int(match_value)

def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
return val is not None and val.day == match_value

return _test


def test_week(model, attribute_name, match_value):
match_value = int(match_value)

def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
return val is not None and val.isocalendar()[1] == match_value

return _test


def test_week_day(model, attribute_name, match_value):
match_value = int(match_value)

def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
return val is not None and val.isoweekday() % 7 + 1 == match_value

return _test


def test_quarter(model, attribute_name, match_value):
match_value = int(match_value)

def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
return val is not None and int((val.month - 1) / 3) + 1 == match_value

return _test


def test_time(model, attribute_name, match_value):
def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
if isinstance(val, datetime.datetime):
return val.time() == match_value
else:
return val == match_value

return _test


def test_hour(model, attribute_name, match_value):
match_value = int(match_value)

def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
return val is not None and val.hour == match_value

return _test


def test_minute(model, attribute_name, match_value):
match_value = int(match_value)

def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
return val is not None and val.minute == match_value

return _test


def test_second(model, attribute_name, match_value):
match_value = int(match_value)

def _test(obj):
try:
val = extract_field_value(obj, attribute_name)
except NullRelationshipValueEncountered:
return False
return val is not None and val.second == match_value

return _test


def test_isnull(model, attribute_name, sense):
def _test(obj):
try:
Expand Down Expand Up @@ -448,17 +302,6 @@ def _test(obj):
'endswith': test_endswith,
'iendswith': test_iendswith,
'range': test_range,
'date': test_date,
'year': test_year,
'month': test_month,
'day': test_day,
'week': test_week,
'week_day': test_week_day,
'quarter': test_quarter,
'time': test_time,
'hour': test_hour,
'minute': test_minute,
'second': test_second,
'isnull': test_isnull,
'regex': test_regex,
'iregex': test_iregex,
Expand Down
49 changes: 45 additions & 4 deletions modelcluster/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import datetime
from functools import lru_cache
from django.core.exceptions import FieldDoesNotExist
from django.db.models import ManyToManyField, ManyToManyRel, Model
from django.db.models import (
DateField,
DateTimeField,
ManyToManyField,
ManyToManyRel,
Model,
TimeField,
)

from modelcluster import datetime_utils


REL_DELIMETER = "__"

Expand Down Expand Up @@ -66,9 +77,24 @@ def get_model_field(model, name):
subject_model=subject_model,
)
)
if getattr(field, "related_model", None):
elif getattr(field, "related_model", None):
traversals.append(TraversedRelationship(subject_model, field))
subject_model = field.related_model
elif (
(
isinstance(field, DateTimeField)
and field_name in datetime_utils.DATETIMEFIELD_TRANSFORM_EXPRESSIONS
) or (
isinstance(field, DateField)
and field_name in datetime_utils.DATEFIELD_TRANSFORM_EXPRESSIONS
) or (
isinstance(field, TimeField)
and field_name in datetime_utils.TIMEFIELD_TRANSFORM_EXPRESSIONS
)
):
transform_field_type = datetime_utils.TRANSFORM_FIELD_TYPES[field_name]
field = transform_field_type()
break
else:
raise FieldDoesNotExist(
"Failed attempting to traverse from {from_field} (a {from_field_type}) to '{to_field}'."
Expand Down Expand Up @@ -114,7 +140,23 @@ def extract_field_value(obj, key, pk_only=False, suppress_fielddoesnotexist=Fals
latest_obj = obj
segments = key.split(REL_DELIMETER)
for i, segment in enumerate(segments, start=1):
if hasattr(source, segment):
if (
(
isinstance(source, datetime.datetime)
and segment in datetime_utils.DATETIMEFIELD_TRANSFORM_EXPRESSIONS
)
or (
isinstance(source, datetime.date)
and segment in datetime_utils.DATEFIELD_TRANSFORM_EXPRESSIONS
)
or (
isinstance(source, datetime.time)
and segment in datetime_utils.TIMEFIELD_TRANSFORM_EXPRESSIONS
)
):
source = datetime_utils.derive_from_value(source, segment)
value = source
elif hasattr(source, segment):
value = getattr(source, segment)
if isinstance(value, Model):
latest_obj = value
Expand All @@ -131,7 +173,6 @@ def extract_field_value(obj, key, pk_only=False, suppress_fielddoesnotexist=Fals
)
)
source = value
continue
elif suppress_fielddoesnotexist:
return None
else:
Expand Down
2 changes: 2 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ class Log(ClusterableModel):
data = models.CharField(max_length=255)

def __str__(self):
if self.time is None:
return "[None] %s" % self.data
return "[%s] %s" % (self.time.isoformat(), self.data)


Expand Down
Loading
Loading