Skip to content

Commit 60cde24

Browse files
author
Aaron Madison
committed
initial commit
0 parents  commit 60cde24

24 files changed

+698
-0
lines changed

.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.idea/*
2+
*.egg-info
3+
*.egg
4+
*.pyo
5+
*.pyc
6+
build/*
7+
dist/*
8+
example.db

LICENSE

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Copyright (c) 2011, IMT Computer Services
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without modification,
5+
are permitted provided that the following conditions are met:
6+
7+
1. Redistributions of source code must retain the above copyright notice,
8+
this list of conditions and the following disclaimer.
9+
10+
2. Redistributions in binary form must reproduce the above copyright
11+
notice, this list of conditions and the following disclaimer in the
12+
documentation and/or other materials provided with the distribution.
13+
14+
15+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
19+
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
22+
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.txt

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
2+
Sometimes when you develop apps for other people, they like to
3+
define their own sets of "rules".
4+
5+
For example, one customer might say the value of a certain field on
6+
a model might be valid when it is between 100 and 1000. Another customer
7+
might consider the same field valid only when the value is between
8+
500 and 1000.
9+
10+
That kind of validation is extremely difficult to do in a modelform's
11+
clean method like you would if you knew beforehand what the valid values
12+
were supposed to be.
13+
14+
This app lets you create some boundaries for various arbitrary parameters
15+
that are tied to some other arbitrary model. Each customer can then define
16+
what the rule parameters will be for their particular organization.
17+
18+
19+
Usage:
20+
First create a file called dynamic_actions.
21+
Inside dynamic actions, you must register your rule class
22+
with the dynamic_rules site.
23+
24+
The rule class must have the following attributes:
25+
key: a string to identify the rule class with the registry
26+
display_name: a name to use for the admin_form to show a readable name
27+
fields: a dictionary of field_names, and django form classes. This declares
28+
the parameters available.
29+
30+
Additionally, the rule class must accept a rule_model and model_to_check
31+
as initialization arguments, and have a run method that accepts
32+
*args and **kwargs.
33+
34+
To see the dynamic rules in action, syncdb from this project and fire
35+
up the admin. Create a rule tied to group_object_id: 1 (i.e. customer 1)
36+
and content type: 'customer'
37+
38+
Add a ModelToCheck model from the sample app that has a value that
39+
violates your rule. Check the runserver console and see that the
40+
violation printed.
41+
42+
This is best used in conjunction with django-dynamic-validation which lets you
43+
track and store violations to the rules, or django-dynamic-manipulation
44+
which lets you manipulate other data because of a triggered rule.

dynamic_rules/__init__.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
from dynamic_rules.sites import site
3+
4+
__all__ = ('site',)
5+
6+
VERSION = "0.1.0"
7+
8+
def autodiscover():
9+
from autoload import autodiscover as discover
10+
discover("dynamic_actions")

dynamic_rules/admin.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
2+
from django.contrib import admin
3+
4+
from djadmin_ext.helpers import BaseAjaxModelAdmin
5+
6+
from dynamic_rules import admin_forms, models
7+
8+
class RuleAdmin(BaseAjaxModelAdmin):
9+
form = admin_forms.RuleForm
10+
list_display = ('name', 'group_object')
11+
12+
admin.site.register(models.Rule, RuleAdmin)

dynamic_rules/admin_forms.py

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from django import forms
2+
3+
from djadmin_ext.admin_forms import BaseAjaxModelForm
4+
5+
from dynamic_rules import models, site
6+
7+
__all__ = ('RuleForm',)
8+
9+
class RuleForm(BaseAjaxModelForm):
10+
ajax_change_field = 'key'
11+
12+
def __init__(self, *args, **kwargs):
13+
super(RuleForm, self).__init__(*args, **kwargs)
14+
rule_choices = [(k, v.display_name) for k, v in site.rules.items()]
15+
self.fields['key'] = forms.ChoiceField(choices=[('', '---------')] + rule_choices)
16+
17+
@property
18+
def dynamic_fields(self):
19+
data = self.data or self.initial
20+
if 'key' in data:
21+
rule_class = site.get_rule_class(data['key'])
22+
self.set_dynamic_field_initial(rule_class)
23+
return rule_class.fields
24+
return {}
25+
26+
def set_dynamic_field_initial(self, rule_class):
27+
if self.instance.pk:
28+
for field_name, field in rule_class.fields.items():
29+
field.initial = self.instance.dynamic_fields[field_name]
30+
31+
def _get_dynamic_data_for_instance(self):
32+
dynamic_data = {}
33+
for field_name in self.dynamic_fields:
34+
dynamic_data[field_name] = self.cleaned_data.get(field_name)
35+
return dynamic_data
36+
37+
def save(self, commit=True):
38+
obj = forms.ModelForm.save(self, False)
39+
obj.dynamic_fields = self._get_dynamic_data_for_instance()
40+
if commit:
41+
obj.save()
42+
return obj
43+
44+
class Meta(object):
45+
model = models.Rule
46+
fields = ('name', 'key', 'group_object_id', 'content_type')

dynamic_rules/models.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from django.db import models
2+
from django.contrib.contenttypes import generic
3+
from django.contrib.contenttypes.models import ContentType
4+
5+
from django_fields import fields as helper_fields
6+
7+
from dynamic_rules import site
8+
9+
class RuleManager(models.Manager):
10+
11+
def get_by_group_object(self, obj):
12+
content_type = ContentType.objects.get_for_model(obj)
13+
return self.filter(content_type=content_type, group_object_id=obj.pk)
14+
15+
def get_by_key(self, group_object, key):
16+
return self.get_by_group_object(group_object).filter(key=key)
17+
18+
class Rule(models.Model):
19+
content_type = models.ForeignKey('contenttypes.ContentType')
20+
group_object_id = models.PositiveIntegerField(db_index=True)
21+
group_object = generic.GenericForeignKey(fk_field='group_object_id')
22+
23+
name = models.CharField(max_length=100)
24+
key = models.CharField(max_length=50)
25+
dynamic_fields = helper_fields.PickleField()
26+
27+
objects = RuleManager()
28+
29+
def __unicode__(self):
30+
return self.name
31+
32+
def run_action(self, validation_object, *args, **kwargs):
33+
rule_class = site.get_rule_class(self.key)
34+
rule_class(self, validation_object).run(*args, **kwargs)

dynamic_rules/sites.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
__all__ = ('site',)
3+
4+
class AlreadyRegistered(Exception):
5+
pass
6+
7+
class NotRegistered(Exception):
8+
pass
9+
10+
class Registry(object):
11+
12+
def __init__(self):
13+
self._registry = {}
14+
15+
def register(self, rule_class):
16+
if rule_class.key in self._registry:
17+
err_msg = "Rule key %s has already been registered as %s." % (rule_class.key, self._registry[rule_class.key])
18+
raise AlreadyRegistered(err_msg)
19+
20+
self._registry[rule_class.key] = rule_class
21+
22+
return rule_class
23+
24+
def unregister(self, rule_class):
25+
self._registry.pop(rule_class.key, None)
26+
27+
def get_rule_class(self, key):
28+
if key not in self._registry:
29+
raise NotRegistered("Rule key %s has not been registered." % key)
30+
return self._registry[key]
31+
32+
@property
33+
def rules(self):
34+
return self._registry
35+
36+
site = Registry()
37+
38+

dynamic_rules/tests/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
from dynamic_rules.tests.test_registry import *
3+
from dynamic_rules.tests.test_admin_forms import *
4+
from dynamic_rules.tests.test_models import *
+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
2+
import mock
3+
4+
from django.utils import unittest
5+
from django import forms
6+
7+
from djadmin_ext.admin_forms import BaseAjaxModelForm
8+
9+
from dynamic_rules import admin_forms, models, site
10+
11+
__all__ = ('AdminRuleFormTests', )
12+
13+
class RuleOne(object):
14+
fields = {
15+
'field_one': forms.IntegerField(),
16+
}
17+
key = "rule_one"
18+
display_name = "Rule One"
19+
20+
class RuleTwo(object):
21+
key = "rule_two"
22+
display_name = "Rule Two"
23+
24+
fields = {
25+
'field_two': forms.CharField(),
26+
'field_three': forms.CharField(),
27+
}
28+
29+
class AdminRuleFormTests(unittest.TestCase):
30+
31+
def setUp(self):
32+
self._original_registry = site._registry.copy()
33+
site._registry = {}
34+
site.register(RuleOne)
35+
site.register(RuleTwo)
36+
37+
def tearDown(self):
38+
site._registry = self._original_registry
39+
40+
def test_rule_form_is_a_subclass_of_base_ajax_model_form(self):
41+
self.assertTrue(issubclass(admin_forms.RuleForm, BaseAjaxModelForm))
42+
43+
def test_sets_ajax_change_field_to_rule(self):
44+
self.assertEqual('key', admin_forms.RuleForm.ajax_change_field)
45+
46+
def test_should_be_a_model_form_for_rule_model_and_limit_fields(self):
47+
self.assertEqual(models.Rule, admin_forms.RuleForm._meta.model)
48+
self.assertEqual(('name', 'key', 'group_object_id', 'content_type'), admin_forms.RuleForm._meta.fields)
49+
50+
def test_sets_rule_key_choices_to_registered_rules(self):
51+
form = admin_forms.RuleForm()
52+
self.assertItemsEqual([
53+
('', '---------'),
54+
(RuleOne.key, RuleOne.display_name),
55+
(RuleTwo.key, RuleTwo.display_name),
56+
], form.fields['key'].choices)
57+
58+
def test_return_empty_dict_when_no_rule_in_data_or_initial(self):
59+
form = admin_forms.RuleForm()
60+
self.assertEqual({}, form.dynamic_fields)
61+
62+
def test_returns_dict_of_rule_fields_from_dynamic_fields_property_when_rule_in_data(self):
63+
form = admin_forms.RuleForm(data={'key':'rule_one'})
64+
self.assertEqual(RuleOne.fields, form.dynamic_fields)
65+
66+
def test_returns_dict_of_rule_fields_from_dynamic_fields_property_when_rule_in_initial(self):
67+
form = admin_forms.RuleForm(initial={'key':'rule_two'})
68+
self.assertEqual(RuleTwo.fields, form.dynamic_fields)
69+
70+
def test_data_trumps_initial_when_getting_rule_class_in_dynamic_fields_property(self):
71+
form = admin_forms.RuleForm(data={'key':'rule_one'}, initial={'key': 'rule_two'})
72+
self.assertEqual(RuleOne.fields, form.dynamic_fields)
73+
74+
def test_sets_initial_data_on_form_field_to_matching_saved_instance_dynamic_fields_value(self):
75+
expected_value = 'value_one'
76+
instance = models.Rule(pk=1, key="rule_one", dynamic_fields={'field_one': expected_value})
77+
form = admin_forms.RuleForm(instance=instance)
78+
self.assertEqual(expected_value, form.fields['field_one'].initial)
79+
80+
def test_does_not_set_initial_data_on_form_fields_when_no_saved_instance(self):
81+
instance = models.Rule(key="rule_one", dynamic_fields={'field_one': "value_one"})
82+
form = admin_forms.RuleForm(instance=instance)
83+
self.assertEqual(None, form.fields['field_one'].initial)
84+
85+
def test_sets_instance_dynamic_fields_to_dict_of_cleaned_data_dynamic_field_values(self):
86+
dynamic_fields = {'field_two': "value_two", 'field_three': "value_three"}
87+
form_data = dict(key="rule_two", name="my_rule", **dynamic_fields)
88+
89+
form = admin_forms.RuleForm(data=form_data)
90+
form.cleaned_data = form_data
91+
92+
form_dynamic_data = form._get_dynamic_data_for_instance()
93+
self.assertEqual(dynamic_fields, form_dynamic_data)
94+
95+
@mock.patch('dynamic_rules.admin_forms.RuleForm._get_dynamic_data_for_instance')
96+
def test_sets_dynamic_fields_on_model_and_returns_model_in_save(self, _get_dynamic_data_for_instance):
97+
form = admin_forms.RuleForm()
98+
model_instance = mock.Mock(spec_set=models.Rule)
99+
100+
with mock.patch('django.forms.ModelForm.save') as modelform_save:
101+
modelform_save.return_value = model_instance
102+
saved_model = form.save(commit=False)
103+
104+
self.assertEqual(model_instance.dynamic_fields, _get_dynamic_data_for_instance.return_value)
105+
self.assertEqual(model_instance, saved_model)
106+
107+
def test_calls_save_on_base_model_form_with_instance_and_commit_equals_false(self):
108+
form = admin_forms.RuleForm()
109+
model_instance = mock.Mock(spec_set=models.Rule)
110+
111+
with mock.patch('django.forms.ModelForm.save') as modelform_save:
112+
modelform_save.return_value = model_instance
113+
form.save(commit=False)
114+
115+
modelform_save.assert_called_once_with(form, False)
116+
self.assertFalse(model_instance.save.called)
117+
118+
def test_saves_model_when_commit_is_true(self):
119+
model_instance = mock.Mock(spec_set=models.Rule)
120+
form = admin_forms.RuleForm()
121+
122+
with mock.patch('django.forms.ModelForm.save') as modelform_save:
123+
modelform_save.return_value = model_instance
124+
form.save()
125+
126+
modelform_save.assert_called_once_with(form, False)
127+
model_instance.save.assert_called_once_with()

0 commit comments

Comments
 (0)