-
Notifications
You must be signed in to change notification settings - Fork 19
/
fields.py
193 lines (167 loc) · 7.04 KB
/
fields.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
import pickle
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from django.db.models import signals
from django.db.models.query_utils import DeferredAttribute
from edtf import EDTFObject, parse_edtf
from edtf.convert import struct_time_to_date, struct_time_to_jd
from edtf.natlang import text_to_edtf
DATE_ATTRS = (
"lower_strict",
"upper_strict",
"lower_fuzzy",
"upper_fuzzy",
)
class EDTFFieldDescriptor(DeferredAttribute):
"""
Descriptor for the EDTFField's attribute on the model instance.
This updates the dependent fields each time this value is set.
"""
def __set__(self, instance, value):
# First set the value we are given
instance.__dict__[self.field.attname] = value
# `update_values` may provide us with a new value to set
edtf = self.field.update_values(instance, value)
if edtf != value:
instance.__dict__[self.field.attname] = edtf
class EDTFField(models.CharField):
def __init__(
self,
verbose_name=None,
name=None,
natural_text_field=None,
direct_input_field=None,
lower_strict_field=None,
upper_strict_field=None,
lower_fuzzy_field=None,
upper_fuzzy_field=None,
**kwargs,
):
kwargs["max_length"] = 2000
(
self.natural_text_field,
self.direct_input_field,
self.lower_strict_field,
self.upper_strict_field,
self.lower_fuzzy_field,
self.upper_fuzzy_field,
) = (
natural_text_field,
direct_input_field,
lower_strict_field,
upper_strict_field,
lower_fuzzy_field,
upper_fuzzy_field,
)
super().__init__(verbose_name, name, **kwargs)
description = (
"A field for storing complex/fuzzy date specifications in EDTF format."
)
descriptor_class = EDTFFieldDescriptor
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.natural_text_field:
kwargs["natural_text_field"] = self.natural_text_field
for attr in DATE_ATTRS:
field = f"{attr}_field"
f = getattr(self, field, None)
if f:
kwargs[field] = f
del kwargs["max_length"]
return name, path, args, kwargs
def from_db_value(self, value, expression, connection):
# Converting values from the database to Python objects
if value is None:
return value
try:
# Try to unpickle if the value was pickled
return pickle.loads(value) # noqa S301
except (pickle.PickleError, TypeError):
# If it fails because it's not pickled data, try parsing as EDTF
return parse_edtf(value, fail_silently=True)
def to_python(self, value):
if isinstance(value, EDTFObject):
return value
if value is None:
return value
return parse_edtf(value, fail_silently=True)
def get_db_prep_save(self, value, connection):
if value:
return pickle.dumps(value)
return super().get_db_prep_save(value, connection)
def get_prep_value(self, value):
# convert python objects to query values
value = super().get_prep_value(value)
if isinstance(value, EDTFObject):
return pickle.dumps(value)
return value
def update_values(self, instance, *args, **kwargs):
"""
Updates the EDTF value from either the natural_text_field, which is parsed
with text_to_edtf() and is used for display, or falling back to the direct_input_field,
which allows directly providing an EDTF string. If one of these provides a valid EDTF object,
then set the date values accordingly.
"""
# Get existing value to determine if update is needed
existing_value = getattr(instance, self.attname, None)
direct_input = getattr(instance, self.direct_input_field, "")
natural_text = getattr(instance, self.natural_text_field, "")
# if direct_input is provided and is different from the existing value, update the EDTF field
if direct_input and (
existing_value is None or str(existing_value) != direct_input
):
edtf = parse_edtf(
direct_input, fail_silently=True
) # ParseException if invalid; should this be raised?
# TODO pyparsing.ParseExceptions are very noisy and dumps the whole grammar (see https://github.com/ixc/python-edtf/issues/46)
# set the natural_text (display) field to the direct_input if it is not provided
if natural_text == "":
setattr(instance, self.natural_text_field, direct_input)
elif natural_text:
edtf_string = text_to_edtf(natural_text)
if edtf_string and (
existing_value is None or str(existing_value) != edtf_string
):
edtf = parse_edtf(
edtf_string, fail_silently=True
) # potetial ParseException if invalid; should this be raised?
else:
edtf = existing_value
else:
if not existing_value:
# No inputs provided and no existing value; TODO log this?
return
# TODO: if both direct_input and natural_text are cleared, should we throw an error?
edtf = existing_value
# Process and update related date fields based on the EDTF object
for attr in DATE_ATTRS:
field_attr = f"{attr}_field"
g = getattr(self, field_attr, None)
if g:
if edtf:
try:
target_field = instance._meta.get_field(g)
except FieldDoesNotExist:
continue
value = getattr(edtf, attr)() # struct_time
if isinstance(target_field, models.FloatField):
value = struct_time_to_jd(value)
elif isinstance(target_field, models.DateField):
value = struct_time_to_date(value)
else:
raise NotImplementedError(
f"EDTFField does not support {type(target_field)} as a derived data"
" field, only FloatField or DateField"
)
setattr(instance, g, value)
else:
setattr(instance, g, None)
return edtf
def contribute_to_class(self, cls, name, **kwargs):
super().contribute_to_class(cls, name, **kwargs)
# Attach update_values so that dependent fields declared
# after their corresponding edtf field don't stay cleared by
# Model.__init__, see Django bug #11196.
# Only run post-initialization values update on non-abstract models
if not cls._meta.abstract:
signals.post_init.connect(self.update_values, sender=cls)