source: telemeta/models/core.py @ 3511758

cremcrem2crem3devdev2diademsdj1.6feature/breadcrumbsfeature/ts-0.5feature/ts-0.5.4feature/writecachegenericinstru_searchlamlam2mapsv3mergenlivemultiprobproductionrelease/1.4.4sabiodsearchsecurityserversocialstoragetelecastertest
Last change on this file since 3511758 was 3511758, checked in by yomguy <yomguy@…>, 3 years ago

fix default value for duration field with Django 1.4 + MySQL

  • Property mode set to 100644
File size: 17.3 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2007-2010 Samalyse SARL
4# Copyright (C) 2010-2011 Parisson SARL
5#
6# This software is a computer program whose purpose is to backup, analyse,
7# transcode and stream any audio content with its metadata over a web frontend.
8#
9# This software is governed by the CeCILL  license under French law and
10# abiding by the rules of distribution of free software.  You can  use,
11# modify and/ or redistribute the software under the terms of the CeCILL
12# license as circulated by CEA, CNRS and INRIA at the following URL
13# "http://www.cecill.info".
14#
15# As a counterpart to the access to the source code and  rights to copy,
16# modify and redistribute granted by the license, users are provided only
17# with a limited warranty  and the software's author,  the holder of the
18# economic rights,  and the successive licensors  have only  limited
19# liability.
20#
21# In this respect, the user's attention is drawn to the risks associated
22# with loading,  using,  modifying and/or developing or reproducing the
23# software by the user in light of its specific status of free software,
24# that may mean  that it is complicated to manipulate,  and  that  also
25# therefore means  that it is reserved for developers  and  experienced
26# professionals having in-depth computer knowledge. Users are therefore
27# encouraged to load and test the software's suitability as regards their
28# requirements in conditions enabling the security of their systems and/or
29# data to be ensured and,  more generally, to use and operate it in the
30# same conditions as regards security.
31#
32# The fact that you are presently reading this means that you have had
33# knowledge of the CeCILL license and that you accept its terms.
34#
35# Authors: Olivier Guilyardi <olivier@samalyse.com>
36#          Guillaume Pellerin <yomguy@parisson.com>
37
38__all__ = ['ModelCore', 'MetaCore', 'DurationField', 'Duration', 'WeakForeignKey',
39           'EnhancedModel', 'CharField', 'TextField', 'IntegerField', 'BooleanField',
40           'DateTimeField', 'FileField', 'ForeignKey', 'FloatField', 'DateField',
41           'RequiredFieldError', 'CoreQuerySet', 'CoreManager', 'word_search_q']
42
43from django.core import exceptions
44from django import forms
45from xml.dom.minidom import getDOMImplementation
46from django.db.models.fields import FieldDoesNotExist
47from django.db.models import Q
48from django.db import models
49import datetime
50from django.utils.translation import ugettext_lazy as _
51import re
52from django.core.exceptions import ObjectDoesNotExist
53from south.modelsinspector import add_introspection_rules
54
55class Duration(object):
56    """Represent a time duration"""
57    def __init__(self, *args, **kwargs):
58        if len(args) and isinstance(args[0], datetime.timedelta):
59            self._delta = datetime.timedelta(days=args[0].days, seconds=args[0].seconds)
60        else:
61            self._delta = datetime.timedelta(*args, **kwargs)
62
63    def __decorate(self, method, other):
64        if isinstance(other, Duration):
65            res = method(other._delta)
66        else:
67            res = method(other)
68        if type(res) == datetime.timedelta:
69            return Duration(res)
70
71        return res
72
73    def __add__(self, other):
74        return self.__decorate(self._delta.__add__, other)
75
76    def __nonzero__(self):
77        return self._delta.__nonzero__()
78
79    def __str__(self):
80        hours   = self._delta.days * 24 + self._delta.seconds / 3600
81        minutes = (self._delta.seconds % 3600) / 60
82        seconds = self._delta.seconds % 60
83
84        return "%.2d:%.2d:%.2d" % (hours, minutes, seconds)
85
86    @staticmethod
87    def fromstr(str):
88        if not str:
89            return Duration()
90
91        test = re.match('^([0-9]+)(?::([0-9]+)(?::([0-9]+))?)?$', str)
92        if test:
93            groups = test.groups()
94            try:
95                hours = minutes = seconds = 0
96                if groups[0]:
97                    hours = int(groups[0])
98                    if groups[1]:
99                        minutes = int(groups[1])
100                        if groups[2]:
101                            seconds = int(groups[2])
102
103                return Duration(hours=hours, minutes=minutes, seconds=seconds)
104            except TypeError:
105                print groups
106                raise
107        else:
108            raise ValueError("Malformed duration string: " + str)
109
110    def as_seconds(self):
111        return self._delta.days * 24 * 3600 + self._delta.seconds
112
113def normalize_field(args, default_value=None):
114    """Normalize field constructor arguments, so that the field is marked blank=True
115       and has a default value by default.
116
117       This behaviour can be disabled by passing the special argument required=True.
118
119       The default value can also be overriden with the default=value argument.
120       """
121    required = False
122    if args.has_key('required'):
123        required = args['required']
124        args.pop('required')
125
126    args['blank'] = not required
127
128    if not required:
129        if not args.has_key('default'):
130            if args.get('null'):
131                args['default'] = None
132            elif default_value is not None:
133                args['default'] = default_value
134
135    return args
136
137# The following is based on Django TimeField
138class DurationField(models.Field):
139    """Duration Django model field. Essentially the same as a TimeField, but
140    with values over 24h allowed.
141
142    The constructor arguments are also normalized with normalize_field().
143    """
144
145    description = _("Duration")
146
147    __metaclass__ = models.SubfieldBase
148
149    default_error_messages = {
150        'invalid': _('Enter a valid duration in HH:MM[:ss] format.'),
151    }
152
153    def __init__(self, *args, **kwargs):
154        super(DurationField, self).__init__(*args, **normalize_field(kwargs, '0'))
155
156    def db_type(self):
157        return 'int'
158
159    def to_python(self, value):
160        if value is None:
161            return None
162        if isinstance(value, int) or isinstance(value, long):
163            return Duration(seconds=value)
164        if isinstance(value, datetime.time):
165            return Duration(hours=value.hour, minutes=value.minute, seconds=value.second)
166        if isinstance(value, datetime.datetime):
167            # Not usually a good idea to pass in a datetime here (it loses
168            # information), but this can be a side-effect of interacting with a
169            # database backend (e.g. Oracle), so we'll be accommodating.
170            return self.to_python(value.time())
171        else:
172            value = str(value)
173        try:
174            return Duration.fromstr(value)
175        except ValueError:
176            raise exceptions.ValidationError(self.error_messages['invalid'])
177
178    def get_prep_value(self, value):
179        return self.to_python(value)
180
181    def get_db_prep_value(self, value, connection=None, prepared=False):
182        # Casts times into the format expected by the backend
183        try:
184            return value.as_seconds()
185        except:
186            return value
187
188    def value_to_string(self, obj):
189        val = self._get_val_from_obj(obj)
190        if val is None:
191            data = ''
192        else:
193            data = unicode(val)
194        return data
195
196    def formfield(self, **kwargs):
197        defaults = {'form_class': forms.CharField}
198        defaults.update(kwargs)
199        return super(DurationField, self).formfield(**defaults)
200
201class ForeignKey(models.ForeignKey):
202    """The constructor arguments of this ForeignKey are normalized
203    with normalize_field(), however the field is marked required by default
204    unless it is allowed to be null."""
205
206    def __init__(self, to, **kwargs):
207        if not kwargs.has_key('required'):
208            if not kwargs.get('null'):
209                kwargs['required'] = True
210
211        super(ForeignKey, self).__init__(to, **normalize_field(kwargs, 0))
212
213class WeakForeignKey(ForeignKey):
214    """A weak foreign key is the same as foreign key but without cascading
215    delete. Instead the reference is set to null when the referenced record
216    get deleted. This emulates the ON DELETE SET NULL sql behaviour.
217
218    This field is automatically allowed to be null, there's no need to pass
219    null=True.
220
221    The constructor arguments are normalized with normalize_field() by the
222    parent ForeignKey
223
224    Warning: must be used in conjunction with EnhancedQuerySet, EnhancedManager,
225    and EnhancedModel
226    """
227    def __init__(self, to, **kwargs):
228        kwargs['null'] = True
229        super(WeakForeignKey, self).__init__(to, **kwargs)
230
231class EnhancedQuerySet(models.query.QuerySet):
232    """QuerySet with added functionalities such as WeakForeignKey handling"""
233
234    def delete(self):
235        CHUNK=1024
236        objects = self.model._meta.get_all_related_objects()
237        ii = self.count()
238        values = self.values_list('pk')
239        for related in objects:
240            i = 0
241            while i < ii:
242                ids = [v[0] for v in values[i:i + CHUNK]]
243                filter = {related.field.name + '__pk__in': ids}
244                q = related.model.objects.filter(**filter)
245                if isinstance(related.field, WeakForeignKey):
246                    update = {related.field.name: None}
247                    q.update(**update)
248                else:
249                    q.delete()
250
251                i += CHUNK
252
253        super(EnhancedQuerySet, self).delete()
254
255class EnhancedManager(models.Manager):
256    """Manager which is bound to EnhancedQuerySet"""
257    def get_query_set(self):
258        return EnhancedQuerySet(self.model)
259
260
261class EnhancedModel(models.Model):
262    """Base model class with added functionality. See EnhancedQuerySet"""
263
264    objects = EnhancedManager()
265
266    def delete(self):
267        if not self.pk:
268            raise Exception("Can't delete without a primary key")
269        self.__class__.objects.filter(pk=self.pk).delete()
270
271    class Meta:
272        abstract = True
273
274class CharField(models.CharField):
275    """This is a CharField with a default max_length of 250.
276
277       The arguments are also normalized with normalize_field()"""
278
279    def __init__(self, *args, **kwargs):
280        if not kwargs.has_key('max_length'):
281            kwargs['max_length'] = 250
282
283        super(CharField, self).__init__(*args, **normalize_field(kwargs, ''))
284
285class IntegerField(models.IntegerField):
286    """IntegerField normalized with normalize_field()"""
287
288    def __init__(self, *args, **kwargs):
289        super(IntegerField, self).__init__(*args, **normalize_field(kwargs, 0))
290
291class BooleanField(models.BooleanField):
292    """BooleanField normalized with normalize_field()"""
293
294    def __init__(self, *args, **kwargs):
295        super(BooleanField, self).__init__(*args, **normalize_field(kwargs, False))
296
297class TextField(models.TextField):
298    """TextField normalized with normalize_field()"""
299
300    def __init__(self, *args, **kwargs):
301        super(TextField, self).__init__(*args, **normalize_field(kwargs, ''))
302
303class DateTimeField(models.DateTimeField):
304    """DateTimeField normalized with normalize_field(). This field is allowed to
305    be null by default unless null=False is passed"""
306
307    def __init__(self, *args, **kwargs):
308        if not kwargs.has_key('null'):
309            kwargs['null'] = True
310        super(DateTimeField, self).__init__(*args, **normalize_field(kwargs))
311
312class FileField(models.FileField):
313    """FileField normalized with normalize_field()"""
314
315    def __init__(self, *args, **kwargs):
316        super(FileField, self).__init__(*args, **normalize_field(kwargs, ''))
317
318class FloatField(models.FloatField):
319    """FloatField normalized with normalize_field()"""
320
321    def __init__(self, *args, **kwargs):
322        super(FloatField, self).__init__(*args, **normalize_field(kwargs, 0))
323
324class DateField(models.DateField):
325    """DateField normalized with normalize_field(). This field is allowed to
326    be null by default unless null=False is passed"""
327
328    def __init__(self, *args, **kwargs):
329        if not kwargs.has_key('null'):
330            kwargs['null'] = True
331        super(DateField, self).__init__(*args, **normalize_field(kwargs))
332
333class RequiredFieldError(Exception):
334    def __init__(self, model, field):
335        self.model = model
336        self.field = field
337        super(Exception, self).__init__('%s.%s is required' % (model._meta.object_name, field.name))
338
339class ModelCore(EnhancedModel):
340
341    @classmethod
342    def required_fields(cls):
343        required = []
344        for field in cls._meta.fields:
345            if not field.blank:
346                required.append(field)
347        return required
348
349    def save(self, force_insert=False, force_update=False):
350        required = self.required_fields()
351        for field in required:
352            if not getattr(self, field.name):
353                raise RequiredFieldError(self, field)
354        super(ModelCore, self).save(force_insert, force_update)
355
356    @classmethod
357    def get_dom_name(cls):
358        "Convert the class name to a DOM element name"
359        clsname = cls.__name__
360        return clsname[0].lower() + clsname[1:]
361
362    @staticmethod
363    def get_dom_field_name(field_name):
364        "Convert the class name to a DOM element name"
365        tokens = field_name.split('_')
366        name = tokens[0]
367        for t in tokens[1:]:
368            name += t[0].upper() + t[1:]
369        return name
370
371    def to_dom(self):
372        "Return the DOM representation of this media object"
373        impl = getDOMImplementation()
374        root = self.get_dom_name()
375        doc = impl.createDocument(None, root, None)
376        top = doc.documentElement
377        top.setAttribute("id", str(self.pk))
378        fields = self.to_dict()
379        for name, value in fields.iteritems():
380            element = doc.createElement(self.get_dom_field_name(name))
381            if isinstance(value, EnhancedModel):
382                element.setAttribute('key', str(value.pk))
383            value = unicode(value)
384            element.appendChild(doc.createTextNode(value))
385            top.appendChild(element)
386        return doc
387
388    def to_dict(self):
389        "Return model fields as a dict of name/value pairs"
390        fields_dict = {}
391        for field in self._meta.fields:
392            fields_dict[field.name] = getattr(self, field.name)
393        return fields_dict
394
395    def to_list(self):
396        "Return model fields as a list"
397        fields_list = []
398        for field in self._meta.fields:
399            fields_list.append({'name': field.name, 'value': getattr(self, field.name)})
400        return fields_list
401
402    @classmethod
403    def field_label(cls, field_name=None):
404        if field_name:
405            try:
406                return cls._meta.get_field(field_name).verbose_name
407            except FieldDoesNotExist:
408                try:
409                    return getattr(cls, field_name).verbose_name
410                except AttributeError:
411                    return field_name
412        else:
413            return cls._meta.verbose_name
414
415    class Meta:
416        abstract = True
417
418class MetaCore:
419    app_label = 'telemeta'
420
421def word_search_q(field, pattern):
422    words = re.split("[ .*-]+", pattern)
423    q = Q()
424    for w in words:
425        if len(w) >= 3:
426            kwargs = {field + '__icontains': w}
427            q &= Q(**kwargs)
428
429    return q
430
431class CoreQuerySet(EnhancedQuerySet):
432    "Base class for all query sets"
433
434    def none(self): # redundant with none() in recent Django svn
435        "Return an empty result set"
436        return self.extra(where = ["0 = 1"])
437
438    def word_search(self, field, pattern):
439        return self.filter(word_search_q(field, pattern))
440
441    def _by_change_time(self, type, from_time = None, until_time = None):
442        "Search between two revision dates"
443        table = self.model._meta.db_table
444        where = []
445        if from_time:
446            where.append("revisions.time >= '%s'" % from_time.strftime('%Y-%m-%d %H:%M:%S'))
447        if until_time:
448            where.append("revisions.time <= '%s'" % until_time.strftime('%Y-%m-%d %H:%M:%S'))
449
450        qs = self
451        if where:
452            where.extend(["revisions.element_type = '%s'" % type, "revisions.element_id = %s.id" % table])
453            qs = qs.extra(where = [" AND ".join(where)],
454                            tables = ['revisions']).distinct()
455        return qs
456
457class CoreManager(EnhancedManager):
458    "Base class for all models managers"
459
460    def none(self, *args, **kwargs):
461        ""
462        return self.get_query_set().none(*args, **kwargs)
463
464    def get(self, **kwargs):
465        if kwargs.has_key('public_id'):
466            try:
467                args = kwargs.copy()
468                args['code'] = kwargs['public_id']
469                args.pop('public_id')
470                return super(CoreManager, self).get(**args)
471            except ObjectDoesNotExist:
472                args = kwargs.copy()
473                args['id'] = kwargs['public_id']
474                args.pop('public_id')
475                return super(CoreManager, self).get(**args)
476
477        return super(CoreManager, self).get(**kwargs)
478
479
480# South introspection rules
481add_introspection_rules([], ["^telemeta\.models\.core\.CharField"])
482add_introspection_rules([], ["^telemeta\.models\.core\.TextField"])
483add_introspection_rules([], ["^telemeta\.models\.core\.FileField"])
484add_introspection_rules([], ["^telemeta\.models\.core\.IntegerField"])
485add_introspection_rules([], ["^telemeta\.models\.core\.BooleanField"])
486add_introspection_rules([], ["^telemeta\.models\.core\.DateTimeField"])
487add_introspection_rules([], ["^telemeta\.models\.core\.DateField"])
488add_introspection_rules([], ["^telemeta\.models\.core\.FloatField"])
489add_introspection_rules([], ["^telemeta\.models\.core\.DurationField"])
490add_introspection_rules([], ["^telemeta\.models\.core\.ForeignKey"])
491add_introspection_rules([], ["^telemeta\.models\.core\.WeakForeignKey"])
492
Note: See TracBrowser for help on using the repository browser.