forked from 5monkeys/django-bananas
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmodels.py
246 lines (194 loc) · 7.78 KB
/
models.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import math
import base64
import os
import uuid
import binascii
from itertools import chain
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
MISSING = object()
class ModelDict(dict):
_nested = None
def __getattr__(self, item):
"""
Try to to get attribute as key item.
Fallback on prefixed nested keys.
Finally fallback on real attribute lookup.
"""
try:
return self.__getitem__(item)
except KeyError:
try:
return self.__getnested__(item)
except KeyError:
return self.__getattribute__(item)
def __getnested__(self, item):
"""
Find existing items prefixed with given item
and return a new ModelDict containing matched keys,
stripped from prefix.
:param str item: Item prefix key to find
:return ModelDict:
"""
# Ensure _nested cache
if self._nested is None:
self._nested = {}
# Try to get previously accessed/cached nested item
value = self._nested.get(item, MISSING)
if value is not MISSING:
# Return previously accessed nested item
return value
else:
# Find any keys matching nested prefix
prefix = item + '__'
keys = [key for key in self.keys() if key.startswith(prefix)]
if keys:
# Construct nested dict of matched keys, stripped from prefix
n = ModelDict({key[len(item)+2:]: self[key] for key in keys})
# Cache and return
self._nested[item] = n
return n
# Item not a nested key, raise
raise KeyError(item)
def expand(self):
keys = list(self)
for key in keys:
field, __, nested_key = key.partition('__')
if nested_key:
if field not in keys:
nested = self.__getnested__(field)
if isinstance(nested, self.__class__):
nested = nested.expand()
self[field] = nested
del self[key]
return ModelDict(self)
@classmethod
def from_model(cls, model, *fields, **named_fields):
"""
Work-in-progress constructor,
consuming fields and values from django model instance.
"""
d = ModelDict()
if not (fields or named_fields):
# Default to all fields
fields = [f.attname for f in model._meta.concrete_fields]
not_found = object()
for name, field in chain(zip(fields, fields), named_fields.items()):
_fields = field.split('__')
value = model
for i, _field in enumerate(_fields, start=1):
# NOTE: we don't want to rely on hasattr here
previous_value = value
value = getattr(previous_value, _field, not_found)
if value is not_found:
if _field in dir(previous_value):
raise ValueError(
'{!r}.{} had an AttributeError exception'
.format(previous_value, _field))
else:
raise AttributeError(
'{!r} does not have {!r} attribute'
.format(previous_value, _field))
elif value is None:
if name not in named_fields:
name = '__'.join(_fields[:i])
break
d[name] = value
return d
class TimeStampedModel(models.Model):
"""
Provides automatic date_created and date_modified fields.
"""
date_created = models.DateTimeField(blank=True, null=True, editable=False,
auto_now_add=True,
verbose_name=_('date created'))
date_modified = models.DateTimeField(blank=True, null=True, editable=False,
auto_now=True,
verbose_name=_('date modified'))
class Meta:
abstract = True
class UUIDModel(models.Model):
"""
Provides auto-generating UUIDField as the primary key for a model.
"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid.uuid4)
class Meta:
abstract = True
class SecretField(models.CharField):
description = _('Generates and stores a random key.')
default_error_messages = {
'random-is-none': _('%(cls)s.get_random_bytes returned None'),
'random-too-short': _('Too few random bytes received from '
'get_random_bytes. Number of'
' bytes=%(num_bytes)s,'
' min_length=%(min_length)s')
}
def __init__(self, verbose_name=None, num_bytes=32, min_bytes=32, auto=True,
**kwargs):
self.num_bytes, self.auto, self.min_length = num_bytes, auto, min_bytes
field_length = self.get_field_length(self.num_bytes)
defaults = {
'max_length': field_length,
}
defaults.update(kwargs)
if self.auto:
defaults['editable'] = False
defaults['blank'] = True
super(SecretField, self).__init__(verbose_name, **defaults)
@staticmethod
def get_field_length(num_bytes):
"""
Return the length of hexadecimal byte representation of ``n`` bytes.
:param num_bytes:
:return: The field length required to store the byte representation.
"""
return num_bytes * 2
def pre_save(self, model_instance, add):
if self.auto and add:
value = self.get_random_str()
setattr(model_instance, self.attname, value)
return value
else:
return super(SecretField, self).pre_save(model_instance, add)
def get_random_str(self):
random = self.get_random_bytes()
self._check_random_bytes(random)
return binascii.hexlify(random).decode('utf8')
def _check_random_bytes(self, random):
if random is None:
raise ValidationError(self.error_messages['random-is-none'],
code='invalid',
params={'cls': self.__class__.__name__})
if len(random) < self.min_length:
raise ValidationError(self.error_messages['random-too-short'],
code='invalid',
params={'num_bytes': len(random),
'min_length': self.min_length})
def get_random_bytes(self):
return os.urandom(self.num_bytes)
class URLSecretField(SecretField):
@staticmethod
def get_field_length(num_bytes):
"""
Get the maximum possible length of a base64 encoded bytearray of
length ``length``.
:param num_bytes: The length of the bytearray
:return: The worst case length of the base64 result.
"""
return math.ceil(num_bytes / 3.0) * 4
@staticmethod
def y64_encode(s):
"""
Implementation of Y64 non-standard URL-safe base64 variant.
See http://en.wikipedia.org/wiki/Base64#Variants_summary_table
:return: base64-encoded result with substituted
``{"+", "/", "="} => {".", "_", "-"}``.
"""
first_pass = base64.urlsafe_b64encode(s)
return first_pass.translate(bytes.maketrans(b'+/=',
b'._-'))
def get_random_str(self):
random = self.get_random_bytes()
self._check_random_bytes(random)
return self.y64_encode(random).decode('utf-8')