Django rest framework conditionally required fields - python

I would like to write a drf validator that will mark a field as required based on the value of an other field.
For example:
class MySerializer(serializers.Serializer):
has_children = fields.BooleanField()
nb_childs = fields.IntegerField(min_value=1, validators=[RequiredIf(field='has_children', value=True)], required=False)
At first i believed the class based validator was the way to do it, by retrieving the value of 'has_children' with a method like this:
def set_context(self, serializer_field):
print serializer_field.parent.initial_data
but the 'initial_data' is not set. Any clue?

Have a look here in the DRF documentation
Basically, to do object-level validation, you need to override the Serializer's validate(self, data) method, do your validation using the data parameter's value (this is the serializer's state provided as a dict to validate) then raise a ValidationError if anything is wrong.
If you need to raise an error for a specific field, then you can pass a dictionary as the parameter to the ValidationError constructor:
raise ValidationError({'yourfield': ['Your message']})

I am using several mixins for that purpose, which are changing field.required attribute and as result error validation messages are generated automatically by DRF
PerFieldMixin
class ConditionalRequiredPerFieldMixin:
"""Allows to use serializer methods to allow change field is required or not"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
for field_name, field in self.fields.items():
method_name = f'is_{field_name}_required'
if hasattr(self, method_name):
field.required = getattr(self, method_name)()
How to use PerFieldMixin
class MySerializer(ConditionalRequiredPerFieldMixin, serializers.ModelSerializer):
subject_id = serializers.CharField(max_length=128, min_length=3, required=False)
def is_subject_id_required(self):
study = self.context['study']
return not study.is_community_study
PerActionMixin
class ActionRequiredFieldsMixin:
"""Required fields per DRF action
Example:
PER_ACTION_REQUIRED_FIELDS = {
'update': ['notes']
}
"""
PER_ACTION_REQUIRED_FIELDS = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.context.get('view'):
action = self.context['view'].action
required_fields = (self.PER_ACTION_REQUIRED_FIELDS or {}).get(action)
if required_fields:
for field_name in required_fields:
self.fields[field_name].required = True
How to use PerActionMixin
see docstrings, for action == update (ie PUT request) - field "notes" will be required)

Related

Pass additional attribute to django-filter

I'm using django-filter together with DRF. I have a favourite-model, which is linked to several other models through a GenericRelation.
To filter for entries which have a favourite-flag, I've created a custom FavouriteFilter, which I add to the respective model. I would like to query for the content_type_id of the respective model in order to limit the results from Favourite. However, I don't know how I can pass down the model to the filter-method in the FavouriteFilter.
Here's a code snippet to illustrate the issue:
class ProjectFilter(BaseFilter):
favourite_only = FavouriteFilter()
class FavouriteFilter(django_filters.BooleanFilter):
"""
A custom filter which returns a users favourites of an element
"""
def __init__(self, *args, **kwargs):
# gettext_lazy breaks the OpenAPI generation => use gettext instead
kwargs['label'] = gettext("My favourites")
super(FavouriteFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
if value == True:
user = get_current_user()
content_type = ContentType.objects.get_for_model(<model>)
return qs.filter(pk__in=Favourite.objects
.filter(owner_id=user)
.filter(content_type_id=content_type)
.values_list('object_id', flat=True)
)
else:
return qs
In this example, the <model>-attribute is missing. How can I pass down this information from Project to the filter?
Keyword arguments can be passed down to the filter, but they need to be removed from the kwarg-dict before the super()-method is called. Otherwise they get passed on to the superclass, the superclass's __init__()-method doesn't know the keyword and a TypeError is thrown:
TypeError: __init__() got an unexpected keyword argument 'model'
In the example above, the superclass is django_filters.BooleanFilter respectively django_filters.Filter.
Using the dict.pop()-method, the keyword is removed from the kwargs-dictionary and at the same time we can save it for further use. Since content_type never changes after initialization, it can already be set in __init__().
Here's a working example of the code above, where Project is the django-model I want to pass down to the filter:
class ProjectFilter(BaseFilter):
favourite_only = FavouriteFilter(model=Project)
class FavouriteFilter(django_filters.BooleanFilter):
"""
A custom filter which returns a users favourites of an element
"""
def __init__(self, *args, **kwargs):
# gettext_lazy breaks the OpenAPI generation => use gettext instead
kwargs['label'] = gettext("My favourites")
model = kwargs.pop('model')
self.content_type = ContentType.objects.get_for_model(model)
super(FavouriteFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
if value == True:
user = get_current_user()
return qs.filter(pk__in=Favourite.objects
.filter(owner_id=user)
.filter(content_type_id=self.content_type)
.values_list('object_id', flat=True)
)
else:
return qs
For my specific use-case, where I'm looking for the model that is using the filter, the model is available through the queryset as qs.model. The code-snippet looks like this:
class ProjectFilter(BaseFilter):
favourite_only = FavouriteFilter()
class FavouriteFilter(django_filters.BooleanFilter):
"""
A custom filter which returns a users favourites of an element
"""
def __init__(self, *args, **kwargs):
# gettext_lazy breaks the OpenAPI generation => use gettext instead
kwargs['label'] = gettext("My favourites")
super(FavouriteFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
if value == True:
user = get_current_user()
content_type = ContentType.objects.get_for_model(qs.model)
return qs.filter(pk__in=Favourite.objects
.filter(owner_id=user)
.filter(content_type_id=content_type)
.values_list('object_id', flat=True)
)
else:
return qs

Django Proxy Field

Is it possible to make a Django Proxy Field that has access to another field, but doesn't save anything to the database for it's own value(s), and doesn't have a database column for itself?
The use case for this is we'd like to store values in a JsonField, but be able to use the built in validations of Django Fields. A second benefit of this would being able to add new fields (with validation capability) without affecting the database schema.
The sudo code would probably look something like this:
from django.db import models
from django.contrib.postgres.fields import JsonField
class ProxyInitMixin(object):
def __init__(self, *args, *kwargs):
# some logic that will hold values if set on the Model
# but won't create a column or save anything to the
# database for this Field.
super(ProxyInitMixin, self).__init__(*args, **kwargs)
class ProxyIntegerField(ProxyInitMixin, models.Field):
pass
class ProxyCharField(ProxyInitMixin, models.Field):
pass
class MyModel(models.Model):
proxy_int = ProxyIntegerField()
proxy_char = ProxyCharField()
data = JsonField()
def save(self, *args, **kwargs):
self.data = {
'foo': self.proxy_int,
'bar': self.proxy_char
}
return super(MyModel, self).save(*args, **kwargs)
There are proxy models in django, But I am not sure if it has something like proxy fields.
For your use case, you can do as mentioned below:
Create a list of fields with each field containing name, type, nullable, etc.
Add a function in your model to return actual django rest framework (DRF) field class instance, corresponding to each field type passed to it.
Use DRF inbuilt field class validation to validate your field data against specified field type in save().
In addition to automatic validation, you will also get automatic type conversion. Eg. If user entered number 1 as text: "1" for a integer field then it will automatically convert it back to integer 1. Same way, it will work for float, Bool, Char, etc
`
from django.db import models
from rest_framework.fields import IntegerField, FloatField, BooleanField, DateTimeField, CharField
class MyModel(models.Model):
FIELDS = [{'field_name': 'proxy_int', 'field_type': 'int', 'null_allowed': 'True'},
{'field_name': 'proxy_char', 'field_type': 'string', 'null_allowed': 'True'}]
data = JsonField()
def field_type(self, field):
if field.field_type == 'int':
return IntegerField()
elif field.field_type == 'float':
return FloatField()
elif field.field_type == 'bool':
return BooleanField()
elif field.field_type == 'date':
return DateTimeField()
elif self.value_type == 'string':
return CharField()
return CharField()
def save(self, *args, **kwargs):
data = kwargs.get('data', {})
new_data = {}
for (field in FIELDS)
field_name = field['field_name']
field_type = field['field_type']
field_value = data.get(field_name, None)
validated_value = self.field_type(field_type).run_validation(field_value)
new_data[field_name] = validated_value
kwargs['data'] = new_data
return super(MyModel, self).save(*args, **kwargs)`
You may try and figure out django's field classes (Instead of DRF) and use them for validation, if required.
You can inherit this new MyModel class to achieve similar capability in other models and to reuse code.
In order to make the field virtual, you need to:
Override the Field.get_attname_column() method, which must return two-tuple attname, None as the value for attname and column.
Set the private_only parameter to True in the Field.contribute_to_class() method.
A proxy field must also have a reference to the concrete field in order to be able to access to it. Here I will use the concrete_field parameter.
class ProxyMixin(object):
"""
A mixin class that must be mixed-in with model fields.
The descriptor interface is also implemented in this mixin
class to keep value getting/setting logic on the Model.
"""
def __init__(self, *args, concrete_field=None, **kwargs):
self._concrete_field = concrete_field
super().__init__(*args, **kwargs)
def check(self, **kwargs):
return [
*super().check(**kwargs),
*self._check_concrete_field(),
]
def _check_concrete_field(self):
try:
self.model._meta.get_field(self._concrete_field)
except FieldDoesNotExist:
return [
checks.Error(
"The %s concrete field references the "
"nonexistent field '%s'." % (self.__class__.__name__, self._concrete_field),
obj=self,
id='myapp.E001',
)
]
else:
return []
def get_attname_column(self):
attname, column = super().get_attname_column()
return attname, None
def contribute_to_class(self, cls, name, private_only=False):
super().contribute_to_class(cls, name, private_only=True)
setattr(cls, name, self)
#property
def concrete_field(self):
"""
Returns the concrete Field instance.
"""
return self.model._meta.get_field(self._concrete_field)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self._concrete_field is not None:
kwargs['concrete_field'] = self._concrete_field
return name, path, args, kwargs
def __get__(self, instance, owner=None):
if instance is None:
return self
return getattr(instance, self._concrete_field)
def __set__(self, instance, value):
setattr(instance, self._concrete_field, value)
If you are sure that the concrete field represents a dict-like object, then you can change the logic for getting / setting value. Maybe something like this:
def __get__(self, instance, owner=None):
if instance is None:
return self
data = getattr(instance, self._concrete_field) or {}
return data.get(self.name, self.get_default())
def __set__(self, instance, value):
data = getattr(instance, self._concrete_field)
if data is None:
setattr(instance, self._concrete_field, {})
data = getattr(instance, self._concrete_field)
data[self.name] = value

Mongoengine custom validation with modify

I am trying to apply custom validation on a Mongoengine modify operation as seen below:
class Form(Document):
fields = ListField(EmbeddedDocumentField(Field))
def modify(self, *args, **kwargs):
for field in self.fields:
if not [field for field in self.fields if field.type == "email"]:
raise ValidationError("Form must have an email field")
super(Form, self).modify(**kwargs)
def update_form(self, modify_kwargs):
return self.modify(**modify_kwargs)
However when I call update_form, the custom validation does not take the updated data into account in modify. Is there some sort of a pre-hook for doing this type of validation?
You're verifying against the objects field attribute rather than kwargs. But make sure each field is an object that contains .type. You shouldn't be using the python reserved word type though.
class Form(Document):
fields = ListField(EmbeddedDocumentField(Field))
def modify(self, *args, **kwargs):
if not [field for field in kwargs.get('fields', []) if field.type == "email"]:
raise ValidationError("Form must have an email field")
super(Form, self).modify(**kwargs)
def update_form(self, modify_kwargs):
return self.modify(**modify_kwargs)

How to use "Readonly Field" outside Admin

I have a form that I need to show my project outside the area of administration, some fields can not be edited but can see them.
To do this would be great "AdminReadonlyField" found in "django.contrib.admin.helpers" The problem is that you can not do.
I have tried to create some widget that can replace this complex class, but I can not get it to work properly with DateTiemField fields.
class UserUpdateForm(forms.ModelForm):
"""
We need field "date_joined" can not be edited but can see them
"""
class Meta:
model = User
fields = ('first_name', 'last_name',
'email', 'date_joined', 'slug')
def __init__(self, user, *args, **kwargs):
kwargs['instance'] = user
super(UserUpdateForm, self).__init__(*args, **kwargs)
self.fields['date_joined'].widget = widgets.CMDateTimeText()
def clean_date_joined(self):
instance = getattr(self, 'instance', None)
if instance and instance.pk:
return instance.date_joined
else:
return self.cleaned_data['date_joined']
My code, something is not right.
class CMDateTimeText(DateTimeBaseInput):
"""
A SplitDateTime Widget that has some admin-specific styling.
Hereda Field and Widget.
"""
format_key = 'DATETIME_FORMAT'
def __init__(self, attrs=None, format=None):
# Use slightly better defaults than HTML's 20x2 box
default_attrs = {'class': 'date_id'}
if attrs:
default_attrs.update(attrs)
super(CMDateTimeText, self).__init__(attrs=default_attrs, format=format)
def render(self, name, value, attrs=None):
if value is None:
value = ''
value = self._format_value(value)
final_attrs = self.build_attrs(attrs, name=name)
return format_html('<p{}>{}</p>', flatatt(final_attrs), conditional_escape(value))
Result image:
any idea how to do "AdminReadonlyField"" any view or form?
So after hours of looking for various solutions, I found out how to do it the Django way.
Simply add the attribute disabled to the field in the form (not the widget!):
# in __init__() with crispy-forms for instance
self.fields['field'].disabled = True
# as form field
field = forms.CharField(disabled=True)
And it works... Django is taking care of not saving the field, if some hacker tampered with it although it's disabled. Only works with Django 1.9+.

Accessing parent model instance from modelform of admin inline

I'm using a TabularInline in Django's admin, configured to show one extra blank form.
class MyChildInline(admin.TabularInline):
model = MyChildModel
form = MyChildInlineForm
extra = 1
The model looks like MyParentModel->MyChildModel->MyInlineForm.
I'm using a custom form so I can dynamically lookup values and populate choices in a field. e.g.
class MyChildInlineForm(ModelForm):
my_choice_field = forms.ChoiceField()
def __init__(self, *args, **kwargs):
super(MyChildInlineForm, self).__init__(*args, **kwargs)
# Lookup ID of parent model.
parent_id = None
if "parent_id" in kwargs:
parent_id = kwargs.pop("parent_id")
elif self.instance.parent_id:
parent_id = self.instance.parent_id
elif self.is_bound:
parent_id = self.data['%s-parent'% self.prefix]
if parent_id:
parent = MyParentModel.objects.get(id=parent_id)
if rev:
qs = parent.get_choices()
self.fields['my_choice_field'].choices = [(r.name,r.value) for r in qs]
This works fine for the inline records bound to an actual record, but for the extra blank form, it doesn't display any values in my choice field, since it doesn't have any record id and there can't lookup the associated MyParentModel record.
I've inspected all the values I could find (args, kwargs, self.data, self.instance, etc) but I can't find any way to access the parent object the tabular inline is bound to. Is there any way to do this?
Update: As of Django 1.9, there is a def get_form_kwargs(self, index) method in the BaseFormSet class. Hence, overriding that passes the data to the form.
This would be the Python 3 / Django 1.9+ version:
class MyFormSet(BaseInlineFormSet):
def get_form_kwargs(self, index):
kwargs = super().get_form_kwargs(index)
kwargs['parent_object'] = self.instance
return kwargs
class MyForm(forms.ModelForm):
def __init__(self, *args, parent_object, **kwargs):
self.parent_object = parent_object
super(MyForm, self).__init__(*args, **kwargs)
class MyChildInline(admin.TabularInline):
formset = MyFormSet
form = MyForm
For Django 1.8 and below:
To pass a value of a formset to the individual forms, you'd have to see how they are constructed. An editor/IDE with "jump to definition" really helps here to dive into the ModelAdmin code, and learn about the inlineformset_factory and it's BaseInlineFormSet class.
From there you'll find that the form is constructed in _construct_form() and you can override that to pass extra parameters. It will likely look something like this:
class MyFormSet(BaseInlineFormSet):
def _construct_form(self, i, **kwargs):
kwargs['parent_object'] = self.instance
return super(MyFormSet, self)._construct_form(i, **kwargs)
#property
def empty_form(self):
form = self.form(
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
parent_object=self.instance,
)
self.add_fields(form, None)
return form
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.parent_object = kwargs.pop('parent_object', None)
super(MyForm, self).__init__(*args, **kwargs)
class MyChildInline(admin.TabularInline):
formset = MyFormSet
form = MyForm
Yes, this involves a private _construct_form function.
update Note: This doesn't cover the empty_form, hence your form code needs to accept the parameters optionally.
I'm using Django 1.10 and it works for me:
Create a FormSet and put the parent object into kwargs:
class MyFormSet(BaseInlineFormSet):
def get_form_kwargs(self, index):
kwargs = super(MyFormSet, self).get_form_kwargs(index)
kwargs.update({'parent': self.instance})
return kwargs
Create a Form and pop an atribute before super called
class MyForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
parent = kwargs.pop('parent')
super(MyForm, self).__init__(*args, **kwargs)
# do whatever you need to with parent
Put that in the inline admin:
class MyModelInline(admin.StackedInline):
model = MyModel
fields = ('my_fields', )
form = MyFrom
formset = MyFormSet
AdminModel has some methods like get_formsets. It receives an object and returns a bunch of formsets. I think you can add some info about parent object to that formset classes and use it later in formset's __init__
Expanding on ilvar's answer a bit, If the form field of interest is constructed from a model field, you can use the following construction to apply custom behavior to it:
class MyChildInline(admin.TabularInline):
model = MyChildModel
extra = 1
def get_formset(self, request, parent=None, **kwargs):
def formfield_callback(db_field):
"""
Constructor of the formfield given the model field.
"""
formfield = self.formfield_for_dbfield(db_field, request=request)
if db_field.name == 'my_choice_field' and parent is not None:
formfield.choices = parent.get_choices()
return formfield
return super(MyChildInline, self).get_formset(
request, obj=obj, formfield_callback=formfield_callback, **kwargs)
return result

Categories