1 # Copyright (C) 2010 Oregon State University et al.
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
18 from datetime
import datetime
19 from decimal
import Decimal
24 from numbers
import Real
26 Real
= float, int, Decimal
28 from django
.core
.exceptions
import ValidationError
29 from django
.core
.validators
import (EMPTY_VALUES
, MaxValueValidator
,
31 from django
.db
import models
32 from django
.db
.models
.fields
import DecimalField
33 from django
.forms
.fields
import CharField
, RegexField
34 from django
.utils
.translation
import ugettext
as _
36 from south
.modelsinspector
import add_introspection_rules
38 from django_fields
.fields
import EncryptedCharField
41 class ModifyingFieldDescriptor(object):
42 """ Modifies a field when set using the field's (overriden) .to_python() method. """
43 def __init__(self
, field
):
46 def __get__(self
, instance
, owner
=None):
48 raise AttributeError('Can only be accessed via an instance.')
49 return instance
.__dict
__[self
.field
.name
]
51 def __set__(self
, instance
, value
):
52 instance
.__dict
__[self
.field
.name
] = self
.field
.to_python(value
)
55 class LowerCaseCharField(models
.CharField
):
57 This is a wrapper around the charfield which forces values to lowercase
58 when assigned and prior to saving to the DB
60 def to_python(self
, value
):
61 value
= super(LowerCaseCharField
, self
).to_python(value
)
62 if isinstance(value
, basestring
):
66 def contribute_to_class(self
, cls
, name
):
67 super(LowerCaseCharField
, self
).contribute_to_class(cls
, name
)
68 setattr(cls
, self
.name
, ModifyingFieldDescriptor(self
))
71 add_introspection_rules([], ["^ganeti_web\.fields\.LowerCaseCharField"])
74 class PatchedEncryptedCharField(EncryptedCharField
):
76 django_fields upstream refuses to fix a bug, so we get to do it ourselves.
78 Feel free to destroy this class and switch back to upstream if
79 https://github.com/svetlyak40wt/django-fields/pull/12 is ever merged into
80 a released version of django_fields.
83 def get_db_prep_value(self
, value
, connection
=None, prepared
=False):
87 return EncryptedCharField
.get_db_prep_value(self
, value
,
88 connection
=connection
,
92 add_introspection_rules([], ["^ganeti_web\.fields\.PatchedEncryptedCharField"])
95 class PreciseDateTimeField(DecimalField
):
97 Custom field which provides sub-second precision.
99 MySQL and other databases follow the SQL92 standard:
101 TIMESTAMP - contains the datetime field's YEAR, MONTH, DAY, HOUR,
104 However, sometimes more precision is needed, and this field provides
105 arbitrarily high-precision datetimes.
107 Internally, this field is a DECIMAL field. The value is stored as a
108 straight UNIX timestamp, with extra digits of precision representing the
109 fraction of a second.
111 This field is not timezone-safe.
113 By default, this field supports six decimal places, for microseconds. It
114 will store a total of eighteen digits for the entire timestamp. Both of
115 these values can be adjusted.
117 The keyword argument ``decimal_places`` controls how many sub-second
118 decimal places will be stored. The keyword argument ``max_digits``
119 controls the total number of digits stored.
122 __metaclass__
= models
.SubfieldBase
124 def __init__(self
, **kwargs
):
125 # Set default values.
126 if not 'decimal_places' in kwargs
:
127 kwargs
['decimal_places'] = 6
128 if not 'max_digits' in kwargs
:
129 kwargs
['max_digits'] = kwargs
['decimal_places'] + 12
131 self
.shifter
= Decimal(10)**kwargs
['decimal_places']
133 super(PreciseDateTimeField
, self
).__init
__(**kwargs
)
135 def get_prep_value(self
, value
):
137 Turn a datetime into a Decimal.
143 # Use Decimal for the math to avoid floating-point loss of precision
144 # or trailing ulps. We want *exactly* as much precision as we had
146 seconds
= Decimal(int(time
.mktime(value
.timetuple())))
147 fraction
= Decimal(value
.microsecond
)
148 return seconds
+ (fraction
/ self
.shifter
)
150 def get_db_prep_save(self
, value
, connection
):
152 Prepare a value for the database.
154 Overridden to handle the datetime-Decimal conversion because
155 DecimalField doesn't otherwise understand our intent.
157 Part of the Django field API.
160 # Cribbed from the DecimalField implementation. Uses
161 # self.get_prep_value instead of self.to_python to ensure that only
162 # Decimals are passed here.
163 return connection
.ops
.value_to_db_decimal(self
.get_prep_value(value
),
167 def to_python(self
, value
):
169 Turn a backend type into a Python type.
171 Part of the Django field API.
176 if isinstance(value
, (datetime
,)):
178 if isinstance(value
, (Decimal
, basestring
)):
179 return datetime
.fromtimestamp(float(value
))
180 if isinstance(value
, Real
):
181 return datetime
.fromtimestamp(value
)
183 raise ValidationError(_('Unable to convert %s to datetime.') % value
)
186 # Migration rules for PDTField. PDTField's serialization is surprisingly
187 # straightforward and doesn't need any help here.
188 add_introspection_rules([], ["^ganeti_web\.fields\.PreciseDateTimeField"])
191 class DataVolumeField(CharField
):
195 def __init__(self
, min_value
=None, max_value
=None, **kwargs
):
196 super(DataVolumeField
, self
).__init
__(**kwargs
)
198 self
.validators
.append(MinValueValidator(min_value
))
200 self
.validators
.append(MaxValueValidator(max_value
))
202 def to_python(self
, value
):
204 Turn a bytecount into an integer, in megabytes.
206 XXX looks like it's actually mebibytes
207 XXX this should handle the SI base2 versions as well (MiB, GiB, etc.)
208 XXX should round up to the next megabyte?
211 if value
in EMPTY_VALUES
:
214 # Make a not-unreasonable attempt to pass through numbers which don't
215 # need the formatting.
222 return int(float(value
))
226 value
= str(value
).upper().strip()
228 matches
= re
.match(r
'([0-9]+(?:\.[0-9]+)?)\s*(M|G|T|MB|GB|TB)?$',
231 raise ValidationError(_('Invalid format.'))
234 unit
= matches
.group(2)
242 multiplier
= 1024 * 1024
244 intvalue
= int(float(matches
.group(1)) * multiplier
)
248 # Migration rules for DVField. DVField doesn't do anything fancy, so the
249 # default rules will work.
250 add_introspection_rules([], ["^ganeti_web\.fields\.DataVolumeField"])
253 class MACAddressField(RegexField
):
255 Form field that validates MAC Addresses.
258 def __init__(self
, *args
, **kwargs
):
259 kwargs
["regex"] = '^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$'
260 super(MACAddressField
, self
).__init
__(*args
, **kwargs
)
263 # Migration rules for MAField. MAField doesn't do anything fancy, so the
264 # default rules will work.
265 add_introspection_rules([], ["^ganeti_web\.fields\.MACAddressField"])
268 class SQLSumIf(models
.sql
.aggregates
.Aggregate
):
271 # XXX not all databases treat 1 and True the same, or have True. Use the
272 # expression 1=1 which always evaluates true with a value compatible with
274 sql_template
= "%(function)s(CASE %(condition)s WHEN 1=1 THEN " \
275 "%(field)s ELSE NULL END)"
278 class SumIf(models
.Aggregate
):
281 def add_to_query(self
, query
, alias
, col
, source
, is_summary
):
282 aggregate
= SQLSumIf(col
, source
=source
,
283 is_summary
=is_summary
, **self
.extra
)
284 query
.aggregates
[alias
] = aggregate