django-modelqueryform¶
django-modelqueryform is a flexible app that helps you build Q object generating forms.
It is a great tool if you want you users to be able to do filtered searches against your models.
Project¶
The project can be found at https://github.com/ckirby/django-modelqueryform
Documentation¶
The full documentation is at https://django-modelqueryform.readthedocs.org.
Requirements¶
- Django 1.11+
- Python 3.4+
Features¶
Useable default FormFields for ModelFields that:
- Have .choices defined or are inherently made of choices (ie. BooleanField and NullBooleanField)
- Are represented as numeric types (eg. IntegerField, FloatField, etc.)
- Text backed fields need code written to handle them. That is easy though, because:
Creation of FormFields, Q objects, and User readable query terms are completely customizable. You can target ModelFields:
- By name (If the field has specific requirements)
- By field type (Use the same widget or Q object builder for all CharFields)
Can follow Model relationships or treat relationship fields as .choices
Provides a new Field and Widget (RangeField, RangeWidget). These allow users to generate a __gte, __lte pair for the orm, optionally also including an __isnull
RangeField
- Dynamically generates min and max boundaries. (Aggregate Min and Max on the model field)
- If null=True on the ModelField allows user to indicate if they want to include null values in the query
RangeWidget
- Returns a MultiWidget with 2 NumberInput widgets (with min and max attributes)
Contents¶
Installation¶
Install django-modelqueryform using pip:
pip install django-modelqueryform
Add modelqueryform to your INSTALLED_APPS setting:
INSTALLED_APPS = (
...
'modelqueryform',
)
Usage¶
For these examples we will use the following models:
class MyModel(models.Model):
age = models.IntegerField()
employed = models.NullBooleanField()#Yes,No,Unknown
degree = models.CharField(max_length = 2, choices = [["HS", "High School"],
["C", "College"],
["G", "Graduate"],
["PG", "Post Graduate"]]
)
class MyInstitution(models.Model):
name = models.CharField(max_length=50)
accredited = models.BooleanField()
def __str__(self):
return "%s" % self.name
To use django-modelqueryform in a project import it into forms.py:
import modelqueryform
Then we can use it as a Base class for your forms:
class MyModelQueryForm(modelqueryform.ModelQueryForm):
model = MyModel
inclue = ['age','employed','degree']
Thats it! Instantiating MyModelQueryForm gives us a form with 3 widgets
- Age (RangeField using a RangeWidget)
- Employed (MultipleChoiceField using a CheckboxSelectMultiple widget)
- Degree (MultipleChoiceField using a CheckboxSelectMultiple widget)
Once the form is POSTed to the view it is used to filter your model:
query_form = MyModelQueryForm(request.POST)
my_models = query_form.process()
process([data_set=None]) generates a Q object which is a logical AND of the Q objects generated for each widget. It uses the resulting Q object to filter the associated model class.
Note
process() optionally accepts a QuerySet of a model class ‘x’ where isinstance(x, ‘form model class’) is True If no QuerySet is passed, the Q object will run against model.objects.all()
Using pretty_print_query() you get a dict() of the form {str(field.label): str(field values)} to parse into a template:
query_form = MyModelQueryForm(request.POST)
query_parameters = query_form.pretty_print_query()
pretty_print_query() also accepts an argument fields_to_print, a list of names that must be a subset of self.changed_data.
Working with Relations¶
django-modelqueryform can work with realtionship fields in two different ways, either following the relation or using the relationship field as a choice field.
Let’s add a new field to MyModel from the example above:
class MyModel(models.Model):
...
institution = models.ForeignKey('MyInstitution')
If we want our users to be able to select for (non)-accredited institions we would instantiate the form like so:
class MyModelQueryForm(modelqueryform.ModelQueryForm):
model = MyModel
inclue = ['age','employed','degree', 'institution__accredited']
traverse_fields = ['institution',]
Alternatively we can use the relationship field as a MultipleChoiceField:
class MyModelQueryForm(modelqueryform.ModelQueryForm):
model = MyModel
inclue = ['age','employed','degree', 'institution']
Warning
To make the choices for a relationship field, django-modelqueryform does an objects.distinct() call. Be aware of the size of the resulting QuerySet
Defaults¶
djagno-modelqueryform tries to provide meaningful default where it can. Default widgets, Q objects, and print representation exist for model fields that are stored as numeric values or have choices (either defined or by default, ie. BooleanField(s))
Note
See Customization for how to handle field type that don’t have defaults
Default Fields
Model Field | Form Field/Widget | Q Object | Print Representation |
---|---|---|---|
AutoField | RangeField / RangeWidget | AND([field__gte=min],[field__lte=max]), OR(field__isnull=True) | ‘MIN - MAX [(include empty values)]’ |
BigIntegerField | RangeField / RangeWidget | AND([field__gte=min],[field__lte=max]), OR(field__isnull=True) | ‘MIN - MAX [(include empty values)]’ |
BinaryField | |||
BooleanField | MultipleChoiceField / CheckboxSelectMultiple | OR([field=value],…) | ‘CHOICE1,CHOICE2,…CHOICEn’ |
CharField | |||
CommaSeparatedIntegerField | |||
DateField | |||
DateTimeField | |||
DecimalField | RangeField / RangeWidget | AND([field__gte=min],[field__lte=max]), OR(field__isnull=True) | ‘MIN - MAX [(include empty values)]’ |
EmailField | |||
FileField | |||
FilePathField | |||
FloatField | RangeField / RangeWidget | AND([field__gte=min],[field__lte=max]), OR(field__isnull=True) | ‘MIN - MAX [(include empty values)]’ |
ImageField | |||
IntegerField | RangeField / RangeWidget | AND([field__gte=min],[field__lte=max]), OR(field__isnull=True) | ‘MIN - MAX [(include empty values)]’ |
IPAddressField | |||
GenericIPAddressField | |||
NullBooleanField | MultipleChoiceField / CheckboxSelectMultiple | OR([field=value],…) | ‘CHOICE1,CHOICE2,…CHOICEn’ |
PositiveIntegerField | RangeField / RangeWidget | AND([field__gte=min],[field__lte=max]), OR(field__isnull=True) | ‘MIN - MAX [(include empty values)]’ |
PositiveSmallIntegerField | RangeField / RangeWidget | AND([field__gte=min],[field__lte=max]), OR(field__isnull=True) | ‘MIN - MAX [(include empty values)]’ |
SlugField | |||
SmallIntegerField | RangeField / RangeWidget | AND([field__gte=min],[field__lte=max]), OR(field__isnull=True) | ‘MIN - MAX [(include empty values)]’ |
TextField | |||
TimeField | |||
URLField | |||
ForeignKey | MultipleChoiceField / CheckboxSelectMultiple | OR([field=value],…) | ‘CHOICE1,CHOICE2,…CHOICEn’ |
ManyToManyField | MultipleChoiceField / CheckboxSelectMultiple | OR([field=value],…) | ‘CHOICE1,CHOICE2,…CHOICEn’ |
OneToOneField | MultipleChoiceField / CheckboxSelectMultiple | OR([field=value],…) | ‘CHOICE1,CHOICE2,…CHOICEn’ |
Customization¶
Customization is neccessary in django-modelqueryform in instances where the default FormField and filters are insufficient or not avilable for model fields that you want to expose to querying
Note
There are no defaults for Model fields that are represented as text and have no choices
You can customize three different aspects of django-modelqueryform Each of these aspects can be customized either by Model Field or Model Field type
- Form field builder
- build_FIELD(model_field)
- build_type_FIELDTYPE(model_field)
- Filter builder
- filter_FIELD(field_name, values)
- filter_type_FIELDTYPE(field_name, values)
- Pretty Print builder
- print_FIELD(field_name, values)
- print_type_FIELDTYPE(field_name, values)
Warning
For fields that have no default you must implement a field builder and a filter builder
For these examples we will use the following Model:
class MyModel(models.Model):
first_name = models.CharField(max_length=15)
last_name = models.CharField(max_length=15)
And the following ModelQueryForm:
class MyModelQueryForm(modelqueryform.ModelQueryForm):
model = MyModel
inclue = ['first_name','last_name']
Form Field Builder¶
This should return a form field object.
By Name:
def build_first_name(model_field):
return CharField(label=model_field.verbose_name,
max_length=model_field.max_length,
required=False
)
Note
If this is all we customize for the example MyModelQueryForm it will raise a NotImplementedError because last_name does not have a field builder
By Type:
def build_type_charfield(model_field):
return CharField(label=model_field.verbose_name,
max_length=model_field.max_length,
required=False
)
Note
No NotImplementedError because this covers the type for both first_name and last_name If there is a name based builder and a type based builder for a field the named builder takes precedence
Filter Builder¶
This should return a Q object.
By Name:
def filter_first_name(field_name, values):
return Q(**{field_name + '__iexact': values})
By Type:
def filter_type_charfield(field_name, values):
return Q(**{field_name + '__contains': values})
Pretty Print Builder¶
By Name:
def print_first_name(field_name, values):
return "Matches %s" % values
By Type:
def print_type_charfield(field_name, values):
return "Contains %s" % values
Reference¶
ModelQueryField¶
-
class
modelqueryform.forms.
ModelQueryForm
(*args, **kwargs)[source]¶ ModelQueryForm builds a django form that allows complex filtering against a model.
Variables: Raises: ImproperlyConfigured – If model is missing
-
_build_form
(model, field_prepend=None)[source]¶ Iterates through model fields to generate modelqueryform fields matching self.include Recursively called to correctly build relationship spanning form fields
Parameters: - model (django.db.model) – Current model to inspect. Alwasy starts with self.model
- field_prepend (str) – Relation field name if using self.traverse
-
_build_form_field
(model_field, name)[source]¶ Build a form field for a given model field
Parameters: - model_field (django.db.models.fields) – field that the resulting form field will filter
- name (String) – The name for the form field (will match a value in self.include)
They type of FormField built is determined in the following order:
- build_FIELD(model_field) (FIELD is the ModelField name)
- build_type_FIELD(model_field) (FIELD is the ModelField type .lower() eg. ‘integerfield’, charfield’, etc.)
modelqueryform.utils.get_multiplechoice_field()
if model_field has .choicesmodelqueryform.utils.get_range_field()
if the ModelField type is in self.numeric_fields()modelqueryform.utils.get_multiplechoice_field()
if the ModelField type is in self.choice_fields()modelqueryform.utils.get_multiplechoice_field()
if the ModelField type is in self.rel_fields()
Warning
You must define either build_FIELD(model_field) or build_type_FIELD(model_field) for ModelFields that do not use a RangeField or MultipleChoiceField
Returns: FormField Raises: NotImplementedError – For fields that do not have a default ModelQueryForm field builder and no custom field builder can be found
-
_test_filter_func_is_Q
(filter_func)[source]¶ Make sure that a filter is a Q object
Parameters: filter_func (Q) – Object to test Raises: TypeError – if filter is not a Q object
-
build_query_from_filters
(filters)[source]¶ Generate a Q object that is a logical AND of a list of Q objects
Note
Override this method to build a more complex Q object than AND(filters.values())
Parameters: filters (dict) – Dict of {Form field name: Q object,…} Returns Q: AND(filters.values()) Raises: TypeError – if any value in the filters dict is not a Q object
-
choice_fields
()[source]¶ Get a list of model fields backed by choice values (Boolean types)
Returns list: Model Field types that are backed by a boolean
-
get_filters
()[source]¶ Get a dict of the POSTed form values as Q objects Form fields will be evaluated in the following order to generate a Q object:
- filter_FIELD(field_name, values) (FIELD is the ModelField name)
- filter_type_FIELD(field_name, values) (FIELD is the ModelField type .lower() eg. ‘integerfield’, charfield’, etc.)
modelqueryform.utils.get_range_field_filter()
if the FormField is a RangeFieldmodelqueryform.utils.get_multiplechoice_field_filter()
if the FormField is a MultipleChoiceField
Warning
You must define either filter_FIELD(field, values) or filter_type_FIELD(field, values) for ModelFields that do not use a RangeField or MultipleChoiceField
Returns Dict: {Form field name: Q object,…} Raises: NotImplementedError – For fields that do not have a default ModelQueryForm filter builder and no custom filter builder can be found
-
get_multichoice_field_print
(form_field, cleaned_field_data)[source]¶ Default string representation of multichoice field
Parameters: - form_field – FormField
- cleaned_field_data (dict) – the cleaned_data for the field
Returns str: Comma delimited get_display_FIELD() for selected choices
-
get_range_field_print
(form_field, cleaned_field_data)[source]¶ Default string representation of multichoice field
Parameters: - form_field – FormField (Unused)
- cleaned_field_data (dict) – the cleaned_data for the field
Returns str: “MIN - MAX [(include empty values)]”
Make choices from a related
Parameters: model_field (ForeignKey, OneToOneField, ManyToManyField) – Field to generate choices from Returns list: [[field.pk, field.__str__()],…] Raises: TypeError – If model_field is not a relationship type
-
numeric_fields
()[source]¶ Get a list of model fields backed by numeric values
Returns list: Model Field types that are backed by a numeric
-
pretty_print_query
(fields_to_print=None)[source]¶ Get an OrderedDict to facilitate printing of generated filter
Parameters: fields_to_print (list) – List of names in changed_data Note
If fields_to_print == None, self.changed_data is used
Returns dict: {form field name: string representation of filter,…}
Raises: - NotImplementedError – For fields that do not have a default print builder and no custom print builder can be found
- ValueError – if any name in the field_to_print is not in self.changed_data
-
process
(data_set=None)[source]¶ Filter a QuerySet with the POSTed form values
Parameters: data_set (QuerySet (Same Model class as self.model)) – QuerySet to filter against Note
If data_set == None, self.model.objects.all() is used
Returns QuerySet: data_set.filter(Q object)
Raises: - ImproperlyConfigured – No data_set to filter
- TypeError – data_set is not an instance (using isinstance()) of self.model
-
RangeWidget¶
Utils¶
-
modelqueryform.utils.
get_choices_from_distinct
(model, field)[source]¶ Generate a list of choices from a distinct() call.
Parameters: - model (django.db.models.Model) – Model to use
- field (django Model Field) – Field whose .distinct values you want
Returns: list – the distinct values of the field in the model
-
modelqueryform.utils.
get_multiplechoice_field
(field, choices)[source]¶ Generate a MultipleChoiceField form element
Parameters: - field (django model field) – Model Field to use
- choices (iterable) – List of choices for form field
Returns: MultipleChoiceField
Raises: ValueError
-
modelqueryform.utils.
get_multiplechoice_field_filter
(field, values)[source]¶ Generate a model filter from a POSTed MultipleChoiceField
Parameters: - field (string) – orm field name
- values (list) – Selected values
Returns: Q – (OR(field: value),…)
-
modelqueryform.utils.
get_range_field
(model, field, name)[source]¶ Generate a RangeField form element
Parameters: - model (django.db.models.Model) – Model to generate a form element for
- field (django model field) – Model Field to use
- name – Name to use for the form field
- name – string
Returns: RangeField
-
modelqueryform.utils.
get_range_field_filter
(field, values)[source]¶ Generate a model filter from a POSTed RangeField
Parameters: - field (string) – orm field name
- values (dict) – RangeField values dict
Returns: Q – AND(OR(field__gte: min, field__lte: max),(field__isnull: allow_empty)
Given an orm relational representation ‘relational_field__field_name’ and the base model of the relation, return the actual terminal Field
Contributing¶
Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.
You can contribute in many ways:
Types of Contributions¶
Report Bugs¶
Report bugs at https://github.com/ckirby/django-modelqueryform/issues.
If you are reporting a bug, please include:
- Your operating system name and version.
- Any details about your local setup that might be helpful in troubleshooting.
- Detailed steps to reproduce the bug.
Fix Bugs¶
Look through the GitHub issues for bugs. Anything tagged with “bug” is open to whoever wants to implement it.
Implement Features¶
Look through the GitHub issues for features. Anything tagged with “feature” is open to whoever wants to implement it.
Write Documentation¶
django-modelqueryform could always use more documentation, whether as part of the official django-modelqueryform docs, in docstrings, or even on the web in blog posts, articles, and such.
Submit Feedback¶
The best way to send feedback is to file an issue at https://github.com/ckirby/django-modelqueryform/issues.
If you are proposing a feature:
- Explain in detail how it would work.
- Keep the scope as narrow as possible, to make it easier to implement.
- Remember that this is a volunteer-driven project, and that contributions are welcome :)
Get Started!¶
Ready to contribute? Here’s how to set up django-modelqueryform for local development.
Fork the django-modelqueryform repo on GitHub.
Clone your fork locally:
$ git clone git@github.com:your_name_here/django-modelqueryform.git
Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:
$ mkvirtualenv django-modelqueryform $ cd django-modelqueryform/ $ python setup.py develop
Create a branch for local development:
$ git checkout -b name-of-your-bugfix-or-feature
Now you can make your changes locally.
5. When you’re done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:
$ flake8 modelqueryform tests
$ python setup.py test
$ tox
To get flake8 and tox, just pip install them into your virtualenv.
Commit your changes and push your branch to GitHub:
$ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature
Submit a pull request through the GitHub website.
Pull Request Guidelines¶
Before you submit a pull request, check that it meets these guidelines:
- The pull request should include tests.
- If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst.
- The pull request should work for Python 2.7, and 3.4, and for PyPy. Check https://travis-ci.org/ckirby/django-modelqueryform/pull_requests and make sure that the tests pass for all supported Python versions.
Credits¶
Development Lead¶
- Chaim Kirby <chaim.kirby@gmail.com>
Contributors¶
None yet. Why not be the first?
\ Sort by:\ best rated\ newest\ oldest\
\\
Add a comment\ (markup):
\``code``
, \ code blocks:::
and an indented block after blank line