Source code for core.forms

# -*- coding: utf-8 -*-
"""
Standard baseclass form definitions & some widget definitions

:subtitle:`Class definitions:`
"""
import magic
import datetime
from django.conf import settings
from django import forms
from core.widgets import SelectDateWidget
from django.forms.widgets import Select, RadioSelect
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import filesizeformat


class InnerQuerySet(object):
    objects = []
    _prefetch_related_lookups = False

    def __init__(self, objects):
        self.objects = objects

    def __iter__(self):
        return iter(self.objects)

    def iterator(self):
        return iter(self.objects)

    def objects(self):
        return self.iterator()


[docs]class QuerysetWrapper(object): """ Queryset wrapper for caching the choices coupled to a ManyToManyField. The querysetwrapper provides functions which otherwise should be called on the queryset directly. Dramatically reduces the amount of queries needed when rendering form fieldsets via a fieldset template. """ _prefetch_related_lookups = False inner_queryset = None def __init__(self, objects): """ Store the objects in the wrapper """ self.inner_queryset = InnerQuerySet(objects) def __iter__(self): return iter(self.inner_queryset.objects)
[docs] def all(self): """ Returns: All stored objects """ return self.inner_queryset
def objects(self): return self.inner_queryset.objects() def __len__(self): """ Returns: The length of the stored objects """ return len(self.inner_queryset.objects)
[docs] def none(self): """ Returns: An empty list """ return []
[docs] def filter(self, **kwargs): """ Filter function which accepts pk and pk__in filters. Returns: A list of objects """ if 'pk' in kwargs: for obj in self.inner_queryset.objects: if str(obj.pk) == kwargs['pk']: return [obj] if 'pk__in' in kwargs: obj_list = [] for obj in self.inner_queryset.objects: if str(obj.pk) in kwargs['pk__in']: obj_list.append(obj) return obj_list raise Exception('The used filter cannot be found: ' + str(kwargs))
[docs]class BaseClassForm(object): ''' Base class form which holds the functions shared among all forms. Automatically adds placeholders '''
[docs] def add_placeholders_to_fields(self): ''' Automatically add the 'placeholder' attribute to appropiate HTML input elements ''' for field_name in self.fields: field = self.fields.get(field_name) if field: # Only do this for textinput, dateinput and emailinput if ((type(field.widget) in (forms.TextInput, forms.DateInput, forms.EmailInput))): field.widget = forms.TextInput( attrs={'placeholder': field.label})
[docs] def get_fields(self): """ Returns: array of all the fields in the fieldset definition for form param """ # Get the fields of a form fields = [] for fieldset in self.fieldsets(): fields += fieldset[1] return fields
[docs] def fieldsets(self): ''' Instead of using fields on forms, fieldsets are used. This allows ordening the fields in sets and hiding/showing different sets based on selected values ''' if hasattr(self, 'Meta'): fieldsets = getattr(self.Meta, 'fieldsets', None) else: fieldsets = None if not hasattr(self, 'form_fieldsets'): if fieldsets: fs = [] for name, fieldset in fieldsets: fs.append( (name, [self[field] for field in fieldset['fields']])) self.form_fieldsets = fs else: self.form_fieldsets = [(None, [field for field in self])] return self.form_fieldsets
[docs] def queryset_speed_up(self): """ Dramatically decreases the amount of queries necessary in template rendering of ManyToManyFields by replacing the queryset with a :class:`QuerysetWrapper` instance. If not replaced, every time the (BoundField) 'field' variable is used in the template the choices are retrieved from the database, resulting in some cases in 40+ queries. Execute this function in the __init__ of forms that have one or more instances of :class:`ModelMultipleChoiceField` """ if hasattr(self, 'Meta'): fieldsets = getattr(self.Meta, 'fieldsets', None) else: fieldsets = None # Replace the queryset with the wrapper # for ModelMultipleChoiceField # Avoiding many database hits when creating # BoundFields for this instance for name, fieldset in fieldsets: for field_name in fieldset['fields']: if ((isinstance(self.fields[field_name], ModelMultipleChoiceField))): self.fields[field_name].queryset =\ QuerysetWrapper( [x for x in self.fields[field_name].queryset])
[docs]class BaseForm(forms.Form, BaseClassForm): ''' Baseclass form which is based on forms.Form ''' def __init__(self, *args, **kwargs): # pop the changed_by_user for audit trailing self.changed_by_user = kwargs.pop('changed_by_user', None) super(BaseForm, self).__init__(*args, **kwargs) self.add_placeholders_to_fields() def save(self, *args, **kwargs): # auto add the changed_by_user to the instance instance = super(BaseForm, self).save(*args, **kwargs) instance.changed_by_user = self.changed_by_user return instance
[docs]class BaseModelForm(forms.ModelForm, BaseClassForm): ''' Baseclass form which is based on forms.ModelForm ''' def __init__(self, *args, **kwargs): # pop the changed_by_user for audit trailing self.changed_by_user = kwargs.pop('changed_by_user', None) super(BaseModelForm, self).__init__(*args, **kwargs) self.add_placeholders_to_fields() def save(self, *args, **kwargs): # auto add the changed_by_user to the instance instance = super(BaseModelForm, self).save(*args, **kwargs) instance.changed_by_user = self.changed_by_user return instance
[docs]class DisplayWidget(forms.Widget): ''' Widget for displaying values ''' def render(self, name, value, attrs=None): return mark_safe(value) def clean(self, value): return value
[docs]class ImageField(forms.ImageField): """ Override ImageField to allow setting a maximum upload size and checking mime_type with help of the 'magic' package. """ def __init__(self, *args, **kwargs): self.max_upload_size = kwargs.pop('max_upload_size', None) if not self.max_upload_size: self.max_upload_size = settings.MAX_UPLOAD_SIZE super(ImageField, self).__init__(*args, **kwargs) def clean(self, *args, **kwargs): data = super(ImageField, self).clean(*args, **kwargs) if data: try: if data.size > self.max_upload_size: raise forms.ValidationError( _('Afbeelding moet kleiner zijn dan %s.' + 'Huidige grootte is %s.') % (filesizeformat(self.max_upload_size), filesizeformat(data.size))) except AttributeError: pass # Check if 'image' is in mime_type mime_type = magic.from_buffer(data.read(1024), mime=True) if 'image' not in mime_type: raise forms.ValidationError( _('Het geuploade bestand kan niet worden herkend' + ' als afbeelding')) data.seek(0) return data
[docs]class MultipleChoiceField(forms.MultipleChoiceField): ''' Django formfield for multiple selections ''' widget = forms.CheckboxSelectMultiple
[docs]class ModelMultipleChoiceField(forms.ModelMultipleChoiceField): ''' Django formfield for multiple selections ''' widget = forms.CheckboxSelectMultiple
[docs]class ChoiceOtherWidget(forms.MultiWidget): ''' Django widget for choice other fields Displays a select box with the option for 'other' allowing to specify a value via a textinput ''' def __init__(self, choices, other_field, maxlength=128, attrs=None): widgets = [ forms.Select( choices=choices, attrs={'class': 'other-choice-select'}), other_field( attrs={ 'class': 'other-choice-text', 'rows': 3, 'maxlength': maxlength })] super(ChoiceOtherWidget, self).__init__(widgets, attrs)
[docs] def choices(self): ''' Returns: The widget choices ''' return self.widgets[0].choices
[docs] def decompress(self, value): ''' Returns: an array with the value or ['other', value] ''' if value in ('', None): return ['', ''] if value not in [i[0] for i in self.widgets[0].choices]: return ['other', value] return [value, '']
[docs] def format_output(self, rendered_widgets): ''' Returns: the rendered widgets seperated by a breakline ''' return '<br />'.join(rendered_widgets)
[docs]class ChoiceOtherField(forms.MultiValueField): ''' Django formfield for choice other fields ''' def __init__(self, choices=[], *args, **kwargs): self._select_required = kwargs.pop('required', True) self.other_field = kwargs.pop('other_field') self.maxlength = kwargs.pop('maxlength', 128) self.choices = choices fields = [forms.TypedChoiceField(choices=choices), forms.CharField()] widget = ChoiceOtherWidget( choices=choices, other_field=self.other_field, maxlength=self.maxlength ) if not self._select_required: kwargs.update({'required': False}) super(ChoiceOtherField, self).__init__( fields=fields, widget=widget, **kwargs)
[docs] def fix_value_from_post(self, post_data, field_name): ''' Used for fixing post data so it can be temporarily stored in the database and later used as form initial data ''' value = self.get_value_from_post(post_data, field_name) post_data[field_name] = value select_name = field_name + '_0' text_name = field_name + '_1' if select_name in post_data: del post_data[select_name] if text_name in post_data: del post_data[text_name]
[docs] def get_value_from_post(self, post_data, field_name): ''' Get the value from the dropbox except when 'other' is selected if 'other' use the value from the textinput ''' rt = None select_name = field_name + '_0' text_name = field_name + '_1' if select_name in post_data: try: select_value = post_data[select_name] if select_value == 'other': if text_name in post_data: rt = post_data[text_name] else: rt = select_value except KeyError: rt = None return rt
[docs] def clean(self, value): ''' Raise error messages if needed ''' if value[0] in (None, '') and self._select_required: raise forms.ValidationError(self.error_messages['required']) if ((value[0] == 'other' and value[1] in (None, '') and self._select_required)): raise forms.ValidationError(self.error_messages['required']) if value[0] != 'other': value = value[0] else: value = super(ChoiceOtherField, self).clean(value) return value
[docs] def compress(self, data_list): ''' data_list is a list of two items. The first item is the value from the TypeChoiceField and the second one is from the text input. ''' if not data_list: return '' if data_list[0] == 'other' and data_list[1] != '': return data_list[1] return data_list[0]
#class HorizontalRadioSelectRenderer(forms.RadioSelect.renderer): # ''' # Django renderer, overrides widget method to put radio # buttons horizontally # instead of vertically. # ''' # def render(self): # pragma: no cover # '''Outputs radios''' # return mark_safe(u'\n'.join([u'%s\n' % w for w in self]))
[docs]class FormRadioSelect(RadioSelect): ''' Django formfield which overrides the radio select field ''' template_name = 'horizontal_select.html'
# def __init__(self, *args, **kwargs): # pragma: no cover # super(FormRadioSelect, self).__init__(*args, **kwargs) # # self.renderer = HorizontalRadioSelectRenderer
[docs]class SelectDateWidgetCustom(SelectDateWidget): ''' Base class for SelectDateWidget, SelectDateTimeWidget and SelectTimeWidget. '''
[docs] def render(self, name, value, attrs=None): ''' Return the html code of the widget. ''' if 'id' in self.attrs: id_ = self.attrs['id'] else: id_ = 'id_%s' % name local_attrs = self.build_attrs(self.attrs) self.values = self.parse_value(value) output = [] index = 0 for (n, fmt) in self.format: select_name = '%s_%s' % (name, n) local_attrs['id'] = '%s_%s' % (id_, n) if hasattr(self, '%s_choices' % n): choices = getattr(self, '%s_choices' % n)(fmt) if not self.required or not self.values[n]: choices.insert(0, (-1, '---')) select = Select(choices=choices) attrs = local_attrs.copy() if attrs['class']: attrs['class'] = attrs['class'] + '_' + n html = select.render(select_name, self.values[n], attrs) index = index + 1 output.append(html) return mark_safe(u'\n'.join(output))
[docs]class DateWidget(SelectDateWidgetCustom): ''' Django Widget for selecting dates '''
[docs] def render(self, name, value, attrs=None): ''' Render datewidget ''' output = super(DateWidget, self).render(name, value, attrs) return mark_safe(output + ' <span class="date_field"></span>')
[docs]class FormDateField(forms.DateField): ''' Django Formfield for dates ''' widget = DateWidget def __init__(self, *args, **kwargs): self.allow_future_date = kwargs.pop('allow_future_date', True) self.allow_before_birth_date = kwargs.pop( 'allow_before_birth_date', True) self.allow_after_deceased = kwargs.pop('allow_after_deceased', False) self.future = kwargs.pop('future', False) self.years = kwargs.pop('years', None) super(FormDateField, self).__init__(*args, **kwargs) if self.years: self.widget.years = self.years self.widget.required = self.required
[docs] def fix_value_from_post(self, post_data, field_name): ''' Used for fixing post data so it can be temporarily stored in the database and later used as form initial data ''' value = self.get_value_from_post(post_data, field_name) post_data[field_name] = value day_name = field_name + '_day' month_name = field_name + '_month' year_name = field_name + '_year' if day_name in post_data: del post_data[day_name] if month_name in post_data: del post_data[month_name] if year_name in post_data: del post_data[year_name]
[docs] def get_value_from_post(self, post_data, field_name): ''' Try to get the value from post_data, used to temporarily store the value. ''' rt = None day_name = field_name + '_day' month_name = field_name + '_month' year_name = field_name + '_year' if ((day_name in post_data and month_name in post_data and year_name in post_data)): try: day = int(post_data[day_name]) month = int(post_data[month_name]) year = int(post_data[year_name]) rt = datetime.date(year, month, day) except ValueError: rt = None return rt
[docs] def clean(self, value): ''' Date validation checks ''' value = super(FormDateField, self).clean(value) if not self.allow_future_date and value not in (None, ''): if datetime.date.today() < value: raise forms.ValidationError( _(u'Datum mag niet in de toekomst liggen.')) if self.future and value not in (None, ''): if datetime.date.today() > value: raise forms.ValidationError( _(u'Alleen toekomstige datum is geldig.')) return value
[docs] def widget_attrs(self, widget): ''' Add datefield CSS class by default ''' attrs = super(FormDateField, self).widget_attrs(widget) attrs.update({'class': 'datefield', }) return attrs
NONE_YES_NO_CHOICES = ( (1, '---------'), (2, 'Yes'), (3, 'No'), )
[docs]class YesNoChoiceField(forms.NullBooleanField): ''' Django formfield for yes/no selections ''' widget = forms.NullBooleanSelect def __init__(self, *args, **kwargs): super(YesNoChoiceField, self).__init__(*args, **kwargs) self.widget.choices = NONE_YES_NO_CHOICES