├── .github └── workflows │ └── python-package.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── codecov.yml ├── dynamic_forms ├── __init__.py ├── apps.py ├── boundfields.py ├── formfields.py ├── forms.py ├── models.py ├── templates │ └── dynamic_forms │ │ └── widgets │ │ ├── formbuilder.html │ │ ├── formrender.html │ │ ├── subfield.html │ │ └── subfield_crispy.html ├── utils.py ├── views.py └── widgets.py ├── example ├── Dockerfile ├── docker-compose.yml ├── example │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── settings.py │ ├── templates │ │ └── example │ │ │ ├── base.html │ │ │ ├── build.html │ │ │ ├── index.html │ │ │ ├── respond.html │ │ │ ├── survey_detail.html │ │ │ └── survey_edit.html │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── manage.py └── requirements.txt ├── pytest.ini ├── requirements-dev.in ├── requirements-dev.txt ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── settings.py └── test_models.py └── tox.ini /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | python-version: ["3.7", "3.8", "3.9", "3.10"] 19 | django-version: ["32", "40"] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v1 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Install dependencies and testing utilities 30 | run: | 31 | sudo apt-get update && sudo apt-get install xmlsec1 32 | python -m pip install --upgrade pip mypy tox 33 | pip install -r requirements-dev.txt 34 | 35 | - name: Pylama linting 36 | run: pylama -o pytest.ini 37 | 38 | - name: MyPy check type annotations 39 | if: ${{ matrix.django-version }} != "40" 40 | run: | 41 | mypy dynamic_forms 42 | pip install django-stubs 43 | 44 | continue-on-error: true 45 | - name: Tests 46 | run: | 47 | python_version=${{ matrix.python-version }} 48 | python_version="$(tr -d "\." <<<$python_version)" 49 | tox -e py${python_version}-django${{ matrix.django-version }} 50 | - uses: codecov/codecov-action@v1 51 | with: 52 | file: ./cov.xml # optional 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[a-z] 2 | *.py[oc] 3 | *.log 4 | 5 | *.egg-info/ 6 | *.tox/ 7 | 8 | *.sqlite3 9 | *.db 10 | 11 | .env 12 | env/ 13 | 14 | .cache 15 | .coverage 16 | 17 | # Python release artifacts 18 | build/ 19 | dist/ 20 | _build/ 21 | 22 | tags 23 | 24 | .idea/ 25 | .vscode/ 26 | .pytest_cache/ 27 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v2.3.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Alexander Skvortsov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | include README.md 3 | recursive-include dynamic_forms *.html *.txt *.xml *.po *.mo *.js 4 | recursive-include dynamic_forms/templates/ * 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dynamic Django Forms 2 | 3 | ## Looking for Maintainer 4 | 5 | I haven't used Django in years, and am not actively maintaining this package. 6 | If anyone is interested in maintaining this package, either on this repo or 7 | as a fork, please let me know in a new issue! 8 | 9 | ## Anyways... 10 | 11 | **dynamic-django-forms** is a simple, reusable app that allows you to build (and respond to) dynamic forms, i.e. forms that have variable numbers and types of fields. A few examples of uses include: 12 | 13 | 1. Building and sending out surveys 14 | 2. Job applications where each job might have a different application forms 15 | 16 | ## Installation 17 | 18 | Install via pip: 19 | 20 | `pip install dynamic-django-forms` 21 | 22 | Add to settings.INSTALLED_APPS: 23 | 24 | ``` python 25 | INSTALLED_APPS = { 26 | "...", 27 | "dynamic_forms", 28 | "..." 29 | } 30 | ``` 31 | 32 | ## Components 33 | 34 | The main functionality of `dynamic-django-forms` is contained within 2 model fields: 35 | 36 | ### Form Builder 37 | 38 | `dynamic_forms.models.FormField` allows you to build and edit forms via a convenient UI, and stores them in JSON-Schema form. It is easy to use both through the admin panel and in any custom template webpage. 39 | 40 | Example Setup: 41 | 42 | ``` python 43 | from dynamic_forms.models import FormField 44 | 45 | class Survey: 46 | # Other Fields Here 47 | form = FormField() 48 | ``` 49 | 50 | Please note that JSON data can saved into the model field as a python `dict` or a valid JSON string. When the value is retrieved from the database, it will be provided as a `list` containing `dict`s for each dynamic form field. 51 | 52 | ### Form Response 53 | 54 | `dynamic_forms.models.ResponseField` allows you to render, and collect responses to, forms built with the Form Builder. It is currently only supported through custom views. All form responses are stored as a dict where the key is the question label, and the value is the user's input. 55 | 56 | Example Setup: 57 | 58 | Model Config: 59 | ``` python 60 | from django.db import models 61 | from dynamic_forms.models import ResponseField 62 | from otherapp.models import Survey 63 | 64 | class SurveyResponse: 65 | # Other Fields Here 66 | survey = models.ForeignKey(Survey, on_delete=models.CASCADE) # Optional 67 | response = ResponseField() 68 | ``` 69 | 70 | Please note that including a ForeignKey link from the model containing responses to the model containing forms isnt technically required; however, it is highly recommended and will make linking the two much easier 71 | 72 | ### Configuring ResponseFields with forms 73 | 74 | You must provide a valid JSON Schema to ResponseField's associated FormField at runtime. This is best done in the view where the dynamic form will be used. Generally speaking, this means you should: 75 | 1. Obtain the JSON Schema Form Data 76 | * Get an instance of a model containing a FormField that has already been built OR 77 | * Provide the form data as a constant 78 | 2. Intercept the Form instance used in the view where the dynamic form will be shown. This could be an automatically generated ModelForm (via a generic Class Based View), or a form instance you have made yourself. 79 | 3. Provide the JSON form data to the form field: 80 | * form_instance.fields['response_field_name_in_form'].add_fields(JSON_DATA) will add the fields in JSON_DATA to the existing fields in the dynamic form. 81 | * form_instance.fields['response_field_name_in_form].replace_fields(JSON_DATA) will remove any fields currently in the dynamic form and replace the with the fields in JSON_DATA 82 | 83 | An example of how to do this can be found in the DynamicFormMixin explained in the next section: 84 | 85 | ``` python 86 | from django.views.generic.edit import FormMixin 87 | 88 | class DynamicFormMixin(FormMixin): 89 | form_field = "form" 90 | form_pk_url_kwarg = "pk" 91 | 92 | response_form_fk_field = None 93 | response_field = "response" 94 | 95 | def _get_object_containing_form(self, pk): 96 | return self.form_model.objects.get(pk=pk) 97 | 98 | def get_form(self, *args, **kwargs): 99 | form = super().get_form(*args, **kwargs) 100 | # Get instance of model containing form used for this response. Save this object as an instance variable for use in form_valid method 101 | form_instance_pk = self.kwargs[self.form_pk_url_kwarg] 102 | self.form_instance = self._get_object_containing_form(form_instance_pk) 103 | # Get json form configuration from form-containing object 104 | json_data = getattr(self.form_instance, self.form_field) 105 | # Add fields in JSON to dynamic form rendering field. 106 | form.fields[self.response_field].add_fields(json_data) 107 | return form 108 | 109 | def form_valid(self, form): 110 | action = form.save(commit=False) 111 | action.survey = self.form_instance 112 | action.save() 113 | return super().form_valid(form) 114 | ``` 115 | 116 | #### Configuration Shortcut 117 | 118 | The process of configuring ResponseFields with forms is somewhat complicated, so a shortcut is provided. 119 | 120 | `dynamic_forms.views.DynamicFormMixin` can be added to Class Based Views extending from `django.views.generic.edit.CreateView` and `django.views.generic.edit.UpdateView`, and will automatically complete configure the dynamic form provided that: 121 | 122 | 1. The model containing the ResponseField has a ForeignKey link to a model containing the FormField. 123 | 2. The following attributes are provided: 124 | 1. `form_model`: The relevant model (not instance) containing a FormField with the wanted dynamic form configuration. I.e. which model is the survey defined in? Default: `None` 125 | 2. `form_field`: The attribute of `form_model` that contains the FormField. I.e. Which field in the Survey model contains the form? Default: `form` 126 | 3. `form_pk_url_kwarg` The [URL Keyword Argument](https://docs.djangoproject.com/en/2.2/topics/http/urls/#passing-extra-options-to-view-functions) containing the primary key of the instance of `form_model` that contains the dynamic form we want? I.e. Which survey are we responding to? Default: `pk` 127 | 4. `response_form_fk_field` The attribute of the model which contains the ResponseField that links via ForeignKey to the model containing the FormField. I.e. Which attribute of the Survey Response model links to the Survey model? Default: `None` 128 | 5. `response_field` The attribute of the Response model which contains the ResponseField. I.e. which attribute of the Survey Response model contains the actual responses? Default: `response` 129 | 130 | Example: 131 | 132 | ``` python 133 | class RespondView(DynamicFormMixin, CreateView): 134 | model = SurveyResponse 135 | fields = ['response'] 136 | template_name = "example/respond.html" 137 | 138 | form_model = Survey 139 | form_field = "form" 140 | form_pk_url_kwarg = "survey_id" 141 | response_form_fk_field = "survey" 142 | response_field = "response" 143 | 144 | def get_success_url(self): 145 | return reverse('survey_detail', kwargs={"survey_id": self.form_instance.pk}) 146 | ``` 147 | 148 | ### Django Crispy Forms Support 149 | 150 | If you are using [Django Crispy Forms](https://github.com/django-crispy-forms/django-crispy-forms) to make your forms look awesome, set use the following setting: 151 | 152 | `USE_CRISPY = True` (false by default) 153 | 154 | Please note that you are responsible for importing any CSS/JS libraries needed by your chosen crispy template pack into the templates where (e.x. bootstrap, uni-form, foundation). 155 | 156 | ## Fields Supported and Limitations 157 | 158 | `dynamic-django-forms` currently supports the following field types: 159 | 160 | | Description | JSON | 161 | |----------------|----------------| 162 | | Checkbox Group | checkbox-group | 163 | | Date Field | date | 164 | | Hidden Input | hidden | 165 | | Number | number | 166 | | Radio Group | radio-group | 167 | | Select | select | 168 | | Text Field | text | 169 | | Email Field | email | 170 | | Text Area | textarea | 171 | 172 | The only major limitation of `dynamic-django-forms`, which is also one of its major features, is the dissociation of dynamic form questions and responses. 173 | 174 | Pros: 175 | 176 | * Responses cannot be changed after submission 177 | * Dynamic forms can be edited, removing, changing, or adding questions, without affecting prior responses 178 | 179 | Cons: 180 | 181 | * Responses cannot be changed after submission 182 | 183 | ## Custom JS for FormBuilder 184 | 185 | On `settings.py` you can use a variable to inject custom JS code before the form builder is initialized. Note that the `options` variable. Note that when this custom JS runs, the following variables are available: 186 | 187 | - `textArea`: This is a hidden textarea input used to submit the JSON form schema. 188 | - `options`: This is a [FormBuilder Options object](https://formbuilder.online/docs/) that you can override and modify to change how the form is displayed. 189 | 190 | ``` 191 | DYNAMIC_FORMS_CUSTOM_JS = 'console.log(1)' 192 | ``` 193 | 194 | ## Example Site 195 | 196 | To run an example site, run `cd example && docker-compose up`. If you do not use docker, you can manually install the requirements with `pip install -r example/requirements.txt` and run the site with `python example/manage.py runserver`. 197 | 198 | ## Planned Improvements 199 | 200 | * Support for some HTML Elements 201 | * Headers 202 | * Paragraph Text 203 | * Dividers 204 | * Support for the following fields: 205 | * Color Field 206 | * Telephone Field 207 | * Star Rating 208 | * Support for "Other" option on radio groups, checkbox groups, and select dropdowns 209 | * User can select "other", at which point an inline text-type input will appear where they can put a custom choice 210 | * Ability to provide default JSON form config via: 211 | * String 212 | * Dict 213 | * File 214 | * Remote URL 215 | * DynamicFormMixin should support slugs 216 | * Ability to customize JSONBuilder settings through Django settings 217 | * Extensive Automated Testing 218 | 219 | ### Possible Improvements 220 | 221 | * Support File Upload Field 222 | 223 | ## Credits 224 | 225 | Huge thanks to Kevin Chappell & Team for developing the awesome open source [Form Builder UI](https://github.com/kevinchappell/formBuilder)! 226 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: 4 | default: 5 | enabled: no -------------------------------------------------------------------------------- /dynamic_forms/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.11" 2 | -------------------------------------------------------------------------------- /dynamic_forms/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DynamicFormsConfig(AppConfig): 5 | name = 'dynamic_forms' 6 | -------------------------------------------------------------------------------- /dynamic_forms/boundfields.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.forms.boundfield import BoundField 3 | from django.template.loader import render_to_string 4 | 5 | 6 | class MultiValueBoundField(BoundField): 7 | def _subfield_as_widget(self, sub_bf): 8 | if getattr(settings, "USE_CRISPY", False): 9 | template = "dynamic_forms/widgets/subfield_crispy.html" 10 | else: 11 | template = "dynamic_forms/widgets/subfield.html" 12 | if sub_bf.field_type == 'HTMLField' or sub_bf.label is None: 13 | label = '' 14 | help_text = '' 15 | else: 16 | label = sub_bf.label_tag() 17 | help_text = sub_bf.field.help_text 18 | 19 | return render_to_string( 20 | template, 21 | context={ 22 | 'label': label, 23 | 'field': sub_bf, 24 | 'help_text': help_text 25 | } 26 | ) 27 | 28 | def as_widget(self, widget=None, attrs=None, only_initial=False): 29 | subfield_widgets = [] 30 | for i, subfield in enumerate(self.field.fields): 31 | sub_bf = BoundField(self.form, subfield, "{}_{}".format(self.name, i)) 32 | sub_bf.field_type = subfield.__class__.__name__ 33 | sub_bf.label = subfield.label 34 | subfield_widgets.append(self._subfield_as_widget(sub_bf)) 35 | return render_to_string( 36 | "dynamic_forms/widgets/formrender.html", 37 | context={'subfields': subfield_widgets} 38 | ) 39 | -------------------------------------------------------------------------------- /dynamic_forms/formfields.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .boundfields import MultiValueBoundField 3 | from .widgets import FormBuilderWidget, FormRenderWidget 4 | from .utils import gen_fields_from_json 5 | 6 | 7 | class FormBuilderField(forms.CharField): 8 | def __init__(self, *args, **kwargs): 9 | kwargs['widget'] = FormBuilderWidget 10 | return super().__init__(*args, **kwargs) 11 | 12 | 13 | class FormRenderField(forms.MultiValueField): 14 | def __init__(self, form_json=[], required=False, **kwargs): 15 | kwargs['error_messages'] = { 16 | 'incomplete': 'Please fill in all required fields.', 17 | } 18 | kwargs['fields'] = gen_fields_from_json(form_json) 19 | kwargs['label'] = "" 20 | kwargs['require_all_fields'] = False 21 | kwargs['required'] = required 22 | del kwargs['max_length'] 23 | super().__init__(**kwargs) 24 | self.configure_widget() 25 | 26 | def get_bound_field(self, form, field_name): 27 | return MultiValueBoundField(form, self, field_name) 28 | 29 | def configure_widget(self): 30 | widgets = [field.widget for field in self.fields] 31 | self.widget = FormRenderWidget(widgets) 32 | 33 | def _configure_new_fields(self, fields): 34 | for f in fields: 35 | f.error_messages.setdefault('incomplete', self.error_messages['incomplete']) 36 | if self.disabled: 37 | f.disabled = True 38 | if self.require_all_fields: 39 | # Set 'required' to False on the individual fields, because the 40 | # required validation will be handled by MultiValueField, not 41 | # by those individual fields. 42 | f.required = False 43 | return tuple(fields) 44 | 45 | def add_fields(self, form_json): 46 | self.fields += self._configure_new_fields(gen_fields_from_json(form_json)) 47 | self.configure_widget() 48 | 49 | def replace_fields(self, form_json): 50 | self.fields = self._configure_new_fields(gen_fields_from_json(form_json)) 51 | self.configure_widget() 52 | 53 | def compress(self, data): 54 | result = {} 55 | for i, val in enumerate(data): 56 | result[self.fields[i].label] = val 57 | return result 58 | -------------------------------------------------------------------------------- /dynamic_forms/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class HTMLField(forms.Field): 5 | def __init__(self, attrs=None, *args, **kwargs): 6 | self.error_messages = {} 7 | self.label_suffix = None 8 | self.help_text = None 9 | self.label = '' 10 | self.initial = '' 11 | self.required = False 12 | self.attrs = {'id': False} 13 | self.show_hidden_initial = False 14 | self.localize = False 15 | self.disabled = False 16 | self.is_hidden = False 17 | -------------------------------------------------------------------------------- /dynamic_forms/models.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.db import models 4 | from .formfields import FormBuilderField, FormRenderField 5 | 6 | 7 | class FormField(models.TextField): 8 | """Stores JSON Schema for form 9 | """ 10 | 11 | def from_db_value(self, value, expression, connection): 12 | if value is None: 13 | return [] 14 | return json.loads(value) 15 | 16 | def to_python(self, value): 17 | if isinstance(value, list): 18 | return value 19 | return json.loads(value) 20 | 21 | def get_prep_value(self, value): 22 | if isinstance(value, str) or value is None: 23 | return value 24 | return json.dumps(value) 25 | 26 | def formfield(self, **kwargs): 27 | kwargs['form_class'] = FormBuilderField 28 | return super().formfield(**kwargs) 29 | 30 | 31 | class ResponseField(models.TextField): 32 | """Stores JSON response to form. 33 | """ 34 | 35 | def from_db_value(self, value, expression, connection): 36 | if value is None: 37 | return value 38 | return json.loads(value) 39 | 40 | def to_python(self, value): 41 | if isinstance(value, dict): 42 | return value 43 | if value is None: 44 | return {} 45 | return json.loads(value) 46 | 47 | def get_prep_value(self, value): 48 | if isinstance(value, str) or value is None: 49 | return value 50 | # Datetime.date is not JSON serializable, so must specify to convert to string 51 | return json.dumps(value, default=str) 52 | 53 | def formfield(self, *args, **kwargs): 54 | kwargs['form_class'] = FormRenderField 55 | return super().formfield(*args, **kwargs) 56 | -------------------------------------------------------------------------------- /dynamic_forms/templates/dynamic_forms/widgets/formbuilder.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 47 | 52 | 55 | -------------------------------------------------------------------------------- /dynamic_forms/templates/dynamic_forms/widgets/formrender.html: -------------------------------------------------------------------------------- 1 |
2 | {% for subfield in subfields %} 3 | {{subfield}} 4 | {% endfor %} 5 |
6 | 7 | 9 | 12 | 15 | 18 | -------------------------------------------------------------------------------- /dynamic_forms/templates/dynamic_forms/widgets/subfield.html: -------------------------------------------------------------------------------- 1 | {% if label != '' %} 2 | {{label}} 3 | {% endif %} 4 | {{field}} 5 | {% if help_text is not None %} 6 |

{{help_text}}

7 | {% endif %} 8 | -------------------------------------------------------------------------------- /dynamic_forms/templates/dynamic_forms/widgets/subfield_crispy.html: -------------------------------------------------------------------------------- 1 | {% load crispy_forms_filters %} 2 | 3 | {{field|as_crispy_field}} 4 | -------------------------------------------------------------------------------- /dynamic_forms/utils.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.utils.html import strip_tags 3 | from .forms import HTMLField 4 | from .widgets import HTMLFieldWidget 5 | 6 | 7 | def _process_checkbox(field_json): 8 | field = forms.MultipleChoiceField() 9 | field.widget = forms.CheckboxSelectMultiple() 10 | return field 11 | 12 | 13 | def _process_date(field_json): 14 | field = forms.DateField() 15 | field.widget.input_type = "date" 16 | return field 17 | 18 | 19 | def _process_email(field_json): 20 | return forms.EmailField() 21 | 22 | 23 | def _process_hidden(field_json): 24 | field = forms.CharField() 25 | field.widget = forms.HiddenInput() 26 | return field 27 | 28 | 29 | def _process_number(field_json): 30 | return forms.FloatField( 31 | min_value=field_json.get("min", None), 32 | max_value=field_json.get("max", None), 33 | widget=forms.NumberInput(attrs={'step': field_json.get("step", "any")}) 34 | ) 35 | 36 | 37 | def _process_radio(field_json): 38 | field = forms.ChoiceField() 39 | field.widget = forms.RadioSelect( 40 | attrs={"required": field_json.get("required", False)} 41 | ) 42 | return field 43 | 44 | 45 | def _process_select(field_json): 46 | if (field_json.get('multiple', False)): 47 | return forms.MultipleChoiceField() 48 | return forms.ChoiceField() 49 | 50 | 51 | def _process_text_input(field_json): 52 | return forms.CharField(max_length=field_json.get("maxlength", None)) 53 | 54 | 55 | def _process_text_area(field_json): 56 | field = forms.CharField() 57 | field.widget = forms.Textarea() 58 | return field 59 | 60 | 61 | def _process_url(field_json): 62 | return forms.URLField() 63 | 64 | 65 | def _process_heading(field_json): 66 | field = HTMLField() 67 | field.widget = HTMLFieldWidget(params=field_json) 68 | return field 69 | 70 | 71 | def _process_paragraph(field_json): 72 | field = HTMLField() 73 | field.widget = HTMLFieldWidget(params=field_json) 74 | return field 75 | 76 | 77 | TYPE_MAPPING = { 78 | 'checkbox-group': _process_checkbox, 79 | 'date': _process_date, 80 | 'email': _process_email, 81 | 'hidden': _process_hidden, 82 | 'number': _process_number, 83 | 'radio-group': _process_radio, 84 | 'select': _process_select, 85 | 'text': _process_text_input, 86 | 'textarea': _process_text_area, 87 | 'header': _process_heading, 88 | 'paragraph': _process_paragraph, 89 | 'url': _process_url 90 | } 91 | 92 | 93 | def process_field_from_json(field_json): 94 | if not isinstance(field_json, dict): 95 | raise TypeError("Each field JSON must be a dictionary") 96 | field_type = field_json['type'] 97 | if field_type == 'text': 98 | field_type = field_json.get('subtype', 'text') 99 | common_field_attrs = { 100 | 'required': field_json.get('required', False), 101 | 'label': strip_tags(field_json.get('label', None)), 102 | 'initial': field_json.get('value', None), 103 | 'help_text': field_json.get('description', None), 104 | } 105 | 106 | common_widget_attrs = { 107 | 'required': field_json.get('required', False), 108 | 'placeholder': field_json.get('placeholder', False), 109 | 'class': field_json.get('className', False), 110 | } 111 | field = TYPE_MAPPING[field_type](field_json) 112 | for attr, val in common_field_attrs.items(): 113 | if field_type not in ['paragraph', 'header', 'hidden']: 114 | setattr(field, attr, val) 115 | if field_type not in ['radio-group', 'hidden']: 116 | for attr, val in common_widget_attrs.items(): 117 | field.widget.attrs[attr] = val 118 | if field_type in ['checkbox-group', 'radio-group', 'select']: 119 | choices = [ 120 | (choice['value'], choice['label']) for choice in field_json['values'] 121 | ] 122 | field.choices = choices 123 | field.widget.choices = choices 124 | if field_type == 'hidden': 125 | setattr(field, 'initial', field_json.get('value', None)) 126 | return field 127 | 128 | 129 | def gen_fields_from_json(form_json): 130 | if not isinstance(form_json, list): 131 | raise TypeError("Form JSON must be a list.") 132 | fields = [] 133 | for field_json in form_json: 134 | fields.append(process_field_from_json(field_json)) 135 | return fields 136 | -------------------------------------------------------------------------------- /dynamic_forms/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic.edit import FormMixin 2 | 3 | 4 | class DynamicFormMixin(FormMixin): 5 | form_field = "form" 6 | form_pk_url_kwarg = "pk" 7 | 8 | response_form_fk_field = None 9 | response_field = "response" 10 | 11 | def _get_object_containing_form(self, pk): 12 | return self.form_model.objects.get(pk=pk) 13 | 14 | def get_form(self, *args, **kwargs): 15 | form = super().get_form(*args, **kwargs) 16 | # Get instance of model containing form used for this response. 17 | # Save this object as an instance variable for use in form_valid method. 18 | form_instance_pk = self.kwargs[self.form_pk_url_kwarg] 19 | self.form_instance = self._get_object_containing_form(form_instance_pk) 20 | # Get json form configuration from form-containing object 21 | json_data = getattr(self.form_instance, self.form_field) 22 | # Add fields in JSON to dynamic form rendering field. 23 | form.fields[self.response_field].add_fields(json_data) 24 | return form 25 | 26 | def form_valid(self, form): 27 | action = form.save(commit=False) 28 | setattr(action, self.response_form_fk_field, self.form_instance) 29 | action.save() 30 | return super().form_valid(form) 31 | -------------------------------------------------------------------------------- /dynamic_forms/widgets.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django import forms 4 | from django.utils.html import format_html 5 | from .forms import HTMLField 6 | from django.conf import settings 7 | 8 | 9 | class FormBuilderWidget(forms.Textarea): 10 | template_name = "dynamic_forms/widgets/formbuilder.html" 11 | 12 | def get_context(self, name, value, attrs): 13 | context = super().get_context(name, value, attrs) 14 | context['DYNAMIC_FORMS_CUSTOM_JS'] = settings.DYNAMIC_FORMS_CUSTOM_JS 15 | return context 16 | 17 | def format_value(self, value): 18 | if value is None: 19 | return None 20 | return json.dumps(value) 21 | 22 | 23 | class FormRenderWidget(forms.MultiWidget): 24 | template_name = "dynamic_forms/widgets/formrender.html" 25 | 26 | def decompress(self, value): 27 | return [] 28 | 29 | 30 | class HTMLFieldWidget(HTMLField): 31 | def __init__(self, attrs=None, params={}): 32 | self.attrs = attrs 33 | self.params = params 34 | super().__init__(attrs) 35 | 36 | def render(self, name, value, attrs=None, renderer=None): 37 | class_html = '' 38 | if 'className' in self.params: 39 | class_html = " class='{0}'".format(self.params['className']) 40 | return format_html("<{0}{2}>{1}".format(self.params['subtype'], self.params['label'], class_html)) 41 | 42 | def get(self): 43 | return False 44 | 45 | def use_required_attribute(self, initial): 46 | return False 47 | 48 | def id_for_label(self, id): 49 | return '' 50 | 51 | def get_context(self): 52 | return {'name': ''} 53 | 54 | def value_from_datadict(self, data, files, name): 55 | return data.get(name) 56 | -------------------------------------------------------------------------------- /example/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.6.6-alpine3.7 2 | 3 | ADD requirements.txt /tmp 4 | RUN pip install -r /tmp/requirements.txt 5 | 6 | EXPOSE 9000 7 | CMD python manage.py migrate && python manage.py runserver 0.0.0.0:9000 8 | -------------------------------------------------------------------------------- /example/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | example: 5 | restart: always 6 | container_name: example 7 | build: . 8 | stdin_open: true 9 | tty: true 10 | volumes: 11 | - .:/app 12 | working_dir: /app 13 | ports: 14 | - "9000:9000" 15 | -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askvortsov1/dynamic-django-forms/1f505d8aceecbee069d9633e9a25a19f9e5e45c6/example/example/__init__.py -------------------------------------------------------------------------------- /example/example/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Survey, SurveyResponse 3 | 4 | admin.site.register(Survey) 5 | admin.site.register(SurveyResponse) 6 | -------------------------------------------------------------------------------- /example/example/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1.11 on 2019-08-10 01:45 2 | 3 | from django.db import migrations, models 4 | import django.db.models.deletion 5 | import dynamic_forms.models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Survey', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('topic', models.CharField(max_length=100)), 21 | ('form', dynamic_forms.models.FormField()), 22 | ], 23 | ), 24 | migrations.CreateModel( 25 | name='SurveyResponse', 26 | fields=[ 27 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 28 | ('response', dynamic_forms.models.ResponseField()), 29 | ('survey', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='example.Survey')), 30 | ], 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /example/example/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askvortsov1/dynamic-django-forms/1f505d8aceecbee069d9633e9a25a19f9e5e45c6/example/example/migrations/__init__.py -------------------------------------------------------------------------------- /example/example/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from dynamic_forms.models import FormField, ResponseField 3 | 4 | 5 | class Survey(models.Model): 6 | topic = models.CharField(max_length=100) 7 | 8 | form = FormField() 9 | 10 | def __str__(self): 11 | return "Survey #{}: {}".format(self.pk, self.topic) 12 | 13 | 14 | class SurveyResponse(models.Model): 15 | survey = models.ForeignKey(Survey, on_delete=models.CASCADE) 16 | response = ResponseField() 17 | -------------------------------------------------------------------------------- /example/example/settings.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Django settings for example project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.10.6. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.10/ref/settings/ 11 | ''' 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'ar249h_c(@5#x)ha_vou=4%plz*#!*l=+4c^jbo6wi%8z222hg' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ['*'] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 'dynamic_forms', 41 | 'crispy_forms', 42 | 'example', 43 | ] 44 | 45 | MIDDLEWARE = [ 46 | 'django.middleware.security.SecurityMiddleware', 47 | 'django.contrib.sessions.middleware.SessionMiddleware', 48 | 'django.middleware.common.CommonMiddleware', 49 | 'django.middleware.csrf.CsrfViewMiddleware', 50 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 51 | 'django.contrib.messages.middleware.MessageMiddleware', 52 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 53 | ] 54 | 55 | ROOT_URLCONF = 'example.urls' 56 | 57 | 58 | TEMPLATES = [ 59 | { 60 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 61 | 'DIRS': [], 62 | 'APP_DIRS': True, 63 | 'OPTIONS': { 64 | 'context_processors': [ 65 | 'django.template.context_processors.debug', 66 | 'django.template.context_processors.request', 67 | 'django.contrib.auth.context_processors.auth', 68 | 'django.contrib.messages.context_processors.messages', 69 | ], 70 | }, 71 | }, 72 | ] 73 | 74 | 75 | WSGI_APPLICATION = 'example.wsgi.application' 76 | 77 | 78 | # Database 79 | # https://docs.djangoproject.com/en/1.10/ref/settings/#databases 80 | 81 | DATABASES = { 82 | 'default': { 83 | 'ENGINE': 'django.db.backends.sqlite3', 84 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 85 | } 86 | } 87 | 88 | 89 | # Password validation 90 | # https://docs.djangoproject.com/en/1.10/ref/settings/#auth-password-validators 91 | 92 | AUTH_PASSWORD_VALIDATORS = [ 93 | { 94 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 95 | }, 96 | { 97 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 98 | }, 99 | { 100 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 101 | }, 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 104 | }, 105 | ] 106 | 107 | # Internationalization 108 | # https://docs.djangoproject.com/en/1.10/topics/i18n/ 109 | 110 | LANGUAGE_CODE = 'en-us' 111 | 112 | TIME_ZONE = 'UTC' 113 | 114 | USE_I18N = True 115 | 116 | USE_L10N = True 117 | 118 | USE_TZ = True 119 | 120 | 121 | # Static files (CSS, JavaScript, Images) 122 | # https://docs.djangoproject.com/en/1.10/howto/static-files/ 123 | 124 | STATIC_URL = '/static/' 125 | 126 | CRISPY_TEMPLATE_PACK = "bootstrap4" 127 | 128 | USE_CRISPY = True 129 | -------------------------------------------------------------------------------- /example/example/templates/example/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% block title %}Dynamic Forms{% endblock %} 9 | 10 | 11 | 12 | {% block content %} 13 | {% endblock %} 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/example/templates/example/build.html: -------------------------------------------------------------------------------- 1 | {% extends 'example/base.html' %} 2 | 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 | {{form.as_p}} 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example/example/templates/example/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'example/base.html' %} 2 | 3 | {% block content %} 4 | Create New Survey 5 | 10 | {% endblock %} 11 | -------------------------------------------------------------------------------- /example/example/templates/example/respond.html: -------------------------------------------------------------------------------- 1 | {% extends 'example/base.html' %} 2 | 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 | {{form}} 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example/example/templates/example/survey_detail.html: -------------------------------------------------------------------------------- 1 | {% extends 'example/base.html' %} 2 | 3 | {% block content %} 4 | All Surveys 5 |
6 | Edit Survey 7 |
8 | Respond To Survey 9 | 10 |

Responses

11 |
12 | {% for surveyresponse in object.surveyresponse_set.all %} 13 | 18 |
19 | {% endfor %} 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /example/example/templates/example/survey_edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'example/base.html' %} 2 | 3 | {% block content %} 4 |
5 | {% csrf_token %} 6 | {{form.as_p}} 7 | 8 |
9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | from . import views 5 | 6 | 7 | urlpatterns = [ 8 | path('', views.IndexView.as_view(), name="home"), 9 | path('survey/new/', views.BuildView.as_view(), name="survey_create"), 10 | path('survey//', views.SurveyDetailView.as_view(), name="survey_detail"), 11 | path('survey//edit/', views.SurveyEditView.as_view(), name="survey_edit"), 12 | path('survey//response/', views.RespondView.as_view(), name="survey_respond"), 13 | path('survey//response//', views.RespondView.as_view(), name="response"), 14 | path('admin/', admin.site.urls), 15 | ] 16 | -------------------------------------------------------------------------------- /example/example/views.py: -------------------------------------------------------------------------------- 1 | from django.urls import reverse 2 | from django.views.generic import TemplateView, DetailView 3 | from django.views.generic.edit import CreateView, UpdateView 4 | from dynamic_forms.views import DynamicFormMixin 5 | from .models import Survey, SurveyResponse 6 | 7 | 8 | class IndexView(TemplateView): 9 | template_name = "example/index.html" 10 | 11 | def get_context_data(self, *args, **kwargs): 12 | context = super().get_context_data(*args, **kwargs) 13 | context['surveys'] = Survey.objects.all() 14 | return context 15 | 16 | 17 | class BuildView(CreateView): 18 | model = Survey 19 | fields = '__all__' 20 | template_name = "example/build.html" 21 | success_url = "/" 22 | 23 | 24 | class SurveyDetailView(DetailView): 25 | model = Survey 26 | pk_url_kwarg = "survey_id" 27 | template_name = "example/survey_detail.html" 28 | 29 | 30 | class SurveyEditView(UpdateView): 31 | model = Survey 32 | fields = '__all__' 33 | pk_url_kwarg = "survey_id" 34 | template_name = "example/survey_edit.html" 35 | 36 | def get_success_url(self): 37 | return reverse('survey_detail', kwargs={"survey_id": self.object.pk}) 38 | 39 | 40 | class RespondView(DynamicFormMixin, CreateView): 41 | model = SurveyResponse 42 | fields = ['response'] 43 | template_name = "example/respond.html" 44 | 45 | form_model = Survey 46 | form_pk_url_kwarg = "survey_id" 47 | response_form_fk_field = "survey" 48 | response_field = "response" 49 | 50 | def get_success_url(self): 51 | return reverse('survey_detail', kwargs={"survey_id": self.form_instance.pk}) 52 | -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.10/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError: 10 | # The above import may fail for some other reason. Ensure that the 11 | # issue is really that Django is missing to avoid masking other 12 | # exceptions on Python 2. 13 | try: 14 | import django 15 | except ImportError: 16 | raise ImportError( 17 | "Couldn't import Django. Are you sure it's installed and " 18 | "available on your PYTHONPATH environment variable? Did you " 19 | "forget to activate a virtual environment?" 20 | ) 21 | raise 22 | execute_from_command_line(sys.argv) 23 | -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | django>=2.0 2 | django-crispy-forms 3 | dynamic-django-forms 4 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pylama] 2 | skip = build/*,dist/*,docs/*,*/manage.py 3 | 4 | [pylama:pycodestyle] 5 | max_line_length = 150 6 | 7 | 8 | [pytest] 9 | python_files = test_*.py 10 | django_find_project = false 11 | norecursedirs = env 12 | DJANGO_SETTINGS_MODULE=tests.settings 13 | filterwarnings = 14 | ignore::DeprecationWarning 15 | -------------------------------------------------------------------------------- /requirements-dev.in: -------------------------------------------------------------------------------- 1 | pylama 2 | codecov 3 | pip-tools 4 | tox 5 | pre-commit 6 | pytest-cov 7 | pytest-django 8 | pytest-pythonpath 9 | python-dateutil 10 | codecov 11 | pip-tools 12 | mypy 13 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file=requirements-dev.txt requirements-dev.in 6 | # 7 | appdirs==1.4.4 8 | # via virtualenv 9 | attrs==20.3.0 10 | # via pytest 11 | certifi==2020.12.5 12 | # via requests 13 | cfgv==3.2.0 14 | # via pre-commit 15 | chardet==4.0.0 16 | # via requests 17 | click==7.1.2 18 | # via pip-tools 19 | codecov==2.1.11 20 | # via -r requirements-dev.in 21 | coverage==5.3.1 22 | # via 23 | # codecov 24 | # pytest-cov 25 | distlib==0.3.1 26 | # via virtualenv 27 | filelock==3.0.12 28 | # via 29 | # tox 30 | # virtualenv 31 | identify==1.5.11 32 | # via pre-commit 33 | idna==2.10 34 | # via requests 35 | iniconfig==1.1.1 36 | # via pytest 37 | mccabe==0.6.1 38 | # via pylama 39 | mypy==0.931 40 | # via -r requirements-dev.in 41 | mypy-extensions==0.4.3 42 | # via mypy 43 | nodeenv==1.5.0 44 | # via pre-commit 45 | packaging==20.8 46 | # via 47 | # pytest 48 | # tox 49 | pip-tools==5.5.0 50 | # via -r requirements-dev.in 51 | pluggy==0.13.1 52 | # via 53 | # pytest 54 | # tox 55 | pre-commit==2.9.3 56 | # via -r requirements-dev.in 57 | py==1.10.0 58 | # via 59 | # pytest 60 | # tox 61 | pycodestyle==2.6.0 62 | # via pylama 63 | pydocstyle==5.1.1 64 | # via pylama 65 | pyflakes==2.2.0 66 | # via pylama 67 | pylama==7.7.1 68 | # via -r requirements-dev.in 69 | pyparsing==2.4.7 70 | # via packaging 71 | pytest==6.2.1 72 | # via 73 | # pytest-cov 74 | # pytest-django 75 | # pytest-pythonpath 76 | pytest-cov==2.10.1 77 | # via -r requirements-dev.in 78 | pytest-django==4.1.0 79 | # via -r requirements-dev.in 80 | pytest-pythonpath==0.7.3 81 | # via -r requirements-dev.in 82 | python-dateutil==2.8.1 83 | # via -r requirements-dev.in 84 | pyyaml==5.3.1 85 | # via pre-commit 86 | requests==2.25.1 87 | # via codecov 88 | six==1.15.0 89 | # via 90 | # python-dateutil 91 | # tox 92 | # virtualenv 93 | snowballstemmer==2.0.0 94 | # via pydocstyle 95 | toml==0.10.2 96 | # via 97 | # pre-commit 98 | # pytest 99 | # tox 100 | tomli==2.0.0 101 | # via mypy 102 | tox==3.20.1 103 | # via -r requirements-dev.in 104 | typing-extensions==4.0.1 105 | # via mypy 106 | urllib3==1.26.5 107 | # via requests 108 | virtualenv==20.2.2 109 | # via 110 | # pre-commit 111 | # tox 112 | 113 | # The following packages are considered to be unsafe in a requirements file: 114 | # pip 115 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [metadata] 5 | description_file = README.md 6 | license_file = LICENSE.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from setuptools import setup, find_packages 3 | 4 | from dynamic_forms import __version__ 5 | 6 | with open('README.md', 'r') as fh: 7 | long_description = fh.read() 8 | 9 | standard_exclude = ["*.py", "*.pyc", "*~", ".*", "*.bak", "Makefile"] 10 | standard_exclude_directories = [ 11 | ".*", "CVS", "_darcs", "./build", 12 | "./dist", "EGG-INFO", "*.egg-info", 13 | "./example" 14 | ] 15 | 16 | setup( 17 | name='dynamic-django-forms', 18 | version=__version__, 19 | description='JSON-Powered Dynamic Forms for Django', 20 | keywords='django,dynamic,forms,json', 21 | author='Alexander Skvortsov', 22 | author_email='sasha.skvortsov109@gmail.com', 23 | maintainer='Alexander Skvortsov', 24 | long_description=long_description, 25 | long_description_content_type='text/markdown', 26 | include_package_data=True, 27 | install_requires=['django'], 28 | license='MIT License', 29 | packages=find_packages(exclude=['tests*', 'docs', 'example']), 30 | url='https://github.com/askvortsov1/dynamic-django-forms', 31 | zip_safe=False, 32 | classifiers=[ 33 | 'Programming Language :: Python :: 3', 34 | 'License :: OSI Approved :: MIT License', 35 | 'Operating System :: OS Independent', 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/askvortsov1/dynamic-django-forms/1f505d8aceecbee069d9633e9a25a19f9e5e45c6/tests/__init__.py -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | SECRET_KEY = 'qwertyuiopasdfghjklzxcvbnm' 2 | 3 | DATABASES = { 4 | 'default': { 5 | 'ENGINE': 'django.db.backends.sqlite3', 6 | 'NAME': './idptest.sqlite', 7 | 'USER': '', 8 | 'PASSWORD': '', 9 | 'HOST': '', 10 | 'PORT': '', 11 | } 12 | } 13 | 14 | INSTALLED_APPS = ( 15 | 'django.contrib.auth', 16 | 'django.contrib.admin', 17 | 'django.contrib.contenttypes', 18 | 'django.contrib.sessions', 19 | 'dynamic_forms', 20 | 'tests', 21 | ) 22 | -------------------------------------------------------------------------------- /tests/test_models.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.django_db 5 | def test_simple(): 6 | assert 1 == 1 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{37,38,39,310}-django{32,40} 4 | 5 | [testenv] 6 | deps = 7 | pytest # PYPI package providing pytest 8 | pytest-django 9 | pytest-runner 10 | pytest-cov 11 | pytest-pythonpath 12 | pytest-mock 13 | commands = pytest {posargs} # substitute with tox' positional arguments --------------------------------------------------------------------------------