|
| 1 | +from django.db import models |
| 2 | +from django.core.exceptions import ValidationError |
| 3 | +from django.utils import six |
| 4 | + |
| 5 | +from collections import defaultdict |
| 6 | +import datetime |
| 7 | +import six |
| 8 | + |
| 9 | +from .helpers import parse |
| 10 | +from .forms import TimedeltaFormField |
| 11 | + |
| 12 | +# TODO: Figure out why django admin thinks fields of this type have changed every time an object is saved. |
| 13 | + |
| 14 | +# Define the different column types that different databases can use. |
| 15 | +COLUMN_TYPES = defaultdict(lambda:"char(20)") |
| 16 | +COLUMN_TYPES["django.db.backends.postgresql_psycopg2"] = "interval" |
| 17 | +COLUMN_TYPES["django.contrib.gis.db.backends.postgis"] = "interval" |
| 18 | + |
| 19 | +class TimedeltaField(six.with_metaclass(models.SubfieldBase, models.Field)): |
| 20 | + """ |
| 21 | + Store a datetime.timedelta as an INTERVAL in postgres, or a |
| 22 | + CHAR(20) in other database backends. |
| 23 | + """ |
| 24 | + _south_introspects = True |
| 25 | + |
| 26 | + description = "A datetime.timedelta object" |
| 27 | + |
| 28 | + def __init__(self, *args, **kwargs): |
| 29 | + self._min_value = kwargs.pop('min_value', None) |
| 30 | + |
| 31 | + if isinstance(self._min_value, (int, float)): |
| 32 | + self._min_value = datetime.timedelta(seconds=self._min_value) |
| 33 | + |
| 34 | + self._max_value = kwargs.pop('max_value', None) |
| 35 | + |
| 36 | + if isinstance(self._max_value, (int, float)): |
| 37 | + self._max_value = datetime.timedelta(seconds=self._max_value) |
| 38 | + |
| 39 | + super(TimedeltaField, self).__init__(*args, **kwargs) |
| 40 | + |
| 41 | + def to_python(self, value): |
| 42 | + if (value is None) or isinstance(value, datetime.timedelta): |
| 43 | + return value |
| 44 | + if isinstance(value, (int, float)): |
| 45 | + return datetime.timedelta(seconds=value) |
| 46 | + if isinstance(value, six.string_types) and value.replace('.','0').isdigit(): |
| 47 | + return datetime.timedelta(seconds=float(value)) |
| 48 | + if value == "": |
| 49 | + if self.null: |
| 50 | + return None |
| 51 | + else: |
| 52 | + return datetime.timedelta(0) |
| 53 | + return parse(value) |
| 54 | + |
| 55 | + def get_prep_value(self, value): |
| 56 | + if self.null and value == "": |
| 57 | + return None |
| 58 | + if (value is None) or isinstance(value, six.string_types): |
| 59 | + return value |
| 60 | + return str(value).replace(',', '') |
| 61 | + |
| 62 | + def get_db_prep_value(self, value, connection=None, prepared=None): |
| 63 | + return self.get_prep_value(value) |
| 64 | + |
| 65 | + def formfield(self, *args, **kwargs): |
| 66 | + defaults = {'form_class':TimedeltaFormField} |
| 67 | + defaults.update(kwargs) |
| 68 | + return super(TimedeltaField, self).formfield(*args, **defaults) |
| 69 | + |
| 70 | + def validate(self, value, model_instance): |
| 71 | + super(TimedeltaField, self).validate(value, model_instance) |
| 72 | + if self._min_value is not None: |
| 73 | + if self._min_value > value: |
| 74 | + raise ValidationError('Less than minimum allowed value') |
| 75 | + if self._max_value is not None: |
| 76 | + if self._max_value < value: |
| 77 | + raise ValidationError('More than maximum allowed value') |
| 78 | + |
| 79 | + def value_to_string(self, obj): |
| 80 | + value = self._get_val_from_obj(obj) |
| 81 | + return unicode(value) |
| 82 | + |
| 83 | + def get_default(self): |
| 84 | + """ |
| 85 | + Needed to rewrite this, as the parent class turns this value into a |
| 86 | + unicode string. That sux pretty deep. |
| 87 | + """ |
| 88 | + if self.has_default(): |
| 89 | + if callable(self.default): |
| 90 | + return self.default() |
| 91 | + return self.get_prep_value(self.default) |
| 92 | + if not self.empty_strings_allowed or (self.null): |
| 93 | + return None |
| 94 | + return "" |
| 95 | + |
| 96 | + def db_type(self, connection): |
| 97 | + return COLUMN_TYPES[connection.settings_dict['ENGINE']] |
| 98 | + |
| 99 | + def deconstruct(self): |
| 100 | + """ |
| 101 | + Break down this field into arguments that can be used to reproduce it |
| 102 | + with Django migrations. |
| 103 | +
|
| 104 | + The thing to to note here is that currently the migration file writer |
| 105 | + can't serialize timedelta objects so we convert them to a float |
| 106 | + representation (in seconds) that we can later interpret as a timedelta. |
| 107 | + """ |
| 108 | + |
| 109 | + name, path, args, kwargs = super(TimedeltaField, self).deconstruct() |
| 110 | + |
| 111 | + if isinstance(self._min_value, datetime.timedelta): |
| 112 | + kwargs['min_value'] = self._min_value.total_seconds() |
| 113 | + |
| 114 | + if isinstance(self._max_value, datetime.timedelta): |
| 115 | + kwargs['max_value'] = self._max_value.total_seconds() |
| 116 | + |
| 117 | + if isinstance(kwargs.get('default'), datetime.timedelta): |
| 118 | + kwargs['default'] = kwargs['default'].total_seconds() |
| 119 | + |
| 120 | + return name, path, args, kwargs |
0 commit comments