Form within a form in Django? - django

I have been looking at the documentation and thought maybe inline-formsets would be the answer. But I am not entirely sure.
Usually whenever you create a ModelForm it is bound to the related Model only. But what if you wanted to edit two models within a form?
In a nutshell, when editing the class conversation, and selecting a Deal class from the dropdown, I would like to be able to change the status of the selected deal class as well (but not the deal_name). All within the same form. Does Django allow that?
class Deal(models.Model):
deal_name = models.CharField()
status = models.ForeignKey(DealStatus)
class Conversation(models.Model):
subject = models.CharField()
deal = models.ForeignKey(Deal, blank=True, null=True)
Update:
The reason I wasn't sure if inline-formssets are the answer is the following behaviour:
View:
call = get_object_or_404(contact.conversation_set.all(), pk=call_id)
ConversationFormSet = inlineformset_factory(Deal, Conversation)
fset = ConversationFormSet(instance=call)
variables = RequestContext(request, {'formset':fset})
return render_to_response('conversation.html', variables)
Template
{{ formset }}
The result I am getting is not what I expected. I am getting three forms of Conversation class, where the first one is filled out (due editing and passing in the isntance). However the Deal DropDown menu is not listed at all. Why?

I found the solution and hope this will help someone else with the same problem in the future. I ended up redesigning my models.
I simply added the status also to my Conversation model.
class Conversation(models.Model):
subject = models.CharField()
deal = models.ForeignKey(Deal, blank=True, null=True)
status = models.ForeignKey(DealStatus)
In the view I added a custom save like this:
if form.is_valid():
call = form.save(commit=False)
deal = get_object_or_404(Deal.objects.all(), pk=call.deal.id)
deal.status = call.status
deal.save()
call.save()
That works nicely.

Another approach is to use signal like this:
def update_deal_status(sender, instance, created, **kwargs):
if created:
deal = Deal.objects.get(id__exact=instance.deal_id)
deal.status = instance.status
deal.save()
signals.post_save.connect(update_deal_status, sender=Conversation)

Related

ReverseManyToOne create object from children through django form

Using:
Python 3.7.3
django 2.2.5
mysql 5.7.27
I have the following models:
class Item(models.Model):
...
class Comments(models.Model):
Item = models.ForeignKey('Item', default=None, null=True, on_delete=models.CASCADE)
Comment = models.TextField(max_length=512, default="", blank=True)
I would like to create a Comments object, when creating the Item through a django form. I tried to do:
class ItemInsertForm(forms.ModelForm):
...
Comments = forms.CharField(required=False,
widget=forms.Textarea(attrs={'placeholder':"Use comments to describe item details \
or issues that other users should know",
'rows':5,
'cols':50,
}
)
)
def clean_Comments(self, *args, **kwargs):
_comment = self.cleaned_data.get('Comments')
_comments = Item.comments_set.create(Comment=_comment)
return _comments
but I get the following error:
'ReverseManyToOneDescriptor' object has no attribute 'create'
Both tables are empty, so no Item and no Comments currently exist. I guess that's why there is no 'create' method available. Is there a way I could achieve what I want?
Or is there another way to manage the comments for the Item object? I created another table to be able to associate multiple comments to the same item and differentiate between them. A character field in the Item class would concatenate all comments in a single string.
I see quite some issues with your code, but since I feel like you gave it an honest shot, I'll try to help you out as best as I can.
First of all your models.py file:
Model names should be singular, so instead of Comments, use Comment.
Class members should be lowercase, so Item and Comment should be changed to item and comment.
Comment.comment is still not very descriptive. The comment is the actual object, it's content is the text within the comment, so text would be more appropriate here.
A ForeignKey with null=True already sets default to None.
Taking this into account and cleaning up your models.py:
class Item(models.Model):
...
class Comment(models.Model):
item = models.ForeignKey(Item, null=True, on_delete=models.CASCADE)
text = models.TextField(max_length=512, default="", blank=True)
Then, moving on to your form:
Since it's a form for creation Comments, a more appropriate name would be CommentForm.
def clean_Comments(self, *args, **kwargs): is a function reserved for doing validation on the Comments field, not for creating an object from the form input. For that you can use the ModelForm's save() method. You only need to define a save method if you're going to perform some custom logic though.
Let's fix those issues first, before I move onto the error message your getting:
class ItemInsertForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['text']
text = forms.CharField(required=False,
widget=forms.Textarea(attrs={'placeholder':"Write your comment to describe item details \
or issues that other users should know",
'rows':5,
'cols':50,
}
)
)
This form, when submitted, will create a Comment object. However, there is still no ability to add the comment to an Item.
To do this, you need to make sure there are Item instances in the database, or allow the user to create one through an ItemForm
There are multiple ways to do this:
Add a ModelChoiceField to the CommentForm, which will allow the user to select an item from a select.
item = forms.ModelChoiceField(queryset=Item.object.all(),
to_field_name = '<item_name>',
empty_label="Select an Item")
When you want to add this form to an something like an ItemDetailPage, you can use the currently viewed Item using something like
item = Item.objects.get(pk=<item_id>)
or
item = Item.objects.create(<item_properties_here>)
then, when saving your form:
comment = form.save()
comment.item = item.
comment.save()
The third way is what you were trying, and why you were getting an error. Retrieve an item, then add the comment saved from the form to the item.comment_set.
Something like this:
item = Item.objects.get(pk=<item_id>)
comment = form.save()
item.comments_set.add(comment)
item.save()

Multiple sub-fields attached to a field, and how to give the user the possibility of adding many of these fields

I am building a form in which users (composers) can add a composition. Within this form, alongside title, year, etc, they also add instrumentation.
Each instrument can have a couple of properties, for example 'doubling', and the number of players.
So, for instance:
title: New composition
instrumentation:
violin
doubled: no
players: 1 (this is the 'DoublingAmount', see the models)
viola
doubled: yes
players: 2 (this is the 'DoublingAmount', see the models)
cello
doubled: no
players: 4 (this is the 'DoublingAmount', see the models)
I have created three different models: one for instrumentation, one for the composition, and then one with a ManyToMany relation via a 'through'.
models.py:
class Composition(models.Model):
title = models.CharField(max_length=120) # max_length = required
INSTRUMENT_CHOICES = [('pno', 'piano'), ('vio1', 'violin_1(orchestral section)'), ('vio2', 'violin_2(orchestral section)'),]
instrumentation = models.ManyToManyField('Instrumentation',
through='DoublingAmount',
related_name='compositions',
max_length=10,
choices=INSTRUMENT_CHOICES,)
class Instrumentation(models.Model):
instrumentation = models.CharField(max_length=10)
class DoublingAmount(models.Model):
DOUBLING_CHOICES =[(1, '1'),(2, '2'), (3, '3')]
doubling = models.BooleanField(default=False)
composition = models.ForeignKey(Composition, related_name='doubling_amount', on_delete=models.SET_NULL, null=True)
instrumentation = models.ForeignKey(Instrumentation, related_name='doubling_amount', on_delete=models.SET_NULL, null=True)
amount = models.IntegerField(choices=DOUBLING_CHOICES, default=1)
forms.py:
from django import forms
from .models import Composition, Instrumentation
class CompositionForm(forms.ModelForm):
title = forms.CharField(label='Title',
widget=forms.TextInput()
description = forms.CharField()
class Meta:
model = Composition
fields = [
'title',
'instrumentation',
]
views.py:
def composition_create_view(request):
form = CompositionForm(request.POST or None)
if form.is_valid():
form.save()
form = CompositionForm()
context = {
'form': form
}
return render(request, "template.html", context)
template.html:
{{ form }}
I can see the drop-down list in my form giving the choice of the name of the instrument, and only that. I'd like to have the possibility of selecting name of instrument, doubling yes/no, and quantity. Then I also want to add more instruments, each with its own name, doubling, and quantity. I know this will have to be done via JavaScript, but I don't know how to build the 'behind the scenes' and get Django grab the new items added by the user.
Update
I've added a new form in forms.py:
class InstrumentationForm(forms.ModelForm):
class Meta:
model = DoublingAmount
fields = [
'doubling',
'amount',
'instrumentation'
]
and linked to the views.py:
form_d = InstrumentationForm(request.POST or None)
[...]
context = {
'form': form,
'form_d': form_d
}
and, finally, the template:
{{ form_d }}
I now have the instrumentation, doubling, and amount fields showing in my template. Am I doing this right? If yes, how can I have Django deal with the user wanting to add or remove instruments? Thanks.
Update 2
The issue, which I just realised, is that if I use the DoublingAmount model to store information about the composition's instrument, the object Composition needs to be already created, which of course it's not may case. One solution maybe is to build another form with only 'DoublingAmount' (e.g. instrumentation) information to be displayed after the user has saved the title of the composition. Still, I don't know how to have more than one instrument within the DoublingAmount model without having several objects of DoublingAmount objects for the same composition.
Is there a simpler way to do all this? For example, a 'sub-class' of the composition model?
Create a Composition and then add one Instrument after another like comments to a blog-post. That would probably be the easiest way. Sadly I dont know how to create an array of objects from a form in Django. It's the right way to have more than one object than, thats why its ManyToMany. I would also suggest OneToMay and just two classes:
Composition and instrument with the fields: name, doubled, player-count. But I also dont really get this musician stuff so maybe I am wrong with that :-)
Another way would be using Ajax or stuff like that and create an array in json and transforming that into your instruments. You would need js anyway if you want to dynamically add more instruments (you would need to create input fields and so on).

Filter M2M in template?

In my model, I have the following M2M field
class FamilyMember(AbstractUser):
...
email_list = models.ManyToManyField('EmailList', verbose_name="Email Lists", blank=True, null=True)
...
The EmailList table looks like this:
class EmailList(models.Model):
name = models.CharField(max_length=50, default='My List')
description = models.TextField(blank=True)
is_active = models.BooleanField(verbose_name="Active")
is_managed_by_user = models.BooleanField(verbose_name="User Managed")
In the app, the user should only see records that is_active=True and is_managed_by_user=True.
In the Admin side, the admin should be able to add a user to any/all of these groups, regardless of the is_active and is_managed_by_user flag.
What happens is that the Admin assigns a user to all of the email list records. Then, the user logs in and can only see a subset of the list (is_active=True and is_managed_by_user=True). This is expected behavior. However, what comes next is not.
The user deselects an email list item and then saves the record. Since M2M_Save first clears all of the m2m records before it calls save() I lose all of the records that the Admin assigned to this user.
How can I keep those? I've tried creating multiple lists and then merging them before the save, I've tried passing the entire list to the template and then hiding the ones where is_managed_by_user=False, and I just can't get anything to work.
What makes this even more tricky for me is that this is all wrapped up in a formset.
How would you go about coding this? What is the right way to do it? Do I filter out the records that the user shouldn't see in my view? If so, how do I merge those missing records before I save any changes that the user makes?
You might want to try setting up a model manager in your models.py to take care of the filtering. You can then call the filter in your views.py like so:
models.py:
class EmailListQuerySet(models.query.QuerySet):
def active(self):
return self.filter(is_active=True)
def managed_by_user(self):
return self.filter(is_managed_by_user=True)
class EmailListManager(models.Manager):
def get_queryset(self):
return EmailListQuerySet(self.model, using=self._db)
def get_active(self):
return self.get_queryset().active()
def get_all(self):
return self.get_queryset().active().managed_by_user()
class EmailList(models.Model):
name = models.CharField(max_length=50, default='My List')
description = models.TextField(blank=True)
is_active = models.BooleanField(verbose_name="Active")
is_managed_by_user = models.BooleanField(verbose_name="User Managed")
objects = EmailListManager()
views.py:
def view(request):
email = EmailList.objects.get_all()
return render(request, 'template.html', {'email': email})
Obviously there is outstanding data incorporated in my example, and you are more than welcome to change the variables/filters according to your needs. However, I hope the above can give you an idea of the possibilities you can try.
In your views you could do email = EmailList.objects.all().is_active().is_managed_by_user(), but the loading time will be longer if you have a lot of objects in your database. The model manager is preferred to save memory. Additionally, it is not reliant on what the user does, so both the admin and user interface have to talk to the model directly (keeping them in sync).
Note: The example above is typed directly into this answer and has not been validated in a text editor. I apologize if there are some syntax or typo errors.

django cbv dynamically exclude field from form based on is_staff / is_superuser

Been trying to determine the "most" elegant solution to dropping a field from a from if the user is not is_staff/is_superuser. Found one that works, with a minimal amount of code. Originally I though to add 'close' to the 'exclude' meta or use two different forms. But this seems to document what's going on. The logic is in the 'views.py' which is where I feel it blongs.
My question: Is this safe? I've not seen forms manipulated in this fashion, it works.
models.py
class Update(models.Model):
denial = models.ForeignKey(Denial)
user = models.ForeignKey(User)
action = models.CharField(max_length=1, choices=ACTION_CHOICES)
notes = models.TextField(blank=True, null=True)
timestamp = models.DateTimeField(default=datetime.datetime.utcnow().replace(tzinfo=utc))
close = models.BooleanField(default=False)
forms.py
class UpdateForm(ModelForm):
class Meta:
model = Update
exclude = ['user', 'timestamp', 'denial', ]
views.py
class UpdateView(CreateView):
model = Update
form_class = UpdateForm
success_url = '/denials/'
template_name = 'denials/update_detail.html'
def get_form(self, form_class):
form = super(UpdateView, self).get_form(form_class)
if not self.request.user.is_staff:
form.fields.pop('close') # ordinary users cannot close tickets.
return form
Yes, your approach is perfectly valid. The FormMixin was designed so you can override methods related to managing the form in the view and it is straightforward to test.
However, should yours or someone else's dynamic modifications of the resulting form object become too extensive, it would probably be best to define several form classes and use get_form_class() to pick the correct form class to instantiate the form object from.

can I use duck-typing with class-based views

I may be completely off the reservation here. (Feel free to tell me if I am.)
My use case is that I have a list of schools. The school model is pretty simple:
class School(models.Model):
name = models.CharField(max_length=100)
mascot = models.CharField(max_length=100, null=True, blank=True)
When my user wants to edit one of these schools, I don't want them editing the master copy. Instead, I want to give them their own copy which they can play with. When they are done editing their copy, they can submit their change, and someone else will approve it. So I have another class for the user's copy of the school:
class UserSchool(models.Model):
name = models.CharField(max_length=100)
mascot = models.CharField(max_length=100, null=True, blank=True)
master_school = models.ForeignKey(School)
user = models.ForeignKey(settings.AUTH_USER_MODEL)
So I set up a form to handle the editing of the UserSchool:
class UserSchoolForm(forms.ModelForm):
class Meta:
model = UserSchool
fields = ['name','mascot']
And now I have my EditSchool form:
class EditSchool(UpdateView):
model = School
success_url = reverse_lazy('list_schools')
form_class = UserSchoolForm
def get(self, request, *args, **kwargs):
school = self.get_object()
# make a copy of the school for this user
user_school, created = UserSchool.objects.get_or_create(
master_school=school, user=request.user,
defaults={'name' : school.name, 'mascot' : school.mascot})
self.object = user_school
form = UserSchoolForm()
context = self.get_context_data(form=form)
return self.render_to_response(context)
I know that get() is making the copy correctly, but when the form displays, there are no values listed in the "name" or "default" fields. My suspicion is that the problem is with the fact that cls.model = School, but self.object is an instance of UserSchool.
Am I close but missing something? Am I completely on the wrong path? Is there a better model for this (like having a single School instance with a special user for "master")?
(And one small complication -- since I'm an old hand at Django, but new a class-based views, I'm trying to use Vanilla Views because I find it easier to figure out what's going on.)
Just to rule out the obvious - you're not passing anything to the form constructor. Have you tried it with instance=user_school? There might be more that needs work but I'd start there.
To expand on this a bit - in your view, you're completely overriding the built in get method. That's fine, but it means that you're bypassing some of the automated behavior of your view superclass. Specifically, the get method of ProcessFormView (one of your ancestor classes) instantiates the form using the get_form method of the view class. FormMixin, another ancestor, defines get_form:
return form_class(**self.get_form_kwargs())
And get_form_kwargs on ModelFormMixin adds self.object to the form's kwargs:
kwargs.update({'instance': self.object})
Because your overridden get method does not call get_form, it also doesn't call get_form_kwargs and therefore doesn't go through the whole path that provides an initial binding for the form.
I personally would try to handle this by modifying the get_object method of your custom view and leaving the rest alone:
class EditSchool(UpdateView):
model = School
success_url = reverse_lazy('list_schools')
form_class = UserSchoolForm
def get_object(self, queryset=None):
school = super(EditSchool, self).get_object(queryset=queryset)
user_school, created = UserSchool.objects.get_or_create(
master_school=school, user=self.request.user,
defaults={'name' : school.name, 'mascot' : school.mascot})
return user_school
There may be more changes needed - I haven't tested this - but both the get and set methods use get_object, and bind it to the form as appropriate.