Merge tag '0.10.2'
[ganeti_webmgr.git] / ganeti_web / fields.py
bloba0a2894f2b8b331c31f27e9bf4a75e1c3e154ed6
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,
16 # USA.
18 from datetime import datetime
19 from decimal import Decimal
20 import time
21 import re
23 try:
24 from numbers import Real
25 except ImportError:
26 Real = float, int, Decimal
28 from django.core.exceptions import ValidationError
29 from django.core.validators import (EMPTY_VALUES, MaxValueValidator,
30 MinValueValidator)
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):
44 self.field = field
46 def __get__(self, instance, owner=None):
47 if instance is 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):
56 """
57 This is a wrapper around the charfield which forces values to lowercase
58 when assigned and prior to saving to the DB
59 """
60 def to_python(self, value):
61 value = super(LowerCaseCharField, self).to_python(value)
62 if isinstance(value, basestring):
63 return value.lower()
64 return value
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):
75 """
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.
81 """
83 def get_db_prep_value(self, value, connection=None, prepared=False):
84 if value is None:
85 return None
87 return EncryptedCharField.get_db_prep_value(self, value,
88 connection=connection,
89 prepared=prepared)
92 add_introspection_rules([], ["^ganeti_web\.fields\.PatchedEncryptedCharField"])
95 class PreciseDateTimeField(DecimalField):
96 """
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,
102 MINUTE, and SECOND.
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.
140 if value is None:
141 return None
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
145 # in the timestamp.
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),
164 self.max_digits,
165 self.decimal_places)
167 def to_python(self, value):
169 Turn a backend type into a Python type.
171 Part of the Django field API.
174 if value is None:
175 return None
176 if isinstance(value, (datetime,)):
177 return value
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):
192 min_value = None
193 max_value = None
195 def __init__(self, min_value=None, max_value=None, **kwargs):
196 super(DataVolumeField, self).__init__(**kwargs)
197 if min_value:
198 self.validators.append(MinValueValidator(min_value))
199 if max_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:
212 return None
214 # Make a not-unreasonable attempt to pass through numbers which don't
215 # need the formatting.
216 try:
217 return int(value)
218 except ValueError:
219 pass
221 try:
222 return int(float(value))
223 except ValueError:
224 pass
226 value = str(value).upper().strip()
228 matches = re.match(r'([0-9]+(?:\.[0-9]+)?)\s*(M|G|T|MB|GB|TB)?$',
229 value)
230 if matches is None:
231 raise ValidationError(_('Invalid format.'))
233 multiplier = 1
234 unit = matches.group(2)
235 if unit is not None:
236 unit = unit[0]
237 if unit == 'M':
238 multiplier = 1
239 elif unit == 'G':
240 multiplier = 1024
241 elif unit == 'T':
242 multiplier = 1024 * 1024
244 intvalue = int(float(matches.group(1)) * multiplier)
245 return intvalue
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):
269 is_ordinal = True
270 sql_function = 'SUM'
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
273 # the database.
274 sql_template = "%(function)s(CASE %(condition)s WHEN 1=1 THEN " \
275 "%(field)s ELSE NULL END)"
278 class SumIf(models.Aggregate):
279 name = 'SUM'
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