source: trunk/db/fields.py @ 53

Revision 53, 12.1 KB checked in by esanchez, 6 years ago (diff)

Fixes the name collision. Fixes #35

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