forms.py 14.3 KB
from django import forms
from django.db.models import Q

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

from connections.models import Entry, POS, Status, SchemaHook
from syntax.models import Schema, SchemaOpinion, InherentSie, Negativity, Predicativity, Aspect, PhraseType
from semantics.models import FrameOpinion, SemanticRole, RoleAttribute

from .query_managers import QueryManager, MultiValueQueryManager, RangesQueryManager, RegexQueryManager

from . import polish_strings

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

class LayoutField(object):
    
    layout = lambda x: x

class CheckboxesLayoutField(object):
    
    layout = bootstrap.InlineCheckboxes

class RadiosLayoutField(object):
    
    layout = bootstrap.InlineRadios

class SelectLayoutField(object):
    
    layout = lambda x: layout.Field(x, css_class='custom-select')

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

#TODO move implementation common for RangesFilter and RegexFilter to an abstract ExpressionFilter?
class RangesFilter(forms.CharField, LayoutField):
    
    def __init__(self, label, entry_lookup, object_lookup, initial='', empty_value='', **kwargs):
        super().__init__(label=label, required=False, initial=initial, empty_value=empty_value, **kwargs)
        self.query_manager = RangesQueryManager(entry_lookup, object_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, entry_lookup, object_lookup, inner_class=None, outer_lookup=None, additional_operators=False, initial='.*', empty_value='.*', **kwargs):
        super().__init__(label=label, required=False, initial=initial, empty_value=empty_value, **kwargs)
        self.query_manager = RegexQueryManager(entry_lookup, object_lookup, inner_class=inner_class, outer_lookup=outer_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)
    
class MultipleChoiceFilter(forms.MultipleChoiceField, CheckboxesLayoutField):
    
    def __init__(self, label, choices, entry_lookup, object_lookup, **kwargs):
        super().__init__(
            label=label,
            choices=choices,
            required=False,
            **kwargs
        )
        self.query_manager = MultiValueQueryManager(entry_lookup, object_lookup, default_conjunction=False)
    
class ModelMultipleChoiceFilter(forms.ModelMultipleChoiceField, CheckboxesLayoutField):
    
    def __init__(self, label, queryset, key, entry_lookup, object_lookup, human_values={}, **kwargs):
        super().__init__(
            label=label,
            queryset=queryset,
            required=False,
            **kwargs
        )
        self.query_manager = MultiValueQueryManager(entry_lookup, object_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, entry_lookup, object_lookup, human_values={}, **kwargs):
        super().__init__(
            label=label,
            queryset=queryset,
            required=False,
            **kwargs
        )
        self.query_manager = SingleValueQueryManager(entry_lookup, object_lookup)
        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])

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

    # MultiWidget is an abstract class, must be subclassed and implement decompress
    class ArgumentWidget(forms.widgets.MultiWidget):
        def decompress(self, value):
            return value if value else [None for i in range(len(self.widgets))]
    
    def layout(x):
        return layout.MultiWidgetField(
            x,
            attrs=(
                { 'class': 'custom-select col-sm-2 mr-2' },
                { 'class': 'custom-select col-sm-2' }
            ),
        )
    
    def __init__(self, **kwargs):
        fields=[
            ModelChoiceFilter(
                label='Rola',
                queryset=SemanticRole.objects.all(),
                key='role',
                query=(lambda value: Q(argument_set__role=value)),
            ),
            ModelChoiceFilter(
                label='Atrybut roli',
                queryset=RoleAttribute.objects.all(),
                key='attribute',
                query=(lambda value: Q(argument_set__role__attribute=value)),
            ),
        ]
        super().__init__(
            label='Argument',
            fields=fields,
            widget=ArgumentFilter.ArgumentWidget(widgets=[field.widget for field in fields]),
            required=False,
            **kwargs
        )
        self.query_manager = MultiQueryManager([f.query_manager for f in fields])
    
    def compress(self, data_list):
        return data_list

# TODO cleaner implementation?
class PhraseoFilter(forms.ChoiceField, RadiosLayoutField):
    
    class PhraseoQueryManager(QueryManager):
        def __init__(self):
            super().__init__(None, None)
        # Assumes this filter is used on either Entry or Schema and uses the
        # fact that both have the same related name for Subentry
        def _make_queries(self, _, value, op):
            if value == '1':
                return [Q(subentries__schemata__phraseologic=True)]
            if value == '0':
                return [~Q(subentries__schemata__phraseologic=True)]
            return []
    
    def __init__(self):
        super().__init__(
            label='Frazeologia',
            choices=(('2', 'dowolnie'), ('1', 'zawiera'), ('0', 'nie zawiera')),
            initial='2',
            required=False,
        )
        self.query_manager = PhraseoFilter.PhraseoQueryManager()
    
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):
    
    layout = lambda x: layout.Field(x, wrapper_class='custom-control custom-switch')
    
    def __init__(self, label):
        super().__init__(
            label=label,
            required=False,
         )

# ==============================================================================
    
class FiltersForm(forms.Form):

    def __init__(self, *args, **kwargs):
        super(FiltersForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_method = 'get'
        self.helper.form_action = 'filters:get_entries'
        self.helper.form_id = 'filters-form'
        self.helper.form_class = 'form-horizontal'
        self.helper.label_class = 'col-sm-2'
        self.helper.field_class = 'col-sm-8'
        
        components = []
        field_groups = []
        all_fields = FiltersForm.base_fields.items()
        for gen, name in (('haseł', 'entry_'), ('schematów', 'schema_'), ('ram', 'frame_')):
            fields = [i for i in all_fields if name in i[0]]
            #field_group = FiltersForm.make_field_group('Filtrowanie według {}'.format(gen), fields, layout.Fieldset)
            field_group = FiltersForm.make_field_group('Filtrowanie według {}'.format(gen), fields, bootstrap.Tab)
            field_groups.append(field_group)
        components.append(bootstrap.TabHolder(*field_groups))
        components.append(layout.Submit('filters-submit', 'Filtruj'))
        self.helper.layout = layout.Layout(*components)
    
    # TODO this is ugly!
    def make_field_group(title, fields, cls):
        args = [title]
        for field_name, field_object in fields:
            args.append(type(field_object).layout(field_name))
        return cls(*args)
    
    def get_queries(self, filter_type=None):
        queries = []
        for key, value in self.cleaned_data.items():
            print(key, value)
            if value and not key.startswith('operator') and not key.startswith('filter'):
                if filter_type is not None and key.split('_')[0] != filter_type:
                    continue
                form_field = self.fields[key]
                operator_key = 'operator_{}'.format(key)
                # TODO conjunction variable is only needed if filter_type is None?
                conjunction = self.cleaned_data.get(operator_key)
                if conjunction is None:
                    conjunction = form_field.query_manager.default_conjunction
                if filter_type is not None:
                    queries += form_field.query_manager.make_object_queries(value)
                else:
                    queries += form_field.query_manager.make_entry_queries(value, conjunction)
        return queries
    
    # ENTRY FILTERS ============================================================
    entry_lemma = RegexFilter(
        label='Lemat',
        entry_lookup='name',
        object_lookup=None,
        max_length=200,
    )
    entry_pos = ModelMultipleChoiceFilter(
        label='Część mowy',
        queryset=POS.objects.exclude(tag='unk'),
        key='tag',
        human_values=polish_strings.POS,
        entry_lookup='pos',
        object_lookup=None,
    )
    entry_phraseology = PhraseoFilter()
    entry_status =  ModelMultipleChoiceFilter(
        label ='Status',
        queryset=Status.objects.all().order_by('-priority'),
        key = 'key',
        entry_lookup='status',
        object_lookup=None,
    )
    entry_num_schemata = RangesFilter(
        label='Liczba schematów',
        entry_lookup='schemata_count',
        object_lookup=None,
    )
    
    # SCHEMA FILTERS ===========================================================
    schema_opinion = ModelMultipleChoiceFilter(
        label='Opinia o schemacie',
        queryset=SchemaOpinion.objects.all(),
        key='key',
        human_values=polish_strings.SCHEMA_OPINION,
        entry_lookup='subentries__schemata__opinion',
        object_lookup='opinion',
        #initial=SchemaOpinion.objects.filter(key__in=('cer', 'vul')),
    )
    operator_schema_opinion = OperatorField()
    schema_type = MultipleChoiceFilter(
        label='Typ',
        choices=((False, 'normalny'), (True, 'frazeologiczny'),),
        entry_lookup='subentries__schemata__phraseologic',
        object_lookup='phraseologic',
    )
    operator_schema_type = OperatorField()
    schema_sie = ModelMultipleChoiceFilter(
        label='Zwrotność',
        queryset=InherentSie.objects.all().order_by('-priority'),
        key='name',
        human_values=polish_strings.TRUE_FALSE_YES_NO,
        entry_lookup='subentries__schemata__inherent_sie',
        object_lookup='inherent_sie',
    )
    operator_schema_sie = OperatorField()
    schema_neg = ModelMultipleChoiceFilter(
        label='Negatywność',
        queryset=Negativity.objects.exclude(name='').order_by('-priority'),
        key='name',
        human_values=polish_strings.NEGATION,
        entry_lookup='subentries__negativity',
        # both Schema and Entry have the same related name for Subentry:
        # can use the same lookup path
        object_lookup=None,
    )
    operator_schema_neg = OperatorField()
    schema_pred = ModelMultipleChoiceFilter(
        label='Predykatywność',
        queryset=Predicativity.objects.all().order_by('-priority'),
        key='name',
        human_values=polish_strings.TRUE_FALSE_YES_NO,
        entry_lookup='subentries__predicativity',
        # both Schema and Entry have the same related name for Subentry:
        # can use the same lookup path
        object_lookup=None,
    )
    operator_schema_pred = OperatorField()
    schema_aspect = ModelMultipleChoiceFilter(
        label='Aspekt',
        queryset=Aspect.objects.exclude(name='').exclude(name='_').order_by('-priority'),
        key='name',
        human_values=polish_strings.ASPECT,
        entry_lookup='subentries__aspect',
        # both Schema and Entry have the same related name for Subentry:
        # can use the same lookup path
        object_lookup=None,
    )
    operator_schema_aspect = OperatorField()
    schema_phrase = RegexFilter(
        label='Fraza',
        max_length=200,
        #initial='.*ręka.* | [xp\(abl\) & xp\(adl\) & [xp\(dur\) | xp\(caus\)]]',
        #initial='xp\(abl\) & xp\(adl\)',
        #initial='np.* !& !np.* | xp.*',
        entry_lookup='subentries__schemata__position__phrase_types__text_rep',
        object_lookup='positions__phrase_types__text_rep',
        inner_class=Schema,
        outer_lookup='subentries__schemata__in',
        additional_operators=True,
    )
    schema_num_positions = RangesFilter(
        label='Liczba pozycyj',
        entry_lookup='subentries__schemata__positions_count',
        object_lookup='positions_count',
    )
    schema_num_phrase_types = RangesFilter(
        label='Liczba typów fraz w pozycji',
        entry_lookup='subentries__schemata__positions__phrases_count',
        object_lookup='positions__phrases_count',
    )
    filter_schema_ = SwitchField('Filtruj schematy')
    
    # FRAME FILTERS ============================================================
    frame_opinion = ModelMultipleChoiceFilter(
        label='Opinia',
        queryset=FrameOpinion.objects.exclude(key='unk').order_by('-priority'),
        key='key',
        human_values=polish_strings.FRAME_OPINION,
        entry_lookup='subentries__schema_hooks__argument_connections__argument__frame__opinion',
        object_lookup='opinion',
    )
    operator_frame_opinion = OperatorField()
    #frame_argument = ArgumentFilter()
    filter_frame_ = SwitchField('Filtruj ramy')