ReverseManyToOne create object from children through django form - django

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()

Related

Django Model queries with relationships. How to do the right join

Let's say I have 2 Models:
class Auction(models.Model):
seller = models.ForeignKey(User, on_delete=models.CASCADE, related_name="seller")
title = models.CharField(max_length=64)
class Watchlist(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='user_watchlist')
auction = models.ForeignKey(Auction, on_delete=models.CASCADE, related_name='auction_watchlist')
The view receives a request, creates a context variable with the auction objects the are:
associated with the user who made the request and
that have been added to the Watchlist Model,
sends it to the template.
I have set up my view to work like this:
#login_required
def watchlist(request):
watchlist_objects = Watchlist.objects.filter(user=request.user)
auction_objects = Auction.objects.filter(auction_watchlist__in=watchlist_objects).all()
context = {'watchlist_auctions': auction_objects}
print(context)
return render(request, "auctions/watchlist.html", context)
-I make the first query to get the list of items in the watchlist associate with the user.
-Then I use that to get another query from the Auction Model and I pass it to the template.
In the template I can access the attributes of Auction to display them. (title, author, and others that I did not include for simplicity)
The question is:
Is this the "right way? Is there a better way to access the attributes in Auction from the first Watchlist query?
It seems to me that I'm doing something overcomplicated.
This is not that bad, considering that it will probably be executed as one query, because of the lazy queryset evaluations. You can skip the .all() if you already have .filter().
However, there is a more convenient way to do this, using lookups that span relationships.:
auction_objects = Auction.objects.filter(auction_watchlist__user_id=request.user.id)

populating a tuple with model objects

I have a simple checklist in my personal info form that users can fill. this checklist gets its choices from tuple and that tuple gets its items from another model called field like this:
class Field(models.Model):
id = models.AutoField(primary_key=True)
slug = models.CharField(max_length=16, default='default')
title = CharField(max_length=32)
INTERESTS = (Field.objects.values_list('slug', 'title'))
everything works just fine. however, when I add a new field object, INTERESTS tuple wont get updated without migrations. how can I update my tuple without any migrations? is it even possible?
this is my simplified models:
class PersonalInfo(models.Model):
interests = MultiSelectField(choices=INTERESTS, blank=True)
and my form:
class Interests(forms.ModelForm):
interests = forms.MultipleChoiceField(widget=forms.CheckboxSelectMultiple, choices=INTERESTS)
class Meta:
model = PersonalInfo
fields = ['interests']
Not an actual answer, but it's too long for a comment. I dont know how much you "simplified the model", but you should change the interest into a ManyToManyField. Right now if any value will be deleted from Field, you will get invalid data in the database. If you will use ManyToManyField, it will it will make sure your DB data consistent will be consistent. So your model will look like this:
class PersonalInfo(models.Model):
interests = models.ManyToManyField(Field)
After doing this, using the ModelForm should handle the data in form for you without doing anything "manually" there.

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.

Form within a form in 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)

Referencing a Django contrib.admin user via a foreign key?

I'm developing a small Django site and I'm using django.contrib.admin to handle content management. I'd like to capture the first name & last name of the author (an Admin user) of an Article on its initial save (and not update it if another user edits the Article).
ie.
class Article(models.Model)
title = models.CharField(max_length=50)
pub_date = models.DateTimeField('date published')
author = ForeignKey(???)
...
What do I need to write to grab this user's first name & last name fields when creating a new Article object? I'd default to their admin username if those fields are blank.
Have your model use the User object:
author = models.ForeignKey(User)
To prevent this field from being changeable on update, check out this other SO post:
Django admin: exclude field on change form only
To change the admin's Select field to use first/last name, you could try this snippet:
http://djangosnippets.org/snippets/1642/
To change the admin's view, assuming you are using the built-in templates, you could add a custom column as described on this post: How do I add a custom column with a hyperlink in the django admin interface?
class AuthorAdmin(admin.ModelAdmin):
list_display = ('author_name',)
def my_author_name(self, obj):
if obj.author.first_name and obj.author.last_name:
return '%s %s' % (obj.author.first_name, obj.author.last_name)
else:
return obj.author.username
my_author_name.allow_tags = True
my_author_name.short_description = 'Author'
I think you are looking for this:
author = models.ForeignKey(User)
It looks like the best way to handle a None or blank result from get_full_name is to just populate User.author with models.ForeignKey(User) and then — at the template level — use the following:
{{ user.get_full_name|default:user.username }}
... via this SO answer. This allows me to perform queries on a User's Articles, but still gracefully handles blank first_name & last_name fields if a User hasn't entered them yet, but will also update dynamically when they have).