How to delete many to many when unassociated in Django? - python

We have a tagging system for users to filter their files by defined tags.
Here's how the models are set up:
class Tags(models.Model):
name = models.CharField(max_length=100)
user = models.ForeignKey(User)
class Files(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=100)
tags = models.ManyToManyField(Tags, null=True, blank=True)
Now, because tags are not required, when we remove tags from a file they don't get deleted. This leaves a bunch of tags saved on our database and we want to clean them up.
I've tried redefining the save method on the Files model, and the clean method.
I've tried connecting an m2m_changed signal on the Files model: https://docs.djangoproject.com/en/dev/ref/signals/#m2m-changed
Last thing I tried was a pre_save signal: https://docs.djangoproject.com/en/dev/ref/signals/#pre-save
I was planning to iterate over the tags and delete the ones with empty files_set, but using these methods I can't reliably figure that out (i.e. I end up removing tags that aren't associated but are about to be associated (because m2m_changed fires several times with different actions)).
Here's what I thought would work:
def handle_tags (sender, instance, *args, **kwargs) :
action = kwargs.get('action')
if action == 'post_clear':
# search through users tags... I guess?
tags = Tags.objects.filter(user=instance.user)
for tag in tags:
if not tag.files_set.exists():
tag.delete()
return
m2m_changed.connect(handle_tags, sender=Files.tags.through)
But, as I said, it will delete a tag before it is added (and if it's added, we obviously don't want to delete it).

You we're on the right track when using the m2m_changed signal.
Your problem is that when responding to the post_clear signal the tags have already been deleted so you won't be able to access them like that.
You actually need to dispatch your method before the tags are deleted, which means handling the pre_clear signal.
Something like this:
#receiver(m2m_changed, sender=Files.tags.through)
def handle_tags(sender, **kwargs):
action = kwargs['action']
if action == "pre_clear":
tags_pk_set = kwargs['instance'].tags.values_list('pk')
elif action == "pre_remove":
tags_pk_set = kwargs.get('pk_set')
else:
return
# I'm using Count() just so I don't have to iterate over the tag objects
annotated_tags = Tags.objects.annotate(n_files=Count('files'))
unreferenced = annotated_tags.filter(pk__in=tags_pk_set).filter(n_files=1)
unreferenced.delete()
I've also added the handling of the pre_remove signal in which you can use the pk_set argument to get the actual tags that will be removed.
UPDATE
Of course the previous listener won't delete the unreferenced tags when deleting the files, since it's only handling the pre_clear and pre_remove signals from the Tags model. In order to do what you want, you should also handle the pre_delete signal of the Files model.
In the code below I've added an utility function remove_tags_if_orphan, a slightly modified version of handle_tags and a new handler called handle_file_deletion to remove the tags which will become unreferenced once the File is deleted.
def remove_tags_if_orphan(tags_pk_set):
"""Removes tags in tags_pk_set if they're associated with only 1 File."""
annotated_tags = Tags.objects.annotate(n_files=Count('files'))
unreferenced = annotated_tags.filter(pk__in=tags_pk_set).filter(n_files=1)
unreferenced.delete()
# This will clean unassociated Tags when clearing or removing Tags from a File
#receiver(m2m_changed, sender=Files.tags.through)
def handle_tags(sender, **kwargs):
action = kwargs['action']
if action == "pre_clear":
tags_pk_set = kwargs['instance'].tags.values_list('pk')
elif action == "pre_remove":
tags_pk_set = kwargs.get('pk_set')
else:
return
remove_tags_if_orphan(tags_pk_set)
# This will clean unassociated Tags when deleting/bulk-deleting File objects
#receiver(pre_delete, sender=Files)
def handle_file_deletion(sender, **kwargs):
associated_tags = kwargs['instance'].tags.values_list('pk')
remove_tags_if_orphan(associated_tags)
Hope this clears things up.

Just to sum up with hopefully a cleaner answer:
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
class Tags(models.Model):
name = models.CharField(max_length=100)
user = models.ForeignKey(User)
class Files(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=100)
tags = models.ManyToManyField(Tags, null=True, blank=True)
#receiver(m2m_changed, sender=Files.tags.through)
def delete_orphean_dateranges(sender, **kwargs):
if kwargs['action'] == 'post_remove':
Tags.objects.filter(pk__in=kwargs['pk_set'], files_set=None).delete()
post_remove ensure that the callback is fired when a Tag was disassociated from a File

I think you go deeper than it required. Just define related_name for Tag, and process post_save signal from File.
class Files(models.Model):
user = models.ForeignKey(User)
name = models.CharField(max_length=100)
tags = models.ManyToManyField(Tags, null=True, blank=True, related_name='files')
def clean_empty_tags(sender, instance, *args, **kwargs):
Tags.objects.filter(user=instance.user, files=None).delete()
post_save.connect(clean_empty_tags, sender=Files)

Related

How to filter a foreign key field in django?

I'm working on a basic social media project ideally where user gets to join a group and post posts in it.
Here's the models i have created:
class Group(models.Model):
name = models.CharField(max_length=255,unique=True)
slug = models.SlugField(allow_unicode=True,unique=True)
description = models.TextField(blank=True,default='')
description_html = models.TextField(editable=False,default='',blank=True)
members = models.ManyToManyField(User,through="GroupMember")
class GroupMember(models.Model):
group = models.ForeignKey(Group,related_name='memberships',on_delete=models.CASCADE)
user = models.ForeignKey(User,related_name='user_groups',on_delete=models.CASCADE)
class Post(models.Model):
user = models.ForeignKey(User,related_name='posts',on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now=True)
message = models.TextField()
message_html = models.TextField(editable=False)
group =
models.ForeignKey(Group,related_name='posts',null=True,blank=True,on_delete=models.CASCADE)
This works fine. Problem is when i create an instance of Post. This design lets me post in any/every group. But what i want is, to let the user post only in group(s) which he's/she's a member of. I don't know how to implement this. Help me out guys.
One quick and simple way would be to overwrite .save() and disallow adding items where user and group is not in GroupMember:
class Post(models.Model):
...
def save(self, *args, **kwargs):
if not GroupMember.objects.filter(user=user, group=group).exists():
raise Exception()
super().save(*args, **kwargs)
Depending on your project and code structure, you could also use a pre_save() signal to prevent writing such posts.

Django many to many relation not saving

Update:
For anyone curious, I figured out what and why and how to fix it.
In my view I had:
fields = ['html', 'tags', 'title', 'text', 'taken_date', 'image']
And am using {{ form.as_p }} in my template. Apparently once that gets posted from the form it really, really doesn't want anything else touching the form fields that wasn't already in the form.
So I took out the 'tags' field from my view and it works.
Thanks to everyone that responded.
Original Question:
Using Django 2.0.1 and PostgreSQL 9.2.18
I'm writing a simple photogallery application. In it I have a photo object and PhotoTag object. The photo can have many tags and the tags can be associated with many photos, thus it needing to be a ManyToManyField.
Upon save of the submitted photo, a post_save receiver calls functions to make thumbnails (which work fine) and a function to update tags.
The photo gets saved fine, update_tags gets called fine, tags get read from the photo fine, tags get saved into PhotoTag fine. But the manytomany table tying the two together does not get the new rows inserted. Unless the code exits abnormally during either the update_tags function or the post_save receiver function, thumbs after update_tags is called.
I've even tried using a connection.cursor to write directly into the m2m table and it has the same behavior.
If I try to call save() on the Photo object again, I just get into an infinite loop due to the post_save signal.
I'm baffled as to what is going on. Any clues?
# models.py
def update_tags(instance):
tags = get_tags(instance.image)
# Set initial values
pt = []
tagid = ''
photoid = instance.id
# Loop through tag list and insert into PhotoTag and m2m relation
for x in range(0, len(tags)):
# Make sure this tag doesn't already exist
if PhotoTag.objects.filter(tag_text=tags[x]).count() == 0:
pt = PhotoTag.objects.create(tag_text=tags[x])
tagid = PhotoTag.objects.latest('id').id
instance.tags.add(pt)
else:
# Only working with new tags right now
pass
return
class Photo(models.Model):
author = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE)
title = models.CharField(max_length=200, null=True, blank=True)
text = models.TextField(null=True, blank=True)
html = models.BooleanField(default=False)
filename = models.CharField(default='', max_length=100, blank=True,
null=True)
image = models.ImageField(upload_to=upload_path)
location = models.CharField(max_length=100, blank=True, null=True)
entry_date = models.DateTimeField(default=timezone.now)
taken_date = models.DateTimeField(blank=True, null=True)
tags = models.ManyToManyField(PhotoTag, blank=True)
#receiver(post_save, sender=Photo)
def thumbs(sender, instance, **kwargs):
"""
Upon photo save, create thumbnails and then
update PhotoTag and m2m with any Exif/XMP tags
in the photo.
"""
mk_thumb(instance.image, 'mid')
mk_thumb(instance.image, 'th')
mk_thumb(instance.image, 'sm')
update_tags(instance)
return
-------------
From views.py
-------------
class PhotoCreate(LoginRequiredMixin, CreateView):
model = Photo
template_name = 'photogallery/photo_edit.html'
fields = ['html', 'tags', 'title', 'text', 'taken_date', 'image']
def get_initial(self):
self.initial = {'entry_date': timezone.now()}
return self.initial
def form_valid(self, form):
form.instance.author = self.request.user
return super(PhotoCreate, self).form_valid(form)
Update:
def save(self, mkthumb='', *args, **kwargs):
super(Photo, self).save(*args, **kwargs)
if mkthumb != "thumbs":
self.mk_thumb(self.image, 'mid')
self.mk_thumb(self.image, 'th')
self.mk_thumb(self.image, 'sm')
self.update_tags()
mkthumb = "thumbs"
return
I had a similar issue where I was trying to add a group when a user instance was saved.
The anwer why this is happening is at the docs and more explicitly (using code) at this ticket.
When saving a ModelForm() (hitting save in the admin), first an instance of the object is saved, then all its signals are triggered etc. The third step is to save all m2m relations using ModelForm().cleaned_data. If ModelForm().cleaned_data['tags'] is None, all the relations created from your signal, will be deleted.
A hackish solution, is to use a post_save signal with transaction.on_commit() which will execute the relevant code after the existing transaction (which includes the procedure of saving all m2m relations) is committed to the database.
def on_transaction_commit(func):
''' Create the decorator '''
def inner(*args, **kwargs):
transaction.on_commit(lambda: func(*args, **kwargs))
return inner
#receiver(post_save, sender=Photo)
#on_transaction_commit
def tags(instance, raw, **kwargs):
"""
Create the relevant tags after the transaction
of instance is committed, unless the database is
populated with fixtures.
"""
if not raw:
update_tags(instance)
A more sound solution if your many to many relation has not blank=True is to use a m2m_changed() signal, as explained in this post or the before mentioned ticket.
The best of all, is to ditch the signals and override the ModelForm().clean() method for the case where a ModelForm() is used, and also override the Model().save() method in case the model is directly saved.
A ModelForm().instance.my_flag will be useful so you can check for an existing Model().my_flag in Model().save() to avoid accessing twice the database.
override your save method like
def save(self, *args, **kwargs):
tags = get_tags(self.image)
# Set initial values
pt = None
# Loop through tag list and insert into PhotoTag and m2m relation
for x in range(0, len(tags)):
# Make sure this tag doesn't already exist
if PhotoTag.objects.filter(tag_text=tags[x]).count() == 0:
pt = PhotoTag.objects.create(tag_text=tags[x])
self.tags.add(pt)
else:
# Only working with new tags right now
pass
super(Photo, self).save(*args, **kwargs)

How to add custom function to admin forms?

I would like to implement a function that updates quantity in LibraryBook each time the admin adds a book in SingleBook on the admin site. I have been searching for means to do so but to no avail. Any pointers including links to documentation would be very much appreciated.
Here is my code:
#models.py
class LibraryBook(models.Model):
book_title = models.CharField(max_length=100, blank=False)
book_author_id = models.ForeignKey(BookAuthors, on_delete=models.CASCADE)
category = models.ForeignKey(BookCategory, on_delete=models.CASCADE)
quantity = models.IntegerField(blank=False, default=0)
number_borrowed = models.IntegerField(default=0)
def __unicode__(self):
return unicode(self.book_title)
class SingleBook(models.Model):
serial_number = models.CharField(primary_key=True , max_length=150, blank=False)
book_id = models.ForeignKey(LibraryBook, on_delete=models.CASCADE)
is_available_returned = models.BooleanField(default=True)
is_borrowed = models.BooleanField(default=False)
def __unicode__(self):
return unicode(self.book_id)
#admin.py
class SingleBookAdmin(admin.ModelAdmin):
list_display = ('book_id', 'serial_number')
class LibraryBookAdmin(admin.ModelAdmin):
list_display = ('book_title', 'book_author_id', 'quantity')
search_fields = ('book_title', 'book_author_id')
fields = ('book_title', 'book_author_id', 'quantity')
PS: I have omitted the import and admin.site.register code
Django==1.9.8
django-material==0.8.0
django-model-utils==2.5.1
psycopg2==2.6.2
wheel==0.24.0
override save_model
If you only want to make the changes when an admin updates a record, the best way is to override the save_model method in ModelAdmin
The save_model method is given the HttpRequest, a model instance, a
ModelForm instance and a boolean value based on whether it is adding
or changing the object. Here you can do any pre- or post-save
operations.
class SingleBookAdmin(admin.ModelAdmin):
list_display = ('book_id', 'serial_number')
def save_model(self, request, obj, form, change):
admin.ModelAdmin.save_model(self, request, obj, form, change)
if obj.is_borrowed:
do something to obj.book_id.quantity
else:
do something to obj.book_id.quantity
post_save signal
from django.dispatch.dispatcher import receiver
from django.db.models.signals import post_save
#receiver(post_save, sender=SingleBook)
def user_updated(sender,instance, **kwargs):
''' Fired when a SingleBook is updated or saved
we will use the opporunity to change quantity'''
# your logic here
Other pointers
If on the other hand, you wanted to make changes based on all user actions, catching the post_save signal is the way to go. In either case, you might want to override the from_db method in the model to keep track of which fields have changed.
You might also want to change quantity and number_borrowed to IntegerFields (unless you are only using sqlite in which case it doesn't matter)
Also book_author_id should probably be book_author and book_id should probably be book (this is not a rule, just a convention to avoid the ugly book_id_id reference)
Use signals. Just attach post_save signal to SingleBook model and update according LibraryBook in it. post_save signal takes created argument, so you can determine if book is newly created or edited and apply your action based on that.
Also attach post_delete signal to decrease counter when SingleBook is removed.
To avoid race conditions (when 2 admins are adding books at the same time), I'm suggesting use of queryset update method together with F on changing LibraryBook counter, example:
LibraryBook.objects.filter(id=single_book.book_id_id).update(quantity=F('quantity') + 1)
Doing it that way will ensure that actual math operation will be performed on database level.

How to handle state / conditionally modify fields within Django forms

I have a django model representing a task. This task will go through multiple states from 'DRAFT' to 'COMPLETION' and during that time, various fields will change from requiring user input to explicitly refusing it. For example, you can't change the date a task was completed while it's in draft. Additionally i want different links to appear on the page based on the state (i.e. if it's in checking I want the submit button to say 'Complete this task')
I originally planned for the status to be a model in it's own right but could not work out a way - beyond hard coding - that would bring any value to those models so opted for an explicit list instead. (Any better approaches here would be appreciated).
The key problem I have is how to manage these fields and their states. It seems like the easiest would be to have conditional statements in the template like {% if task.status = 'ACCEPTED' %} but that seems like putting an awful lot of business logic into a template. I also get the impression that disabling a field once you're already in a template is much harder than the form.
My current approach is to use the view to manage these states but that seems messy and doesn't (for me) solve how to change link names and the like in the template.
if task.status = Task.ACCEPTED:
form.fields['datereceived'].disabled = True
if task.status = Task.COMPLETED:
...
Is the view the place to manage these and is there a more pythonic/djangonic to manage these without overloading the template?
Sample code so excuse bugs:
Model
class Task(models.Model):
STATUS_CHOICES = (
(DRAFT, DRAFT),
(ALLOCATED, ALLOCATED),
(ACCEPTED, ACCEPTED),
(CHECKING, CHECKING),
(COMPLETED, COMPLETED),
(WITHDRAWN, WITHDRAWN),
(ON_HOLD, ON_HOLD),
)
status = models.CharField(max_length=20, choices=STATUS_CHOICES,default=DRAFT)
datereceived = models.DateField(blank=True, null=True)
dateworked = models.DateField(blank=True, null=True)
datechecked = models.DateField(blank=True, null=True)
datecompleted = models.DateField(blank=True, null=True)
datedue = models.DateField(blank=True, null=True)
Modelform
class TaskForm(forms.ModelForm):
class Meta:
model = Task
#fields = All fields listed individually but hard to pick out for sample
widgets = {
'datereceived': forms.DateInput(attrs={'class':'datepicker'}),
'datedue': forms.DateInput(attrs={'class':'datepicker'}),
'datecompleted': forms.DateInput(attrs={'class':'datepicker'}),
}
Try putting the logic in the form instantiation code as so:
class TaskForm(forms.ModelForm):
class Meta:
model = Task
def handle_state(self, *args, **kwargs):
task = getattr(self, 'instance', None)
if task:
if task.status = Task.ACCEPTED:
self.fields['datereceived'].disabled = True
elif task.status = Task.COMPLETED:
...
def __init__(self, *args, **kwargs):
super(TaskForm, self).__init__(*args, **kwargs)
self.handle_state(*args, **kwargs)
you can use finite state machine. django-fsm to handle states of your task. In this you can define the source and target state of every transition. for reference you can see this example. https://distillery.com/blog/building-for-flexibility-using-finite-state-machines-in-django/

Django Admin filter by function / filter only by first object in reverse foreign key lookup

I am trying to build a filter by function in django. From what I've learned by googling this is quite hard to achieve.
So here is my code:
class TrackingEventType(models.Model):
name = models.CharField(blank=False, null=False, max_length=255)
class TrackingEvent(models.Model):
datetime = models.DateTimeField(blank=False, null=False, default=datetime.now, verbose_name="Zeitpunkt")
event_type = models.ForeignKey(TrackingEventType, help_text="Art des Events")
tracking = models.ForeignKey('Tracking')
class Meta:
ordering = ['-datetime']
class Tracking(models.Model):
tracking_no = models.CharField(blank=False, null=False, max_length=10, unique=True, verbose_name="Tracking Nummer")
def get_last_event(self):
"""
Todo: return the latest event.
"""
return TrackingEvent.objects.filter(tracking=self.id).first()
get_last_event.short_description = 'Last event'
class Meta:
ordering = ['-tracking_no']
My goal is to make it possible to filter Tracking objects by their last events type name. Displaying the result of the funtion in django admin is easy, but adding a corresponding filter isn't.
My idea was also to try to build a filter something like:
trackingevent__set__first__event_type__name
But yeah, that would be too easy :)
Any inputs are welcome.
As you've discovered it isn't trivial to filter in that manner. If you are accessing that information regularly it is probably also not very efficient either.
I would suggest that you store a reference to the latest tracking event in the Tracking model itself:
class Tracking(models.Model)
# ...
last_event = models.ForeignKey(TrackingEvent, null=True)
You would then use signals to update this reference whenever a new tracking event is created. Something along the lines of:
from django.db.models.signals import post_save
from django.dispatch import receiver
#receiver(post_save, sender=TrackingEvent)
def update_latest_tracking_event(sender, instance, created, **kwargs):
# Is this a new event?
if created:
# If yes, then update the Tracking reference
tracking = instance.tracking
tracking.last_event = instance
tracking.save()
(Please read the documentation on where to put this code).
Once all this is in place it becomes easy to filter based on the last tracking event type:
# I'm just guess what event types you have...
cancellation = TrackingEventType.objects.get(name='cancel')
Tracking.objects.filter(last_event__event_type=cancellation)

Categories