generic_fields.py 6.63 KB
from django import forms
from django.db.models import Q

# TODO: import particular classes?
import crispy_forms.bootstrap as bootstrap
import crispy_forms.layout as layout

from .query_managers import (
    SingleValueQueryManager,
    MultiValueQueryManager,
    RangesQueryManager,
    SingleRegexQueryManager,
    RegexQueryManager,
    ComboQueryManager
)


class LayoutField(object):
    
    def layout(self, x, **kwargs):
        return layout.Field(x, **kwargs)


class CheckboxesLayoutField(object):
    
    def layout(self, x, **kwargs):
        return bootstrap.InlineCheckboxes(x, **kwargs)


class RadiosLayoutField(object):
    
    def layout(self, x, **kwargs):
        return bootstrap.InlineRadios(x, **kwargs)


class SelectLayoutField(object):
    
    def layout(self, x, **kwargs):
        return layout.Field(x, css_class='custom-select', **kwargs)

# ==============================================================================


#TODO move implementation common for RangesFilter and RegexFilter to an abstract ExpressionFilter?
class RangeFilter(forms.CharField, LayoutField):
    
    def __init__(self, label, lookup, initial='', empty_value='', **kwargs):
        super().__init__(label=label, required=False, initial=initial, empty_value=empty_value, **kwargs)
        self.query_manager = RangesQueryManager(lookup)
    
    # can’t use static default_validators since the query manager is a class field
    def validate(self, value):
        self.query_manager.expression_validator(value)
    

class RegexFilter(forms.CharField, LayoutField):
    
    def __init__(self, label, lookup, additional_operators=False, initial='.*', empty_value='.*', autocomplete=None, **kwargs):
        self.css_class = 'regex-autocomplete' if autocomplete else None
        self.data_autocomplete = autocomplete if autocomplete else None
        super().__init__(label=label, required=False, initial=initial, empty_value=empty_value, **kwargs)
        self.query_manager = RegexQueryManager(lookup, additional_operators=additional_operators)
        
    # can’t use static default_validators since validation depends on the
    # query_manager instance (allowed operators)
    def validate(self, value):
        self.query_manager.expression_validator(value)
    
    def layout(self, x, **kwargs):
        return layout.Field(x, css_class=self.css_class, data_autocomplete=self.data_autocomplete, **kwargs)


class SingleRegexFilter(forms.CharField, LayoutField):
    
    def __init__(self, label, lookup, initial='.*', empty_value='.*', **kwargs):
        super().__init__(label=label, required=False, initial=initial, empty_value=empty_value, **kwargs)
        self.query_manager = SingleRegexQueryManager(lookup)
    

class MultipleChoiceFilter(forms.MultipleChoiceField, CheckboxesLayoutField):
    
    def __init__(self, label, choices, lookup, **kwargs):
        super().__init__(
            label=label,
            choices=choices,
            required=False,
            **kwargs
        )
        self.query_manager = MultiValueQueryManager(lookup, default_conjunction=False)
    

class ModelMultipleChoiceFilter(forms.ModelMultipleChoiceField, CheckboxesLayoutField):
    
    def __init__(self, label, queryset, key, lookup, human_values={}, **kwargs):
        super().__init__(
            label=label,
            queryset=queryset,
            required=False,
            **kwargs
        )
        self.query_manager = MultiValueQueryManager(lookup, default_conjunction=False)
        self.key = key
        self.human_values = human_values
    
    def label_from_instance(self, obj):
        return self.human_values.get(obj.__dict__[self.key], obj.__dict__[self.key])


class ModelChoiceFilter(forms.ModelChoiceField, SelectLayoutField):
    
    def __init__(self, label, queryset, key, lookup, human_values={}, **kwargs):
        super().__init__(
            label=label,
            queryset=queryset,
            required=False,
            **kwargs
        )
        self.query_manager = SingleValueQueryManager(lookup)
        # str object or callable
        self.key = key
        self.human_values = human_values
    
    def label_from_instance(self, obj):
        if type(self.key) == str:
            return self.human_values.get(obj.__dict__[self.key], obj.__dict__[self.key])
        else:
            return self.key(obj)


# MultiValueField is an abstract class, must be subclassed and implement compress
class ComboFilter(forms.MultiValueField, LayoutField):

    # MultiWidget is an abstract class, must be subclassed and implement decompress
    class ComboWidget(forms.widgets.MultiWidget):
        def decompress(self, value):
            return value if value else [None for i in range(len(self.widgets))]
    
    def layout(self, x, **kwargs):
        attrs = []
        for i, field in enumerate(self.fields):
            cls = ['col-sm-3']
            if i < len(self.fields) - 1:
                cls.append('mr-4')
            if type(field) == SwitchField:
                cls += ['custom-switch']
            elif type(field) == ModelChoiceFilter:
                cls.append('custom-select')
            elif type(field) == SingleRegexFilter:
                cls.append('form-control')
            attrs.append({'class' : ' '.join(cls)})
        return layout.MultiWidgetField(x, attrs=attrs, **kwargs)
    
    def __init__(self, label, inner_class, outer_lookup, fields, negation_field=False, **kwargs):
        neg_fields = [SwitchField('!')] if negation_field else []
        all_fields = neg_fields + fields
        super().__init__(
            label=label,
            fields=all_fields,
            widget=self.ComboWidget(widgets=[field.widget for field in all_fields]),
            required=False,
            **kwargs
        )
        self.query_fields = fields
        self.query_manager = ComboQueryManager(inner_class, outer_lookup, [f.query_manager for f in fields], negation_field)
    
    def compress(self, data_list):
        return data_list


class OperatorField(forms.ChoiceField, RadiosLayoutField):
    
    def __init__(self):
        super().__init__(
            label=' ',
            choices=((False, 'lub'), (True, 'i')),
            initial=False,
            required=False,
         )
    
    def to_python(self, value):
        if value in ('1', 'true', 'True', True):
            return True
        if value in ('0', 'false', 'False', False):
            return False
        return None


class SwitchField(forms.BooleanField, LayoutField):
    
    def __init__(self, label):
        super().__init__(label=label, required=False)
    
    def layout(self, x, **kwargs):
         return layout.Field(x, wrapper_class='custom-switch float-right font-weight-bold', **kwargs)