I have two models, Book and Page, where Page has a foreign key relation to Book. In admin.py, the webpage to modify a book has inline elements for Page objects.
models.py:
class Book(models.Model):
# ...
class Page(models.Model):
book = models.ForeignKey(Book, editable=False)
number = models.IntegerField('Page number')
# ...
admin.py:
class PageAdminInline(admin.TabularInline):
model = Page
extra = 1
class BookAdmin(admin.ModelAdmin):
inlines = [PageAdminInline]
admin.site.register(Book, BookAdmin)
In the inline forms, a field allows to set the page number for each page. With this configuration, how can I validate the global condition that all pages numbers of a book should be different and numbered from one to the number of Page objects associated to the book?
I suppose I have to override a clean() method related to a book model or form somewhere, but I don't know how to access from there the data related to the pages that the user inputs.
Edit
Based on Emett's suggestion, I have tried to override the clean() method of the Page model:
class Page(models.Model):
book = models.ForeignKey(Book, editable=False)
number = models.IntegerField('Page number')
def clean():
book_pages = Page.objects.filter(book=self.book)
# ... [apply condition on the book_pages QuerySet]
super(Page, self).clean()
This does not work: if I modify the page number of all pages in the admin site for a book, book_pages will contain objects that have the old page numbers.
In addition, having the condition checked in Page also means that it will be tested n times if I have n pages, while just checking it once should be sufficient.
An easier solution would be putting unique_together in django model. IE
class Page(models.Model):
book = models.ForeignKey(Book, editable=False)
number = models.IntegerField('Page number')
class Meta:
app_label = 'page'
db_table = 'pages'
verbose_name = 'Page'
verbose_name_plural = 'Pages'
unique_together = (('book', 'number'),)
Another workaround if you don't want to use unique_together, then create a form, use it in inline, ie:
class PageForm(forms.ModelForm):
class Meta:
model = Page
fields ='__all__'
def clean():
cleaned_data = self.cleaned_data
book = cleaned_data.get('book')
number = cleaned_data.get('number')
page_qset = Page.objects.filter(book=book, number=number)
if len(page_qset) > 0: # inefficient solution, using it for forcibly executing query
raise forms.ValidationError('Already exists')
return super().clean()
class PageAdminInline(admin.TabularInline):
form = PageForm
Related
I have these two models:
class User(AbstractUser):
is_teacher = models.BooleanField(default=False, null=False)
class Course(models.Model):
teacher = models.ForeignKey(User, on_delete=models.CASCADE, related_name='teacher_courses')
students = models.ManyToManyField(User, blank=True, related_name='student_courses')
Course model has a ManyToMany field and a ForeignKey to User model. In django's admin page, you are able to see a course's student/teacher. Is there a way to make it as you can have a list of a user's courses in admin page to see/add/remove courses for a user?
You can define a callable on your ModelAdmin class and add it to list_display. To make the courses editable on an user's page use sub classes of InlineModelAdmin.
class TeacherCourseInlineAdmin(admin.TabularInline):
model = Course
fk_name = "teacher"
class StudentCourseInlineAdmin(admin.TabularInline):
model = Course
fk_name = "student"
class UserAdmin(admin.ModelAdmin):
list_display = ("username", "teacher_courses")
inlines = [TeacherCourseInlineAdmin, StudentCourseInlineAdmin]
def get_queryset(self, *args, **kwargs):
return super().get_queryset(*args, **kwargs).prefetch_related("teacher_courses")
#admin.display(description='Courses')
def teacher_courses(self, user):
return [c.name for c in user.teacher_courses.all()]
Note that it makes sense to override ModelAdmin.get_queryset() to add a call to prefetch_related() so that Django fetches all related courses in one extra query instead of performing one additional query for every user object.
I have an example model which has a fk relation with user model and Blog model. Now I have a get api which only requires certain fields of user to be displayed.
My model:
class Example(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
null=True,
related_name="user_examples",
)
blog = models.ForeignKey(
Blog,
on_delete=models.CASCADE,
null=True,
related_name="blog_examples",
)
/................./
Now my view:
class ExampleView(viewsets.ModelViewSet):
queryset = Example.objects.all()
serializer_class = ExampleSerializer
def list(self, request, *args, **kwargs):
id = self.kwargs.get('pk')
queryset = Example.objects.filter(blog=id)
serializer = self.serializer_class(queryset,many=True)
return Response(serializer.data,status=200)
My serializer:
class ExampleSerializer(serializers.ModelSerializer):
class Meta:
model = Example
fields = ['user','blog','status']
depth = 1
Now when I call with this get api, I get all example objects that is required but all the unnecessary fields of user like password, group etc . What I want is only user's email and full name. Same goes with blog, I only want certain fields not all of them. Now how to achieve this in a best way??
You will have to specify the required fields in nested serializers. e.g.
class BlogSerializer(serializers.ModelSerializer):
class Meta:
model = Blog
fields = ['title', 'author']
class ExampleSerializer(serializers.ModelSerializer):
blog = BlogSerializer()
class Meta:
model = Example
fields = ['user','blog','status']
are you setting depth in serializer's init method or anywhere else? beacause ideally it should only display id's and not anything else. if yes then set depth to zero and use serializer's method field to return data that you need on frontend. I can provide you with example code samples
I have a model which I will update using an updateView generic class based function. How can I make specific fields as read only ?
example :
Models.py:
class Employee(models.Model):
emp_no = models.IntegerField( primary_key=True)
birth_date = models.DateField()
first_name = models.CharField(max_length=14)
last_name = models.CharField(max_length=16)
gender = models.CharField(max_length=1)
hire_date = models.DateField()
class Meta:
verbose_name = ('employee')
verbose_name_plural = ('employees')
# db_table = 'employees'
def __str__(self):
return "{} {}".format(self.first_name, self.last_name)
views.py :
class EmployeeUpdate(UpdateView):
model = Employee
fields = '__all__'
How can I make the first_name readonly inside a UpdateView ?
Note: I want to have the model(charfield) the same, But it should be read only inide an UpdateView.
When one wants to customize their forms the easiest way is to make a form class. A generic view is not really meant to provide all features a form class does, even though it makes life a little easy by generating the form for you by itself.
You want to be using a ModelForm [Django docs] and set disabled=True [Django docs] on your field:
from django import forms
class EmployeeUpdateForm(forms.ModelForm):
first_name = forms.CharField(max_length=14, disabled=True)
class Meta:
model = Employee
fields = '__all__'
Note: The disabled boolean argument, when set to True, disables a form field using the disabled HTML attribute so that it won’t be
editable by users. Even if a user tampers with the field’s value
submitted to the server, it will be ignored in favor of the value from
the form’s initial data.
Now in your view you simply want to set the form_class attribute:
class EmployeeUpdate(UpdateView):
model = Employee
form_class = EmployeeUpdateForm
I have two models, one with M2M relation and a related name. I want to include all fields in the serializer and the related field.
models.py:
class Pizza(models.Model):
name = models.CharField(max_length=50, unique=True)
toppings = models.ManyToManyField(Topping, null=True, blank=True, related_name='pizzas')
class Topping(models.Model):
name = models.CharField(max_length=50, unique=True)
price = models.IntegerField(default=0)
serializer.py:
class ToppingSerializer(serializers.ModelSerializer):
class Meta:
model = Topping
fields = '__all__'
This works but it doesn't include the related field.
fields = ['name', 'price', 'pizzas']
This works exactly as I want, but what happens when Toppings model has a lot of fields. I want to do something like :
fields = ['__all__', 'pizzas']
This syntax results in an error saying:
Field name __all__ is not valid for model
Is there a way to achieve the wanted behavior? Or the fields must be typed manually when using a related name ?
Like #DanEEStart said, DjangoRestFramework don't have a simple way to extend the 'all' value for fields, because the get_field_names methods seems to be designed to work that way.
But fortunately you can override this method to allow a simple way to include all fields and relations without enumerate a tons of fields.
I override this method like this:
class ToppingSerializer(serializers.ModelSerializer):
class Meta:
model = Topping
fields = '__all__'
extra_fields = ['pizzas']
def get_field_names(self, declared_fields, info):
expanded_fields = super(ToppingSerializer, self).get_field_names(declared_fields, info)
if getattr(self.Meta, 'extra_fields', None):
return expanded_fields + self.Meta.extra_fields
else:
return expanded_fields
Note that this method only change the behaviour of this serializer, and the extra_fields attribute only works on this serializer class.
If you have a tons of serializer like this, you can create a intermediate class to include this get_fields_names method in one place and reuse'em many times. Some like this:
class CustomSerializer(serializers.HyperlinkedModelSerializer):
def get_field_names(self, declared_fields, info):
expanded_fields = super(CustomSerializer, self).get_field_names(declared_fields, info)
if getattr(self.Meta, 'extra_fields', None):
return expanded_fields + self.Meta.extra_fields
else:
return expanded_fields
class ToppingSerializer(CustomSerializer):
class Meta:
model = Topping
fields = '__all__'
extra_fields = ['pizzas']
class AnotherSerializer(CustomSerializer):
class Meta:
model = Post
fields = '__all__'
extra_fields = ['comments']
I just checked the source code of Django Rest Framework.
The behaviour you want seems not to be supported in the Framework.
The fields option must be a list, a tuple or the text __all__.
Here is a snippet of the relevant source code:
ALL_FIELDS = '__all__'
if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)):
raise TypeError(
'The `fields` option must be a list or tuple or "__all__". '
'Got %s.' % type(fields).__name__
)
You cannot add 'all' additionally to the tuple or list with fields...
The fields="__all__" option can work by specifying an additional field manually as per the following examples. This is by far the cleanest solution around for this issue.
Nested Relationships
http://www.django-rest-framework.org/api-guide/relations/#nested-relationships
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = '__all__'
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True, read_only=True)
class Meta:
model = Album
fields = '__all__'
I would assume this would work for any of the other related field options listed on the same page: http://www.django-rest-framework.org/api-guide/relations/#serializer-relations
Reverse relation example
class TrackSerializer(serializers.ModelSerializer):
album = AlbumSerializer(source='album_id')
class Meta:
model = Track
fields = '__all__'
Note: Created using Django Rest Framework version 3.6.2, subject to change. Please add a comment if any future changes break any examples posted above.
Hi I could achieve the expected result by using Django's _meta API , which seems to be available since Django 1.11. So in my serializer I did:
model = MyModel
fields = [field.name for field in model._meta.fields]
fields.append('any_other_field')
In programming there's always many ways to achieve the same result, but this one above, has really worked for me.
Cheers!
If you are trying to basically just add extra piece of information into the serialized object, you don't need to change the fields part at all. To add a field you do:
class MySerializer(serializers.ModelSerializer):
...
new_field = serializers.SerializerMethodField('new_field_method')
def new_field_method(self, modelPointer_):
return "MY VALUE"
Then you can still use
class Meta:
fields = '__all__'
to include all the fields and the other fields defined in your serializer you can just say exclude = ()
class ToppingSerializer(serializers.HyperlinkedModelSerializer):
pizzas = '<>' #the extra attribute value
class Meta:
model = Topping
exclude = ()
This will list all the field values with the extra argument pizzas
This is how i did it, much more easier
class OperativeForm(forms.ModelForm):
class Meta:
model = Operative
fields = '__all__'
exclude = ('name','objective',)
widgets = {'__all__':'required'}
Building on top of #Wand's wonderful answer:
def build_fields(mdl,extra=[],exclude=[]):
fields = [field.name for field in mdl._meta.fields if field.name not in exclude]
fields += extra
return fields
Usage:
model = User
fields = build_fields(model, ['snippets'], ['password'])
Will return all fields from the User model, with the related field snippets, without the password field.
I'm not sure to save my ManyToMany relationship. I found my exact problem in this thread: Django embedded ManyToMany form, except instead of Sales and Products models, I have models that make up a movie.
I tried the solution, but I receive a syntax error. I don't understand how Django should link the EquipmentModel, LightModel, and ActorModel to the ManyToMany relationship in MovieModel. So far (before trying the other thread's solution), the CharFields that are displayed on the form for LightModel, EquipmentModel, and ActorModel are not linked to the ManyToManyField in MovieModel. So when I save the forms and try to access a particular Movie's actors, all I see is a blank list. The solution from the other thread seems to make sense since it tries to link the models to the ManyToMany relationship in MovieModel, but I don't understand how Django knows which MovieModel to add to (how does it get the correct movieID?).
On a side note, is there a way to check for duplicate movies when the user presses the 'Submit' button on the form? I want to avoid creating duplicates.
views.py:
def add_movie(request, movieID=""):
if request.method == "POST":
form = MovieModelForm(request.POST)
eform = EquipmentModelForm(request.POST)
lform = LightModelForm(request.POST)
aform = ActorModelForm(request.POST)
print 'checking form'
print request.POST.items()
if form.is_valid() and eform.is_valid() and lform.is_valid() and aform.is_valid():
print 'form is valid'
movie_to_add = form.save()
e = eform.save()
l = lform.save()
a = aform.save()
movie_to_add.actors.add(a)
movie_to_add.lights.add(l)
movie_to_add.equipments.add(e)
# return HttpResponseRedirect('/data')
else:
# code for create forms ....
return render_to_response('add_movie.html', {'form':form, 'eform':eform,'lform':lform, 'aform':aform,}, context_instance=RequestContext(request))
Other code that may help:
forms.py
class LightModelForm(forms.ModelForm):
class Meta:
model = LightModel
class ActorModelForm(forms.ModelForm):
class Meta:
model = ActorModel
class EquipmentModelForm(forms.ModelForm):
class Meta:
model = EquipmentModel
class MovieModelForm(forms.ModelForm):
class Meta:
model = MovieModel
fields = ("title", "rank")
models.py
class EquipmentModel(models.Model):
equip = models.CharField(max_length=20)
class ActorModel(models.Model):
actor = models.CharField(max_length=20)
class LightModel(models.Model):
light = models.CharField(max_length=20)
class MovieModel(models.Model):
rank = models.DecimalField(max_digits=5000, decimal_places=3)
title = models.CharField(max_length=20)
equipments = models.ManyToManyField(EquipmentModel, blank=True, null=True)
actors = models.ManyToManyField(ActorModel, blank=True, null=True)
lights = models.ManyToManyField(LightModel, blank=True, null=True)
def __str__(self):
return self.title
Edit: removed unnecessary init and fields thanks to DTing
Edit2: Fixed!
There is a whole lot of stuff going wrong here in addition to what spulec said.
Your models.py look okay.
class EquipmentModel(models.Model):
equip = models.CharField(max_length=20)
class ActorModel(models.Model):
actor = models.CharField(max_length=20)
class LightModel(models.Model):
light = models.CharField(max_length=20)
class MovieModel(models.Model):
rank = models.DecimalField(max_digits=5000, decimal_places=3)
title = models.CharField(max_length=20)
equipments = models.ManyToManyField(EquipmentModel, blank=True, null=True)
actors = models.ManyToManyField(ActorModel, blank=True, null=True)
lights = models.ManyToManyField(LightModel, blank=True, null=True)
def __str__(self):
return self.title
You don't need to override the __init__ method on forms if you are not changing anything on init. You also don't need to be explicit about the fields if you want to include them all.
class LightModelForm(forms.ModelForm):
class Meta:
model = LightModel
class ActorModelForm(forms.ModelForm):
class Meta:
model = ActorModel
class EquipmentModelForm(forms.ModelForm):
class Meta:
model = EquipmentModel
class MovieModelForm(forms.ModelForm):
class Meta:
model = MovieModel
fields = ("title", "rank")
your view doesn't really make sense unless for every movie you are trying to add you also want to:
add a new movie to the db using the submitted post data
create one actor object and add to db
create one light object and add to db
create one equipment object and add to db
take those three objects and add them to another movie's m2m relationships.
This other movie is some movie that you pulled from the urlconf and passed to your view, not the one you just created.
This all seems a little strange.
what i think you want to do is create all the equipment, actors and lights objects so they are in your db already, and use the default m2m formfield widget to select them when adding a movie.
so:
forms.py
class MovieModelForm(forms.ModelForm):
class Meta:
model = MovieModel
urls.py:
url(r'^add_movie/$', add_movie)
views.py:
def add_movie(request):
if request.method=='POST':
form = MovieModelForm(request.POST)
if form.is_valid():
form.save()
return HttpResponse('success')
else:
form = MovieModelForm()
context = {'form':form }
return render_to_response('some_template.html', context,context_instance=RequestContext(request))
you could combine adding actors, lights, and equipment into the same form but that's a bit much for me to write out right now.
As far as modifying your original code to add those lights, actors, and equipment to the movie you just created, you could do this:
if form.is_valid() and eform.is_valid() and lform.is_valid() and aform.is_valid():
new_movie = form.save()
e = eform.save()
l = lform.save()
a = aform.save()
new_movie.actors.add(a)
new_movie.lights.add(l)
new_movie.equipments.add(e)
Change it to:
movie_to_add = get_object_or_404(MovieModel, id=movieID)