Django default queryset and widget for ModelForm custom field - python

I have a Site model, and I am trying to create a SiteSelectorField that extends django.forms.ModelMultipleChoiceField, that uses my custom SiteSelectorWidget and Site.objects.all() as the queryset
Without the custom form field, my forms.py code looks like this (and works):
sites = forms.ModelMultipleChoiceField(queryset=Site.objects.all(), widget=SiteSelectorWidget())
I would like to limit the arguments passed, so I can do this
sites = SiteSelectorField()
But when I create the SiteSelectorField class, as below, Django tells me "SiteSelectorField' object has no attribute 'validators"
class SiteSelectorField(forms.ModelMultipleChoiceField):
queryset = Site.objects.all()
widget = SiteSelectorWidget()
def __init__(self, *args, **kwargs):
pass
How can I specify a default queryset and widget for this field so they don't need to be passed?

Delete the def __init__ method and code. By putting "pass" inside there, you're overriding the default functionality of ModelMultipleChoiceField, which your class inherits from, that would utilize the queryset.
Edit:
Re-structure your __init__ method like so:
def __init__(self, *args, **kwargs):
if not 'queryset' in kwargs:
kwargs['queryset'] = Site.objects.all()
return super(SiteSelectorField, self).__init__(*args, **kwargs)

Related

How to add placeholder text to a Django Admin field

I'd like to add placeholder text to a field in the Django Admin change form. In a regular ModelForm you can do this by overriding the field's widget or by modifying self.fields['my_field'].widget in the ModelForm __init__() method. How do I do something similar for a Django Admin?
The documented way is to override get_form():
The base implementation uses modelform_factory() to subclass form,
modified by attributes such as fields and exclude.
If you look at the docs for modelform_factory you'll see that you can pass widgets as kwarg. So this should work:
class MyModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
kwargs['widgets'] = {
'name': forms.TextInput(attrs={'placeholder': 'e.g. John Doe'})
}
return super().get_form(request, obj, **kwargs)
or, if you want to be sure you're not overriding any widgets (if you're inheriting from a subclass of ModelAdmin):
kwargs['widgets'] = kwargs.get('widgets', {})
kwargs['widgets'].update({'name': ...})
Override the render_change_form() method on your ModelAdmin, which provides access to the form instance:
class Address(model.Model):
street = models.CharField(max_length=50)
class AddressAdmin(admin.ModelAdmin):
def render_change_form(self, request, context, *args, **kwargs):
form_instance = context['adminform'].form
form_instance.fields['street'].widget.attrs['placeholder'] = 'Your street'
return super().render_change_form(request, context, *args, **kwargs)
This approach would be the same for other field attributes like attributes like autocomplete, autofocus, min, max, required, type or pattern. You also have access to context["original"] which provides the model instance, in case you'd like to change the behavior based on the model instance.
The source code is the best reference for this:
https://docs.djangoproject.com/en/2.2/_modules/django/contrib/admin/options/#ModelAdmin
This is a way of doing it without having to manually add placeholder text to each field:
admin.py
from django import forms
class MyModelAdmin(admin.ModelAdmin):
def render_change_form(self, request, context, *args, **kwargs):
form_instance = context['adminform'].form
for key, field in form_instance.fields.items():
if isinstance(field.widget, (forms.TextInput, forms.EmailInput)):
field.widget.attrs.update({'placeholder': field.label})
return super().render_change_form(request, context, *args, **kwargs)
Another way to do this is:
class MyModelAdmin(model.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
#--> Get form
form = super().get_form(request, obj, **kwargs)
#--> Add placeholder for field
form.base_fields['the_field_name'].widget.attrs['placeholder'] = "My_Place_Holder_Text"
#--> Return form
return form
#---
#---
This is similar to the answer of dirkgroten. The advantage here is that there is no need to worry about the widget used for the field.

simplest way to override Django admin inline to request formfield_for_dbfield for each instance

I would like to provide different widgets to input form fields for the same type of model field in a Django admin inline.
I have implemented a version of the Entity-Attribute-Value paradigm in my shop application (I tried eav-django and it wasn't flexible enough). In my model it is Product-Parameter-Value (see Edit below).
Everything works as I want except that when including an admin inline for the Parameter-Value pair, the same input formfield is used for every value. I understand that this is the default Django admin behaviour because it uses the same formset for each Inline row.
I have a callback on my Parameter that I would like to use (get_value_formfield). I currently have:
class SpecificationValueAdminInline(admin.TabularInline):
model = SpecificationValue
fields = ('parameter', 'value')
readonly_fields = ('parameter',)
max_num = 0
def get_formset(self, request, instance, **kwargs):
"""Take a copy of the instance"""
self.parent_instance = instance
return super().get_formset(request, instance, **kwargs)
def formfield_for_dbfield(self, db_field, **kwargs):
"""Override admin function for requesting the formfield"""
if self.parent_instance and db_field.name == 'value':
# Notice first() on the end -->
sv_instance = SpecificationValue.objects.filter(
product=self.parent_instance).first()
formfield = sv_instance.parameter.get_value_formfield()
else:
formfield = super().formfield_for_dbfield(db_field, **kwargs)
return formfield
formfield_for_dbfield is only called once for each admin page.
How would I override the default behaviour so that formfield_for_dbfield is called once for each SpecificationValue instance, preferably passing the instance in each time?
Edit:
Here is the model layout:
class Product(Model):
specification = ManyToManyField('SpecificationParameter',
through='SpecificationValue')
class SpecificationParameter(Model):
"""Other normal model fields here"""
type = models.PositiveSmallIntegerField(choices=TUPLE)
def get_value_formfield(self):
"""
Return the type of form field for parameter instance
with the correct widget for the value
"""
class SpecificationValue(Model):
product = ForeignKey(Product)
parameter = ForeignKey(SpecificationParameter)
# To store and retrieve all types of value, overrides CharField
value = CustomValueField()
The way I eventually solved this is using the form = attribute of the Admin Inline. This skips the form generation code of the ModelAdmin:
class SpecificationValueForm(ModelForm):
class Meta:
model = SpecificationValue
def __init__(self, instance=None, **kwargs):
super().__init__(instance=instance, **kwargs)
if instance:
self.fields['value'] = instance.parameter.get_value_formfield()
else:
self.fields['value'].disabled = True
class SpecificationValueAdminInline(admin.TabularInline):
form = SpecificationValueForm
Using standard forms like this, widgets with choices (e.g. RadioSelect and CheckboxSelectMultiple) have list bullets next to them in the admin interface because the <ul> doesn't have the radiolist class. You can almost fix the RadioSelect by using AdminRadioSelect(attrs={'class': 'radiolist'}) but there isn't an admin version of the CheckboxSelectMultiple so I preferred consistency. Also there is an aligned class missing from the <fieldset> wrapper element.
Looks like I'll have to live with that!

How to filter model choice field options based on user in django?

Here is my form:
class RecipeForm(forms.Form):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(RecipeForm, self).__init__(*args, **kwargs)
Recipebase_id = forms.ModelChoiceField(queryset=Recipebase.objects.filter(user = self.user))
title = forms.CharField(max_length=500)
instructions = forms.CharField(max_length=500)
I want to filter model choice field based on user as you can see from the filter. But it gives the following error:
name 'self' is not defined
Any suggestions would be highly appreciated.
The self. would work only for objects created from a class. In this case you are not creating one, so it would not work as you would expect.
Instead, you need to override the queryset in the __init__ like this:
class RecipeForm(forms.Form):
Recipebase_id = forms.ModelChoiceField(queryset=Recipebase.objects.none())
def __init__(self, *args, **kwargs):
user = kwargs.pop('user') #Throws an error if user is not present
super(RecipeForm, self).__init__(*args, **kwargs)
qs = Recipebase.objects.filter(user=user)
self.fields['Recipebase_id'].queryset = qs
Another way to achieve the same is to make user a required argument in the form
class RecipeForm(forms.Form):
Recipebase_id = forms.ModelChoiceField(queryset=Recipebase.objects.none())
def __init__(self, user, *args, **kwargs):
super(RecipeForm, self).__init__(*args, **kwargs)
qs = Recipebase.objects.filter(user=user)
self.fields['Recipebase_id'].queryset = qs
And the view code would look like this:
form = RecipeForm(request.POST, user=request.user) #user would be passed in as a kwarg to the form class.
Putting your code starting at "Recipebase_id" at the indentation level you have it causes python to execute it at the time the file is parsed/imported. Self is passed into a method when the class is instantiated and the instance method is called, so at parse time self does not exist.
It's unclear to me if you want the Recipebase_id, title and instructions set in the init method. If you do, indent them to the same level as the lines above it. If not, then you'll need to get the value of user from somewhere other than self.

Dynamically limiting queryset of related field

Using Django REST Framework, I want to limit which values can be used in a related field in a creation.
For example consider this example (based on the filtering example on https://web.archive.org/web/20140515203013/http://www.django-rest-framework.org/api-guide/filtering.html, but changed to ListCreateAPIView):
class PurchaseList(generics.ListCreateAPIView)
model = Purchase
serializer_class = PurchaseSerializer
def get_queryset(self):
user = self.request.user
return Purchase.objects.filter(purchaser=user)
In this example, how do I ensure that on creation the purchaser may only be equal to self.request.user, and that this is the only value populated in the dropdown in the form in the browsable API renderer?
I ended up doing something similar to what Khamaileon suggested here. Basically I modified my serializer to peek into the request, which kind of smells wrong, but it gets the job done... Here's how it looks (examplified with the purchase-example):
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
def get_fields(self, *args, **kwargs):
fields = super(PurchaseSerializer, self).get_fields(*args, **kwargs)
fields['purchaser'].queryset = permitted_objects(self.context['view'].request.user, fields['purchaser'].queryset)
return fields
class Meta:
model = Purchase
permitted_objects is a function which takes a user and a query, and returns a filtered query which only contains objects that the user has permission to link to. This seems to work both for validation and for the browsable API dropdown fields.
Here's how I do it:
class PurchaseList(viewsets.ModelViewSet):
...
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
context = self.get_serializer_context()
return serializer_class(*args, request_user=self.request.user, context=context, **kwargs)
class PurchaseSerializer(serializers.ModelSerializer):
...
def __init__(self, *args, request_user=None, **kwargs):
super(PurchaseSerializer, self).__init__(*args, **kwargs)
self.fields['user'].queryset = User._default_manager.filter(pk=request_user.pk)
The example link does not seem to be available anymore, but by reading other comments, I assume that you are trying to filter the user relationship to purchases.
If i am correct, then i can say that there is now an official way to do this. Tested with django rest framework 3.10.1.
class UserPKField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context['request'].user
queryset = User.objects.filter(...)
return queryset
class PurchaseSeriaizer(serializers.ModelSerializer):
users = UserPKField(many=True)
class Meta:
model = Purchase
fields = ('id', 'users')
This works as well with the browsable API.
Sources:
https://github.com/encode/django-rest-framework/issues/1985#issuecomment-328366412
https://medium.com/django-rest-framework/limit-related-data-choices-with-django-rest-framework-c54e96f5815e
I disliked the style of having to override the init method for every place where I need to have access to user data or the instance at runtime to limit the queryset. So I opted for this solution.
Here is the code inline.
from rest_framework import serializers
class LimitQuerySetSerializerFieldMixin:
"""
Serializer mixin with a special `get_queryset()` method that lets you pass
a callable for the queryset kwarg. This enables you to limit the queryset
based on data or context available on the serializer at runtime.
"""
def get_queryset(self):
"""
Return the queryset for a related field. If the queryset is a callable,
it will be called with one argument which is the field instance, and
should return a queryset or model manager.
"""
# noinspection PyUnresolvedReferences
queryset = self.queryset
if hasattr(queryset, '__call__'):
queryset = queryset(self)
if isinstance(queryset, (QuerySet, Manager)):
# Ensure queryset is re-evaluated whenever used.
# Note that actually a `Manager` class may also be used as the
# queryset argument. This occurs on ModelSerializer fields,
# as it allows us to generate a more expressive 'repr' output
# for the field.
# Eg: 'MyRelationship(queryset=ExampleModel.objects.all())'
queryset = queryset.all()
return queryset
class DynamicQuersetPrimaryKeyRelatedField(LimitQuerySetSerializerFieldMixin, serializers.PrimaryKeyRelatedField):
"""Evaluates callable queryset at runtime."""
pass
class MyModelSerializer(serializers.ModelSerializer):
"""
MyModel serializer with a primary key related field to 'MyRelatedModel'.
"""
def get_my_limited_queryset(self):
root = self.root
if root.instance is None:
return MyRelatedModel.objects.none()
return root.instance.related_set.all()
my_related_model = DynamicQuersetPrimaryKeyRelatedField(queryset=get_my_limited_queryset)
class Meta:
model = MyModel
The only drawback with this is that you would need to explicitly set the related serializer field instead of using the automatic field discovery provided by ModelSerializer. i would however expect something like this to be in rest_framework by default.
In django rest framework 3.0 the get_fields method was removed. But in a similar way you can do this in the init function of the serializer:
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Purchase
def __init__(self, *args, **kwargs):
super(PurchaseSerializer, self).__init__(*args, **kwargs)
if 'request' in self.context:
self.fields['purchaser'].queryset = permitted_objects(self.context['view'].request.user, fields['purchaser'].queryset)
I added the if check since if you use PurchaseSerializer as field in another serializer on get methods, the request will not be passed to the context.
First to make sure you only allow "self.request.user" when you have an incoming http POST/PUT (this assumes the property on your serializer and model is named "user" literally)
def validate_user(self, attrs, source):
posted_user = attrs.get(source, None)
if posted_user:
raise serializers.ValidationError("invalid post data")
else:
user = self.context['request']._request.user
if not user:
raise serializers.ValidationError("invalid post data")
attrs[source] = user
return attrs
By adding the above to your model serializer you ensure that ONLY the request.user is inserted into your database.
2) -about your filter above (filter purchaser=user) I would actually recommend using a custom global filter (to ensure this is filtered globally). I do something for a software as a service app of my own and it helps to ensure each http request is filtered down (including an http 404 when someone tries to lookup a "object" they don't have access to see in the first place)
I recently patched this in the master branch so both list and singular views will filter this
https://github.com/tomchristie/django-rest-framework/commit/1a8f07def8094a1e34a656d83fc7bdba0efff184
3) - about the api renderer - are you having your customers use this directly? if not I would say avoid it. If you need this it might be possible to add a custom serlializer that would help to limit the input on the front-end
Upon request # gabn88, as you may know by now, with DRF 3.0 and above, there is no easy solution.
Even IF you do manage to figure out a solution, it won't be pretty and will most likely fail on subsequent versions of DRF as it will override a bunch of DRF source which will have changed by then.
I forget the exact implementation I used, but the idea is to create 2 fields on the serializer, one your normal serializer field (lets say PrimaryKeyRelatedField etc...), and another field a serializer method field, which the results will be swapped under certain cases (such as based on the request, the request user, or whatever). This would be done on the serializers constructor (ie: init)
Your serializer method field will return a custom query that you want.
You will pop and/or swap these fields results, so that the results of your serializer method field will be assigned to the normal/default serializer field (PrimaryKeyRelatedField etc...) accordingly. That way you always deal with that one key (your default field) while the other key remains transparent within your application.
Along with this info, all you really need is to modify this: http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
I wrote a custom CustomQueryHyperlinkedRelatedField class to generalize this behavior:
class CustomQueryHyperlinkedRelatedField(serializers.HyperlinkedRelatedField):
def __init__(self, view_name=None, **kwargs):
self.custom_query = kwargs.pop('custom_query', None)
super(CustomQueryHyperlinkedRelatedField, self).__init__(view_name, **kwargs)
def get_queryset(self):
if self.custom_query and callable(self.custom_query):
qry = self.custom_query()(self)
else:
qry = super(CustomQueryHyperlinkedRelatedField, self).get_queryset()
return qry
#property
def choices(self):
qry = self.get_queryset()
return OrderedDict([
(
six.text_type(self.to_representation(item)),
six.text_type(item)
)
for item in qry
])
Usage:
class MySerializer(serializers.HyperlinkedModelSerializer):
....
somefield = CustomQueryHyperlinkedRelatedField(view_name='someview-detail',
queryset=SomeModel.objects.none(),
custom_query=lambda: MySerializer.some_custom_query)
#staticmethod
def some_custom_query(field):
return SomeModel.objects.filter(somefield=field.context['request'].user.email)
...
I did the following:
class MyModelSerializer(serializers.ModelSerializer):
myForeignKeyFieldName = MyForeignModel.objects.all()
def get_fields(self, *args, **kwargs):
fields = super(MyModelSerializer, self).get_fields()
qs = MyModel.objects.filter(room=self.instance.id)
fields['myForeignKeyFieldName'].queryset = qs
return fields
I looked for a solution where I can set the queryset upon creation of the field and don't have to add a separate field class. This is what I came up with:
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Purchase
fields = ["purchaser"]
def get_purchaser_queryset(self):
user = self.context["request"].user
return Purchase.objects.filter(purchaser=user)
def get_extra_kwargs(self):
kwargs = super().get_extra_kwargs()
kwargs["purchaser"] = {"queryset": self.get_purchaser_queryset()}
return kwargs
The main issue for tracking suggestions regarding this seems to be drf#1985.
Here's a re-usable generic serializer field that can be used instead of defining a custom field for every use case.
class DynamicPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
"""A PrimaryKeyRelatedField with ability to set queryset at runtime.
Pass a function in the `queryset_fn` kwarg. It will be passed the serializer `context`.
The function should return a queryset.
"""
def __init__(self, queryset_fn=None, **kwargs):
assert queryset_fn is not None, "The `queryset_fn` argument is required."
self.queryset_fn = queryset_fn
super().__init__(**kwargs)
def get_queryset(self):
return self.queryset_fn(context=self.context)
Usage:
class MySerializer(serializers.ModelSerializer):
my_models = DynamicPrimaryKeyRelatedField(
queryset_fn=lambda context: MyModel.objects.visible_to_user(context["request"].user)
)
# ...
Same works for serializers.SlugRelatedField.

How do I override this method on a Django Model Field

I'm trying to modify an existing Django Mezzanine setup to allow me to blog in Markdown. Mezzanine has a "Core" model that has content as an HtmlField which is defined like so:
from django.db.models import TextField
class HtmlField(TextField):
"""
TextField that stores HTML.
"""
def formfield(self, **kwargs):
"""
Apply the class to the widget that will render the field as a
TincyMCE Editor.
"""
formfield = super(HtmlField, self).formfield(**kwargs)
formfield.widget.attrs["class"] = "mceEditor"
return formfield
The problem comes from the widget.attrs["class"] of mceEditor. My thoughts were to monkey patch the Content field on the Blog object
class BlogPost(Displayable, Ownable, Content):
def __init__(self, *args, **kwargs):
super(BlogPost, self).__init__(*args, **kwargs)
self._meta.get_field('content').formfield = XXX
My problems are my python skills aren't up to the task of replacing a bound method with a lambda that calls super.
formfield is called by the admin when it wants to create a field for display on the admin pages, so I need to patch that to make the BlogPost widget objects NOT have the class of mceEditor (I'm trying to leave mceEditor on all the other things)
How do you craft the replacement function? I'm pretty sure I attach it with
setattr(self._meta.get_field('content'), 'formfield', method_i_dont_know_how_to_write)
You could change the used formfield in the admin's method formfield_for_dbfield:
class BlogAdmin(admin.ModelAdmin):
def formfield_for_dbfield(self, db_field, **kwargs):
field = super(BlogAdmin, self).formfield_for_dbfield(db_field, **kwargs)
if db_field.name == 'content':
field.widget = ....
field.widget.attrs['class'] = ...
return field
If you really want to do the monkey-patching, it should be something like that:
class BlogPost(Displayable, Ownable, Content):
def __init__(self, *args, **kwargs):
super(BlogPost, self).__init__(*args, **kwargs)
def formfield_new(self, *args, **kwargs):
# do here what you would like to do
return formfield
instancemethod = type(self._meta.get_field('content').formfield)
self._meta.get_field('content').formfield = instancemethod(formfield_new,
self, BlogPost)
I realize this question was answered several months ago, but just in case anyone else comes across it, Mezzanine now provides the ability to completely modify the WYSIWYG editor field. Take a look a the docs for it here:
http://mezzanine.jupo.org/docs/admin-customization.html#wysiwyg-editor

Categories