Django Manytomany add - django

I have 2 models in my Project with Many to Many relationship. On saving model Event, I read from the event_attendees file and add it to the attendees field in the Event. No errors/exceptions shown but attendee is not added to the attendees field. Do I need to save the model again after altering with the attendees field? If so, how to do that (calling save method from add_attendees will cause the program into infinite loop)?
class Attendee(models.Model):
name = models.CharField(max_length=100)
class Event(models.Model):
name = models.CharField(max_length=100)
event_attendees = models.FileField(upload_to='documents/', blank=True)
attendees = models.ManyToManyField(Attendee, blank=True)
def save(self, *args, **kwargs):
super().save()
self.add_attendees()
def add_attendees(self):
with open(self.event_attendees.url[1:]) as csv_file:
# Some code here
for row in csv_reader:
# Some code here
attendee = Attendee(name=name)
attendee.save()
self.attendees.add(attendee)
print(self.attendees.all()) # attendee added
print(attendee.event_attended) # event present with attendee
#Refresh template to check changes -> Changes lost

It's the Attendee object that you haven't saved.
You can shortcut it by using the create method on the m2m field:
for row in csv_reader:
self.attendees.create(name=whatever)
(Note, please don't blindly catch exceptions. Django will already do that and report a useful error page. Only catch the exceptions you are actually going to deal with.)

Apparently, the feature worked when I used non-admin web dashboard. While using by-default-created /admin dashboard, this feature was not working. I am assuming from the results that the admin side code calls different methods while saving the model object even though I have overridden the save method (and hence my save method along with other methods should be called). I will update with more info if I find it.

Related

Django objects uniqueness hell with M2M fields

class Badge(SafeDeleteModel):
owner = models.ForeignKey(settings.AUTH_USER_MODEL,
blank=True, null=True,
on_delete=models.PROTECT)
restaurants = models.ManyToManyField(Restaurant)
identifier = models.CharField(max_length=2048) # not unique at a DB level!
I want to ensure that for any badge, for a given restaurant, it must have a unique identifier. Here are the 4 ideas I have had:
idea #1: using unique_together -> Does not work with M2M fields as explained [in documentation]
(https://docs.djangoproject.com/en/2.1/ref/models/options/#unique-together)
idea #2: overriding save() method. Does not fully work with M2M, because when calling add or remove method, save() is not called.
idea #3: using an explicite through model, but since I'm live in production, I'd like to avoid taking risks on migrating important structures like theses. EDIT: after thinking of it, I don't see how it could help actually.
idea #4: Using a m2m_changedsignal to check the uniqueness anytime the add() method is called.
I ended up with the idea 4 and thought everything was OK, with this signal...
#receiver(m2m_changed, sender=Badge.restaurants.through)
def check_uniqueness(sender, **kwargs):
badge = kwargs.get('instance', None)
action = kwargs.get('action', None)
restaurant_pks = kwargs.get('pk_set', None)
if action == 'pre_add':
for restaurant_pk in restaurant_pks:
if Badge.objects.filter(identifier=badge.identifier).filter(restaurants=restaurant_pk):
raise BadgeNotUnique(MSG_BADGE_NOT_UNIQUE.format(
identifier=badge.identifier,
restaurant=Restaurant.objects.get(pk=restaurant_pk)
))
...until today when I found in my database lots of badges with the same identifier but no restaurant (should not happend at the business level)
I understood there is no atomicity between the save() and the signal.
Which means, if the user have an error about uniqueness when trying to create a badge, the badge is created but without restaurants linked to it.
So, the question is: how do you ensure at the model level that if the signal raises an Error, the save() is not commited?
Thanks!
I see two separate issues here:
You want to enforce a particular constraint on your data.
If the constraint is violated, you want to revert previous operations. In particular, you want to revert the creation of the Badge instance if any Restaurants are added in the same request that violate the constraint.
Regarding 1, your constraint is complicated because it involves multiple tables. That rules out database constraints (well, you could probably do it with a trigger) or simple model-level validation.
Your code above is apparently effective at preventing adds that violate the constraint. Note, though, that this constraint could also be violated if the identifier of an existing Badge is changed. Presumably you want to prevent that as well? If so, you need to add similar validation to Badge (e.g. in Badge.clean()).
Regarding 2, if you want the creation of the Badge instance to be reverted when the constraint is violated, you need to make sure the operations are wrapped in a database transaction. You haven't told us about the views where these objects area created (custom views? Django admin?) so it's hard to give specific advice. Essentially, you want to have this:
with transaction.atomic():
badge_instance.save()
badge_instance.add(...)
If you do, an exception thrown by your M2M pre_add signal will rollback the transaction, and you won't get the leftover Badge in your database. Note that admin views are run in a transaction by default, so this should already be happening if you're using the admin.
Another approach is to do the validation before the Badge object is created. See, for example, this answer about using ModelForm validation in the Django admin.
I'm afraid the correct way to achieve this really is by adapting the "through" model. But remember that at database level this "through" model already exists, and therefore your migration would simply be adding a unique constraint. It's a rather simple operation, and it doesn't really involve any real migrations, we do it often in production environments.
Take a look at this example, it pretty much sums everything you need.
You can specify your own connecting model for your M2M-models, and then add a unique_together constraint in the meta class of the membership model
class Badge(SafeDeleteModel):
...
restaurants = models.ManyToManyField(Restaurant, through='BadgeMembership')
class BadgeMembership(models.Model):
restaurant = models.ForeignKey(Restaurant, null=False, blank=False, on_delete=models.CASCADE)
badge = models.ForeignKey(Badge, null=False, blank=False, on_delete=models.CASCADE)
class Meta:
unique_together = (("restaurant", "badge"),)
This creates an object that's between the Badge and Restaurant which will be unique for each badge per restaurant.
Optional: Save check
You can also add a custom save function where you can manually check for uniqueness. In this way you can manually raise an exception.
class BadgeMembership(models.Model):
restaurant = models.ForeignKey(Restaurant, null=False, blank=False, on_delete=models.CASCADE)
badge = models.ForeignKey(Badge, null=False, blank=False, on_delete=models.CASCADE)
def save(self, *args, **kwargs):
# Only save if the object is new, updating won't do anything
if self.pk is None:
membershipCount = BadgeMembership.objects.filter(
Q(restaurant=self.restaurant) &
Q(badge=self.badge)
).count()
if membershipCount > 0:
raise BadgeNotUnique(...);
super(BadgeMembership, self).save(*args, **kwargs)

Checksums are calculated after the form validation: how to notify the users of an error

Django 1.11.6
I'm developing a document archive. So, I calculate checksums to control that documents are still present, that they are not corrupt etc.
Another side of this is that I can control whether a file has already been uploaded. It is important: no need to duplicate the files.
I upload files through Django admin. There is a form prepared.
But the problem is: form validators seems to be not very useful here.
At least I can't invent how to use a form validator here.
But post_save signal are useful: here we have a file already uploaded. We calculate the checksum of the file (using md5).
But if unique=True for the checksum, IntegrityError is risen.
It is Ok and predictable. But could you help me understand how to notify the user about this? Any method would suffice. It is just for our staff: no need to organize a brilliant html layout. But what is important is to show that the uploaded file coincides with an existing file with the following id.
Could you help me with this?
models.py
class FileChecksum(models.Model):
checksum = models.CharField(blank=True,
null=False,
unique=True,
max_length=255,
verbose_name=_("checksum"))
def __str__(self):
return self.checksum
class Image(models.Model):
file = models.ImageField(blank=False,
verbose_name=_("Image"),
max_length=255,
upload_to=get_sheet_path)
file_checksum = models.OneToOneField(FileChecksum,
blank=True,
null=True)
#property
def checksum(self):
pass
#checksum.setter
def checksum(self, new_checksum):
pass
signals.py
#receiver(post_save, sender=Image)
def save_file_checksum(sender, instance, **kwargs):
new_checksum = get_checksum(instance.file.path)
instance.checksum = new_checksum
admin.py
class ImageForm(FileMixin,
ModelForm):
model = Image
class ImageAdmin(admin.ModelAdmin):
form = ImageForm
admin.site.register(Image, ImageAdmin)
You have to calculate it before you save. That way your form can benefit from it and form validation will take care of bubbling up the error to the user. Decouple the get_checksum function away from the signals and calculate it as part of the form validation.
I had the same situation where I had a non-trivial hash calculation. So as soon as all the other basic validation stuff was done, I calculated the has with a function and if it clashed, made it part of the form (invariants) validation. If it didn't clash, it got used in about-to-be-created instance.
But I also, made it so that if you try to create an instance of the model, but don't have a hash set, a pre_save signal would create it. Useful for code paths where the item isn't coming in from a regular form.

Django AttributeError Modifying Field Without Tracking Change History

I am currently running a production Django webapp that holds historic item information using the SimpleHistory feature.
I have a navigation bar that displays all current revisions of all items to click on and view their separate pages. That being said, I wanted to have the ability to select which items to show/hide in the navigation bar by updating a boolean field on the item admin pages.
So, I modified the item models to have a field to do such:
class Item(models.Model)
field1 = models.CharField()
field2 = models.CharField()
...
hide_item = models.BooleanField('Item hidden:', default=True) #don't want history on this field
reason_for_change = models.CharField()
changed_by = models.ForeignKey(User, null=True)
accepted_by = models.ForeignKey(User, null=True)
accepted_date = models.DateTimeField()
history = HistoricalRecords()
def __unicode__(self):
return self.name
def save(self, *args, **kwargs):
super(Item, self).save(*args, **kwargs)
#property
def _history_user(self):
return self.changed_by
#_history_user.setter
self.changed_by = value
After making the migrations, this field showed up in the admin pages to my delight, but unfortunately I am not able to modify this field without receiving the following error:
AttributeError: can't set attribute
C:\Python27\lib\site-packages\simple_history\admin.py in save_model, line 151
151. obj._history_user = request.user
I think it might have to do with the fact that all changes to item field need to be tracked using the SimpleHistory feature, but for this particular field I don't want to track and store the history of its changes, I just want to be able to enable and disable at will in the admin page.
I also noticed, that if I make a new instance of an Item on the webapp and check the value of hide_item on the admin item page, it is False when it should be True by default. On the contrary, if I attempt to add a new Item instance within the admin page, hide_item is set to True by default as expected...
Right now I think my best solution might be to make another model that holds hide/display information for all items and keep it separate from the item models.
Wondering if anyone might now how to accomplish this.
Thanks
Might not be the most elegant way to do it, but I ended up making a separate model that stores show/hide information and syncs with the item to be displayed.
I did this using a BooleanField for the show/hide and a readonly OneToOne(Item) field to sync with the item I want to display.
Worked well.

How to delete an object in response to a m2m_changed signal in Django?

I have two models Image and Category related via a m2m relation (defined in Category). Images may be under several categories. The API allows to remove an image from a category. In response to that I need to remove the image when it has no categories.
I have the following:
#receiver(m2m_changed, sender=Category.images.through)
def delete_image_if_has_no_categories(sender, instance, action, reverse,
model, pk_set, **kwargs):
# we only watch reverse signal, because in other cases the images are
# being moved or copied, so don't have to be deleted.
if reverse and action == 'post_remove':
if not instance.categories.exists():
instance.delete()
I have placed several debug logs to check the code is being run. And it runs. But the images remain in the DB after the instance.delete().
I have the remove_from_category view inside a transaction.atomic, but it does not help.
Any ideas?
Update
The view call this method in our Image model:
def remove_from_category(self, category_id):
self.categories.remove(category_id)
The view is called via a REST API like this DELETE /category/<catid>/<image-id>.
The images field in the Category model is defined like this:
class Category(MPTTModel):
images = models.ManyToManyField(
'Image',
related_name='categories',
null=True, blank=True,
)
Would the MPTTModel be the culprit? I'm using django-mptt==0.6.0.
I guess u can call ur method to remove when the selected item that u want delete appeas on pre_clear method (django always save the last change in ur m2m field in the pre_clear, so if the attribute is there but didnt on ur post_add this obj is being deleted, so u can force trigger ur function there
check this answer mine
https://stackoverflow.com/a/39331735/6373505

Django, auto setting a field during a save, based on other Admin page inputs

I'm looking for the correct way to set full_name in SuperPerson instance.
class Suffix(models.Mode):
suffix = models.CharField(max_length=255)
def __unicode__(self):
return u'%s'%(self.suffix)
class Person(models.Model):
first_name= models.CharField(max_length=255)
last_name= models.CharField(max_length=255)
suffixes= models.ManyToManyField(Suffix, blank=True, null=True)
full_name= models.CharField(max_length=255)
class SuperPerson(Person):
ignore_this_field= model.CharField(max_length=255)
full_name is hidden from the user on the Admin page, and is to be automatically be updated based on the other inputs on Admin page when the Admin page save button is hit.
I have tried overriding save like this and variations:
def save(self, *args, **kwargs):
# Attempt to get data into the database so I can access it
super(SuperPerson,self).save(*args,**kwargs)
self.full_name = self.first_name + self.last_name
for suf in self.suffixes.all():
self.full_name+= suf.__unicode__()
# Now save the copy with full_name set as I wish
super(SuperPerson,self).save(*args,**kwargs)
This method works if I hit the save button in the Admin page twice, which is unacceptable for my use cases, seems like the new self.suffixes I have entered from the Admin page hasn't made it into database with the first super.save when I call self.suffixes.all().
I tried making full_name a property with decorator, but I also need to be able to filter Person and SuperPerson dbs using full_name, so that didn't work, unless someone can tell me how to filter with a property. Though I would rather have the value saved in the db.
I tried pre_save and post_save signals - neither worked.
#receiver(pre_save, sender=SuperPerson)
def set_full_name(sender, instance, **kwargs):
instance.full_name = instance.first_name + instance.last_name
for suf in instance.suffixes.all():
instance.full_name+= ', ' + suf.__unicode__()
Edit: - this has same effect - the instance suffixes do not match what was in the Admin page.
What is the right way to save full_name based on other inputs? Oh and I'm hoping to avoid messing with Admin forms.
ADDITIONAL INFORMATION:
It seems the problem is specifically that the suffixes field is not being updated by the time I'm trying to use it. I can update full_name to something else, like appending a string representing the current date, I just cannot access the suffixes.
Thanks, Dale
SOLUTION:
#receiver(m2m_changed, sender=Person.suffixes.through)
def set_full_name_after_ManyToMany_saved(sender, instance, **kwargs):
instance.full_name = instance.first_name + instance.last_name
for suf in instance.suffixes.all():
instance.full_name+= ', ' + suf.__unicode__()
print 'Saving As', instance.full_name
instance.save()
I'm curious why I had to use Person.suffixes.through instead of SuperPerson, Suffixes or Person.suffixes - is there good documentation on this somewhere, I couldn't find it. And, it runs the code 4 times, but at least ends up with the correct result in the final run.
Many thanks to Danny and burhan
The problem is your m2m relationship with Suffix, or rather the way that django admin saves m2m relationships.
A pretty good explanation is in this answer to Why is adding site to an object doesn't seem to work in a save() override in the Django admin?
When you save a model via admin forms it's not an atomic transaction. The main object gets saved first (to make sure it has a PK), then the M2M is cleared and the new values set to whatever came out of the form.
post_save() is actually still too early. That's where the instance was saved, not its relationships.
You need to connect to the m2m_changed signal: https://docs.djangoproject.com/en/dev/ref/signals/#m2m-changed
or wait for Django 1.4 where ModelAdmin gives you a "when all is done" signal:
https://code.djangoproject.com/ticket/16115