├── requirements.txt ├── .gitignore ├── AUTHORS ├── django_remote_forms ├── __init__.py ├── utils.py ├── forms.py ├── widgets.py └── fields.py ├── LICENSE ├── setup.py └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=1.3.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.*~ 2 | *.egg-info 3 | *.pyc 4 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Tareque Hossain 2 | Carlo Costino 3 | -------------------------------------------------------------------------------- /django_remote_forms/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Carlo Costino, Tareque Hossain' 2 | __version__ = (0, 0, 1) 3 | 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | -------------------------------------------------------------------------------- /django_remote_forms/utils.py: -------------------------------------------------------------------------------- 1 | from django.utils.functional import Promise 2 | from django.utils.encoding import force_unicode 3 | 4 | 5 | def resolve_promise(o): 6 | if isinstance(o, dict): 7 | for k, v in o.items(): 8 | o[k] = resolve_promise(v) 9 | elif isinstance(o, (list, tuple)): 10 | o = [resolve_promise(x) for x in o] 11 | elif isinstance(o, Promise): 12 | try: 13 | o = force_unicode(o) 14 | except: 15 | # Item could be a lazy tuple or list 16 | try: 17 | o = [resolve_promise(x) for x in o] 18 | except: 19 | raise Exception('Unable to resolve lazy object %s' % o) 20 | elif callable(o): 21 | o = o() 22 | 23 | return o 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Tareque Hossain 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from ez_setup import use_setuptools 7 | use_setuptools() 8 | from setuptools import setup 9 | 10 | setup( 11 | name='django-remote-forms', 12 | version='0.0.1', 13 | description='A platform independent form serializer for Django.', 14 | author='WiserTogether Tech Team', 15 | author_email='tech@wisertogether.com', 16 | url='http://github.com/wisertoghether/django-remote-forms/', 17 | long_description=open('README.md', 'r').read(), 18 | packages=[ 19 | 'django_remote_forms', 20 | ], 21 | package_data={ 22 | }, 23 | zip_safe=False, 24 | requires=[ 25 | ], 26 | install_requires=[ 27 | ], 28 | classifiers=[ 29 | 'Development Status :: Pre Alpha', 30 | 'Environment :: Web Environment', 31 | 'Framework :: Django', 32 | 'Intended Audience :: Developers', 33 | 'License :: MIT', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Topic :: Utilities' 37 | ], 38 | ) 39 | -------------------------------------------------------------------------------- /django_remote_forms/forms.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | 3 | from django_remote_forms import fields, logger 4 | from django_remote_forms.utils import resolve_promise 5 | 6 | 7 | class RemoteForm(object): 8 | def __init__(self, form, *args, **kwargs): 9 | self.form = form 10 | 11 | self.all_fields = set(self.form.fields.keys()) 12 | 13 | self.excluded_fields = set(kwargs.pop('exclude', [])) 14 | self.included_fields = set(kwargs.pop('include', [])) 15 | self.readonly_fields = set(kwargs.pop('readonly', [])) 16 | self.ordered_fields = kwargs.pop('ordering', []) 17 | 18 | self.fieldsets = kwargs.pop('fieldsets', {}) 19 | 20 | # Make sure all passed field lists are valid 21 | if self.excluded_fields and not (self.all_fields >= self.excluded_fields): 22 | logger.warning( 23 | 'Excluded fields %s are not present in form fields' % (self.excluded_fields - self.all_fields)) 24 | self.excluded_fields = set() 25 | 26 | if self.included_fields and not (self.all_fields >= self.included_fields): 27 | logger.warning( 28 | 'Included fields %s are not present in form fields' % (self.included_fields - self.all_fields)) 29 | self.included_fields = set() 30 | 31 | if self.readonly_fields and not (self.all_fields >= self.readonly_fields): 32 | logger.warning( 33 | 'Readonly fields %s are not present in form fields' % (self.readonly_fields - self.all_fields)) 34 | self.readonly_fields = set() 35 | 36 | if self.ordered_fields and not (self.all_fields >= set(self.ordered_fields)): 37 | logger.warning( 38 | 'Readonly fields %s are not present in form fields' % (set(self.ordered_fields) - self.all_fields)) 39 | self.ordered_fields = [] 40 | 41 | if self.included_fields | self.excluded_fields: 42 | logger.warning( 43 | 'Included and excluded fields have following fields %s in common' % ( 44 | set(self.ordered_fields) - self.all_fields 45 | ) 46 | ) 47 | self.excluded_fields = set() 48 | self.included_fields = set() 49 | 50 | # Extend exclude list from include list 51 | self.excluded_fields |= (self.included_fields - self.all_fields) 52 | 53 | if not self.ordered_fields: 54 | if hasattr(self.form.fields, 'keyOrder'): 55 | self.ordered_fields = self.form.fields.keyOrder 56 | else: 57 | self.ordered_fields = self.form.fields.keys() 58 | 59 | self.fields = [] 60 | 61 | # Construct ordered field list considering exclusions 62 | for field_name in self.ordered_fields: 63 | if field_name in self.excluded_fields: 64 | continue 65 | 66 | self.fields.append(field_name) 67 | 68 | # Validate fieldset 69 | fieldset_fields = set() 70 | if self.fieldsets: 71 | for fieldset_name, fieldsets_data in self.fieldsets: 72 | if 'fields' in fieldsets_data: 73 | fieldset_fields |= set(fieldsets_data['fields']) 74 | 75 | if not (self.all_fields >= fieldset_fields): 76 | logger.warning('Following fieldset fields are invalid %s' % (fieldset_fields - self.all_fields)) 77 | self.fieldsets = {} 78 | 79 | if not (set(self.fields) >= fieldset_fields): 80 | logger.warning('Following fieldset fields are excluded %s' % (fieldset_fields - set(self.fields))) 81 | self.fieldsets = {} 82 | 83 | def as_dict(self): 84 | """ 85 | Returns a form as a dictionary that looks like the following: 86 | 87 | form = { 88 | 'non_field_errors': [], 89 | 'label_suffix': ':', 90 | 'is_bound': False, 91 | 'prefix': 'text'. 92 | 'fields': { 93 | 'name': { 94 | 'type': 'type', 95 | 'errors': {}, 96 | 'help_text': 'text', 97 | 'label': 'text', 98 | 'initial': 'data', 99 | 'max_length': 'number', 100 | 'min_length: 'number', 101 | 'required': False, 102 | 'bound_data': 'data' 103 | 'widget': { 104 | 'attr': 'value' 105 | } 106 | } 107 | } 108 | } 109 | """ 110 | form_dict = OrderedDict() 111 | form_dict['title'] = self.form.__class__.__name__ 112 | form_dict['non_field_errors'] = self.form.non_field_errors() 113 | form_dict['label_suffix'] = self.form.label_suffix 114 | form_dict['is_bound'] = self.form.is_bound 115 | form_dict['prefix'] = self.form.prefix 116 | form_dict['fields'] = OrderedDict() 117 | form_dict['errors'] = self.form.errors 118 | form_dict['fieldsets'] = getattr(self.form, 'fieldsets', []) 119 | 120 | # If there are no fieldsets, specify order 121 | form_dict['ordered_fields'] = self.fields 122 | 123 | initial_data = {} 124 | 125 | for name, field in [(x, self.form.fields[x]) for x in self.fields]: 126 | # Retrieve the initial data from the form itself if it exists so 127 | # that we properly handle which initial data should be returned in 128 | # the dictionary. 129 | 130 | # Please refer to the Django Form API documentation for details on 131 | # why this is necessary: 132 | # https://docs.djangoproject.com/en/dev/ref/forms/api/#dynamic-initial-values 133 | form_initial_field_data = self.form.initial.get(name) 134 | 135 | # Instantiate the Remote Forms equivalent of the field if possible 136 | # in order to retrieve the field contents as a dictionary. 137 | remote_field_class_name = 'Remote%s' % field.__class__.__name__ 138 | try: 139 | remote_field_class = getattr(fields, remote_field_class_name) 140 | remote_field = remote_field_class(field, form_initial_field_data, field_name=name) 141 | except Exception, e: 142 | logger.warning('Error serializing field %s: %s', remote_field_class_name, str(e)) 143 | field_dict = {} 144 | else: 145 | field_dict = remote_field.as_dict() 146 | 147 | if name in self.readonly_fields: 148 | field_dict['readonly'] = True 149 | 150 | form_dict['fields'][name] = field_dict 151 | 152 | # Load the initial data, which is a conglomerate of form initial and field initial 153 | if 'initial' not in form_dict['fields'][name]: 154 | form_dict['fields'][name]['initial'] = None 155 | 156 | initial_data[name] = form_dict['fields'][name]['initial'] 157 | 158 | if self.form.data: 159 | form_dict['data'] = self.form.data 160 | else: 161 | form_dict['data'] = initial_data 162 | 163 | return resolve_promise(form_dict) 164 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # django-remote-forms 2 | 3 | A package that allows you to serialize django forms, including fields and widgets into Python 4 | dictionary for easy conversion into JSON and expose over API 5 | 6 | Please go through my [djangocon US 2012 talk](http://www.slideshare.net/tarequeh/django-forms-in-a-web-api-world) 7 | to understand the problem sphere, motivations, challenges and implementation of Remote Forms 8 | 9 | ## Sample Implementation 10 | 11 | If you don't mind digging around a little bit to learn about different the components that might be 12 | necessary for an implementation of django-remote-forms, check out 13 | django Remote Admin [django-remote-admin](https://github.com/tarequeh/django-remote-admin) 14 | 15 | ## Usage 16 | 17 | ### Minimal Example 18 | 19 | ```python 20 | from django_remote_forms.forms import RemoteForm 21 | 22 | form = LoginForm() 23 | remote_form = RemoteForm(form) 24 | remote_form_dict = remote_form.as_dict() 25 | ``` 26 | 27 | Upon converting the dictionary into JSON, it looks like this: 28 | 29 | ```json 30 | { 31 | "is_bound": false, 32 | "non_field_errors": [], 33 | "errors": {}, 34 | "title": "LoginForm", 35 | "fields": { 36 | "username": { 37 | "title": "CharField", 38 | "required": true, 39 | "label": "Username", 40 | "initial": null, 41 | "help_text": "This is your django username", 42 | "error_messages": { 43 | "required": "This field is required.", 44 | "invalid": "Enter a valid value." 45 | }, 46 | "widget": { 47 | "title": "TextInput", 48 | "is_hidden": false, 49 | "needs_multipart_form": false, 50 | "is_localized": false, 51 | "is_required": true, 52 | "attrs": { 53 | "maxlength": "30" 54 | }, 55 | "input_type": "text" 56 | }, 57 | "min_length": 6, 58 | "max_length": 30 59 | }, 60 | "password": { 61 | "title": "CharField", 62 | "required": true, 63 | "label": "Password", 64 | "initial": null, 65 | "help_text": "", 66 | "error_messages": { 67 | "required": "This field is required.", 68 | "invalid": "Enter a valid value." 69 | }, 70 | "widget": { 71 | "title": "PasswordInput", 72 | "is_hidden": false, 73 | "needs_multipart_form": false, 74 | "is_localized": false, 75 | "is_required": true, 76 | "attrs": { 77 | "maxlength": "128" 78 | }, 79 | "input_type": "password" 80 | }, 81 | "min_length": 6, 82 | "max_length": 128 83 | } 84 | }, 85 | "label_suffix": ":", 86 | "prefix": null, 87 | "csrfmiddlewaretoken": "2M3MDgfzBmkmBrJ9U0MuYUdy8vgeCCgw", 88 | "data": { 89 | "username": null, 90 | "password": null 91 | } 92 | } 93 | ``` 94 | 95 | ### An API endpoint serving remote forms 96 | 97 | ```python 98 | from django.core.serializers.json import simplejson as json, DjangoJSONEncoder 99 | from django.http import HttpResponse 100 | from django.middleware.csrf import CsrfViewMiddleware 101 | from django.views.decorators.csrf import csrf_exempt 102 | 103 | from django_remote_forms.forms import RemoteForm 104 | 105 | from my_awesome_project.forms import MyAwesomeForm 106 | 107 | 108 | @csrf_exempt 109 | def my_ajax_view(request): 110 | csrf_middleware = CsrfViewMiddleware() 111 | 112 | response_data = {} 113 | if request.method == 'GET': 114 | # Get form definition 115 | form = MyAwesomeForm() 116 | elif request.raw_post_data: 117 | request.POST = json.loads(request.raw_post_data) 118 | # Process request for CSRF 119 | csrf_middleware.process_view(request, None, None, None) 120 | form_data = request.POST.get('data', {}) 121 | form = MyAwesomeForm(form_data) 122 | if form.is_valid(): 123 | form.save() 124 | 125 | remote_form = RemoteForm(form) 126 | # Errors in response_data['non_field_errors'] and response_data['errors'] 127 | response_data.update(remote_form.as_dict()) 128 | 129 | response = HttpResponse( 130 | json.dumps(response_data, cls=DjangoJSONEncoder), 131 | mimetype="application/json" 132 | ) 133 | 134 | # Process response for CSRF 135 | csrf_middleware.process_response(request, response) 136 | return response 137 | ``` 138 | 139 | ## djangocon Proposal 140 | 141 | This is a bit lengthy. But if you want to know more about my motivations behind developing django-remote-forms 142 | then read on. 143 | 144 | 145 | >In our quest to modularize the architecture of web applications, we create self-containing backend 146 | >systems that provide web APIs for programmatic interactions. This gives us the flexibility to 147 | >separate different system components. A system with multiple backend components e.g. user profile 148 | >engine, content engine, community engine, analytics engine may have a single frontend application 149 | >that fetches data from all of these components using respective web APIs. 150 | 151 | >With the increased availability of powerful JavaScript frameworks, such frontend applications are 152 | >often purely JS based to decrease application footprint, increase deployment flexibility and 153 | >separate presentation from data. The separation is very rewarding from a software engineering 154 | >standpoint but imposes several limitations on system design. Using django to construct the API for 155 | >arbitrary consumers comes with the limitation of not being able to utilize the powerful django form 156 | >subsystem to drive forms on these consumers. But is there a way to overcome this restriction? 157 | 158 | >This is not a trivial problem to solve and there are only a few assumptions we can make about the 159 | >web API consumer. It can be a native mobile or desktop - application or browser. We advocate that 160 | >web APIs should provide sufficient information about 'forms' so that they can be faithfully 161 | >reproduced at the consumer end. 162 | 163 | >Even in a API backend built using django, forms are essential for accepting, filtering, processing 164 | >and saving data. The django form subsystem provides many useful features to accomplish these tasks. 165 | >At the same time it facilitates the process of rendering the form elements in a browser 166 | >environment. The concepts of form fields combined with widgets can go a long way in streamlining 167 | >the interface to interact with data. 168 | 169 | >We propose an architecture to serialize information about django forms (to JSON) in a framework 170 | >independent fashion so that it can be consumed by any frontend application that renders HTML. Such 171 | >information includes but is not limited to basic form configurations, security tokens (if 172 | >necessary), rendering metadata and error handling instructions. We lovingly name this architecture 173 | >django-remote-forms. 174 | 175 | >At WiserTogether, we are in the process of building a component based architecture that strictly 176 | >provides data endpoints for frontend applications to consume. We are working towards developing 177 | >our frontend application for web browsers using backbone.js as MVC and handlebars as the templating 178 | >engine. django-remote-forms helps us streamline our data input interface with the django forms 179 | >living at the API backend. 180 | -------------------------------------------------------------------------------- /django_remote_forms/widgets.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from django.utils.dates import MONTHS 4 | from collections import OrderedDict 5 | 6 | 7 | class RemoteWidget(object): 8 | def __init__(self, widget, field_name=None): 9 | self.field_name = field_name 10 | self.widget = widget 11 | 12 | def as_dict(self): 13 | widget_dict = OrderedDict() 14 | widget_dict['title'] = self.widget.__class__.__name__ 15 | widget_dict['is_hidden'] = self.widget.is_hidden 16 | widget_dict['needs_multipart_form'] = self.widget.needs_multipart_form 17 | widget_dict['is_localized'] = self.widget.is_localized 18 | widget_dict['is_required'] = self.widget.is_required 19 | widget_dict['attrs'] = self.widget.attrs 20 | 21 | return widget_dict 22 | 23 | 24 | class RemoteInput(RemoteWidget): 25 | def as_dict(self): 26 | widget_dict = super(RemoteInput, self).as_dict() 27 | 28 | widget_dict['input_type'] = self.widget.input_type 29 | 30 | return widget_dict 31 | 32 | 33 | class RemoteTextInput(RemoteInput): 34 | def as_dict(self): 35 | return super(RemoteTextInput, self).as_dict() 36 | 37 | 38 | class RemotePasswordInput(RemoteInput): 39 | def as_dict(self): 40 | return super(RemotePasswordInput, self).as_dict() 41 | 42 | 43 | class RemoteHiddenInput(RemoteInput): 44 | def as_dict(self): 45 | return super(RemoteHiddenInput, self).as_dict() 46 | 47 | 48 | class RemoteEmailInput(RemoteInput): 49 | def as_dict(self): 50 | widget_dict = super(RemoteEmailInput, self).as_dict() 51 | 52 | widget_dict['title'] = 'TextInput' 53 | widget_dict['input_type'] = 'text' 54 | 55 | return widget_dict 56 | 57 | 58 | class RemoteNumberInput(RemoteInput): 59 | def as_dict(self): 60 | widget_dict = super(RemoteNumberInput, self).as_dict() 61 | 62 | widget_dict['title'] = 'TextInput' 63 | widget_dict['input_type'] = 'text' 64 | 65 | return widget_dict 66 | 67 | 68 | class RemoteURLInput(RemoteInput): 69 | def as_dict(self): 70 | widget_dict = super(RemoteURLInput, self).as_dict() 71 | 72 | widget_dict['title'] = 'TextInput' 73 | widget_dict['input_type'] = 'text' 74 | 75 | return widget_dict 76 | 77 | 78 | class RemoteMultipleHiddenInput(RemoteHiddenInput): 79 | def as_dict(self): 80 | widget_dict = super(RemoteMultipleHiddenInput, self).as_dict() 81 | 82 | widget_dict['choices'] = self.widget.choices 83 | 84 | return widget_dict 85 | 86 | 87 | class RemoteFileInput(RemoteInput): 88 | def as_dict(self): 89 | return super(RemoteFileInput, self).as_dict() 90 | 91 | 92 | class RemoteClearableFileInput(RemoteFileInput): 93 | def as_dict(self): 94 | widget_dict = super(RemoteClearableFileInput, self).as_dict() 95 | 96 | widget_dict['initial_text'] = self.widget.initial_text 97 | widget_dict['input_text'] = self.widget.input_text 98 | widget_dict['clear_checkbox_label'] = self.widget.clear_checkbox_label 99 | 100 | return widget_dict 101 | 102 | 103 | class RemoteTextarea(RemoteWidget): 104 | def as_dict(self): 105 | widget_dict = super(RemoteTextarea, self).as_dict() 106 | widget_dict['input_type'] = 'textarea' 107 | return widget_dict 108 | 109 | 110 | class RemoteTimeInput(RemoteInput): 111 | def as_dict(self): 112 | widget_dict = super(RemoteTimeInput, self).as_dict() 113 | 114 | widget_dict['format'] = self.widget.format 115 | widget_dict['input_type'] = 'time' 116 | 117 | return widget_dict 118 | 119 | 120 | class RemoteDateInput(RemoteTimeInput): 121 | def as_dict(self): 122 | widget_dict = super(RemoteDateInput, self).as_dict() 123 | 124 | widget_dict['input_type'] = 'date' 125 | 126 | current_year = datetime.datetime.now().year 127 | widget_dict['choices'] = [{ 128 | 'title': 'day', 129 | 'data': [{'key': x, 'value': x} for x in range(1, 32)] 130 | }, { 131 | 'title': 'month', 132 | 'data': [{'key': x, 'value': y} for (x, y) in MONTHS.items()] 133 | }, { 134 | 'title': 'year', 135 | 'data': [{'key': x, 'value': x} for x in range(current_year - 100, current_year + 1)] 136 | }] 137 | 138 | return widget_dict 139 | 140 | 141 | class RemoteDateTimeInput(RemoteTimeInput): 142 | def as_dict(self): 143 | widget_dict = super(RemoteDateTimeInput, self).as_dict() 144 | 145 | widget_dict['input_type'] = 'datetime' 146 | 147 | return widget_dict 148 | 149 | 150 | class RemoteCheckboxInput(RemoteWidget): 151 | def as_dict(self): 152 | widget_dict = super(RemoteCheckboxInput, self).as_dict() 153 | 154 | # If check test is None then the input should accept null values 155 | check_test = None 156 | if self.widget.check_test is not None: 157 | check_test = True 158 | 159 | widget_dict['check_test'] = check_test 160 | widget_dict['input_type'] = 'checkbox' 161 | 162 | return widget_dict 163 | 164 | 165 | class RemoteSelect(RemoteWidget): 166 | def as_dict(self): 167 | widget_dict = super(RemoteSelect, self).as_dict() 168 | 169 | widget_dict['choices'] = [] 170 | for key, value in self.widget.choices: 171 | widget_dict['choices'].append({ 172 | 'value': key, 173 | 'display': value 174 | }) 175 | 176 | widget_dict['input_type'] = 'select' 177 | 178 | return widget_dict 179 | 180 | 181 | class RemoteNullBooleanSelect(RemoteSelect): 182 | def as_dict(self): 183 | return super(RemoteNullBooleanSelect, self).as_dict() 184 | 185 | 186 | class RemoteSelectMultiple(RemoteSelect): 187 | def as_dict(self): 188 | widget_dict = super(RemoteSelectMultiple, self).as_dict() 189 | 190 | widget_dict['input_type'] = 'selectmultiple' 191 | widget_dict['size'] = len(widget_dict['choices']) 192 | 193 | return widget_dict 194 | 195 | 196 | class RemoteRadioInput(RemoteWidget): 197 | def as_dict(self): 198 | widget_dict = OrderedDict() 199 | widget_dict['title'] = self.widget.__class__.__name__ 200 | widget_dict['name'] = self.widget.name 201 | widget_dict['value'] = self.widget.value 202 | widget_dict['attrs'] = self.widget.attrs 203 | widget_dict['choice_value'] = self.widget.choice_value 204 | widget_dict['choice_label'] = self.widget.choice_label 205 | widget_dict['index'] = self.widget.index 206 | widget_dict['input_type'] = 'radio' 207 | 208 | return widget_dict 209 | 210 | 211 | class RemoteRadioFieldRenderer(RemoteWidget): 212 | def as_dict(self): 213 | widget_dict = OrderedDict() 214 | widget_dict['title'] = self.widget.__class__.__name__ 215 | widget_dict['name'] = self.widget.name 216 | widget_dict['value'] = self.widget.value 217 | widget_dict['attrs'] = self.widget.attrs 218 | widget_dict['choices'] = self.widget.choices 219 | widget_dict['input_type'] = 'radio' 220 | 221 | return widget_dict 222 | 223 | 224 | class RemoteRadioSelect(RemoteSelect): 225 | def as_dict(self): 226 | widget_dict = super(RemoteRadioSelect, self).as_dict() 227 | 228 | widget_dict['choices'] = [] 229 | for key, value in self.widget.choices: 230 | widget_dict['choices'].append({ 231 | 'name': self.field_name or '', 232 | 'value': key, 233 | 'display': value 234 | }) 235 | 236 | widget_dict['input_type'] = 'radio' 237 | 238 | return widget_dict 239 | 240 | 241 | class RemoteCheckboxSelectMultiple(RemoteSelectMultiple): 242 | def as_dict(self): 243 | return super(RemoteCheckboxSelectMultiple, self).as_dict() 244 | 245 | 246 | class RemoteMultiWidget(RemoteWidget): 247 | def as_dict(self): 248 | widget_dict = super(RemoteMultiWidget, self).as_dict() 249 | 250 | widget_list = [] 251 | for widget in self.widget.widgets: 252 | # Fetch remote widget and convert to dict 253 | widget_list.append() 254 | 255 | widget_dict['widgets'] = widget_list 256 | 257 | return widget_dict 258 | 259 | 260 | class RemoteSplitDateTimeWidget(RemoteMultiWidget): 261 | def as_dict(self): 262 | widget_dict = super(RemoteSplitDateTimeWidget, self).as_dict() 263 | 264 | widget_dict['date_format'] = self.widget.date_format 265 | widget_dict['time_format'] = self.widget.time_format 266 | 267 | return widget_dict 268 | 269 | 270 | class RemoteSplitHiddenDateTimeWidget(RemoteSplitDateTimeWidget): 271 | def as_dict(self): 272 | return super(RemoteSplitHiddenDateTimeWidget, self).as_dict() 273 | -------------------------------------------------------------------------------- /django_remote_forms/fields.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from collections import OrderedDict 4 | 5 | from django.conf import settings 6 | 7 | from django_remote_forms import logger, widgets 8 | 9 | 10 | class RemoteField(object): 11 | """ 12 | A base object for being able to return a Django Form Field as a Python 13 | dictionary. 14 | 15 | This object also takes into account if there is initial data for the field 16 | coming in from the form directly, which overrides any initial data 17 | specified on the field per Django's rules: 18 | 19 | https://docs.djangoproject.com/en/dev/ref/forms/api/#dynamic-initial-values 20 | """ 21 | 22 | def __init__(self, field, form_initial_data=None, field_name=None): 23 | self.field_name = field_name 24 | self.field = field 25 | self.form_initial_data = form_initial_data 26 | 27 | def as_dict(self): 28 | field_dict = OrderedDict() 29 | field_dict['title'] = self.field.__class__.__name__ 30 | field_dict['required'] = self.field.required 31 | field_dict['label'] = self.field.label 32 | field_dict['initial'] = self.form_initial_data or self.field.initial 33 | field_dict['help_text'] = self.field.help_text 34 | 35 | field_dict['error_messages'] = self.field.error_messages 36 | 37 | # Instantiate the Remote Forms equivalent of the widget if possible 38 | # in order to retrieve the widget contents as a dictionary. 39 | remote_widget_class_name = 'Remote%s' % self.field.widget.__class__.__name__ 40 | try: 41 | remote_widget_class = getattr(widgets, remote_widget_class_name) 42 | remote_widget = remote_widget_class(self.field.widget, field_name=self.field_name) 43 | except Exception, e: 44 | logger.warning('Error serializing %s: %s', remote_widget_class_name, str(e)) 45 | widget_dict = {} 46 | else: 47 | widget_dict = remote_widget.as_dict() 48 | 49 | field_dict['widget'] = widget_dict 50 | 51 | return field_dict 52 | 53 | 54 | class RemoteCharField(RemoteField): 55 | def as_dict(self): 56 | field_dict = super(RemoteCharField, self).as_dict() 57 | 58 | field_dict.update({ 59 | 'max_length': self.field.max_length, 60 | 'min_length': self.field.min_length 61 | }) 62 | 63 | return field_dict 64 | 65 | 66 | class RemoteIntegerField(RemoteField): 67 | def as_dict(self): 68 | field_dict = super(RemoteIntegerField, self).as_dict() 69 | 70 | field_dict.update({ 71 | 'max_value': self.field.max_value, 72 | 'min_value': self.field.min_value 73 | }) 74 | 75 | return field_dict 76 | 77 | 78 | class RemoteFloatField(RemoteIntegerField): 79 | def as_dict(self): 80 | return super(RemoteFloatField, self).as_dict() 81 | 82 | 83 | class RemoteDecimalField(RemoteIntegerField): 84 | def as_dict(self): 85 | field_dict = super(RemoteDecimalField, self).as_dict() 86 | 87 | field_dict.update({ 88 | 'max_digits': self.field.max_digits, 89 | 'decimal_places': self.field.decimal_places 90 | }) 91 | 92 | return field_dict 93 | 94 | 95 | class RemoteTimeField(RemoteField): 96 | def as_dict(self): 97 | field_dict = super(RemoteTimeField, self).as_dict() 98 | 99 | field_dict['input_formats'] = self.field.input_formats 100 | 101 | if (field_dict['initial']): 102 | if callable(field_dict['initial']): 103 | field_dict['initial'] = field_dict['initial']() 104 | 105 | # If initial value is datetime then convert it using first available input format 106 | if (isinstance(field_dict['initial'], (datetime.datetime, datetime.time, datetime.date))): 107 | if not len(field_dict['input_formats']): 108 | if isinstance(field_dict['initial'], datetime.date): 109 | field_dict['input_formats'] = settings.DATE_INPUT_FORMATS 110 | elif isinstance(field_dict['initial'], datetime.time): 111 | field_dict['input_formats'] = settings.TIME_INPUT_FORMATS 112 | elif isinstance(field_dict['initial'], datetime.datetime): 113 | field_dict['input_formats'] = settings.DATETIME_INPUT_FORMATS 114 | 115 | input_format = field_dict['input_formats'][0] 116 | field_dict['initial'] = field_dict['initial'].strftime(input_format) 117 | 118 | return field_dict 119 | 120 | 121 | class RemoteDateField(RemoteTimeField): 122 | def as_dict(self): 123 | return super(RemoteDateField, self).as_dict() 124 | 125 | 126 | class RemoteDateTimeField(RemoteTimeField): 127 | def as_dict(self): 128 | return super(RemoteDateTimeField, self).as_dict() 129 | 130 | 131 | class RemoteRegexField(RemoteCharField): 132 | def as_dict(self): 133 | field_dict = super(RemoteRegexField, self).as_dict() 134 | 135 | # We don't need the pattern object in the frontend 136 | # field_dict['regex'] = self.field.regex 137 | 138 | return field_dict 139 | 140 | 141 | class RemoteEmailField(RemoteCharField): 142 | def as_dict(self): 143 | return super(RemoteEmailField, self).as_dict() 144 | 145 | 146 | class RemoteFileField(RemoteField): 147 | def as_dict(self): 148 | field_dict = super(RemoteFileField, self).as_dict() 149 | 150 | field_dict['max_length'] = self.field.max_length 151 | 152 | return field_dict 153 | 154 | 155 | class RemoteImageField(RemoteFileField): 156 | def as_dict(self): 157 | return super(RemoteImageField, self).as_dict() 158 | 159 | 160 | class RemoteURLField(RemoteCharField): 161 | def as_dict(self): 162 | return super(RemoteURLField, self).as_dict() 163 | 164 | 165 | class RemoteBooleanField(RemoteField): 166 | def as_dict(self): 167 | return super(RemoteBooleanField, self).as_dict() 168 | 169 | 170 | class RemoteNullBooleanField(RemoteBooleanField): 171 | def as_dict(self): 172 | return super(RemoteNullBooleanField, self).as_dict() 173 | 174 | 175 | class RemoteChoiceField(RemoteField): 176 | def as_dict(self): 177 | field_dict = super(RemoteChoiceField, self).as_dict() 178 | 179 | field_dict['choices'] = [] 180 | for key, value in self.field.choices: 181 | field_dict['choices'].append({ 182 | 'value': key, 183 | 'display': value 184 | }) 185 | 186 | return field_dict 187 | 188 | 189 | class RemoteModelChoiceField(RemoteChoiceField): 190 | def as_dict(self): 191 | return super(RemoteModelChoiceField, self).as_dict() 192 | 193 | 194 | class RemoteTypedChoiceField(RemoteChoiceField): 195 | def as_dict(self): 196 | field_dict = super(RemoteTypedChoiceField, self).as_dict() 197 | 198 | field_dict.update({ 199 | 'coerce': self.field.coerce, 200 | 'empty_value': self.field.empty_value 201 | }) 202 | 203 | return field_dict 204 | 205 | 206 | class RemoteMultipleChoiceField(RemoteChoiceField): 207 | def as_dict(self): 208 | return super(RemoteMultipleChoiceField, self).as_dict() 209 | 210 | 211 | class RemoteModelMultipleChoiceField(RemoteMultipleChoiceField): 212 | def as_dict(self): 213 | return super(RemoteModelMultipleChoiceField, self).as_dict() 214 | 215 | 216 | class RemoteTypedMultipleChoiceField(RemoteMultipleChoiceField): 217 | def as_dict(self): 218 | field_dict = super(RemoteTypedMultipleChoiceField, self).as_dict() 219 | 220 | field_dict.update({ 221 | 'coerce': self.field.coerce, 222 | 'empty_value': self.field.empty_value 223 | }) 224 | 225 | return field_dict 226 | 227 | 228 | class RemoteComboField(RemoteField): 229 | def as_dict(self): 230 | field_dict = super(RemoteComboField, self).as_dict() 231 | 232 | field_dict.update(fields=self.field.fields) 233 | 234 | return field_dict 235 | 236 | 237 | class RemoteMultiValueField(RemoteField): 238 | def as_dict(self): 239 | field_dict = super(RemoteMultiValueField, self).as_dict() 240 | 241 | field_dict['fields'] = self.field.fields 242 | 243 | return field_dict 244 | 245 | 246 | class RemoteFilePathField(RemoteChoiceField): 247 | def as_dict(self): 248 | field_dict = super(RemoteFilePathField, self).as_dict() 249 | 250 | field_dict.update({ 251 | 'path': self.field.path, 252 | 'match': self.field.match, 253 | 'recursive': self.field.recursive 254 | }) 255 | 256 | return field_dict 257 | 258 | 259 | class RemoteSplitDateTimeField(RemoteMultiValueField): 260 | def as_dict(self): 261 | field_dict = super(RemoteSplitDateTimeField, self).as_dict() 262 | 263 | field_dict.update({ 264 | 'input_date_formats': self.field.input_date_formats, 265 | 'input_time_formats': self.field.input_time_formats 266 | }) 267 | 268 | return field_dict 269 | 270 | 271 | class RemoteIPAddressField(RemoteCharField): 272 | def as_dict(self): 273 | return super(RemoteIPAddressField, self).as_dict() 274 | 275 | 276 | class RemoteSlugField(RemoteCharField): 277 | def as_dict(self): 278 | return super(RemoteSlugField, self).as_dict() 279 | --------------------------------------------------------------------------------