Source code for disbi.forms

"""
Forms used throughout the DISBi app.
"""
# standard library
from collections import namedtuple

# third-party
from more_itertools import unique_everseen

# Django
from django import forms
from django.conf import settings
from django.db.models.fields.related import ForeignKey
from django.utils.translation import ugettext_lazy as _

# DISBi
from disbi.utils import camelize, construct_none_displayer, get_choices


# ----------------------------- functions -----------------------------

[docs]def make_ChoiceField(model, attribute, label=None, empty_choice=None): """ Return a ChoiceField and the maxiumn number of choices. The ChoiceField contains all distinct values of an attribute of model. Addtionally a NULL option is inserted as first choice. Args: model (Model): The model from which the field should be constructed. attribute (Field): The attribute of the model. label (str): The label used when displaying the form (default None). empty_choice (tuple): 2-tuple of a value label pair, used for enabling the user to choose an empty condition, e.g. no stress. Returns: ChoiceField: A Choicefield based on the available options in the DB. int: The number of availablbe options. Raises: IndexError: Raises error when no entries are found in the DB. """ try: entries = model.objects.values_list(attribute, flat=True).distinct() max_num = len(entries) model_field = model._meta.get_field(attribute) # Format human-readable label according to type if model_field.choices: # The field is a choice field. Use DB and display names as # specified in the models. choices = list(zip(get_choices(model_field.choices, style='db'), get_choices(model_field.choices, style='display'))) elif isinstance(entries[0], str): # The field is a free form string field. # Split on SEPARATOR, flatten list and make it unique. entries = [e.split(settings.DISBI['SEPARATOR']) for e in entries if e] entries = sum(entries, []) entries = list(unique_everseen(entries)) entries = [e for e in entries if e != settings.DISBI['EMPTY_STR']] choices = [(e, e) for e in entries if e] else: # The field is something else. DB and display values are the same. choices = [(e, e) for e in entries if e] if empty_choice is not None: choices.append(empty_choice) choices.insert(0, (None, construct_none_displayer(entries))) # Use label if given if label is not None: field = forms.ChoiceField(choices=choices, label=label) else: field = forms.ChoiceField(choices=choices) return field, max_num except IndexError: raise Exception('No entries for field {}'.format(attribute))
[docs]def construct_direct_select_form(model): """ Construct a form for directly selecting model instances. Args: model (models.Model): A Django model, for which the form is constructed. """ cls_name = model.__name__ + 'Form' entries = model.objects.all() mymax_num = len(entries) myselect_field = forms.ModelChoiceField(queryset=entries) form = type(cls_name, (forms.Form,), { model.__name__.lower(): myselect_field, 'max_num': mymax_num,} ) return form
[docs]def construct_foreign_select_field(model, field): """ Construct a form for directly selecting related model instances. Args: model (models.Model): A Django model, for which the form is constructed. field (fields.related.ForeignKey): The field of the realated instances. """ entries = field.related_model.objects.filter(pk__in= model.objects.values_list(field.name, flat=True).distinct() ) max_num = len(entries) select_field = forms.ModelChoiceField(queryset=entries) return select_field, max_num
[docs]def construct_modelfieldsform(model, exclude=['id'], direct_select=False): """ Construct forms based on a model and available values in the DB. Args: model (models.Model): A Django model, for which the forms are constructed. Keyword Args: exclude (iterable): Fields for which no forms should be constructed. direct_select (bool): Determines whether a form for directly selecting model instances is constructed. Returns: list: A list of namedtuples with the form classes and the prefix. """ NamedFormClass = namedtuple('NamedFormClass', ['classname', 'prefix']) formclasses = [] if direct_select: form = construct_direct_select_form(model) formclasses.append(NamedFormClass(form, model.__name__.lower())) fields = [f for f in model._meta.get_fields() if f.concrete] for field in fields: if field.name not in exclude: # Construct class name and form label. cls_name = camelize(field.name) + 'Form' mylabel = field.verbose_name\ if field.verbose_name != field.name.replace('_', ' ')\ else None # If dealing with a foreign key field, get the related objects. if isinstance(field, ForeignKey): myselect_field, mymax_num = construct_foreign_select_field(model, field) # When dealing else with a normal field use make_Choicefield(). else: # Check whether an empty value representation was added to the model. try: myempty_choice = (settings.DISBI['EMPTY_STR'], field.di_empty) except AttributeError: myempty_choice = None myselect_field, mymax_num = make_ChoiceField(model, field.name, label=mylabel, empty_choice=myempty_choice) form = type(cls_name, (forms.Form,), { field.name: myselect_field, 'max_num': mymax_num,} ) formclasses.append(NamedFormClass(form, field.name)) return formclasses
[docs]def construct_forms(experiment_model): """Wrapper for `construct_modelfieldsform` with appropriate arguments.""" exclude_fields = [ field.name for field in experiment_model._meta.get_fields() if field.concrete and not getattr(field, 'di_choose', False) ] return construct_modelfieldsform( experiment_model, direct_select=True, exclude=exclude_fields )
[docs]def foldchange_form_factory(experiments): """ Create a form with two fields for selecting experiments. The fields are select widgets allowing to calculate a fold change between the two. Args: experiments (Queryset): The selectable experiments. Returns: Form: The constructed form. """ class FoldChangeForm(forms.Form): """ Form for getting two experiments that should be compared. """ dividend = forms.ModelChoiceField(queryset=experiments) divisor = forms.ModelChoiceField(queryset=experiments) def clean(self): """ Check whether the two selected experiments are different. """ cleaned_data = super().clean() if cleaned_data.get('dividend') == cleaned_data.get('divisor'): raise forms.ValidationError( _('Selected same experiment for calculating fold change'), code='divided by self', ) return FoldChangeForm