Merge branch 'bug/13869_disk_defaults' into release/0.10.1
[ganeti_webmgr.git] / ganeti_web / fields.py
blob297e6ea0bbf2092d980bac9e5cbefe3d9214a9b2
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 PatchedEncryptedCharField(EncryptedCharField):
42 """
43 django_fields upstream refuses to fix a bug, so we get to do it ourselves.
45 Feel free to destroy this class and switch back to upstream if
46 https://github.com/svetlyak40wt/django-fields/pull/12 is ever merged into
47 a released version of django_fields.
48 """
50 def get_db_prep_value(self, value, connection=None, prepared=False):
51 if value is None:
52 return None
54 return EncryptedCharField.get_db_prep_value(self, value,
55 connection=connection,
56 prepared=prepared)
59 add_introspection_rules([], ["^ganeti_web\.fields\.PatchedEncryptedCharField"])
62 class PreciseDateTimeField(DecimalField):
63 """
64 Custom field which provides sub-second precision.
66 MySQL and other databases follow the SQL92 standard:
68 TIMESTAMP - contains the datetime field's YEAR, MONTH, DAY, HOUR,
69 MINUTE, and SECOND.
71 However, sometimes more precision is needed, and this field provides
72 arbitrarily high-precision datetimes.
74 Internally, this field is a DECIMAL field. The value is stored as a
75 straight UNIX timestamp, with extra digits of precision representing the
76 fraction of a second.
78 This field is not timezone-safe.
80 By default, this field supports six decimal places, for microseconds. It
81 will store a total of eighteen digits for the entire timestamp. Both of
82 these values can be adjusted.
84 The keyword argument ``decimal_places`` controls how many sub-second
85 decimal places will be stored. The keyword argument ``max_digits``
86 controls the total number of digits stored.
87 """
89 __metaclass__ = models.SubfieldBase
91 def __init__(self, **kwargs):
92 # Set default values.
93 if not 'decimal_places' in kwargs:
94 kwargs['decimal_places'] = 6
95 if not 'max_digits' in kwargs:
96 kwargs['max_digits'] = kwargs['decimal_places'] + 12
98 self.shifter = Decimal(10)**kwargs['decimal_places']
100 super(PreciseDateTimeField, self).__init__(**kwargs)
102 def get_prep_value(self, value):
104 Turn a datetime into a Decimal.
107 if value is None:
108 return None
110 # Use Decimal for the math to avoid floating-point loss of precision
111 # or trailing ulps. We want *exactly* as much precision as we had
112 # in the timestamp.
113 seconds = Decimal(int(time.mktime(value.timetuple())))
114 fraction = Decimal(value.microsecond)
115 return seconds + (fraction / self.shifter)
117 def get_db_prep_save(self, value, connection):
119 Prepare a value for the database.
121 Overridden to handle the datetime-Decimal conversion because
122 DecimalField doesn't otherwise understand our intent.
124 Part of the Django field API.
127 # Cribbed from the DecimalField implementation. Uses
128 # self.get_prep_value instead of self.to_python to ensure that only
129 # Decimals are passed here.
130 return connection.ops.value_to_db_decimal(self.get_prep_value(value),
131 self.max_digits,
132 self.decimal_places)
134 def to_python(self, value):
136 Turn a backend type into a Python type.
138 Part of the Django field API.
141 if value is None:
142 return None
143 if isinstance(value, (datetime,)):
144 return value
145 if isinstance(value, (Decimal, basestring)):
146 return datetime.fromtimestamp(float(value))
147 if isinstance(value, Real):
148 return datetime.fromtimestamp(value)
150 raise ValidationError(_('Unable to convert %s to datetime.') % value)
153 # Migration rules for PDTField. PDTField's serialization is surprisingly
154 # straightforward and doesn't need any help here.
155 add_introspection_rules([], ["^ganeti_web\.fields\.PreciseDateTimeField"])
158 class DataVolumeField(CharField):
159 min_value = None
160 max_value = None
162 def __init__(self, min_value=None, max_value=None, **kwargs):
163 super(DataVolumeField, self).__init__(**kwargs)
164 if min_value:
165 self.validators.append(MinValueValidator(min_value))
166 if max_value:
167 self.validators.append(MaxValueValidator(max_value))
169 def to_python(self, value):
171 Turn a bytecount into an integer, in megabytes.
173 XXX looks like it's actually mebibytes
174 XXX this should handle the SI base2 versions as well (MiB, GiB, etc.)
175 XXX should round up to the next megabyte?
178 if value in EMPTY_VALUES:
179 return None
181 # Make a not-unreasonable attempt to pass through numbers which don't
182 # need the formatting.
183 try:
184 return int(value)
185 except ValueError:
186 pass
188 try:
189 return int(float(value))
190 except ValueError:
191 pass
193 value = str(value).upper().strip()
195 matches = re.match(r'([0-9]+(?:\.[0-9]+)?)\s*(M|G|T|MB|GB|TB)?$',
196 value)
197 if matches is None:
198 raise ValidationError(_('Invalid format.'))
200 multiplier = 1
201 unit = matches.group(2)
202 if unit is not None:
203 unit = unit[0]
204 if unit == 'M':
205 multiplier = 1
206 elif unit == 'G':
207 multiplier = 1024
208 elif unit == 'T':
209 multiplier = 1024 * 1024
211 intvalue = int(float(matches.group(1)) * multiplier)
212 return intvalue
215 # Migration rules for DVField. DVField doesn't do anything fancy, so the
216 # default rules will work.
217 add_introspection_rules([], ["^ganeti_web\.fields\.DataVolumeField"])
220 class MACAddressField(RegexField):
222 Form field that validates MAC Addresses.
225 def __init__(self, *args, **kwargs):
226 kwargs["regex"] = '^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$'
227 super(MACAddressField, self).__init__(*args, **kwargs)
230 # Migration rules for MAField. MAField doesn't do anything fancy, so the
231 # default rules will work.
232 add_introspection_rules([], ["^ganeti_web\.fields\.MACAddressField"])
235 class SQLSumIf(models.sql.aggregates.Aggregate):
236 is_ordinal = True
237 sql_function = 'SUM'
238 # XXX not all databases treat 1 and True the same, or have True. Use the
239 # expression 1=1 which always evaluates true with a value compatible with
240 # the database.
241 sql_template = "%(function)s(CASE %(condition)s WHEN 1=1 THEN " \
242 "%(field)s ELSE NULL END)"
245 class SumIf(models.Aggregate):
246 name = 'SUM'
248 def add_to_query(self, query, alias, col, source, is_summary):
249 aggregate = SQLSumIf(col, source=source,
250 is_summary=is_summary, **self.extra)
251 query.aggregates[alias] = aggregate