Commits

Tyler Butler committed fd643af

LookupTableModel, ChoicesEnum, and unit tests.

After much experimentation, I've settled on this implementation, though it's
not perfect. Need to clean up the unit tests more.

  • Participants
  • Parent commits dc8d07b

Comments (0)

Files changed (10)

File .idea/runConfigurations/Nosetests.xml

     <option name="FOLDER_NAME" value="$PROJECT_DIR$/potpourri_test/tests" />
     <option name="TEST_TYPE" value="TEST_FOLDER" />
     <option name="PARAMS" value="--verbosity 2" />
+    <RunnerSettings RunnerId="PyDebugRunner" />
     <RunnerSettings RunnerId="PythonRunner" />
+    <ConfigurationWrapper RunnerId="PyDebugRunner" />
     <ConfigurationWrapper RunnerId="PythonRunner" />
     <method />
   </configuration>

File potpourri/classes.py

 import collections
+from flufl.enum import Enum
+from potpourri.mixins import _ChoicesMixin
 
 class CaseInsensitiveDict(collections.Mapping):
     """A dict whose keys are not case-sensitive."""
 
     def actual_key_case(self, k):
         return self._s.get(k.lower())
+
+
+class ChoicesEnum(Enum, _ChoicesMixin):
+    """
+    A class with enum-like qualities that can also be used to provide values for the ``choices`` argument to some
+    Django fields.
+    """
+    pass

File potpourri/functions.py

         return None
 
     return pattern.sub(lambda m: m.group()[:1] + " " + m.group()[1:], stringAsCamelCase)
+

File potpourri/management.py

             klass.install()
             if verbosity >= 1: print "Model '%s' installed.\n" % name
 
-post_syncdb.connect(install_model)
+post_syncdb.connect(install_model)

File potpourri/mixins.py

 from flufl.enum import Enum
+from model_utils import Choices
 from potpourri.functions import space_out_camel_case
 
 class InstallableModelMixin(object):
         names = tuple(space_out_camel_case(v.name) for v in cls)
         return tuple(zip(values, names))
 
+    @classmethod
+    def count(cls):
+        return len(cls.choices())
 
-class ChoicesEnum(Enum, _ChoicesMixin):
-    """
-    A class with enum-like qualities that can also be used to provide values for the ``choices`` argument to some
-    Django fields.
-    """
-    pass
+
+class IntegerChoices(Choices):
+    def __init__(self, *choices):
+        values = range(len(choices))
+        t = tuple(zip(values, choices, (space_out_camel_case(name) for name in choices)))
+        super(IntegerChoices, self).__init__(*t)
+
+    def __len__(self):
+        return len(self._choices)
+
+    def count(self):
+        return len(self._choices)

File potpourri/models.py

+from django import forms
+from django.contrib.humanize.templatetags.humanize import apnumber
+from django.core import exceptions
+from django.db import models
+from django.template.defaultfilters import pluralize
+from django.utils.text import capfirst
+
+from potpourri.mixins import InstallableModelMixin
+from potpourri.functions import space_out_camel_case
+
+__author__ = 'tyler@tylerbutler.com'
+
+class LookupTableModel(models.Model, InstallableModelMixin):
+    class Meta:
+        abstract = True
+
+    @classmethod
+    def install(cls, *args, **kwargs):
+        cls._attrs = []
+        if cls is not LookupTableModel:
+            try:
+                choices = getattr(cls, 'choices')
+            except AttributeError as e:
+                print "Error: A subclass of LookupTableModel must define a choices property."
+                raise
+            for choice in choices:
+                mgr = getattr(cls, 'objects')
+                mgr.get_or_create(id=int(choice))
+
+    def __getattr__(self, item):
+        if item == '__members__':
+            return self._attrs
+        elif item in self._attrs:
+            return getattr(self, item)
+        else:
+            return super(LookupTableModel, self).__getattribute__(item)
+
+    @classmethod
+    def make_choices(cls):
+        values = tuple(int(v) for v in cls.enum)
+        names = tuple(space_out_camel_case(v.name) for v in cls.enum)
+        return tuple(zip(values, names))
+
+
+# based on http://djangosnippets.org/snippets/1200/
+class MultiSelectFormField(forms.MultipleChoiceField):
+    widget = forms.CheckboxSelectMultiple
+
+    def __init__(self, *args, **kwargs):
+        self.max_choices = kwargs.pop('max_choices', 0)
+        super(MultiSelectFormField, self).__init__(*args, **kwargs)
+
+    def clean(self, value):
+        if not value and self.required:
+            raise forms.ValidationError(self.error_messages['required'])
+        if value and self.max_choices and len(value) > self.max_choices:
+            raise forms.ValidationError('You must select a maximum of %s choice%s.'
+            % (apnumber(self.max_choices), pluralize(self.max_choices)))
+        return value
+
+
+class MultiSelectField(models.Field):
+    __metaclass__ = models.SubfieldBase
+
+    def get_internal_type(self):
+        return "CharField"
+
+    def get_choices_default(self):
+        return self.get_choices(include_blank=False)
+
+    def _get_FIELD_display(self, field):
+        value = getattr(self, field.attname)
+        choicedict = dict(field.choices)
+
+    def formfield(self, **kwargs):
+        # don't call super, as that overrides default widget if it has choices
+        defaults = {'required': not self.blank, 'label': capfirst(self.verbose_name),
+                    'help_text': self.help_text, 'choices': self.choices}
+        if self.has_default():
+            defaults['initial'] = self.get_default()
+        defaults.update(kwargs)
+        return MultiSelectFormField(**defaults)
+
+    def get_db_prep_value(self, value):
+        if isinstance(value, basestring):
+            return value
+        elif isinstance(value, list):
+            return ",".join(value)
+
+    def to_python(self, value):
+        if isinstance(value, list):
+            return value
+        elif value is None:
+            return ''
+        return value.split(",")
+
+    def contribute_to_class(self, cls, name):
+        super(MultiSelectField, self).contribute_to_class(cls, name)
+        if self.choices:
+            func = lambda self, fieldname=name, choicedict=dict(self.choices):",".join(
+                [choicedict.get(value, value) for value in getattr(self, fieldname)])
+            setattr(cls, 'get_%s_display' % self.name, func)
+
+    def value_to_string(self, obj):
+        value = self._get_val_from_obj(obj)
+        return self.get_db_prep_value(value)
+
+    def validate(self, value, model_instance):
+        arr_choices = self.get_choices_selected(self.get_choices_default())
+        for opt_select in value:
+            if opt_select not in arr_choices:
+                raise exceptions.ValidationError(self.error_messages['invalid_choice'] % value)
+        return
+
+    def get_choices_selected(self, arr_choices=''):
+        if not arr_choices:
+            return False
+        list = []
+        for choice_selected in arr_choices:
+            list.append(choice_selected[0])
+        return list
+

File potpourri_test/models.py

 from django.db import models
+from potpourri.models import LookupTableModel
 from potpourri.fields import PickledObjectField
-from potpourri.mixins import ChoicesEnum
-
-class Gender(ChoicesEnum):
-    (Male,
-     Female,
-     Unknown) = range(3)
+from potpourri.classes import ChoicesEnum
 
 class PickleModel(models.Model):
     pickle = PickledObjectField()
     def __unicode__(self):
         return "PickleModel(id=%s):\npickle:\t%s" % (self.id, self.pickle)
 
+
+class Gender(ChoicesEnum):
+    (Male,
+     Female,
+     Unknown) = range(3)
+
+
 class Account(models.Model):
     name = models.CharField(max_length=25)
     gender = models.IntegerField(choices=Gender.choices())
 
     def __unicode__(self):
         return "%s, %s" % (self.name, Gender[self.gender].name)
+
+
+class Colors(ChoicesEnum):
+    (Red,
+     Blue,
+     Green,
+     Purple,
+     Yellow,
+     Orange) = range(6)
+
+
+class CarOptions(ChoicesEnum):
+    (AutomaticWindows,
+     AutomaticLocks,
+     AntiLockBrakes,
+     AirConditioning) = range(4)
+
+
+class CarOptionsTable(LookupTableModel):
+    choices = CarOptions
+    id = models.IntegerField(primary_key=True, choices=choices.choices())
+
+
+class CustomerInquiry(models.Model):
+    name = models.CharField(max_length=255)
+    preferences = models.ManyToManyField(CarOptionsTable)

File potpourri_test/settings.py

 import sys
 from path import path
 
-#APP_PACKAGE_NAME = 'potpourri'
-
 TEST_PROJECT_ROOT = path(__file__).dirname().abspath()
 APP_ROOT = TEST_PROJECT_ROOT.dirname().abspath()#.dirname().abspath()#.joinpath(APP_PACKAGE_NAME)
 
 }
 
 INSTALLED_APPS = (
-    #'django.contrib.auth',
-    #'django.contrib.contenttypes',
     'django_nose',
     'potpourri',
     'potpourri_test'

File potpourri_test/tests/tests.py

 import os
+
 os.environ['DJANGO_SETTINGS_MODULE'] = 'potpourri_test.settings'
 
 from django.db import connection
+from django.db.utils import IntegrityError
 from django.test.utils import setup_test_environment, teardown_test_environment
 from django.utils import unittest
-from potpourri_test.models import PickleModel, Account, Gender
+
+from potpourri_test.models import PickleModel, Account, Gender, CarOptionsTable, CustomerInquiry, CarOptions, Colors
 
 class PotpourriTestCase(unittest.TestCase):
     def setUp(self):
         connection.creation.destroy_test_db(self.test_db)
         teardown_test_environment()
 
+
 class PickleFieldTest(PotpourriTestCase):
     """Pickle Field Tests"""
+
     def setUp(self):
         super(PickleFieldTest, self).setUp()
-        self.dict = {'foo': 1, 'bar': 2, 'baz': 3}
+        self.dict = Colors.Green
         self.created_model, self.was_created = PickleModel.objects.get_or_create(pickle=self.dict)
 
     def test_create_model_with_pickle_field(self):
         self.assertEqual(len(retrieved_model), 1)
         self.assertEqual(retrieved_model[0].pickle, self.dict)
 
+    def test_pickled_enum(self):
+        model = PickleModel.objects.create(pickle=Colors.Blue)
+        q1 = PickleModel.objects.filter(pickle=Colors.Red).count()
+        q2 = PickleModel.objects.get(pickle=Colors.Blue)
 
-class LookupModelTestCase(PotpourriTestCase):
+        self.assertEqual(q1, 0)
+        self.assertEqual(q2.pickle, Colors.Blue)
+
+
+class ChoicesEnumUsageTest(PotpourriTestCase):
+    """Tests the usage of a ChoicesEnum subclass in a model."""
+
     def test_creation(self):
         acc = Account.objects.create(name='male user', gender=Gender.Male)
         Account.objects.create(name='female user', gender=Gender.Female)
 
         self.assertIsNot(acct.gender, Gender.Female)
         self.assertNotEqual(acct.gender, Gender.Female)
+
+
+class LookupTableModelTest(PotpourriTestCase):
+    def test_installation(self):
+        db_count = CarOptionsTable.objects.all().count()
+        choices_count = CarOptionsTable.choices.count()
+        self.assertEqual(db_count, choices_count)
+
+    def test_creation(self):
+        inquiry = CustomerInquiry.objects.create(name='test order')
+
+        inquiry.preferences.add(CarOptions.AirConditioning)
+        inquiry.save()
+        self.assertEqual(CustomerInquiry.objects.all().count(), 1)
+        self.assertEqual(CustomerInquiry.objects.all()[0].preferences.count(), 1)
+        return inquiry
+
+    def test_add_preferences(self):
+        self.test_creation()
+        inquiry = CustomerInquiry.objects.all()[0]
+        inquiry.preferences.add(CarOptions.AutomaticLocks)
+        inquiry.preferences.add(CarOptions.AutomaticWindows)
+        inquiry.save()
+
+        self.assertEqual(CustomerInquiry.objects.all()[0].preferences.count(), 3)
+
+    def test_add_already_exists(self):
+        inquiry = self.test_creation()
+        self.assertRaises(IntegrityError, inquiry.preferences.add, CarOptions.AirConditioning)
+
+    def test_lookup_method(self):
+        CustomerInquiry.objects.create(name='test order').preferences.add(CarOptions.AutomaticLocks)
+        inquiry = CustomerInquiry.objects.all()[0]
+
+        self.assertEqual(inquiry.preferences.count(), 1)
+        self.assertEqual(inquiry.preferences.all()[0], CarOptionsTable.objects.get(pk=CarOptions.AutomaticLocks))
+        self.assertEqual(CustomerInquiry.objects.filter(preferences__pk=CarOptions.AutomaticLocks).count(), 1)
+        self.assertRaises(CustomerInquiry.DoesNotExist,
+                          CustomerInquiry.objects.get, preferences__pk=CarOptions.AirConditioning)
+
+    def test_query(self):
+        #todo: break this into smaller tests
+        inquiry = self.test_creation()
+        q1 = CustomerInquiry.objects.filter(
+            preferences__pk=CarOptions.AutomaticWindows).count()
+        q2 = CustomerInquiry.objects.filter(
+            preferences__pk=CarOptions.AirConditioning).count()
+
+        self.assertEqual(inquiry.preferences.count(), 1)
+        self.assertEqual(inquiry.preferences.all()[0], CarOptionsTable.objects.get(pk=CarOptions.AirConditioning))
+        self.assertRaises(CustomerInquiry.DoesNotExist,
+                          CustomerInquiry.objects.get, preferences__pk=CarOptions.AutomaticLocks)
+        self.assertEqual(q1, 0)
+        self.assertEqual(q2, 1)

File requirements.txt

 python-dateutil==1.5
 Django>=1.3.1
 django-nose
+django-model-utils>=1.0.0
 
 # Only required to generate documentation
 sphinx