Commits

Matthew Schinckel  committed 650d3e2

Initial import

  • Participants

Comments (0)

Files changed (12)

File .bookmarks

Empty file added.
+recursive-include timedelta *
+django-timedelta-field
+==========================
+
+PostgreSQL can store data as INTERVAL type, which is close to meaning the
+same as python's timedelta object (although better in a couple of ways).
+
+I have lots of use for timedelta objects, and having code that basically
+wrapped integer objects as a number of seconds was common. This module
+combines the two:
+
+    * a timedelta.TimedeltaField() object that transparently converts
+      to and from datetime.timedelta
+    
+    * storage of the data as an INTERVAL in PostgreSQL, or a string in
+      other databases. (Other databases will be considered if I ever
+      use them, or receive patches).
+
+The coolest part of this package is the way it manipulates strings entered
+by users, and presents them. Any string of the format:
+
+    [X weeks,] [Y days,] [Z hours,] [A minutes,] [B seconds]
+
+will be converted to a timedelta object. Even shortened versions can be used:
+hrs, hr or h will also suffice.  The parsing ignores trailing 's', but is
+smart about adding them in when presenting the data to the user.
+
+To use, install the package, and use the field::
+
+    from django.db import models
+    import timedelta
+    
+    class MyModel(models.Model):
+        the_timedelta = timedelta.TimedeltaField()
+
+
+Todo
+-------------
+
+Handle strings with times in other languages. I'm not really sure about how
+to do this, but it may be useful.
+from distutils.core import setup
+
+setup(
+    name = "timedelta",
+    version = "0.2",
+    description = "TimedeltaField for django models",
+    url = "http://bitbucket.org/schinckel/django-timedelta-field/",
+    author = "Matthew Schinckel",
+    author_email = "matt@schinckel.net",
+    packages = [
+        "timedelta",
+    ],
+    classifiers = [
+        'Programming Language :: Python',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: OS Independent',
+        'Framework :: Django',
+    ],
+)

File timedelta/__init__.py

+from fields import TimedeltaField

File timedelta/fields.py

+from django.db import models
+
+import datetime
+from collections import defaultdict
+import re
+
+from models import nice_repr, parse
+from forms import TimedeltaFormField
+
+SECS_PER_DAY = 60*60*24
+
+# TODO: Figure out why django admin thinks fields of this type have changed every time an object is saved.
+
+class TimedeltaField(models.Field):
+    """
+    Store a datetime.timedelta as an integer.
+    
+    We don't subclass models.IntegerField, as that would then use the
+    AdminIntegerWidget or whatever in the admin, and we want to use
+    our custom widget.
+    """
+    __metaclass__ = models.SubfieldBase
+    _south_introspects = True
+    
+    description = "A datetime.timedelta object"
+    
+    def to_python(self, value):
+        if (value is None) or isinstance(value, datetime.timedelta):
+            return value
+        if isinstance(value, int):
+            return datetime.timedelta(seconds=value)
+        if value == "":
+            return datetime.timedelta(0)
+        return parse(value)
+    
+    def get_db_prep_value(self, value):
+        if (value is None) or isinstance(value, (str, unicode)):
+            return value
+        return str(value).replace(',', '')
+    
+    def formfield(self, *args, **kwargs):
+        defaults = {'form_class':TimedeltaFormField}
+        defaults.update(kwargs)
+        return super(TimedeltaField, self).formfield(*args, **defaults)
+    
+    def value_to_string(self, obj):
+        value = self._get_val_from_obj(obj)
+        return unicode(value)
+    
+    def get_default(self):
+        """
+        Needed to rewrite this, as the parent class turns this value into a
+        unicode string. That sux pretty deep.
+        """
+        if self.has_default():
+            if callable(self.default):
+                return self.default()
+            return self.get_db_prep_value(self.default)
+        if not self.empty_strings_allowed or (
+            self.null #and not \
+            #connection.features.interprets_empty_strings_as_nulls
+        ):
+            return None
+        return ""
+        
+    def db_type(self, connection):
+        """
+        Postgres allows us to store stuff as an INTERVAL type. This is 
+        useful, and we can then use database logic to do tests.
+        """
+        if connection.settings_dict['ENGINE'] == "django.db.backends.postgresql_psycopg2":
+            return 'interval'
+        else:
+            return 'char(20)'
+

File timedelta/forms.py

+from django import forms
+from django.utils.translation import ugettext_lazy as _
+
+import datetime
+from collections import defaultdict
+
+from widgets import TimedeltaWidget
+from models import parse
+
+class TimedeltaFormField(forms.Field):
+    default_error_messages = {
+        'invalid':_('Enter a valid time span: e.g. "3 days, 4 hours, 2 minutes"')
+    }
+    
+    def __init__(self, *args, **kwargs):
+        defaults = {'widget':TimedeltaWidget}
+        defaults.update(kwargs)
+        super(TimedeltaFormField, self).__init__(*args, **defaults)
+        
+    def clean(self, value):
+        # import pdb; pdb.set_trace()
+        super(TimedeltaFormField, self).clean(value)
+        if value == '' and not self.required:
+            return u''
+        
+        data = defaultdict(float)
+        try:
+            return parse(value)
+            for part in value.split(','):
+                value, which = part.strip().split(' ')
+                if not which.endswith('s'):
+                    which += "s"
+                if which == "wks":
+                    which = "weeks"
+                if which == "mins":
+                    which = "minutes"
+                if which == "secs":
+                    which = "seconds"
+                data[which] = float(value)
+        except TypeError:
+            raise forms.ValidationError(self.error_messages['invalid'])
+            
+        return datetime.timedelta(**data)

File timedelta/models.py

+"""
+This is in the models.py file so I can run doctests on it!
+"""
+
+import re
+import datetime
+
+def nice_repr(timedelta, display="long"):
+    """
+    Turns a datetime.timedelta object into a nice string repr.
+    
+    display can be "minimal", "short" or "long" [default].
+    
+    >>> from datetime import timedelta as td
+    >>> nice_repr(td(days=1, hours=2, minutes=3, seconds=4))
+    '1 day, 2 hours, 3 minutes, 4 seconds'
+    >>> nice_repr(td(days=1, seconds=1), "minimal")
+    '1d, 1s'
+    """
+    
+    result = ""
+    
+    weeks = timedelta.days / 7
+    days = timedelta.days % 7
+    hours = timedelta.seconds / 3600
+    minutes = (timedelta.seconds % 3600) / 60
+    seconds = timedelta.seconds % 60
+    
+    if display == 'minimal':
+        words = ["w", "d", "h", "m", "s"]
+    elif display == 'short':
+        words = [" wks", " days", " hrs", " min", " sec"]
+    else:
+        words = [" weeks", " days", " hours", " minutes", " seconds"]
+    
+    values = [weeks, days, hours, minutes, seconds]
+    
+    for i in range(4):
+        if values[i]:
+            if values[i] == 1 and len(words[i]) > 1:
+                result += "%i%s, " % (values[i], words[i].rstrip('s'))
+            else:
+                result += "%i%s, " % (values[i], words[i])
+    
+    return result[:-2]
+
+def parse(string):
+    """
+    Parse a string into a timedelta object.
+    """
+    d = re.match(r'((?P<days>\d+) days )?(?P<hours>\d+):'
+                 r'(?P<minutes>\d+):(?P<seconds>\d+)',
+                 str(string))
+    if d: 
+        d = d.groupdict(0)
+    else:
+        # TODO: Test this, and make it prettier...
+        d = re.match(
+                     r'((?P<weeks>\d+)\W*w((ee)?k(s)?)(,)?\W*)?'
+                     r'((?P<days>\d+)\W*d(ay(s)?)?(,)?\W*)?'
+                     r'((?P<hours>\d+)\W*h(ou)?r(s)?(,)?\W*)?'
+                     r'((?P<minutes>\d+)\W*m(in(ute)?)?(s)?(,)?\W*)?'
+                     r'((?P<seconds>\d+)\W*s(ec(ond)?(s)?)?)?\W*',
+                     str(string)).groupdict()
+    return datetime.timedelta(**dict(( (k, int(v)) for k,v in d.items() 
+        if v is not None )))

File timedelta/templatetags/__init__.py

Empty file added.

File timedelta/templatetags/timedelta.py

+from django import template
+register = template.Library()
+
+# Don't really like using relative imports, but no choice here!
+from ..models import nice_repr
+
+@register.filter(name='timedelta')
+def timedelta(value, display="long"):
+    return nice_repr(value, display)

File timedelta/tests.py

+from unittest import TestCase
+import datetime
+from forms import TimedeltaFormField
+from widgets import TimedeltaWidget
+
+class TimedeltaWidgetTest(TestCase):
+    def test_render(self):
+        """
+        >>> t = TimedeltaWidget()
+        >>> t.render('', datetime.timedelta(days=1), {})
+        u'<input type="text" name="" value="1 day" />'
+        >>> t.render('', datetime.timedelta(days=0), {})
+        u'<input type="text" name="" />'
+        >>> t.render('', datetime.timedelta(seconds=1), {})
+        u'<input type="text" name="" value="1 second" />'
+        >>> t.render('', datetime.timedelta(seconds=10), {})
+        u'<input type="text" name="" value="10 seconds" />'
+        >>> t.render('', datetime.timedelta(seconds=30), {})
+        u'<input type="text" name="" value="30 seconds" />'
+        >>> t.render('', datetime.timedelta(seconds=60), {})
+        u'<input type="text" name="" value="1 minute" />'
+        >>> t.render('', datetime.timedelta(seconds=150), {})
+        u'<input type="text" name="" value="2 minutes, 30 seconds" />'
+        >>> t.render('', datetime.timedelta(seconds=1800), {})
+        u'<input type="text" name="" value="30 minutes" />'
+        >>> t.render('', datetime.timedelta(seconds=3600), {})
+        u'<input type="text" name="" value="1 hour" />'
+        >>> t.render('', datetime.timedelta(seconds=3601), {})
+        u'<input type="text" name="" value="1 hour, 1 second" />'
+        >>> t.render('', datetime.timedelta(seconds=19800), {})
+        u'<input type="text" name="" value="5 hours, 30 minutes" />'
+        >>> t.render('', datetime.timedelta(seconds=91800), {})
+        u'<input type="text" name="" value="1 day, 1 hour, 30 minutes" />'
+        >>> t.render('', datetime.timedelta(seconds=302400), {})
+        u'<input type="text" name="" value="3 days, 12 hours" />'
+        """
+
+class TimedeltaFormFieldTest(TestCase):
+    def test_clean(self):
+        """
+        >>> t = TimedeltaFormField()
+        >>> t.clean('1 day')
+        datetime.timedelta(1)
+        >>> t.clean('1 days')
+        datetime.timedelta(1)
+        >>> t.clean('1 second')
+        datetime.timedelta(0, 1)
+        >>> t.clean('1 sec')
+        datetime.timedelta(0, 1)
+        >>> t.clean('10 seconds')
+        datetime.timedelta(0, 10)
+        >>> t.clean('30 seconds')
+        datetime.timedelta(0, 30)
+        >>> t.clean('1 minute, 30 seconds')
+        datetime.timedelta(0, 90)
+        >>> t.clean('2.5 minutes')
+        datetime.timedelta(0, 150)
+        >>> t.clean('2 minutes, 30 seconds')
+        datetime.timedelta(0, 150)
+        >>> t.clean('.5 hours')
+        datetime.timedelta(0, 1800)
+        >>> t.clean('30 minutes')
+        datetime.timedelta(0, 1800)
+        >>> t.clean('1 hour')
+        datetime.timedelta(0, 3600)
+        >>> t.clean('5.5 hours')
+        datetime.timedelta(0, 19800)
+        >>> t.clean('1 day, 1 hour, 30 mins')
+        datetime.timedelta(1, 5400)
+        >>> t.clean('8 min')
+        datetime.timedelta(0, 480)
+        >>> t.clean('3 days, 12 hours')
+        datetime.timedelta(3, 43200)
+        >>> t.clean('3.5 day')
+        datetime.timedelta(3, 43200)
+        >>> t.clean('1 week')
+        datetime.timedelta(7)
+        >>> t.clean('2 weeks, 2 days')
+        datetime.timedelta(16)
+        """

File timedelta/widgets.py

+from django import forms
+import datetime
+
+from models import nice_repr, parse
+
+class TimedeltaWidget(forms.TextInput):
+    def __init__(self, *args, **kwargs):
+        return super(TimedeltaWidget, self).__init__(*args, **kwargs)
+        
+    def render(self, name, value, attrs):
+        if value is None:
+            value = ""
+        elif isinstance(value, (str, unicode)):
+            pass
+        else:
+            if isinstance(value, int):
+                value = datetime.timedelta(seconds=value)
+            value = nice_repr(value)
+        return super(TimedeltaWidget, self).render(name, value, attrs)
+    
+    def _has_changed(self, initial, data):
+        """
+        We need to make sure the objects are of the canonical form.
+        
+        
+        """
+        if not isinstance(initial, datetime.timedelta):
+            initial = parse(initial)
+        if not isinstance(data, datetime.timedelta):
+            data = parse(data)
+        
+        return initial != data