Initial commit. Basic models mostly done.

This commit is contained in:
tcaxle
2020-04-11 13:03:48 +01:00
commit 840e3c86f9
5761 changed files with 650959 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
from .array import * # NOQA
from .hstore import * # NOQA
from .jsonb import * # NOQA
from .ranges import * # NOQA

View File

@@ -0,0 +1,218 @@
import copy
from itertools import chain
from django import forms
from django.contrib.postgres.validators import (
ArrayMaxLengthValidator, ArrayMinLengthValidator,
)
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from ..utils import prefix_validation_error
class SimpleArrayField(forms.CharField):
default_error_messages = {
'item_invalid': _('Item %(nth)s in the array did not validate:'),
}
def __init__(self, base_field, *, delimiter=',', max_length=None, min_length=None, **kwargs):
self.base_field = base_field
self.delimiter = delimiter
super().__init__(**kwargs)
if min_length is not None:
self.min_length = min_length
self.validators.append(ArrayMinLengthValidator(int(min_length)))
if max_length is not None:
self.max_length = max_length
self.validators.append(ArrayMaxLengthValidator(int(max_length)))
def clean(self, value):
value = super().clean(value)
return [self.base_field.clean(val) for val in value]
def prepare_value(self, value):
if isinstance(value, list):
return self.delimiter.join(str(self.base_field.prepare_value(v)) for v in value)
return value
def to_python(self, value):
if isinstance(value, list):
items = value
elif value:
items = value.split(self.delimiter)
else:
items = []
errors = []
values = []
for index, item in enumerate(items):
try:
values.append(self.base_field.to_python(item))
except ValidationError as error:
errors.append(prefix_validation_error(
error,
prefix=self.error_messages['item_invalid'],
code='item_invalid',
params={'nth': index + 1},
))
if errors:
raise ValidationError(errors)
return values
def validate(self, value):
super().validate(value)
errors = []
for index, item in enumerate(value):
try:
self.base_field.validate(item)
except ValidationError as error:
errors.append(prefix_validation_error(
error,
prefix=self.error_messages['item_invalid'],
code='item_invalid',
params={'nth': index + 1},
))
if errors:
raise ValidationError(errors)
def run_validators(self, value):
super().run_validators(value)
errors = []
for index, item in enumerate(value):
try:
self.base_field.run_validators(item)
except ValidationError as error:
errors.append(prefix_validation_error(
error,
prefix=self.error_messages['item_invalid'],
code='item_invalid',
params={'nth': index + 1},
))
if errors:
raise ValidationError(errors)
def has_changed(self, initial, data):
try:
value = self.to_python(data)
except ValidationError:
pass
else:
if initial in self.empty_values and value in self.empty_values:
return False
return super().has_changed(initial, data)
class SplitArrayWidget(forms.Widget):
template_name = 'postgres/widgets/split_array.html'
def __init__(self, widget, size, **kwargs):
self.widget = widget() if isinstance(widget, type) else widget
self.size = size
super().__init__(**kwargs)
@property
def is_hidden(self):
return self.widget.is_hidden
def value_from_datadict(self, data, files, name):
return [self.widget.value_from_datadict(data, files, '%s_%s' % (name, index))
for index in range(self.size)]
def value_omitted_from_data(self, data, files, name):
return all(
self.widget.value_omitted_from_data(data, files, '%s_%s' % (name, index))
for index in range(self.size)
)
def id_for_label(self, id_):
# See the comment for RadioSelect.id_for_label()
if id_:
id_ += '_0'
return id_
def get_context(self, name, value, attrs=None):
attrs = {} if attrs is None else attrs
context = super().get_context(name, value, attrs)
if self.is_localized:
self.widget.is_localized = self.is_localized
value = value or []
context['widget']['subwidgets'] = []
final_attrs = self.build_attrs(attrs)
id_ = final_attrs.get('id')
for i in range(max(len(value), self.size)):
try:
widget_value = value[i]
except IndexError:
widget_value = None
if id_:
final_attrs = {**final_attrs, 'id': '%s_%s' % (id_, i)}
context['widget']['subwidgets'].append(
self.widget.get_context(name + '_%s' % i, widget_value, final_attrs)['widget']
)
return context
@property
def media(self):
return self.widget.media
def __deepcopy__(self, memo):
obj = super().__deepcopy__(memo)
obj.widget = copy.deepcopy(self.widget)
return obj
@property
def needs_multipart_form(self):
return self.widget.needs_multipart_form
class SplitArrayField(forms.Field):
default_error_messages = {
'item_invalid': _('Item %(nth)s in the array did not validate:'),
}
def __init__(self, base_field, size, *, remove_trailing_nulls=False, **kwargs):
self.base_field = base_field
self.size = size
self.remove_trailing_nulls = remove_trailing_nulls
widget = SplitArrayWidget(widget=base_field.widget, size=size)
kwargs.setdefault('widget', widget)
super().__init__(**kwargs)
def to_python(self, value):
value = super().to_python(value)
return [self.base_field.to_python(item) for item in value]
def clean(self, value):
cleaned_data = []
errors = []
if not any(value) and self.required:
raise ValidationError(self.error_messages['required'])
max_size = max(self.size, len(value))
for index in range(max_size):
item = value[index]
try:
cleaned_data.append(self.base_field.clean(item))
except ValidationError as error:
errors.append(prefix_validation_error(
error,
self.error_messages['item_invalid'],
code='item_invalid',
params={'nth': index + 1},
))
cleaned_data.append(None)
else:
errors.append(None)
if self.remove_trailing_nulls:
null_index = None
for i, value in reversed(list(enumerate(cleaned_data))):
if value in self.base_field.empty_values:
null_index = i
else:
break
if null_index is not None:
cleaned_data = cleaned_data[:null_index]
errors = errors[:null_index]
errors = list(filter(None, errors))
if errors:
raise ValidationError(list(chain.from_iterable(errors)))
return cleaned_data

View File

@@ -0,0 +1,58 @@
import json
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
__all__ = ['HStoreField']
class HStoreField(forms.CharField):
"""
A field for HStore data which accepts dictionary JSON input.
"""
widget = forms.Textarea
default_error_messages = {
'invalid_json': _('Could not load JSON data.'),
'invalid_format': _('Input must be a JSON dictionary.'),
}
def prepare_value(self, value):
if isinstance(value, dict):
return json.dumps(value)
return value
def to_python(self, value):
if not value:
return {}
if not isinstance(value, dict):
try:
value = json.loads(value)
except json.JSONDecodeError:
raise ValidationError(
self.error_messages['invalid_json'],
code='invalid_json',
)
if not isinstance(value, dict):
raise ValidationError(
self.error_messages['invalid_format'],
code='invalid_format',
)
# Cast everything to strings for ease.
for key, val in value.items():
if val is not None:
val = str(val)
value[key] = val
return value
def has_changed(self, initial, data):
"""
Return True if data differs from initial.
"""
# For purposes of seeing whether something has changed, None is
# the same as an empty dict, if the data or initial value we get
# is None, replace it w/ {}.
initial_value = self.to_python(initial)
return super().has_changed(initial_value, data)

View File

@@ -0,0 +1,62 @@
import json
from django import forms
from django.utils.translation import gettext_lazy as _
__all__ = ['JSONField']
class InvalidJSONInput(str):
pass
class JSONString(str):
pass
class JSONField(forms.CharField):
default_error_messages = {
'invalid': _('%(value)s” value must be valid JSON.'),
}
widget = forms.Textarea
def to_python(self, value):
if self.disabled:
return value
if value in self.empty_values:
return None
elif isinstance(value, (list, dict, int, float, JSONString)):
return value
try:
converted = json.loads(value)
except json.JSONDecodeError:
raise forms.ValidationError(
self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
if isinstance(converted, str):
return JSONString(converted)
else:
return converted
def bound_data(self, data, initial):
if self.disabled:
return initial
try:
return json.loads(data)
except json.JSONDecodeError:
return InvalidJSONInput(data)
def prepare_value(self, value):
if isinstance(value, InvalidJSONInput):
return value
return json.dumps(value)
def has_changed(self, initial, data):
if super().has_changed(initial, data):
return True
# For purposes of seeing whether something has changed, True isn't the
# same as 1 and the order of keys doesn't matter.
data = self.to_python(data)
return json.dumps(initial, sort_keys=True) != json.dumps(data, sort_keys=True)

View File

@@ -0,0 +1,117 @@
import warnings
from psycopg2.extras import DateRange, DateTimeTZRange, NumericRange
from django import forms
from django.core import exceptions
from django.forms.widgets import HiddenInput, MultiWidget
from django.utils.deprecation import RemovedInDjango31Warning
from django.utils.translation import gettext_lazy as _
__all__ = [
'BaseRangeField', 'IntegerRangeField', 'DecimalRangeField',
'DateTimeRangeField', 'DateRangeField', 'FloatRangeField',
'HiddenRangeWidget', 'RangeWidget',
]
class RangeWidget(MultiWidget):
def __init__(self, base_widget, attrs=None):
widgets = (base_widget, base_widget)
super().__init__(widgets, attrs)
def decompress(self, value):
if value:
return (value.lower, value.upper)
return (None, None)
class HiddenRangeWidget(RangeWidget):
"""A widget that splits input into two <input type="hidden"> inputs."""
def __init__(self, attrs=None):
super().__init__(HiddenInput, attrs)
class BaseRangeField(forms.MultiValueField):
default_error_messages = {
'invalid': _('Enter two valid values.'),
'bound_ordering': _('The start of the range must not exceed the end of the range.'),
}
hidden_widget = HiddenRangeWidget
def __init__(self, **kwargs):
if 'widget' not in kwargs:
kwargs['widget'] = RangeWidget(self.base_field.widget)
if 'fields' not in kwargs:
kwargs['fields'] = [self.base_field(required=False), self.base_field(required=False)]
kwargs.setdefault('required', False)
kwargs.setdefault('require_all_fields', False)
super().__init__(**kwargs)
def prepare_value(self, value):
lower_base, upper_base = self.fields
if isinstance(value, self.range_type):
return [
lower_base.prepare_value(value.lower),
upper_base.prepare_value(value.upper),
]
if value is None:
return [
lower_base.prepare_value(None),
upper_base.prepare_value(None),
]
return value
def compress(self, values):
if not values:
return None
lower, upper = values
if lower is not None and upper is not None and lower > upper:
raise exceptions.ValidationError(
self.error_messages['bound_ordering'],
code='bound_ordering',
)
try:
range_value = self.range_type(lower, upper)
except TypeError:
raise exceptions.ValidationError(
self.error_messages['invalid'],
code='invalid',
)
else:
return range_value
class IntegerRangeField(BaseRangeField):
default_error_messages = {'invalid': _('Enter two whole numbers.')}
base_field = forms.IntegerField
range_type = NumericRange
class DecimalRangeField(BaseRangeField):
default_error_messages = {'invalid': _('Enter two numbers.')}
base_field = forms.DecimalField
range_type = NumericRange
class FloatRangeField(DecimalRangeField):
base_field = forms.FloatField
def __init__(self, **kwargs):
warnings.warn(
'FloatRangeField is deprecated in favor of DecimalRangeField.',
RemovedInDjango31Warning, stacklevel=2,
)
super().__init__(**kwargs)
class DateTimeRangeField(BaseRangeField):
default_error_messages = {'invalid': _('Enter two valid date/times.')}
base_field = forms.DateTimeField
range_type = DateTimeTZRange
class DateRangeField(BaseRangeField):
default_error_messages = {'invalid': _('Enter two valid dates.')}
base_field = forms.DateField
range_type = DateRange