Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@ model like so:
.. code:: python

from django.db import models
from dateutil.relativedelta import relativedelta
from relativedeltafield import RelativeDeltaField

class MyModel(models.Model):
rdfield=RelativeDeltaField()
rdfield = RelativeDeltaField()
rdchoices = RelativeDeltaField(choices=[
('P1M', '1 month'),
(relativedelta(months=2), '2 month'),
])

Then later, you can use it:

Expand Down Expand Up @@ -78,6 +83,10 @@ app to always force ``full_clean()`` on ``save()``, so you can be
sure that after a ``save()``, your fields are both normalized
and validated.

You can use `relativedeltafield.forms.RelativeDeltaChoiceField` to update
`RelativeDeltaField` using a `Select` widget. If you provide `choices` argument
to `RelativeDeltaField`, then `ModelForm` and `admin` will automatically use
`RelativeDeltaChoiceField`.

Limitations and pitfalls
------------------------
Expand Down
45 changes: 42 additions & 3 deletions src/relativedeltafield/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
import re
from datetime import timedelta

import django
import relativedeltafield.forms
from dateutil.relativedelta import relativedelta
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import ugettext_lazy as _

from datetime import timedelta
from dateutil.relativedelta import relativedelta

try:
from django.db.migrations.serializer import BaseSerializer
from django.db.migrations.writer import MigrationWriter
has_migration_serializer = True
except ImportError:
has_migration_serializer = False

try:
from django.utils.deconstruct import deconstructible
has_migration_deconstructible = True
except ImportError:
has_migration_deconstructible = False


__version__ = '1.1.2'

Expand Down Expand Up @@ -90,6 +104,14 @@ class RelativeDeltaField(models.Field):
}
description = _("RelativeDelta")

def __init__(self, *args, **kwargs):
if 'choices' in kwargs:
kwargs['choices'] = [
(self.to_python(value), label)
for value, label in
kwargs['choices']
]
super().__init__(*args, **kwargs)

def db_type(self, connection):
if connection.vendor == 'postgresql':
Expand Down Expand Up @@ -147,3 +169,20 @@ def from_db_value(self, value, expression, connection, context=None):
def value_to_string(self, obj):
val = self.value_from_object(obj)
return '' if val is None else format_relativedelta(val)

def formfield(self, *args, **kwargs):
kwargs.setdefault('choices_form_class', relativedeltafield.forms.RelativeDeltaChoiceField)
return super().formfield(*args, **kwargs)


if has_migration_serializer:
# Django 2.2 and up
class RelativeDeltaSerializer(BaseSerializer):
def serialize(self):
return repr(self.value), {'from dateutil.relativedelta import relativedelta'}

MigrationWriter.register_serializer(relativedelta, RelativeDeltaSerializer)

elif has_migration_deconstructible:
# Django 2.1 and lower
deconstructible(relativedelta)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this migration stuff needed, exactly?

I've used django-relativedelta in a few projects and it never required any special code.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makemigrations does not know how to serialise relativedelta if the model fields' arguments contains a relativedelta object, and this tells it how. choices are defined by a "list of (relativedelta, str) pair", so we need this when choices is defined. Without this, you'd get an error when doing makemigrations:

ValueError: Cannot serialize: relativedelta(days=+1)
There are some values Django cannot serialize into migration files.
For more, see https://docs.djangoproject.com/en/3.1/topics/migrations/#migration-serializing

Without this serialisation stuffs, default wouldn't work either RelativeDeltaField(default=relativedelta(days=1)).

24 changes: 24 additions & 0 deletions src/relativedeltafield/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import relativedeltafield
from dateutil.relativedelta import relativedelta
from django import forms


class RelativeDeltaChoiceField(forms.TypedChoiceField):
def _set_choices(self, value):
value = [(self.prepare_value(val), label) for val, label in value]
super()._set_choices(value)
choices = property(forms.TypedChoiceField._get_choices, _set_choices)

def to_python(self, value):
if value in self.empty_values:
return None
return relativedeltafield.parse_relativedelta(value)

def prepare_value(self, value):
if isinstance(value, relativedelta):
return relativedeltafield.format_relativedelta(value)
return super().prepare_value(value)

def valid_value(self, value):
value = self.prepare_value(value)
return super().valid_value(value)
72 changes: 72 additions & 0 deletions tests/test_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
from dateutil.relativedelta import relativedelta
from django import forms
from django.test import TestCase
from testapp.models import Interval, IntervalWithChoice

IntervalForm = forms.modelform_factory(model=IntervalWithChoice, fields=forms.ALL_FIELDS)


class RelativeDeltaFormFieldTest(TestCase):
def setUp(self):
self.obj = Interval.objects.create(value=relativedelta(years=1))

def test_unbound_form_rendering(self):
form = IntervalForm()

self.assertHTMLEqual(
str(form['value']),
'''
<select name="value" id="id_value">
<option value="" selected>---------</option>
<option value="P1M">1 month</option>
<option value="P3M">3 months</option>
<option value="P6M">6 months</option>
</select>
'''
)

def test_bound_form_rendering(self):
self.obj = Interval.objects.create(value=relativedelta(months=3))
form = IntervalForm(instance=self.obj)

self.assertHTMLEqual(
str(form['value']),
'''
<select name="value" id="id_value">
<option value="">---------</option>
<option value="P1M">1 month</option>
<option selected value="P3M">3 months</option>
<option value="P6M">6 months</option>
</select>
'''
)

def test_form_submission_string_choice(self):
self.obj = IntervalWithChoice.objects.create(value=relativedelta(months=1))
data = {'value': 'P3M'}
form = IntervalForm(data, instance=self.obj)
self.assertTrue(form.is_valid())
form.save()

self.obj.refresh_from_db()
self.assertEqual(self.obj.value, relativedelta(months=3))

def test_form_submission_relativedelta_choice(self):
self.obj = IntervalWithChoice.objects.create(value=relativedelta(months=1))
data = {'value': 'P6M'}
form = IntervalForm(data, instance=self.obj)
self.assertTrue(form.is_valid())
form.save()

self.obj.refresh_from_db()
self.assertEqual(self.obj.value, relativedelta(months=6))

def test_form_submission_empty(self):
self.obj = IntervalWithChoice.objects.create(value=relativedelta(months=1))
data = {'value': ''}
form = IntervalForm(data, instance=self.obj)
self.assertTrue(form.is_valid())
form.save()

self.obj.refresh_from_db()
self.assertIsNone(self.obj.value)
22 changes: 22 additions & 0 deletions tests/testproject/testapp/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 3.1.1 on 2020-09-28 19:01

import relativedeltafield
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Interval',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', relativedeltafield.RelativeDeltaField(blank=True, null=True)),
],
),
]
22 changes: 22 additions & 0 deletions tests/testproject/testapp/migrations/0002_intervalwithchoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 3.1.1 on 2020-09-28 22:05

from dateutil.relativedelta import relativedelta
from django.db import migrations, models
import relativedeltafield


class Migration(migrations.Migration):

dependencies = [
('testapp', '0001_initial'),
]

operations = [
migrations.CreateModel(
name='IntervalWithChoice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('value', relativedeltafield.RelativeDeltaField(blank=True, choices=[(relativedelta(months=+1), '1 month'), (relativedelta(months=+3), '3 months'), (relativedelta(months=+6), '6 months')], null=True)),
],
),
]
Empty file.
11 changes: 11 additions & 0 deletions tests/testproject/testapp/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
from dateutil.relativedelta import relativedelta
from django.db import models
from relativedeltafield import RelativeDeltaField


class Interval(models.Model):
value=RelativeDeltaField(null=True, blank=True)


class IntervalWithChoice(models.Model):
CHOICES = [
(relativedelta(months=1), '1 month'),
('P3M', '3 months'),
(relativedelta(months=6), '6 months'),
]
value=RelativeDeltaField(null=True, blank=True, choices=CHOICES)
9 changes: 8 additions & 1 deletion tests/testproject/testproject/admin.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from django.contrib import admin

from testapp.models import Interval
from testapp.models import Interval, IntervalWithChoice


@admin.register(Interval)
class IntervalAdmin(admin.ModelAdmin):
list_display = ['value']
list_filter = ['value']


@admin.register(IntervalWithChoice)
class IntervalWithChoiceAdmin(admin.ModelAdmin):
list_display = ['value']
list_filter = ['value']