Django Rest Framework MultipleChoiceField with dynamic options - python

So I just started using Django Rest Framework and one of my serializers has a MultipleChoiceField in which the choices are simply all the instances of another model.
Here is the serializer in question:
class ObjectTypeSerializer(serializers.ModelSerializer):
def get_field_choices():
return sorted([
(p.id, p.name) for p in Parameter.objects.all()
])
object_fields = serializers.MultipleChoiceField(
choices=get_field_choices()
)
instance_fields = serializers.MultipleChoiceField(
choices=get_field_choices()
)
labels = serializers.SlugRelatedField(
queryset=Label.objects.all(),
many=True, allow_null=True, slug_field='name'
)
class Meta:
model = ObjectType
fields = ('id', 'name', 'object_fields',
'instance_fields', 'labels')
However, when I add a new Parameter object, the choices are not updated. In regular Django forms, I solved this simply using
forms.ChoiceField(choices=[(p.id, p.name) for p in Parameter.objects.all()])
and it would update the choices when a new parameter is added without restarting the server. How can I accomplish the same thing with Django Rest Framework serializers?
Any help is appreciated. Thanks!

When your choices are models, the most straightforward approach is to use some derivative of RelatedField. Given that you're using p.id, does PrimaryKeyRelatedField work for you? (Please update your question if it doesn't)
If the default behavior (using model's __unicode__ for the display value) is not what you desire, you can always subclass it and redefine the display_value method:
class CustomPKRelatedField(serializers.PrimaryKeyRelatedField):
"""A PrimaryKeyRelatedField derivative that uses named field for the display value."""
def __init__(self, **kwargs):
self.display_field = kwargs.pop("display_field", "name")
super(CustomPKRelatedField, self).__init__(**kwargs)
def display_value(self, instance):
# Use a specific field rather than model stringification
return getattr(instance, self.display_field)
...
class ObjectTypeSerializer(serializers.ModelSerializer):
...
object_fields = CustomPKRelatedField(queryset=Parameter.objects.all(), many=True)
instance_fields = CustomPKRelatedField(queryset=Parameter.objects.all(), many=True)
...
...
If all you need is so BrowsableAPIRenderer would render a nice-looking <select>, I believe that's all you need to do.
The ChoiceField and MultipleChoiceField are designed to work on a static dataset. They even preprocess things at __init__ to allow grouping. This is why new items don't appear there - those fields essentially "cache" results forever (until the server restart).
If, for some reason, you really need it to be ChoiceField-derivative, you can set up post_save and post_delete singal listeners and update fields' choices (and grouped_choices if you're not on a very bleeding edge version where a PR to allow choices to be set dynamically is already included) attributes. Check the ChoiceField source code for the details. That would be a dirty hack, though. ;)

Related

Django Rest Framework how to forbid users to change their username?

I'm creating UserSerializer and want to allow users to create new accounts but forbid them to change their usernames. There is a read_only attribute that I can apply but then users won't be able to set a username when creating a new one. But without that It allows me to change it. There is also a required attribute which unfortunately cannot be used with read_only. There is no other relevant attribute.
One solution is to create 2 different Serializers one for creating User and another from changing him, but that seems the ugly and wrong thing to do. Do you have any suggestions on how to accomplish that without writing 2 serializers?
Thanks for any advice.
PS: I'm using python3.6 and django2.1
EDIT: I'm using generics.{ListCreateAPIView|RetrieveUpdateDestroyAPIView} classes for views. Like this:
class UserList(generics.ListCreateAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetails(generics.RetrieveUpdateAPIView):
# this magic means (read only request OR accessing user is the same user being edited OR user is admin)
permission_classes = (perm_or(ReadOnly, perm_or(IsUserOwner, IsAdmin)),)
queryset = User.objects.all()
serializer_class = UserSerializer
EDIT2: There is a duplicate question (probably mine is duplicate) here
Assuming you are using a viewset class for your view, then you could override the init method of serializer as,
class UserSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'view' in self.context and self.context['view'].action in ['update', 'partial_update']:
self.fields.pop('username', None)
class Meta:
....
If you are trying to update the username field while update (HTTP PUT) or partial update (HTTP PATCH), the serializer will remove the username field from the list of fields and hence it wont affect the data/model
UPDATE
Why the above answer not woking with documentaion API?
From the doc
Note: By default include_docs_urls configures the underlying SchemaView to generate public schemas. This means that views will not be instantiated with a request instance. i.e. Inside the view self.request will be None.
In the answer, the fields are pops out dynamically with the help of a request object.
So, If you wish to handle API documentaion also, define multiple serializer and use get_serializer_class() method efficently. That's the DRF way.
Perhaps, one of the possible approaches would be to create a RegistrationSerializer which you use only in registration process/endpoint.
And then, you create another serializer UserSerializer where you make username read_only field and you use this serializer everywhere else ( eg. when updating user).
Anwser from #JPG is pretty accurate, but it has one limitation. You can use the serializer only in DRF views, because in other views or anywhere else the context will not have view.actions. To fix it self.instance can be used. It will make the code shorter and more versatile. Also instead of popping the field its better to make it read only, so that it can still be viewed but cannot be changed.
class UserSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance is not None: # if object is being created the instance doesn't exist yet, otherwise it exists.
# self.fields.pop('username', None)
self.fields.get('username').read_only = True # An even better solution is to make the field read only instead of popping it.
class Meta:
....
Another possible solution is to use CreateOnlyDefault() which is a builtin feature in DRF now. You can read more about it here in the docs

django - dynamic manager

I have a model that has an owner field.
class MyModel(models.Model):
owner = models.CharField(...)
I extended the django User class and added an ownership filed
class AppUser(User):
ownership = models.CharField(...)
I want to create a Manager for MyModel so it will retrieve only objects that correspond with ownership of the currently logged in user.
For example (using Django REST framework):
class MyModelAPI(APIView):
def get(self, request, format=None):
# This query will automatically add a filter of owner=request.user.ownership
objs = MyModel.objects.all()
# rest of code ...
All of the examples of managers user constant values in their queries and i'm looking for something more dynamic. Is this thing even possible?
Thanks
This is not possible with a custom manager because a model manager is instantiated at class loading time. Hence, it is stateless with regard to the http-request-response cycle and could only provide some custom method that you would have to pass the user to anyway. So why don't you just add some convenience method/property on your model (a manager seems unnecessary for this sole purpose)
class MyModel(models.Model):
...
#clsmethod
def user_objects(cls, user):
return cls.objects.filter(owner=user.ownership)
Then, in your view:
objs = MyModel.user_objects(request.user)
For a manager-based solution, look at this question. Another interesting solution is a custom middleware that makes the current user available via some function/module attribute which can be accessed in acustom manager's get_queryset() method, as described here.

Django form exclude options in select field

I have one model that has a ManyToMany Field (let's call it "Options") with another Model
When I create the ModelForm it displays all options.
Is there any way to exclude some option values or to show only some of them?
Here is an example:
models.py
class Options (model.Models):
name = ...
...
class Anything (model.Models):
...
options = ManyToManyField(Options)
values of "Options" in my DB:
["OK",
"OK_2",
"NOT_OK",
"OK_3,
"NOT_OK_2"]
Let's say that I need to show ONLY the "OK" values and hide or not to show the "NOT_OK" values.
Is there any way to do this with ModelForms?
You certainly can filter the queryset for a foreign key field or m2m on the related model by using a Form or more commonly a ModelForm.
The reason doing this at form level is useful is because that filtering could well be based on business logic which is not applicable in all cases and so allows more flexibility than defining it against the model for example.
While you can do this while defining the form fields it is best to do it once the form has been constructed and so it takes place at runtime and not compile time (I have just experienced a few interesting occasions where this has caused me some issues, however that was an earlier version of Django!)
The following ModelForm would do the job:
class AnythingForm(ModelForm):
options = forms.MultipleChoiceField()
def __init__(self, **kwargs):
super(AnythingForm, self).__init__(self, **kwargs)
self.fields['options'].queryset = Option.objects.filter({pass in your filters here...})
class Meta:
model = Anything
You can pass the limit_choices_to parameter to your ManyToMany field:
from django.db.models import Q
class Anything (models.Model):
options = models.ManyToManyField(Options,
limit_choices_to=Q(name__startswith='OK'))
In django 1.7 you can even pass a callable in case if list of choices should be changed dynamically.

django modeladmin, readonly_fields and boolean fields

I've a simple model with a boolean field in it, and the related admin view:
# in models.py
class MyModel(models.Model):
...
my_field = models.BooleanField(...)
# in admin.py
class MyModelAdmin(admin.ModelAdmin):
readonly_fields ("my_field", ...)
My problem is that now my boolean field appears always empty, independently from the actual value of the field itself.
I didn't find any solution to this problem, does it happen only to me?
I don't know if it may be relevant, but I'm using grappelli == 2.4.5
Thanks
Ok,
after some searching I've found a solution (perfectible, but a good starting point). I've simply overridden the get_form(...) model in my concretization of ModelAdmin:
def get_form(self, *args, **kwargs):
form = super(SupplierAdmin, self).get_form(*args, **kwargs)
for field_name in self.fake_readonly_fields:
form.base_fields[field_name].widget.attrs["disabled"] = "disabled"
return form
I renamed the list of my readonly fields to fake_readonly_fields, in order not to mess with Django readonly_fields. This works for textboxes, checkboxes and selects (I guess also for radio buttons, but I didn't verify it...). Now I'm looking for a solution for upload file inputs ...
Btw I don't know if this solution can cause "security" problems (e.g. some crafted message to the server can overcome my html-disabled fields, and pass new data to overwrite old values ...) but that's a different (still relevant) topic

Enable Django format localization by default

This is about the Format Localization feature that was implemented in Django 1.2.
In order to use this feature, you must add a localize=True parameter to all your form fields. I am trying to implement this localization in my app but the problem is that I am creating my forms dynamically by using the inlineformset_factory method that Django provides, so I cannot simply add a new parameter to the form field.
So I tried to enable this feature by default in all models, without needing to add a new parameter for all fields. I created a BaseInlineFormSet subclass and hard-coded the parameter in it.
class MyBaseInlineFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super(MyBaseInlineFormSet, self).__init__(*args, **kwargs)
for form in self.forms:
for key, field in form.fields.iteritems():
if field.__class__ == forms.DecimalField:
form.fields[key].localize = True
That worked only 50%. When submitted, the forms are being validated correctly by Django now (it's accepting commas instead of only dot) but the fields are still being displayed incorrectly.
I guess I could javascript my way out of this problem, but I prefer to avoid doing that.
Any ideas on how to solve this?
Django 1.2 is 3 years old now. Django 1.6 provides a nice way to solve your dilemma:
From the docs:
By default, the fields in a ModelForm will not localize their data. To enable localization for fields, you can use the localized_fields attribute on the Meta class.
>>> from django.forms import ModelForm
>>> from myapp.models import Author
>>> class AuthorForm(ModelForm):
... class Meta:
... model = Author
... localized_fields = ('birth_date',)
If localized_fields is set to the special value __all__, all fields will be localized
I've have not used it - (still to picka project to develop in Django) -
but it seens to be the case of subclassing -
Instead of having your fields inheriting from forms.DecimalField, make them be a:
class LocalizedDecimalField(forms.DecimalField):
localize = True

Categories

Resources