Django manytomany signals? [duplicate] - python

This question already has answers here:
django manytomanyfield .add() method
(2 answers)
Closed 4 years ago.
Let's say I have such model
class Event(models.Model)
users_count = models.IntegerField(default=0)
users = models.ManyToManyField(User)
How would you recommend to update users_count value if Event add/delete some users ?

If possible in your case, you could introduce Participation model which would join Event and User:
class Participation(models.Model):
user = models.ForeignKey(User)
event = models.ForeignKey(Event)
class Event(models.Model):
users = models.ManyToManyField(User, through='Participation')
And handle pre_save signal sent by Participation to update instance.event counts. It would simplify handling of m2m significantly. And in most cases, it turns out later on that some logic and data fits best in the middle model. If that's not your case, try a custom solution (you should not have many code paths adding Users to Events anyway).

I have a fixed the problem using the built-in signal django.db.models.signals.m2m_changed.
In my case, I have to update a related instance of another model each time the ManyToMany change and as you know, overriding Model.save() does not works.
Here my (french & simplified) models :
class BaseSupport(EuidModel):
nom = models.CharField(max_length=100, blank=True)
periodicite = models.CharField('périodicité', max_length=16,
choices=PERIODICITE_CHOICES)
jours_de_parution_semaine = models.ManyToManyField('JourDeLaSemaine', blank=True)
class Meta:
abstract = True
class Support(BaseSupport):
pass
def save(self, *args, **kwargs):
create_cahier_principal = False
if not self.pk:
create_cahier_principal = True
super(Support, self).save(*args, **kwargs)
if create_cahier_principal:
c = Cahier.objects.create(support=self,ordre=1, numero=1,
nom=self.nom, nom_court=self.nom_court,
euid=self.euid, periodicite=self.periodicite)
class Cahier(BaseSupport):
"""Ex : Cahier Saumon du Figaro Quotidien."""
support = models.ForeignKey('Support', related_name='cahiers')
ordre = models.PositiveSmallIntegerField()
numero = models.PositiveSmallIntegerField(u'numéro', null=True, blank=True)
def sync_m2m_cahier_principal(sender, **kwargs):
if kwargs['action'] not in ('post_add', 'post_clear', 'post_remove'):
return
support = kwargs['instance']
cahier_principal = support.cahiers.get(euid=support.euid)
cahier_principal.jours_de_parution_semaine.clear()
if kwargs['action'] == 'post_clear':
return
for jour in support.jours_de_parution_semaine.all():
cahier_principal.jours_de_parution_semaine.add(jour)
m2m_changed.connect(sync_m2m_cahier_principal,
sender=Support.jours_de_parution_semaine.through)
Maybe this solution is a far from ideal but I hate monkey-patching Django !

Overriding save() may not help you because the update to the M2M is not atomic and happens after the save of the Event instance (I haven't studied the delete() semantics but they are probably similar). This was discussed in another thread.
People are talking about and working on this problem. The best solution I have seen so far is this MonkeyPatch by gregoirecachet. I don't know if this will make it into 1.2 or not. Probably not since the Release Manager (James Bennett) is trying to get people to respect the freeze dates (a major one just passed).

Related

ManyToMany field selects all model instances by default / changing default get_queryset

I changed my .all method so it would select only instances with published=True:
class EventManager(models.Manager):
def all(self, *args, **kwargs):
return super().get_queryset().filter(published=True, *args, **kwargs)
This is related to the problem model fields:
class Event(models.Model):
related_events = models.ManyToManyField('self', blank=True, related_name='related')
published = models.BooleanField(default=False)
objects = EventManager()
As a result ManyToManyField ends up selecting all the Event instances.
What would you suggest me to do in order to save the published functionality and be able to manually add related events? Thank you.
As far as I know, Django does not use Model.objects as manager, but the Model._basemanager, which normally should return all objects.
You can use limit_choices_to [Django-doc] here to limit the choices of the many-to-many field, like:
from django.db.models import Q
class Event(models.Model):
related_events = models.ManyToManyField(
'self',
limit_choices_to=Q(published=True)
blank=False,
related_name='related'
)
published = models.BooleanField(default=False)
objects = EventManager()
You probably also want to remove blank=True, since that means that by default, you make the field not to show op in the forms. So if you want to manually edit the related events, then.blank=False.
Furthermore a ManyToManyField to 'self' is by default symmatrical. This thus means that if event1 is in the related_events of event2, then event2 is in related_events of event1 as well. If you do not want that, you might want to add symmetrical=False [Django-doc].
Note that there are still some scenario's where non-published events might end up in the related events. For example by adding a published event to the related events, and then "unpublish" it.
As for the manager, I think you better patch the get_queryset method:
class EventManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(published=True)
Otherwise there are lot of ways to "circumvent" the filtering. For example: Event.objects.filter(id__gt=-1) would still give me all Events, but since I did not call .all(), this would thus not filter on published.
In the ModelAdmin, you could aim to specify the queryset for this ManyToManyField with:
class EventAdmin(admin.ModelAdmin):
def get_field_queryset(self, db, db_field, request):
if db_field.name == 'event_dates':
return db_field.remote_field.model.base_manager.all()
else:
super(EventAdmin, self).get_field_queryset(db, db_field, request)
That's what I ended up doing in order to show only published events in my html and show all the events (published and unpublished) in admin dashboard.
class EventManager(models.Manager):
"""
changing get_queryset() to show only published events.
if all is set to True it will show both published and unpublished
if False, which is default it will show only published ones
"""
def get_queryset(self, all=False):
if not all:
return super().get_queryset().filter(published=True)
else:
return super().get_queryset()
class Event(models.Model):
related_events = models.ManyToManyField('self', blank=True, related_name='related')
published = models.BooleanField(default=False)
objects = EventManager()
And in ModelAdmin I call get_queryset with all set to True, otherwise I won't be able to see unpublished ones.
class EventAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return Event.objects.get_queryset(all=True)
I could not simply change my model's all method because it would mess with my ManyToManyField by adding all the model instances to to it. So I did all this.

One table can not be foreign key to others except one table

For example
class Room(models.Model):
visitor = models.ForeignKey(Visitor)
number = models.PositiveIntegerField()
capacity = models.ForeignKey(Capacity, on_delete=models.PROTECT)
floor = models.ForeignKey(Floor, on_delete=models.PROTECT)
price = models.PositiveIntegerField()
is_premium = models.BooleanField(default=False)
is_vip = models.BooleanField(default=False)
expiry_date = models.DateTimeField()
class Meta:
unique_together = ('')
def __str__(self):
return '№{0}'.format(self.number)
class Reserved(models.Model):
room = models.ForeignKey(Room)
begin_date = models.DateTimeField()
def __str__(self):
return 'Reserved Room {0}'.format(self.room)
class Busy(models.Model):
room = models.ForeignKey(Room)
Table Room can not be connected to Tables Reserved and Busy at the same time. Room should be reserved or busy. Is there way put validation for this?
I tried to use unique_together but if for fields of table
Thanks
There is no way to enforce this at DB level nor a simple way to do it in Django level. With your structure you should add some validation before creating (or modifying) both Busy and Reserved. Something like:
class Busy(models.Model):
room = models.ForeignKey(Room)
def __save__(self, *args, **kwargs):
if Reserved.object.filter(room=self.room).exists():
raise RuntimeError('Trying to make a reserved room busy.')
super(Busy, self).__save__(*args, **kwargs)
If you are creating Busy and Reserved objects concurrently it's subject to race condition. I suggest to move room state into Room model itself and add some helper functions (something like in room_manager.py beside models.py) to change its state and make sure related models are created/modified in a consistent manner.
The only way to ensure at the database level that you have one single "status" at a given time for a room is to have your relationship the other way round - with Room have a foreign key on whatever represents the status. To make this work you'll need to use either some form of model inheritance or django's "generic" relationship (which can be handy sometimes but are really not SQL-friendly).
Here's an example using the very simplest form of "model inheritance" (which is not actually inheritance at all):
class Status(models.Model):
BUSY = 1
RESERVED = 2
TYPES = (
(BUSY,"busy"),
(RESERVED,"reserved")
)
type = models.CharField("Status type", max_length=10, choices=TYPES)
# only used for reservations
begin_date = models.DateTimeField(null=True, blank=True)
def save(self, *args, **kw):
# TODO : this should belong to `full_clean()`,
# cf the FineManual model's validation
if self.type == self.RESERVED and not self.begin_date:
raise ValueError("RESERVED statuses need a begin_date")
super(Status, self).save(*args, **kw)
class Room(models.Model):
status = models.ForeignKey(Status)
Note that this allows for a same status to be used for multiple rooms at the same time, which might be a problem too. Using a OneToOneField field instead might help on the Django side but will still be treated as a foreign key at the database level.

Django Model for mandatory/excatly one cross table relations

I am trying to find guidance on how to build a model representing the following:
We have IT environments (consisting of multiple components, like webservers, databases, etc.)
SLAs are not specific to one environment, it's more a set of general contracts a concrete environment references to (aka should be a separate table)
each environment must have at least one or more SLA(s) associated
From all SLAs associated to an environment exactly one must have the state "effective"
I implemented a model sufficiently reflecting the first two points (at least, I think so), but especially the last point seems to be cumbersome.
Sufficiently meaning, using this implementation, the relation using the cross table is optional, not mandatory. This is OK currently, but not in the long run.
class Environment(models.Model):
fullname = models.CharField(max_length=45)
...
sla = models.ManyToManyField(SLA, through='EnvironmentSLA')
creation_date = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = (('fullname', 'projectid', 'regionid', 'account'),)
class SLA(models.Model):
description = models.CharField(max_length=255)
reaction_time = models.CharField(max_length=45)
service_level = models.CharField(max_length=45)
creation_date = models.DateTimeField(auto_now_add=True)
class EnvironmentSLA(models.Model):
PLANNED = 'pl'
EFFECTIVE = 'ef'
DEPRECATED = 'dp'
SLA_STATE = (
( PLANNED, 'planned' ),
( EFFECTIVE, 'effective'),
( DEPRECATED, 'deprecated'),
)
environment = models.ForeignKey('Environment', on_delete=models.CASCADE)
sla = models.ForeignKey(SLA, on_delete=models.CASCADE)
state = models.CharField(max_length=2, choices=SLA_STATE, default=PLANNED)
So my questions are:
Am I generally on the right track, but capturing the last constraint is not possible solely focusing on the model?
What would be an elegant way?
We implemented a save() method for the model EnvironmentSLA that does the following:
check if the object to be saved set state == EFFECTIVE
if not just save the object
try to get currently effective SLA
change the state SLA of the currently effective SLA to DEPRECATED
save the object
The save()-function looks like this:
class EnvironmentSLA(models.Model):
[...]
def save(self, *args, **kwargs):
if self.state != self.EFFECTIVE:
super(EnvironmentSLA, self).save(*args, **kwargs)
return
try:
effective_sla = EnvironmentSLA.objects.filter(environment=self.environment, state=self.EFFECTIVE).get()
except Exception as e:
effective_sla = None
if effective_sla:
effective_sla.state = self.DEPRECATED
effective_sla.save()
super(EnvironmentSLA, self).save(*args, **kwargs)
This way we don't loose the defined SLAs but always have only one active SLA.

Django: Overriding the Save (Either Model or Admin Section)

I've been playing around with Django recently, however I'm stuck on how to approach this problem. I have a 'Person' model which has a one to many relationship with a 'Voucher' Model. Now the person has a quota and each time a Voucher is generated, the quota will decrease by one. What I'm stuck on is doing this through the save method. So how does one do this?
Below are my models:
class Person(AbstractDetail):
# Note: Model to maintain information about Person
vc = models.IntegerField(default = 3, verbose_name = 'Vouchers',
null = False, validators = [ validate_voucher ])
class Voucher(models.Model):
# Note: Model to maintain information about Voucher
vc = models.CharField (max_length = 25, verbose_name = 'Voucher ID',
help_text = 'Voucher Identifier')
ps = models.ForeignKey(Person, on_delete = models.CASCADE,
verbose_name = 'Person')
Don't do it in save(), it's easily messed up with django. Try to use django signal post_save():
from django.db.models.signals import post_save
#receiver(post_save, sender=Voucher)
def decrease_quota(sender, instance, created, *args, **kwargs):
if created:
instance.vc -= 1
instance.save()
Check django doc for django signals.
I'd take a slight performance hit in favour of avoiding the duplication of the voucher count information (call me old-fashioned).
In stead of saving the voucher count in Person.vc, I'd evaluate the voucher count when needed by creating a function for that in the Voucher object manager:
def get_voucher_count(self, person):
return self.filter(ps=person).count()
OK, I manage to work it out (with the help of Shang Wang & Ytsen de Boer answer as well, which I'm grateful for). The way which I did it was to create an instance of the object Person and take away from it there.
Fortunately enough, I quick time check the doc's which Shang just showed me (and look way deeper as well). Below is the way to do it:
#receiver(post_save, sender = Voucher, dispatch_uid = "voucher_identifier")
def decrease_quota(sender, instance, **kwargs):
obj = Person.objects.get(pk = instance.ps.id)
if obj.vc < 4 and obj.vc > 0:
obj.vc = obj.vc - 1
obj.save()
Of course I have to do abit more to it (simple validations and stuff like that), but this how I needed it.

Integer Field Math in Django

from django.db import models
from django.contrib.auth.models import User
class Product(models.Model):
name = models.CharField(max_length = 127)
description = models.TextField()
code = models.CharField(max_length = 30)
lot_no = models.CharField(max_length = 30)
inventory = models.IntegerField()
commited = models.IntegerField()
available = models.IntegerField()
reorder = models.IntegerField()
created_date = models.DateField(auto_now_add = True)
comment_user = models.ForeignKey(User, null=True)
comment_txt = models.TextField()
def __unicode__(self):
return self.code + " - " + self.name + " - " + self.lot_no + " - " + str(self.created_date)
#property
def available(self):
return self.inventory - self.commited
Im trying to have available calculated by (inventory - self) when a person enters in the data for inventory and commited in django admin template. But I'm not sure how.
Thanks,
Jon
Try overriding the save method on the model:
def save(self, *args, **kwargs):
"update number available on save"
self.available = self.inventory - self.committed
super(Product, self).save(*args, **kwargs)
You could also put logic in there that would do something if self.available became negative.
It seems like you may have two problems; the overlapping available property+field and availability not showing up as you expect in the admin.
Choose one way (property or field) to represent the availability and go with it. Don and Seth have shown a way to do it using a field and Daniel and Ignacio have suggested going with a property.
Since you really want this field to show up in the admin just go with the field; give it a helpful help_text="...", remove the #property, and override save().
class Product(models.Model):
# ...
availability = models.IntegerField(help_text="(updated on save)")
# Use Seth's save()
def save(self, *args, **kwargs):
self.availability = self.inventory - self.commited
super(Product, self).save(*args, **kwargs)
This is not the best way to do things in terms of normalized data but it will probably be the simplest solution to your current problem.
If you are using trunk instead of Django-1.1.1, you can also use readonly_fields in the admin.
Don is correct that you have the name available duplicated, because you have both a field and a property. Drop the field.
This is what I said when I gave you the solution to this problem in your original question - I explicitly said "drop the existing 'available' field". Following half a solution is never going to work.
However I fundamentally disagree with Seth and Don who recommend overriding the save() function to calculate this value. That is a totally unnecessary duplication of data. The property is the correct solution.
You've bound both a Django field and a vanilla Python property to the same name on your model. One of these attributes is masking the other, which is why you're getting unexpected behavior in the Django admin. This is almost certainly not what you want.
Override the save method and remove your def available property entirely.
Ignacio is trying to help you keep your data normalized by not storing information in your DB twice. It's a good practice to follow in the general case, but there are many times when you want to store calculated values in your DB. This seems like a practical use of data duplication.
The property is actually removing the available integer field off the admin page it seems
See the Django documentation for model properties

Categories

Resources