Add FormOptions class, use it for the DetailForm

Signed-off-by: Roberto Rosario <roberto.rosario.gonzalez@gmail.com>
This commit is contained in:
Roberto Rosario
2019-04-14 15:28:56 -04:00
parent 33f84ec327
commit 5127712711
4 changed files with 163 additions and 14 deletions

View File

@@ -52,6 +52,9 @@
* Update middleware to new style classes.
* Add server side invalid document template.
* Move tag specific JavaScript to the tags app.
* Reduce form boilerplate code with new FormOptions class.
* Use FormOptions for the DetailForm class.
* DetailForm now support help text on extra fields.
3.1.11 (2019-04-XX)
===================

View File

@@ -76,6 +76,9 @@ Other changes
* Update middleware to new style classes.
* Add server side invalid document template.
* Move tag specific JavaScript to the tags app.
* Reduce form boilerplate code with new FormOptions class.
* Use FormOptions for the DetailForm class.
* DetailForm now support help text on extra fields.
Removals
--------

View File

@@ -4,14 +4,16 @@ import os
from django import forms
from django.conf import settings
from django.contrib.admin.utils import label_for_field
from django.contrib.auth import get_user_model
from django.core.exceptions import FieldDoesNotExist
from django.db import models
from django.utils.module_loading import import_string
from django.utils.translation import ugettext_lazy as _
from .classes import Package
from .models import UserLocaleProfile
from .utils import return_attrib
from .utils import introspect_attribute, resolve_attribute
from .widgets import DisableableSelectWidget, PlainWidget, TextAreaDiv
@@ -37,26 +39,98 @@ class ChoiceForm(forms.Form):
selection = forms.MultipleChoiceField(widget=DisableableSelectWidget())
class FormOptions(object):
def __init__(self, form, kwargs, options=None):
"""
Option definitions will be iterated. The option value will be
determined in the following order: as passed via keyword
arguments during form intialization, as form get_... method or
finally as static Meta options. This is to allow a form with
Meta options or method to be overrided at initialization
and increase the usability of a single class.
"""
for name, default_value in self.option_definitions.items():
try:
# Check for a runtime value via kwargs
value = kwargs.pop(name)
except KeyError:
try:
# Check if there is a get_... method
value = getattr(self, 'get_{}'.format(name))()
except AttributeError:
try:
# Check the meta class options
value = getattr(options, name)
except AttributeError:
value = default_value
setattr(self, name, value)
class DetailFormOption(FormOptions):
# Dictionary list of option names and default values
option_definitions = {'extra_fields': []}
class DetailForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.extra_fields = kwargs.pop('extra_fields', ())
self.opts = DetailFormOption(
form=self, kwargs=kwargs, options=getattr(self, 'Meta', None)
)
super(DetailForm, self).__init__(*args, **kwargs)
for extra_field in self.extra_fields:
result = return_attrib(self.instance, extra_field['field'])
label = 'label' in extra_field and extra_field['label'] or None
for extra_field in self.opts.extra_fields:
obj = extra_field.get('object', self.instance)
field = extra_field['field']
result = resolve_attribute(
attribute=field, obj=obj
)
label = extra_field.get('label', None)
# If label is not specified try to get it from the object itself
if not label:
attribute_name, obj = introspect_attribute(
attribute_name=field, obj=obj
)
if not obj:
label = _('None')
else:
try:
label = getattr(
getattr(obj, attribute_name), 'short_description'
)
except AttributeError:
label = label_for_field(
name=attribute_name, model=obj
)
help_text = extra_field.get('help_text', None)
# If help_text is not specified try to get it from the object itself
if not help_text:
if obj:
try:
field_object = obj._meta.get_field(field_name=field)
except FieldDoesNotExist:
field_object = field
help_text = getattr(
field_object, 'help_text', None
)
# TODO: Add others result types <=> Field types
if isinstance(result, models.query.QuerySet):
self.fields[extra_field['field']] = \
forms.ModelMultipleChoiceField(
queryset=result, label=label)
self.fields[field] = forms.ModelMultipleChoiceField(
queryset=result, label=label
)
else:
self.fields[extra_field['field']] = forms.CharField(
label=extra_field['label'],
initial=return_attrib(
self.instance,
extra_field['field'], None
),
self.fields[field] = forms.CharField(
initial=resolve_attribute(
obj=obj,
attribute=field
), label=label, help_text=help_text,
widget=extra_field.get('widget', PlainWidget)
)

View File

@@ -4,6 +4,8 @@ import logging
import types
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist
from django.db.models.constants import LOOKUP_SEP
from django.urls import resolve as django_resolve
from django.urls.base import get_script_prefix
from django.utils.datastructures import MultiValueDict
@@ -51,11 +53,78 @@ def get_user_label_text(context):
return context['request'].user.get_full_name() or context['request'].user
def introspect_attribute(attribute_name, obj):
"""
Resolve the attribute of model. Supports nested reference using dotted
paths or double underscore.
"""
try:
# Try as a related field
obj._meta.get_field(field_name=attribute_name)
except (AttributeError, FieldDoesNotExist):
attribute_name = attribute_name.replace('__', '.')
try:
# If there are separators in the attribute name, traverse them
# to the final attribute
attribute_part, attribute_remaining = attribute_name.split(
'.', 1
)
except ValueError:
return attribute_name, obj
else:
related_field = obj._meta.get_field(field_name=attribute_part)
return introspect_attribute(
attribute_name=attribute_part,
obj=related_field.related_model,
)
else:
return attribute_name, obj
def resolve(path, urlconf=None):
path = '/{}'.format(path.replace(get_script_prefix(), '', 1))
return django_resolve(path=path, urlconf=urlconf)
def resolve_attribute(attribute, obj, kwargs=None):
"""
Resolve the attribute of an object. Behaves like the Python REPL but with
an unified dotted path schema regardless of the attribute type.
Supports callables, dictionaries, properties, related model fields.
"""
if not kwargs:
kwargs = {}
# Try as a callable
try:
return attribute(obj, **kwargs)
except TypeError:
# Try as a dictionary
try:
return obj[attribute]
except TypeError:
try:
# If there are dots in the attribute name, traverse them
# to the final attribute
result = reduce_function(getattr, attribute.split('.'), obj)
try:
# Try it as a method
return result(**kwargs)
except TypeError:
# Try it as a property
return result
except AttributeError:
# Try as a related model field
if LOOKUP_SEP in attribute:
attribute_replaced = attribute.replace(LOOKUP_SEP, '.')
return resolve_attribute(
obj=obj, attribute=attribute_replaced, kwargs=kwargs
)
else:
raise
def return_attrib(obj, attrib, arguments=None):
if isinstance(attrib, types.FunctionType):
return attrib(obj)