If I have two models in Django application like this:
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
author = models.ForeignKey(Author)
title = models.CharField(max_length=100)
How can I create a single form that allows you add both an Author and a Book simultaneously. If the author exists in the system, I could simply display the book Form and link that to the author but it is very often that I need to allow my users to create the book and the author simultaneously.
How can I do this?
Thanks.
You can write a custom form, which will check if the author exists in the system use existing, if no, create new with provided name.
class CustomForm(forms.ModelForm):
author = forms.CharField()
def save(self, commit=True):
author, created = Author.objects.get_or_create(name=self.cleaned_data['author'])
instance = super(CustomForm,self).save(commit=commit)
instance.author = author
if commit:
instance.save()
return instance
class Meta:
model=Book
Not sure this code is working, but I suppose it can explain my idea.
You can create a view that handles multiple forms - see http://collingrady.wordpress.com/2008/02/18/editing-multiple-objects-in-django-with-newforms/ for an excellent example.
You'd have to ensure that the rendering of the form objects are done in the template with only one tag and one submit button.
Related
Supposing some standard Django relational setup like this:
models.py
class Book(models.Model):
title = models.CharField(max_length=30)
class Page(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE)
text = models.CharField(max_length=100)
I'd like to create a book and all its pages with one request. If we start with serializers like this:
serializers.py
class PageSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = '__all__'
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = ('title', 'pages')
pages = PageSerializer(many=True)
Then the problem is that the PageSerializer now requires a book foreign key. But I don't know the key of the book until I've created the book, which is only after I've sent the POST request. So I cannot include the book pk in the POST data that the client sends.
An obvious solution is to override the create function on the Book serializer. But then I am still faced with the problem that the validators will say that the book field is required and the POST data will fail to validate.
I could make book a not-required field on the PageSerialzer. But this seems very bad. The book field IS required. And the BookSerializer create method will be able to supply it. It's just the client that doesn't know it.
So my suspicion is that the best way to do this is to leave book as required on the PageSerializer, but somehow make it so that the validators on the BookSerializer don't check for whether that is in the POST data when I post to BookSerializer.
Is this the correct way to achieve what I want? And if so, how do I do it? Thank you.
Why not try handling it in the create viewset. You can validate the data for the Book object first, before creating it. Then validate the data for the Page object using the created Book object and the other data sent from the request to the page.
I'd link your ViewSet to a BookCreateSerializer, and from this specific serializer I'd then add a function to not only verify the received data but make sure you link the parent's id to the child's one during creation.
IMPORTANT NOTE
This works if a parent only has one child, not sure about when passing multiple children.
Here is what is could look like.
BookCreateSerializer:
class BookCreateSerializer(serializers.ModelSerializer):
"""
Serializer to create a new Book model in DB
"""
pages = PageCreateSerializer()
class Meta:
model = Book
fields = [
'title',
'pages'
]
def create(self, validated_data):
page_data = validated_data.pop('page')
book = Book.objects.create(**validated_data)
Page.objects.create(book=book, **page_data)
return book
PageCreateSerializer
class PageCreateSerializer(serializers.ModelSerializer):
"""
Serializer to create a new Page model in DB
"""
class Meta:
model = Page
fields = [
'book',
'text'
]
To make sure that your Book instance understands what a page field is in the serializer, you have to define a related_name in its child's Model (Page). The name you choose is up to you. It could look like:
class Page(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='page')
text = models.CharField(max_length=100)
I am working on Django 1.11. I have two models, the first one has a foreign key like this :
class Book(models.Model):
name=models.CharField(max_length=100)
owner = models.ForeignKey(Owner, on_delete=models.PROTECT, null=True, default=None)
other_properties
class Owner(models.Model):
some_property
I use the generic.UpdateView to update my Books :
class BookUpdateView(generic.UpdateView):
model = Book
fields = ['owner']
This works but now I want to create an Owner from the BookUpdateView page, and then modify the book to set the owner property to the newly created Owner.
Is it possible to have an inline form to create an Owner inside the BookUpdateView ? (not in the admin pages)
Or maybe to have a separate create view for the owner which redirect to the BookUpdateView from where it was called. But only when it was called from there ?
Thanks for any suggestion!
Yes you can create Owner inside your BookUpdateView using form_valid() method. This method executes when form is valid.
class BookUpdateView(generic.UpdateView):
model = Book
fields = ['owner']
def form_valid(self,form):
owner = Owner.objects.create(define_some_property)
.......
return super(BookUpdateView, self).form_valid(form)
You can refer to documentation - https://docs.djangoproject.com/en/2.1/ref/class-based-views/mixins-editing/#django.views.generic.edit.FormMixin.form_valid
I have two models:
class Author(models.Model):
name = models.CharField(max_length=100)
create_report = models.BooleanField(default=False)
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.PROTECT)
title = models.CharField(max_length=100)
They are registered in admin like this:
class BookInline(admin.TabularInline):
model = PurchaseOrderItem
#admin.register(PurchaseOrder)
class AuthorAdmin(admin.ModelAdmin):
inlines = (PurchaseOrderInline,)
I create an author and two books through Django admin. After I hit the 'Save' button, if Author.create_report == True I would like to see a report saying the following:
Author Whoever-he-is has written the following books:
Title-of-the-first-book
Title-of-the-first-book
(Where the report should appear or how to render the template are not relevant questions here, let's skip them.)
My first idea was to overwrite Author.save() method:
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
if self.create_report:
self.write_report(name=self.name, books=self.book_set.all())
The problem is that Author.save() method is executed before Book objects are saved and so self.book_set.all() is empty.
One workaround would be to use some other ModelAdmin function (for instance log_addition()), which fires after Book objects are saved, but as I wish to use the same functionality out of admin, too, it is not the best solution.
Can I somehow achieve the result without using the admin layer functions?
The book_set.all() will be Null initially, because there is no Book related to the Author at the time of author creation. So, what I'm suggesting is, generate the report whenever a Book is created.
class Author(models.Model):
name = models.CharField(max_length=100)
create_report = models.BooleanField(default=False)
class Book(models.Model):
author = models.ForeignKey(Author, on_delete=models.PROTECT)
title = models.CharField(max_length=100)
def save(self, *args, **kwargs):
pk = self.pk # pk will be None like objects if self is new instance
super().save(*args, **kwargs)
if not pk and self.author.create_report:
write_report(name=self.author.name, books=self.author.book_set.all())
I am having a similar issue. I think this is exactly the reason why Django introduced signals. You can fire and catch a signal of the created Books/Authors and then fire an action from there that creates some sort of report.
But I get your point (since I am feeling the same pain) ... id would be great is this would work straight from the model.
I think an example will explain it better.
Lets say I am making an application for various Libraries to show what books they have available.
The site will have Users that are registered through the built-in user registration system and User model. These Users (think of it like library members) can visit the Libraries and browse and checkout the available books.
The site will also allow Libraries to register themselves, and part of that registration is declaring a "Librarian," a person who controls what books are available in their particular Library.
I want to create a Library Registration Form that takes the information of this Library and its Librarian and not only creates an instance of the Library model, but also an instance of the User model. The Librarian automatically becomes a User.
This is my current models.py:
class Library(models.Model):
name = models.CharField(max_length=200)
website = models.URLField()
street = models.CharField(max_length=200)
city = models.CharField(max_length=200)
state_or_province = models.CharField(max_length=200)
postal_code = models.CharField(max_length=200)
date_registered = models.DateField(auto_now_add=True)
librarian = models.OneToOneField(User, on_delete=models.CASCADE)
#receiver(post_save, sender=Library)
def create_user(sender, instance, created, **kwargs):
if created:
User.objects.create(user=instance)
instance.user.save()
I am currently lost as to how to build views.py and forms.py. I am not even sure that model is built correctly, since I need the form to include not only the Library information, but also User information (first_name, last_name, email, password...). Do I need to duplicate that information in the Library model in order for it to pass to the form?
Basically, I don't have a good grasp of how to models connect to one another via Django tools and files. If anyone can point me in the right direction, I would appreciate it. Thanks!
You can do this with standard django, however it is quite long.
Or you can use django-extra-views, to make your life nice and easy.
class LibrarianInline(GenericInlineFormSet):
model = User
fields = '__all__'
class LibraryInline(CreateWithInlinesView):
model = Library
inlines = [LibrarianInline]
fields = '__all__'
There is also a simpler way of doing it with standard django. Force the librarian to be created first and only then allow them to create a Library.
urls.py
urlpatterns = [
url(r'^/create-librarian$',
LibrarianCreateView.as_view(), name='create_librarian'),
url(r'^/create-library/for/(?P<librarian_id>\d+)$',
LibraryCreateView.as_view(), name='create_library'),
]
views.py
from django.shotcuts import reverse
from django.generic.views import CreateView
class LibrarianCreateView(CreateView):
model = User
def form_valid(self, form):
librarian = form.save(commit=True)
return redirect('create_library', {'librarian_id': librarian.id})
class LibraryCreateView(CreateView):
model = Library
def form_valid(self, form):
library = form.save(commit=False)
librarian_id = self.kwargs['librarian_id']
# You can do validation here if you fancy
library.librarian_id = librarian_id
library.save()
return self.get_success_url()
By requiring the id of the Librarian to create the Library, it prevents it being created without a librarian.
I need to implement the following:
The user shall be presented with a form that will have a drop down choice menu consisting of property names. There are two types of properties: general properties, i.e. properties common for all users and custom properties, i.e. properties that each user has defined prior to that. The models would look something like that:
class GeneralPropertyName(models.Model):
name = models.CharField(max_length=20)
class CustomPropertyName(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=20)
The drop down menu should have all general properties and only those custom properties that pertain to the user.
First question: how to define such a model?
I need to: 1. somehow unify both properties, 2. take only those items from CustomPropertyName that pertain to the user
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(GeneralPropertyName) ??UNIFY??? ForeignKey(CustomPropertyName)
Second, is there anything special that needs to be done with ModelForm?
class SpecDataForm(ModelForm):
class Meta:
model = SpecData
And the 3rd question is what needs to be done in the view? I will need to use inline formsets since I will have a few dynamic forms like that.
def index(request):
user = User.objects.get(username=request.user.username)
specdataFormSet = inlineformset_factory(User, SpecData, form=SpecDataForm, extra=30)
...
specdata_formset = specdataFormSet(instance=user, prefix='specdata_set')
...
Thanks.
EDIT: Adjusted juliocesar's suggestion to include formsets. Somehow I am getting the following error message: Cannot resolve keyword 'property' into field. Choices are: id, name, selection_title, user
def index(request):
user = User.objects.get(username=request.user.username)
user_specdata_form = UserSpecDataForm(user=user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
You can use a GenericForeignKey to handle it, but you still need more to solve your further questions about forms and view.
I have made an example of how you solve your problem (logged user can select from General properties and his Custom properties, non-logged user only can select General properties). I used model inheritance for the properties (In your sample code it seems that a CustomPropertyName is a PropertyName with other fields). I think inheritance is an easier and a more basic concept than ContentTypes and it fits to your needs.
NOTE: I remove some code like imports to simplify the code.
1) models.py file:
class PropertyName(models.Model):
name = models.CharField(max_length=20)
def __unicode__(self):
return self.name
class CustomPropertyName(PropertyName): # <-- Inheritance!!
user = models.ForeignKey(User)
def __unicode__(self):
return self.name
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = models.ForeignKey(PropertyName)
NOTES: The field SpecData.property points to PropertyName since all properties are saved in the PropertyName's database table.
2) forms.py file:
from django import forms
from django.db.models import Q
from models import SpecData, PropertyName
def UserSpecDataForm(user=None):
UserPropertiesQueryset = PropertyName.objects.filter(Q(custompropertyname__user=None) | Q(custompropertyname__user__id=user.id))
class SpecDataForm(forms.ModelForm):
property = forms.ModelChoiceField(queryset=UserPropertiesQueryset)
class Meta:
model = SpecData
exclude = ('user',)
return SpecDataForm
NOTES: The trick here is to generate the form SpecDataForm dynamically, by filtering properties according the user specified in the parameter.
3) views.py file:
from forms import UserSpecDataForm
def index(request):
if request.POST:
form = UserSpecDataForm(request.user)(request.POST) # instance=user
if form.is_valid():
spec_data = form.save(commit=False)
spec_data.user = request.user
spec_data.save()
else:
form = UserSpecDataForm(request.user)()
return render_to_response('properties.html', {'form': form}, context_instance=RequestContext(request))
NOTES: Nothing special here, just a call to form.UserSpecDataForm(request.user) that returns the form class and then instantiate. Also setted the logged-in user to the object returned on save since It was excluded in the form to not show in front-end.
Following this basic example you can do the same with formsets if you need it.
UPDATE:
Formset can be used by adding following code to the view:
user_specdata_form = UserSpecDataForm(user=request.user)
SpecdataFormSet = inlineformset_factory(User, SpecData, form=user_specdata_form, extra=30)
The complete project sample can be downloaded from http://ge.tt/904Wg7O1/v/0
Hope this helps
1a) have you looked into django's ContentType framework this will allow you to have generic foreign keys and you can put restrictions on what types of models are acceptable to store in.
1b) I think that the validation for accepting what type of foreign key is acceptable shouldn't be in your model but should be part of your form validation before saving.
2) If you do use a model form you're going to have to define your own custom widget for the propery field. This means you're probably going to have to write you're own render function to render the html from the field. You should also define your own validation function on the form to make sure that only the appropriate data is acceptable to save.
3) I don't think you'll have to do anything you aren't already doing in the views
Use GenericForeignKey:
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
property = GenericForeignKey('content_type', 'object_id')
You can use this to combine the two fields(type & id) into a single choice field.
One way is that you have only one model, make user nullable:
class PropertyName(models.Model):
user = models.ForeignKey(User, null=True, blank=True)
name = models.CharField(max_length=20)
class SpecData(models.Model):
user = models.ForeignKey(User)
selection_title = models.CharField(max_length=20)
property = ForeignKey(PropertyName)
So, if user is not set, it is a general property. If it is set, it is related to this user.
However, please note that if you need unique property names, that NULL != NULL.
Of course, the suggested GenericForeignKey solution is better for some cases.
Also, you can easily make the normal (non-model) form with that you describe and separate form logic from model logic.