diff --git a/.travis.yml b/.travis.yml index 9fe2010..13a414a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,30 @@ +dist: xenial language: python -python: - - "2.7" - - "3.4" - - "3.5" -env: - - DJANGO_VERSION=1.8.17 - - DJANGO_VERSION=1.9.12 - - DJANGO_VERSION=1.10.6 - - DJANGO_VERSION=1.11 +matrix: + include: + - python: "2.7" + env: DJANGO_VERSION=1.10.8 + - python: "2.7" + env: DJANGO_VERSION=1.11.20 + - python: "3.4" + env: DJANGO_VERSION=1.11 + - python: "3.4" + env: DJANGO_VERSION=2.0.13 + - python: "3.5" + env: DJANGO_VERSION=2.1.8 + - python: "3.5" + env: DJANGO_VERSION=2.2.1 + - python: "3.6" + env: DJANGO_VERSION=2.1.8 + - python: "3.6" + env: DJANGO_VERSION=2.2.1 + install: - pip install Django==$DJANGO_VERSION - pip install six - pip install -e . script: - ./example/manage.py test formfield -branches: - only: - - master +notifications: + email: + - jose@linux.com diff --git a/Makefile b/Makefile index 5f42d5a..fe22099 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ test: - DJANGO_SETTINGS_MODULE=formfield.example.settings django-admin.py test formfield \ No newline at end of file + DJANGO_SETTINGS_MODULE=example.settings django-admin.py test formfield diff --git a/example/sample_app/migrations/0001_initial.py b/example/sample_app/migrations/0001_initial.py new file mode 100644 index 0000000..ce239fe --- /dev/null +++ b/example/sample_app/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.20 on 2019-05-17 10:18 +from __future__ import unicode_literals + +from django.db import migrations, models +import formfield.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Person', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=255)), + ('meta_info', formfield.fields.ModelFormField(blank=True, null=True)), + ], + ), + ] diff --git a/example/sample_app/migrations/__init__.py b/example/sample_app/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/settings.py b/example/settings.py index ee5aafb..bf37827 100755 --- a/example/settings.py +++ b/example/settings.py @@ -1,7 +1,7 @@ # Django settings for example project. DEBUG = True -TEMPLATE_DEBUG = DEBUG + import os, sys APP = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) PROJ_ROOT = os.path.abspath(os.path.dirname(__file__)) @@ -66,14 +66,8 @@ # Make this unique, and don't share it with anybody. SECRET_KEY = 'g2_39yupn*6j4p*cg2%w643jiq-1n_annua*%i8+rq0dx9p=$n' -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.load_template_source', -) -MIDDLEWARE_CLASSES = ( +MIDDLEWARE = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -82,11 +76,24 @@ ROOT_URLCONF = 'example.urls' -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + str(PROJ_ROOT + "/templates"), + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + INSTALLED_APPS = ( 'django.contrib.admin', @@ -95,8 +102,9 @@ 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.staticfiles', + 'django.contrib.messages', 'formfield', - 'sample_app', + 'example.sample_app', ) TEST_RUNNER = 'django.test.runner.DiscoverRunner' diff --git a/example/urls.py b/example/urls.py index c437dc7..236d261 100755 --- a/example/urls.py +++ b/example/urls.py @@ -1,19 +1,29 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from django.conf.urls import include, url +import django from django.contrib import admin + +try: + from django.urls import path +except (ImportError, ): + from django.conf.urls import url + + +major = django.VERSION[0] + admin.autodiscover() urlpatterns = [ - url(r'^formfield/', include('formfield.urls')), - url(r'^admin/', include(admin.site.urls)), ] -# -# urlpatterns = urlpatterns + patterns('', -# (r'^static/(?P.*)$', 'django.views.static.serve', -# {'document_root': settings.MEDIA_ROOT}), -# ) if settings.DEBUG else urlpatterson +if major == 2: + urlpatterns.append( + path('admin/', admin.site.urls), + ) +else: + urlpatterns.append( + url('^admin/', admin.site.urls) + ) diff --git a/formfield/__init__.py b/formfield/__init__.py index 1b03585..1e94150 100644 --- a/formfield/__init__.py +++ b/formfield/__init__.py @@ -5,11 +5,11 @@ from __future__ import unicode_literals __version_info__ = { - 'major': 0, - 'minor': 4, + 'major': 1, + 'minor': 0, 'micro': 0, 'releaselevel': 'final', - 'serial': 1 + 'serial': 0 } diff --git a/formfield/fields.py b/formfield/fields.py index fd1c445..4849b16 100644 --- a/formfield/fields.py +++ b/formfield/fields.py @@ -26,6 +26,14 @@ def __init__(self, *args, **kwargs): super(JSONField, self).__init__(*args, **kwargs) + def from_db_value(self, value, *args, **kwargs): + if isinstance(value, six.string_types): + try: + return json.loads(value, **self.load_kwargs) + except (ValueError, ): + pass + return value + def to_python(self, value): if isinstance(value, six.string_types): @@ -51,7 +59,7 @@ def value_to_string(self, obj): class FormField(forms.MultiValueField): """The form field we can use in forms""" - def __init__(self, form, **kwargs): + def __init__(self, form, *args, **kwargs): import inspect if inspect.isclass(form) and issubclass(form, forms.Form): form_class = form @@ -75,7 +83,8 @@ def __init__(self, form, **kwargs): self.max_length = kwargs.pop('max_length', None) - super(FormField, self).__init__(**kwargs) + fields = [] + super(FormField, self).__init__(fields, *args, **kwargs) self.fields = [f.field for f in self.form] @@ -97,7 +106,17 @@ def clean(self, value): raise ValidationError( 'Error found in Form Field: Nothing to validate') - data = dict((bf.name, value[i]) for i, bf in enumerate(self.form)) + if isinstance(value, dict): + # MultiValueField iterates back through each field. A nested FormField + # Will get called twice: once for the parent form and once from the superclass + # The second time, the value is a dict, and needs to be re-converted to + # avoid errors + data = value + value_list = [data[x.name] for x in self.form] + value = value_list + else: + data = dict((bf.name, value[i]) for i, bf in enumerate(self.form)) + self.form = form = self.form.__class__(data) if not form.is_valid(): error_dict = list(form.errors.items()) @@ -128,10 +147,3 @@ def formfield(self, form_class=FormField, **kwargs): # Need to supply form to FormField return super(ModelFormField, self).formfield(form_class=form_class, form=self.form, **kwargs) - -try: - from south.modelsinspector import add_introspection_rules - add_introspection_rules([], ["^formfield\.fields\.JSONField"]) - add_introspection_rules([], ["^formfield\.fields\.ModelFormField"]) -except ImportError: - pass diff --git a/formfield/static/css/formfield.css b/formfield/static/css/formfield.css new file mode 100644 index 0000000..e211d92 --- /dev/null +++ b/formfield/static/css/formfield.css @@ -0,0 +1,26 @@ +div.formfield > ul > li > div.formfield > ul { + margin-left: 50px; +} + +div.formfield > ul > li{ + padding: 5px 0; + border-bottom: 1px solid #eee; + +} + +div.formfield > ul > li.ff > label { + display: block; + background-color: #eee; + clear: both; + width: 100%; + padding: 5px 10px; + font-weight: bold; + margin-bottom: 5px; +} + +div.formfield > ul > li > label { + display: block; + padding: 4px 10px 0 0; + float: left; + width: 120px; +} diff --git a/formfield/templates/django/forms/widgets/formfield.html b/formfield/templates/django/forms/widgets/formfield.html new file mode 100644 index 0000000..2b2211b --- /dev/null +++ b/formfield/templates/django/forms/widgets/formfield.html @@ -0,0 +1,10 @@ + +
+ +
diff --git a/formfield/widgets.py b/formfield/widgets.py index d1ebd9f..b65e012 100644 --- a/formfield/widgets.py +++ b/formfield/widgets.py @@ -1,12 +1,21 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- from django import forms +from django.utils.safestring import mark_safe class FormFieldWidget(forms.MultiWidget): """ This widget will render each field found in the supplied form. """ + + template_name = 'django/forms/widgets/formfield.html' + + class Media: + css = { + 'all': ('css/formfield.css', ) + } + def __init__(self, fields, attrs=None): self.fields = fields # Retreive each field widget for the form @@ -43,14 +52,19 @@ def format_label(self, field, counter): """ Format the label for each field """ - return '' % ( - counter, field.field.required and 'class="required"', field.label) + if field.field.widget.is_hidden: + return '' + required = field.field.required and 'class="required"' or '' + return mark_safe('' % ( + counter, required, field.label)) def format_help_text(self, field, counter): """ Format the help text for the bound field """ - return '

%s

' % field.help_text + help_text = mark_safe('

%s

' % field.help_text) + is_visible = not field.field.widget.is_hidden + return (help_text and is_visible and help_text) or '' def format_output(self, rendered_widgets): """ @@ -65,3 +79,14 @@ def format_output(self, rendered_widgets): ret.append(u'') return ''.join(ret) + + def get_context(self, name, value, attrs): + context = super(FormFieldWidget, self).get_context(name, value, attrs) + + # Add in the missing labels and help_text, since we are rendering + # a subform, we need to show the field names per input + for i, sub in enumerate(context['widget']['subwidgets']): + sub['label'] = self.format_label(self.fields[i], i) + sub['help_text'] = self.format_help_text(self.fields[i], i) + + return context