source: trunk/db/fields.py @ 66

Revision 66, 13.7 KB checked in by msaelices, 6 years ago (diff)

Integrated EncryptedField?. I've changed implementation a little, adding a formfield method to EncriptedField?. Fixes #42.

Line 
1# -*-  coding: utf-8 -*-
2import os, re, shutil
3import Image
4
5from django import oldforms
6from django import newforms as forms
7from django.conf import settings
8from django.core.validators import ValidationError
9from django.db import models
10from django.db.models import signals
11from django.dispatch import dispatcher
12from django.template import defaultfilters
13from django.utils.functional import curry
14from django.utils.encoding import smart_unicode
15
16from cmsutils import forms as cmsutilsforms
17from cmsutils.forms.fields import ESCCCField as ESCCCFormField
18from cmsutils.forms.fields import LatitudeLongitudeField
19from cmsutils.forms.widgets import GoogleMapsWidget
20from cmsutils.oldforms.fields import SpanishDateField as oldformsSpanishDateField
21from cmsutils.map_utils import MapPosition
22from cmsutils.utils import encrypt, decrypt
23
24
25class AutoSlugField(models.SlugField):
26    def __init__(self, autofromfield, **kwargs): 
27        models.SlugField.__init__(self, **kwargs)
28        self.autofromfield = autofromfield
29        self.editable=False
30
31    def sluggify(self,name, objects):
32        slug = defaultfilters.slugify(name)
33        slug_num = slug
34        n = 2
35        while objects.filter(slug__exact=slug_num):
36            slug_num = slug + u'-%s' % n
37            n += 1
38        return slug_num
39
40    def pre_save(self, instance, add):
41        value = getattr(instance,self.autofromfield)
42        if instance.id:
43            slug = self.sluggify(value, instance.__class__.objects.exclude(id=instance.id))
44        else:
45            slug = self.sluggify(value, instance.__class__.objects.all())
46        setattr(instance, self.name, slug)
47        return slug
48
49    def get_internal_type(self):
50        return 'SlugField'
51
52
53class DynamicMixin(object):
54    """Allows model instance to specify upload_to dynamically.
55
56    Model class should have a method like:
57
58        def get_upload_to(self, attname):
59            return 'path/to/%d' % self.id
60
61    Based on:
62    http://scottbarnham.com/blog/2007/07/31/uploading-images-to-a-dynamic-
63    path-with-django/
64    """
65
66    def save_form_data(self, instance, data):
67        self.data = data
68        return
69
70    def _post_save(field, instance, created=False, raw=False):
71        if not getattr(field, 'real_save', None):
72            if hasattr(instance, 'get_upload_to'):
73                field.upload_to = instance.get_upload_to(field.attname)
74                if getattr(field, 'data', None):
75                    if field.data and isinstance(field.data, forms.fields.UploadedFile):
76                        field.real_save=1
77                        getattr(instance, "save_%s_file" % field.name)(field.data.filename, field.data.content, save=True)
78                        del(field.data)
79                        field.real_save=0
80
81    def get_internal_type(self):
82        return 'FileField'
83
84
85class DynamicFileField(DynamicMixin, models.FileField):
86
87    def contribute_to_class(self, cls, name):
88        """Hook up events so we can access the instance."""
89        models.FileField.contribute_to_class(self, cls, name)
90        dispatcher.connect(self._post_save, signals.post_save, sender=cls)
91
92
93class DynamicImageField(DynamicMixin, models.ImageField):
94
95    def contribute_to_class(self, cls, name):
96        """Hook up events so we can access the instance."""
97        models.ImageField.contribute_to_class(self, cls, name)
98        dispatcher.connect(self._post_save, signals.post_save, sender=cls)
99
100
101def auto_rename(file_path, new_name):
102    """Renames a file, keeping the extension.
103
104    Parameters:
105        - file_path: the file path relative to MEDIA_ROOT
106        - new_name: the new basename of the file (no extension)
107
108    Returns the new file path on success or the original file_path on error."""
109    if not file_path:
110        return ''
111    path = os.path.dirname(file_path)
112    curr_name = os.path.basename(file_path)
113    original, ext = os.path.splitext(curr_name)
114    new_path = os.path.join(path, new_name + ext).replace('\\', '/')
115    if new_path != file_path:
116        # Try to rename
117        try:
118            shutil.move(os.path.join(settings.MEDIA_ROOT, file_path), os.path.join(settings.MEDIA_ROOT, new_path))
119        except IOError:
120            # Error? Restore original name
121            new_path = file_path
122
123    return new_path
124
125
126def auto_resize(file_path, width=None, height=None, max_width=None, max_height=None):
127    """
128    Resize an image to fit an area.
129    Useful to avoid storing large files.
130
131    At least one of max_width or max_height, or both width and height,
132    must be set. width and height take precedence over max_width and max_height.
133    """
134    # Return if no file given or no maximum size passed
135    if not (file_path and (width and height) or (max_width or max_height)):
136        return
137
138    real_path = os.path.join(settings.MEDIA_ROOT, file_path)
139    img = Image.open(real_path)
140    w, h = img.size
141    if width and height and (w > width or h > height):
142        w = int(width)
143        h = int(height)
144    elif max_width or max_height:
145        w = int(max_width or w)
146        h = int(max_height or h)
147    else:
148        return
149    img.thumbnail((w, h), Image.ANTIALIAS) # resize maintaining aspect ratio
150    img.save(real_path)
151
152
153class ResizingImageField(DynamicImageField):
154    """ImageField that automatically resizes uploaded images to a maximum width
155    or height.
156
157    Borrowed from http://code.djangoproject.com/wiki/CustomUploadAndFilters """
158
159    def __init__(self, verbose=None, width=None, height=None,
160                 max_width=None, max_height=None, **kwargs):
161        self.width, self.height = width, height
162        self.max_width, self.max_height = max_width, max_height
163        self.width_field, self.height_field = 'width', 'height'
164        super(ResizingImageField, self).__init__(verbose, **kwargs)
165
166    def save_file(self, new_data, new_object, original_object, change, rel, save=True):
167        super(ResizingImageField, self).save_file(new_data, new_object, original_object, change, rel, save)
168        # Get upload info
169        upload_field_name = self.get_manipulator_field_names('')[0]
170        field = new_data.get(upload_field_name, False)
171        # File uploaded?
172        if field:
173            # Resizing image
174            auto_resize(getattr(new_object, self.attname), width=self.width, height=self.height,
175                        max_width=self.max_width, max_height=self.max_height)
176
177    def get_internal_type(self):
178        return 'ImageField'
179
180
181class ESPhoneNumberField(models.PhoneNumberField):
182    """Custom model field to store Spanish phone numbers (9-digit integers that
183    start by 6, 8 or 9).
184
185    When creating a newforms form it uses the field
186    django.contrib.localflavor.es.forms.ESPhoneNumberField
187    """
188
189    def __init__(self, *args, **kwargs):
190        super(ESPhoneNumberField, self).__init__(*args, **kwargs)
191        self.validator_list.append(self.validate)
192        self.max_length=9
193
194    def get_internal_type(self):
195        return 'PhoneNumberField'
196
197    def get_manipulator_field_objs(self):
198        return [oldforms.TextField]
199
200    def validate(self, field_data, all_data):
201        # regex de django.contrib.localflavor.es.forms.ESPhoneNumberField
202        if not re.match(r'^[689]\d{8}$', field_data):
203            from django.core.validators import ValidationError
204            message = u"Los números de teléfono españoles deben tener " \
205                u"9 dígitos y empezar por 6, 8 o 9. " \
206                u"%s no es un número válido." % field_data
207            raise ValidationError, message
208
209    def formfield(self, **kwargs):
210        from django.contrib.localflavor.es.forms import ESPhoneNumberField \
211            as ESPhoneNumberFormField
212        defaults = {'form_class': ESPhoneNumberFormField}
213        defaults.update(kwargs)
214        return super(ESPhoneNumberField, self).formfield(**defaults)
215
216
217class SpanishDateField(models.DateField):
218    def formfield(self, **kwargs):
219        kwargs.update({'form_class': cmsutilsforms.fields.SpanishDateField})
220        return super(SpanishDateField, self).formfield(**kwargs)
221
222    def get_manipulator_field_objs(self):
223        return [oldformsSpanishDateField]
224
225    def get_internal_type(self):
226        return 'DateField'
227
228
229class SpanishDateTimeField(models.DateTimeField):
230    def formfield(self, **kwargs):
231        kwargs.update({'form_class': cmsutilsforms.fields.SpanishDateTimeField})
232        return super(SpanishDateTimeField, self).formfield(**kwargs)
233
234    def get_manipulator_field_objs(self):
235        return [oldformsSpanishDateField, oldforms.TimeField]
236
237    def get_internal_type(self):
238        return 'DateTimeField'
239
240
241class ESCCCField(models.CharField):
242    """Custom model field to store Spanish bank account numbers
243    (CCC short of "Código de Cuenta Cliente").
244
245    When creating a newforms form it uses the field
246    cmsutils.forms.fields.ESCCCField
247
248    """
249
250    def __init__(self, *args, **kwargs):
251        super(ESCCCField, self).__init__(*args, **kwargs)
252        self.validator_list.append(self.validate)
253        self.max_length=23
254
255    def get_internal_type(self):
256        return 'CharField'
257
258    def get_manipulator_field_objs(self):
259        return [oldforms.TextField]
260
261    def validate(self, field_data, all_data):
262        # from django.contrib.localflavor.es.forms.ESCCCField
263        control_str = [1, 2, 4, 8, 5, 10, 9, 7, 3, 6]
264        m = re.match(r'^(\d{4})[ -]?(\d{4})[ -]?(\d{2})[ -]?(\d{10})$', field_data)
265        entity, office, checksum, account = m.groups()
266        get_checksum = lambda d: str(11 - sum([int(digit) * int(control) for digit, control in zip(d, control_str)]) % 11).replace('10', '1').replace('11', '0')
267        if get_checksum('00' + entity + office) + get_checksum(account) != checksum:
268            raise ValidationError, ESCCCFormField.error_messages['checksum']
269
270    def formfield(self, **kwargs):
271        defaults = {'form_class': ESCCCFormField}
272        defaults.update(kwargs)
273        return super(ESCCCField, self).formfield(**defaults)
274
275
276class GoogleMapsPositionField(models.Field):
277    """Custom model field to store a position's latitude and longitude, using
278    by default LatitudeLongitudeField newforms field and GoogleMapsWidget to
279    render a Google Maps map to choose the location easily.
280
281    Sample usage:
282
283    location = GoogleMapsPositionField(_(u'location'), blank=True, null=True,
284        initial=MapPosition(37.303006,-6.302719,14), size=(500,309))
285
286    Note to parameters:
287        * initial is the default center of the map when there is no value.
288        * default is the default position for a new point in the map.
289        * size should be a 2-valued tuple with the map desired width and height.
290
291    """
292    __metaclass__ = models.SubfieldBase
293
294    def __init__(self, *args, **kwargs):
295        kwargs['max_length'] = 24
296        self.initial = kwargs.pop('initial', None)
297        self.default = kwargs.pop('default', None)
298        self.width, self.height = kwargs.pop('size', (400, 247))
299
300        super(GoogleMapsPositionField, self).__init__(*args, **kwargs)
301
302    def to_python(self, value):
303        if not value:
304            return None
305        if isinstance(value, MapPosition):
306            return value
307        else:
308            coords = value.split(",")
309            if len(coords)<2:
310                coords = ("0","0")
311            coords = [float(c.strip()) for c in coords]
312            if len(coords)==2:
313                lat, lng = coords
314                zoom = self.initial and self.initial.zoom or None
315                return MapPosition(lat, lng, zoom)
316            if len(coords)==3:
317                lat, lng, zoom = coords
318                return MapPosition(lat, lng, zoom)
319
320
321    def db_type(self):
322        return 'char(%s)' % self.max_length
323
324    def get_internal_type(self):
325        return 'CharField'
326
327    def get_db_prep_save(self, value):
328        if value:
329            return u"%.6f,%.6f" % (value.latitude, value.longitude)
330        else:
331            if self.default and self.default is not models.fields.NOT_PROVIDED:
332                return u"%.6f,%.6f" % (self.default.latitude, self.default.longitude)
333            else:
334                return None
335
336    def formfield(self, **kwargs):
337        defaults = {'form_class': LatitudeLongitudeField}
338        defaults.update(kwargs)
339        defaults['widget'] = GoogleMapsWidget(initial=self.initial,
340                                              attrs={'width': self.width, 'height': self.height})
341
342        return super(GoogleMapsPositionField, self).formfield(**defaults)
343
344
345class EncryptedField(models.CharField):
346    def __init__(self, verbose_name=None, cipher_key=None, **kwargs):
347        self.input_type = "password"
348        self.key = smart_unicode(kwargs.pop('cipher_key', settings.SECRET_KEY))
349        models.CharField.__init__(self, verbose_name=verbose_name, max_length=200, **kwargs)
350
351    def _encrypt(self, value):
352        if value is not None:
353            return encrypt(smart_unicode(value), self.key)
354        else:
355            return None
356
357    def _decrypt(self, value):
358        if value is not None:
359            return decrypt(smart_unicode(value), self.key)
360        else:
361            return None
362
363    def _decrypt_field(self, cls, field):
364        return field._decrypt(getattr(cls, field.name))
365
366    def contribute_to_class(self, cls, name):
367        self.set_attributes_from_name(name)
368        cls._meta.add_field(self)
369        setattr(cls, 'get_decrypted_%s' % self.name, curry(self._decrypt_field, field=self))
370
371    def get_manipulator_field_objs(self):
372        return [oldforms.PasswordField]
373
374    def get_internal_type(self):
375        return "TextField"
376
377    def get_db_prep_save(self, value):
378        value = self._encrypt(value)
379        return super(EncryptedField, self).get_db_prep_save(value)
380
381    def formfield(self, **kwargs):
382        defaults = {'form_class': forms.CharField}
383        defaults.update(kwargs)
384        defaults['widget'] = forms.PasswordInput()
385
386        return super(EncryptedField, self).formfield(**defaults)
Note: See TracBrowser for help on using the repository browser.