diff --git a/.travis.yml b/.travis.yml index 96802cf..a5d4944 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,47 +1,35 @@ sudo: false language: python - -python: - - 2.7 - - 3.3 - - 3.4 - - 3.5 +python: 3.5 env: - - DJANGO=1.6 - - DJANGO=1.7 - - DJANGO=1.8 - -matrix: - exclude: - - python: 3.5 - env: DJANGO=1.6 - - python: 3.5 - env: DJANGO=1.7 - include: - - python: 3.4 - env: DJANGO=1.8 COVERAGE=true COVERALLS_REPO_TOKEN=LdECqqwg7eelQx9w8gvooUZCFIaCqGZCv - allow_failures: - - env: COVERAGE=true + matrix: + - ENV=py27-django15 + - ENV=py27-django16 + - ENV=py27-django17 + - ENV=py27-django18 + - ENV=py27-django19 + - ENV=py27-django110 + - ENV=py27-django111 + - ENV=py33-django15 + - ENV=py33-django16 + - ENV=py33-django17 + - ENV=py33-django18 + - ENV=py34-django15 + - ENV=py34-django16 + - ENV=py34-django17 + - ENV=py34-django18 + - ENV=py34-django19 + - ENV=py34-django110 + - ENV=py34-django111 + - ENV=py35-django18 + - ENV=py35-django19 + - ENV=py35-django110 + - ENV=py35-django111 + - ENV=coverage install: - - pip install flake8 - - pip install -q "Django>=$DJANGO,<$DJANGO.99" - - make install - -script: - - make flake8 - - make test - -after_script: - - if [ "$COVERAGE" == "true" ]; then - pip install --quiet python-coveralls; - make coverage; - coverage report; - coveralls --ignore-errors; - fi + - pip install tox -notifications: - email: - - hannes@5monkeys.se +script: tox -e $ENV diff --git a/README.rst b/README.rst index d2cf40d..afaf761 100644 --- a/README.rst +++ b/README.rst @@ -37,6 +37,32 @@ Create an Enum-class and pass it as first argument to the Django model EnumField class Beer(models.Model): style = enum.EnumField(BeerStyle, default=BeerStyle.LAGER) +You can also set default value on your enum class using ``__default__`` +attribute + +.. code:: python + + class BeerStyle(enum.Enum): + LAGER = 0 + STOUT = 1 + WEISSBIER = 2 + + __default__ = LAGER + + + class Beer(models.Model): + style_default_lager = enum.EnumField(BeerStyle) + style_default_stout = enum.EnumField(BeerStyle, default=BeerStyle.STOUT) + + +When you set __default__ attribute, you can access default value via +``.default()`` method of your enum class + +.. code:: python + + assert BeerStyle.default() == BeerStyle.LAGER + + .. code:: python Beer.objects.create(style=BeerStyle.STOUT) diff --git a/django_enumfield/db/fields.py b/django_enumfield/db/fields.py index a2ab19e..232cede 100644 --- a/django_enumfield/db/fields.py +++ b/django_enumfield/db/fields.py @@ -25,8 +25,8 @@ class EnumField(base): def __init__(self, enum, *args, **kwargs): kwargs['choices'] = enum.choices() - if 'default' not in kwargs: - kwargs['default'] = enum.default() + if enum.default() is not None: + kwargs.setdefault('default', enum.default()) self.enum = enum super(EnumField, self).__init__(self, *args, **kwargs) @@ -78,6 +78,8 @@ def _setup_validation(self, sender, **kwargs): enum = self.enum def set_enum(self, new_value): + if isinstance(new_value, models.NOT_PROVIDED): + new_value = None if hasattr(self, private_att_name): # Fetch previous value from private enum attribute. old_value = getattr(self, private_att_name) @@ -88,6 +90,7 @@ def set_enum(self, new_value): if new_value is not None and not isinstance(new_value, Enum): new_value = enum.get(new_value) setattr(self, private_att_name, new_value) + self.__dict__[att_name] = new_value # Run validation for new value. validators.validate_valid_transition(enum, old_value, new_value) @@ -95,6 +98,7 @@ def get_enum(self): return getattr(self, private_att_name) def delete_enum(self): + self.__dict__[att_name] = None return setattr(self, private_att_name, None) if not sender._meta.abstract: diff --git a/django_enumfield/enum.py b/django_enumfield/enum.py index a918db1..4251ea7 100644 --- a/django_enumfield/enum.py +++ b/django_enumfield/enum.py @@ -26,6 +26,7 @@ class Enum(NativeIntEnum): """ A container for holding and restoring enum values """ __labels__ = {} + __default__ = None __transitions__ = {} def __ge__(self, other): @@ -80,13 +81,16 @@ def choices(cls, blank=False): @classmethod def default(cls): - """ Default Enum value. Override this method if you need another default value. + """ Default Enum value. Set default value to `__default__` attribute + of your enum class or override this method if you need another + default value. Usage: IntegerField(choices=my_enum.choices(), default=my_enum.default(), ... :return Default value, which is the first one by default. :rtype: enum member """ - return tuple(cls)[0] + if cls.__default__ is not None: + return cls(cls.__default__) @classmethod def field(cls, **kwargs): diff --git a/django_enumfield/tests/models.py b/django_enumfield/tests/models.py index cc21747..1735103 100644 --- a/django_enumfield/tests/models.py +++ b/django_enumfield/tests/models.py @@ -9,6 +9,8 @@ class LampState(Enum): OFF = 0 ON = 1 + __default__ = OFF + class Lamp(models.Model): state = EnumField(LampState) @@ -29,7 +31,18 @@ class PersonStatus(Enum): } +class PersonStatusDefault(Enum): + UNBORN = 0 + ALIVE = 1 + DEAD = 2 + REANIMATED = 3 + VOID = 4 + + __default__ = UNBORN + + class Person(models.Model): + example = models.CharField(max_length=100, default='foo') status = EnumField(PersonStatus, default=PersonStatus.ALIVE) def save(self, *args, **kwargs): @@ -42,12 +55,16 @@ class BeerStyle(Enum): STOUT = 1 WEISSBIER = 2 + __default__ = LAGER + class BeerState(Enum): FIZZY = 0 STALE = 1 EMPTY = 2 + __default__ = FIZZY + class Beer(models.Model): style = EnumField(BeerStyle) diff --git a/django_enumfield/tests/test_enum.py b/django_enumfield/tests/test_enum.py index 656fe67..1b20d13 100644 --- a/django_enumfield/tests/test_enum.py +++ b/django_enumfield/tests/test_enum.py @@ -1,5 +1,6 @@ from django.test.client import RequestFactory from django.db import IntegrityError +from django.db.models.fields import NOT_PROVIDED from django.forms import ModelForm, TypedChoiceField from django.test import TestCase from django.utils import six @@ -8,18 +9,23 @@ from django_enumfield.enum import Enum, BlankEnum from django_enumfield.exceptions import InvalidStatusOperationError from django_enumfield.tests.models import Person, PersonStatus, Lamp, \ - LampState, Beer, BeerStyle, BeerState, LabelBeer + LampState, Beer, BeerStyle, BeerState, LabelBeer, PersonStatusDefault class EnumFieldTest(TestCase): + def test_enum_field_init(self): - field = EnumField(PersonStatus) - self.assertEqual(field.default, PersonStatus.UNBORN) - self.assertEqual(len(PersonStatus.choices()), len(field.choices)) - field = EnumField(PersonStatus, default=PersonStatus.ALIVE) - self.assertEqual(field.default, PersonStatus.ALIVE) - field = EnumField(PersonStatus, default=None) - self.assertEqual(field.default, None) + for enum, default in { + PersonStatus: NOT_PROVIDED, + PersonStatusDefault: PersonStatusDefault.UNBORN, + }.items(): + field = EnumField(enum) + self.assertEqual(field.default, default) + self.assertEqual(len(enum.choices()), len(field.choices)) + field = EnumField(enum, default=enum.ALIVE) + self.assertEqual(field.default, enum.ALIVE) + field = EnumField(enum, default=None) + self.assertEqual(field.default, None) def test_enum_field_save(self): # Test model with EnumField WITHOUT __transitions__ @@ -83,6 +89,24 @@ def test_enum_field_del_save(self): self.assertEqual(beer.state, None) self.assertEqual(beer.style, BeerStyle.STOUT) + def test_enum_field_modelform_create(self): + class PersonForm(ModelForm): + class Meta: + model = Person + fields = ('status',) + + request_factory = RequestFactory() + request = request_factory.post('', data={'status': '2'}) + form = PersonForm(request.POST) + self.assertTrue(isinstance(form.fields['status'], TypedChoiceField)) + self.assertTrue(form.is_valid()) + person = form.save() + self.assertTrue(person.status, PersonStatus.DEAD) + + request = request_factory.post('', data={'status': '99'}) + form = PersonForm(request.POST, instance=person) + self.assertFalse(form.is_valid()) + def test_enum_field_modelform(self): person = Person.objects.create() @@ -148,7 +172,11 @@ def test_choices(self): self.assertEqual(blank, (BlankEnum.BLANK.value, BlankEnum.BLANK)) def test_default(self): - self.assertEqual(PersonStatus.default(), PersonStatus.UNBORN) + for enum, default in { + PersonStatus: None, + PersonStatusDefault: PersonStatusDefault.UNBORN, + }.items(): + self.assertEqual(enum.default(), default) def test_field(self): self.assertTrue(isinstance(PersonStatus.field(), EnumField)) diff --git a/django_enumfield/tests/urls.py b/django_enumfield/tests/urls.py new file mode 100644 index 0000000..454bfb7 --- /dev/null +++ b/django_enumfield/tests/urls.py @@ -0,0 +1 @@ +urlpatterns = () diff --git a/run_tests.py b/run_tests.py index 65d1ab2..97ab730 100644 --- a/run_tests.py +++ b/run_tests.py @@ -39,6 +39,8 @@ def main(): from django.test.utils import get_runner test_runner = get_runner(settings)(verbosity=2, interactive=True) + if '--failfast' in sys.argv: + test_runner.failfast = True failures = test_runner.run_tests(['django_enumfield']) sys.exit(failures) diff --git a/tox.ini b/tox.ini index b9bb558..bbd2485 100644 --- a/tox.ini +++ b/tox.ini @@ -1,67 +1,32 @@ [tox] -envlist = py27_django15, py27_django16, py27_django17, py27_django18, - py33_django15, py33_django16, py33_django17, py33_django18, - py34_django15, py34_django16, py34_django17, py34_django18 +envlist = + py{27,34}-django{15,16,17,18,19,110,111} + py33-django{15,16,17,18} + py35-django{18,19,110,111} + coverage [testenv] -commands = {envpython} setup.py test - -[testenv:py27_django15] -basepython = python2.7 -deps = - Django==1.5.7 - -[testenv:py27_django16] -basepython = python2.7 -deps = - Django==1.6.4 - -[testenv:py27_django17] -basepython = python2.7 -deps = - Django==1.7.1 - -[testenv:py27_django18] -basepython = python2.7 -deps = - Django==1.8.1 - -[testenv:py33_django15] -basepython = python3.3 -deps = - Django==1.5.7 - -[testenv:py33_django16] -basepython = python3.3 -deps = - Django==1.6.4 - -[testenv:py33_django17] -basepython = python3.3 -deps = - Django==1.7.1 - -[testenv:py33_django18] -basepython = python3.3 -deps = - Django==1.8.1 - -[testenv:py34_django15] -basepython = python3.4 -deps = - Django==1.5.7 - -[testenv:py34_django16] -basepython = python3.4 -deps = - Django==1.6.4 - -[testenv:py34_django17] -basepython = python3.4 -deps = - Django==1.7.1 - -[testenv:py34_django18] -basepython = python3.4 -deps = - Django==1.8.1 +deps= + django15: Django>=1.5,<1.6 + django16: Django>=1.6,<1.7 + django17: Django>=1.7,<1.8 + django18: Django>=1.8,<1.9 + django19: Django>=1.9,<1.10 + django110: Django>=1.10,<1.11 + django111: Django>=1.11,<1.12 +commands = make test + +[testenv:coverage] +basepython = python3.5 +passenv = TOXENV CI TRAVIS TRAVIS_* +deps = + Django>=1.8,<1.9 + flake8 + python-coveralls +setenv = + COVERALLS_REPO_TOKEN=LdECqqwg7eelQx9w8gvooUZCFIaCqGZCv +commands = + make flake8 + make coverage + coverage report + coveralls --ignore-errors