Skip to content

Commit 04162ae

Browse files
committed
First real commit
1 parent a5b95ad commit 04162ae

22 files changed

+2784
-1
lines changed

.gitignore

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Byte-compiled / optimized / DLL files
2+
__pycache__/
3+
*.py[cod]
4+
5+
# C extensions
6+
*.so
7+
8+
# Distribution / packaging
9+
.Python
10+
env/
11+
build/
12+
develop-eggs/
13+
dist/
14+
downloads/
15+
eggs/
16+
.eggs/
17+
lib/
18+
lib64/
19+
parts/
20+
sdist/
21+
var/
22+
*.egg-info/
23+
.installed.cfg
24+
*.egg
25+
26+
# PyInstaller
27+
# Usually these files are written by a python script from a template
28+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
29+
*.manifest
30+
*.spec
31+
32+
# Installer logs
33+
pip-log.txt
34+
pip-delete-this-directory.txt
35+
36+
# Unit test / coverage reports
37+
htmlcov/
38+
.tox/
39+
.coverage
40+
.coverage.*
41+
.cache
42+
nosetests.xml
43+
coverage.xml
44+
*,cover
45+
46+
# Translations
47+
*.mo
48+
*.pot
49+
50+
# Django stuff:
51+
*.log
52+
53+
# Sphinx documentation
54+
docs/_build/
55+
56+
# PyBuilder
57+
target/
58+
59+
# Vim temp files
60+
*~
61+
*.swp
62+
*.swo
63+

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

README.md

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,116 @@
11
# django-param-field
2-
A Django model field that uses a DSL to define, generate, and validate, custom forms
2+
3+
A Django model field that uses a DSL to define, generate, and validate, custom forms.
4+
5+
After reading this [fantastic presentation](http://es.slideshare.net/Siddhi/creating-domain-specific-languages-in-python)
6+
on how flexible can be a DSL to generate forms, I created a django implementation for
7+
some of my projects.
8+
9+
10+
## Requirement
11+
12+
It has been tested on
13+
14+
* Python 3
15+
* Django 1.9, 1.10
16+
17+
18+
## Installation
19+
20+
For now:
21+
22+
```bash
23+
$ python setup.py install
24+
```
25+
26+
And pypi as soon as it is available
27+
28+
```bash
29+
$ pip intall django-param-field
30+
```
31+
32+
## Usage
33+
34+
Add the param_field to INSTALLED_APPS
35+
36+
```python
37+
# settings.py
38+
INSTALLED_APPS = [
39+
...
40+
'param_field',
41+
]
42+
```
43+
44+
Add the field to your model:
45+
46+
```python
47+
from djang.db import models
48+
from param_field import ParamField, ParamDict
49+
50+
class CustomProduct(models.Model):
51+
name = models.CharField(max_length=44)
52+
...
53+
params = ParamField(blank=True, max_length=3000)
54+
```
55+
56+
Now that you have a working model create a new one, and add the
57+
parameters that it will accept:
58+
59+
```python
60+
params = """
61+
width: Dimmension-> max:50.0 min:5.0
62+
height: Dimmension-> max:40.0 min:3.0"""
63+
64+
CustomProduct.objects.create(
65+
name='Custom wooden box",
66+
parms=parms)
67+
```
68+
69+
70+
The formview handling this parameters, once it has retrieved the model,
71+
72+
```python
73+
from django.shortcuts import render, get_object_or_404
74+
from django.views.generic import FormView
75+
from django import forms
76+
from .models import CustomProduct
77+
78+
class CustomProductFormView(FormView):
79+
template_name = 'product_form.html'
80+
form_class = forms.Form
81+
82+
def dispatch(self, request, *args, **kwargs):
83+
"""Find requested CustomProduct it's needed both in post and get
84+
request so the form can be genereted"""
85+
pk = self.kwargs['pk']
86+
self.product = get_object_or_404(CustomProduct, pk=pk)
87+
return super().dispatch(request, *args, **kwargs)
88+
89+
def get_context_data(self, **kwargs):
90+
"""Send product info to template"""
91+
context = super().get_context_data(**kwargs)
92+
context['product'] = self.product
93+
return context
94+
95+
def get_form(self, form_class=None):
96+
"""Generate """
97+
return self.product.params.form(**self.get_form_kwargs())
98+
99+
def form_valid(self, form):
100+
"""Do what ever you want with the form, at this point it's a
101+
validated django form like any other"""
102+
custom_parameters = form.cleaned_data.copy()
103+
...
104+
```
105+
106+
Available parameters ....
107+
108+
## Testing
109+
110+
Once the app has been added to settings.py, you can run the tests with:
111+
112+
```bash
113+
$ python manage.py test paramfield
114+
```
115+
116+

param_field/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .models import ParamField
2+
from .params import ParamDict

param_field/admin.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from django.contrib import admin
2+
3+
# Register your models here.

param_field/apps.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ParamFieldConfig(AppConfig):
5+
name = 'param_field'

param_field/fields.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.db import models
2+
from django import forms
3+
from django.utils.translation import ugettext_lazy as _
4+
from django.core.exceptions import ObjectDoesNotExist
5+
6+

param_field/forms.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
from django import forms
2+
from .validators import *
3+
from .params import *
4+
5+
6+
class ForeignKeyField(forms.ModelChoiceField):
7+
pass
8+
9+
class ForeignKeyChoiceField(forms.ChoiceField):
10+
pass
11+
12+
class GeneratorField(forms.CharField):
13+
def to_python(self, value):
14+
"""Convert part generator slug"""
15+
from .models import PartGenerator
16+
try:
17+
obj = PartGenerator.objects.get(slug=value)
18+
return obj
19+
except (ValueError, TypeError, ObjectDoesNotExist):
20+
raise forms.ValidationError(
21+
_("Unknown generator %(value)s"),
22+
params={'value': value})
23+
24+
25+
26+
27+
FORM_FIELD_CLASS = {
28+
BoolParam: forms.BooleanField,
29+
TextParam: forms.CharField,
30+
TextAreaParam: forms.CharField,
31+
IntegerParam: forms.IntegerField,
32+
DecimalParam: forms.DecimalField,
33+
DimmensionParam: forms.DecimalField,
34+
GeneratorParam: ForeignKeyField,
35+
FileParam: ForeignKeyField,
36+
ImageParam: ForeignKeyField,
37+
}
38+
39+
40+
def expand_name(name):
41+
"""Convert param name string from 'this_is_a_name' to 'This is a name' so
42+
it can be used as the a field label
43+
44+
Arguments:
45+
-name (string):
46+
Returns:
47+
string
48+
"""
49+
chars = '*+-_=,;.:\\/[]{}()<>#@$%&'
50+
text = name.lower()
51+
for c in chars:
52+
text = text.replace(c, ' ')
53+
text = text[0].upper() + text[1:]
54+
return text
55+
56+
57+
58+
def ForeignKeyFieldFactory(param, name):
59+
"""Form field factory for all params using one"""
60+
choices = param.get_choices()
61+
if choices:
62+
# Construct a query that returns the models present in choices.
63+
choices = [getattr(c, param.model_field) for c in choices]
64+
query_args = {param.model_field+'__in': choices_field}
65+
query = param.model_class.objects.filter(query_args)
66+
else:
67+
query = param.model_class.objects.all()
68+
69+
field_args = {
70+
'help_text': param.help_text or None,
71+
'label': param.label or expand_name(name),
72+
'initial': param.default,
73+
'required': param.default is None,
74+
'queryset': query}
75+
76+
return forms.ModelChoiceField(**field_args)
77+
78+
79+
def StdFieldFactory(param, name):
80+
"""Form field factory for fields using standard fields, that is:
81+
Bool, Text, TextArea, Integer, Decimal, Dimmension"""
82+
field_args = {
83+
'help_text': param.help_text or None,
84+
'label': param.label or expand_name(name),
85+
'initial': param.default,
86+
'required': param.default is None,
87+
'validators': [ParamFormFieldValidator(param),]}
88+
89+
# Generate correct Field type or ChoiceField
90+
choices = param.get_choices()
91+
if choices:
92+
field_args['coerce'] = param.native_type
93+
field_args['choices'] = choices
94+
field_args.pop('validators', None)
95+
return forms.TypedChoiceField(**field_args)
96+
else:
97+
return FORM_FIELD_CLASS[type(param)](**field_args)
98+
99+
100+
def ParamFieldFactory(param, name):
101+
field_class = FORM_FIELD_CLASS[type(param)]
102+
if field_class == ForeignKeyField:
103+
return ForeignKeyFieldFactory(param, name)
104+
else:
105+
return StdFieldFactory(param, name)
106+
107+
108+
class ParamInputForm(forms.Form):
109+
"""
110+
Form with fields for the parametes of a PartGenerator, can
111+
be used to validate inputs
112+
113+
Arguments:
114+
- params: ParamDict
115+
116+
Examples:
117+
https://jacobian.org/writing/dynamic-form-generation/
118+
http://stackoverflow.com/questions/5871730/need-a-minimal-django-file-upload-example
119+
"""
120+
def __init__(self, *args, **kwargs):
121+
"""
122+
Argument:
123+
part_gen (PartGenerator): Form's part generator instance
124+
125+
"""
126+
self._params = kwargs.pop('params')
127+
super(ParamInputForm, self).__init__(*args, **kwargs)
128+
129+
# Add all visible fields from ParamDict to the form
130+
for name, param in self._params.items():
131+
if not param.visible or not isinstance(param, Param):
132+
continue
133+
self.fields[name] = ParamFieldFactory(param, name)
134+
135+
def clean(self):
136+
# When a form's non-string and non-required field is not supplied by
137+
# the user, None is asigned by default. Here all those fields are
138+
# removed after validation so the default value for the parameter
139+
# is used by PartGenerator.
140+
cd = dict((k, v) for k, v in self.cleaned_data.items() if v is not None)
141+
return self.cleaned_data
142+
self.cleaned_data = cd
143+
return self.cleaned_data
144+
145+

param_field/limits.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from decimal import Decimal
2+
3+
INT_MAX = 999999
4+
INT_MIN = -999999
5+
REAL_MAX = Decimal("999999.9999")
6+
REAL_MIN = Decimal("-999999.9999")
7+
STRING_MAX_LENGTH = 300
8+
LABEL_MAX_LENGTH = 40
9+
HELP_TEXT_MAX_LENGTH = 200
10+

0 commit comments

Comments
 (0)