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.
Related
I am writing tests for a large Django application, as part of this process I am gradually creating factories for all models of the different apps within the Django project.
However, I've run into some confusing behavior with FactoryBoy where it almost seems like SubFactories have an max depth beyond which no instances are generated.
The error occurs when I try to run the following test:
def test_subfactories(self):
""" Verify that the factory is able to initialize """
user = UserFactory()
self.assertTrue(user)
self.assertTrue(user.profile)
self.assertTrue(user.profile.tenant)
order = OrderFactory()
self.assertTrue(order)
self.assertTrue(order.user.profile.tenant)
The last line will fail (AssertionError: None is not true), running this test through a debugger reveals that indeed order.user.profile.tenant returns None instead of the expected Tenant instance.
There are quite a few factories / models involved here, but the layout is relatively simple.
The User (django default) and the Profile model are linked through a OneToOneField, which (after some trouble) is represented by the UserFactory and ProfileFactory
#factory.django.mute_signals(post_save)
class ProfileFactory(factory.django.DjangoModelFactory):
class Meta:
model = yuza_models.Profile
django_get_or_create = ('user',)
user = factory.SubFactory('yuza.factories.UserFactory')
birth_date = factory.Faker('date_of_birth')
street = factory.Faker('street_name')
house_number = factory.Faker('building_number')
city = factory.Faker('city')
country = factory.Faker('country')
avatar_file = factory.django.ImageField(color='blue')
tenant = factory.SubFactory(TenantFactory)
#factory.django.mute_signals(post_save)
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = auth_models.User
username = factory.Sequence(lambda n: "user_%d" % n)
first_name = factory.Faker('first_name')
last_name = factory.Faker('last_name')
email = factory.Faker('email')
is_staff = False
is_superuser = False
is_active = True
last_login = factory.LazyFunction(timezone.now)
#factory.post_generation
def profile(self, create, extracted):
if not create:
return
if extracted is None:
ProfileFactory(user=self)
The TenantFactory below is represented as a SubFactory on the ProfileFactory above.
class TenantFactory(factory.django.DjangoModelFactory):
class Meta:
model = elearning_models.Tenant
name = factory.Faker('company')
slug = factory.LazyAttribute(lambda obj: text.slugify(obj.name))
name_manager = factory.Faker('name')
title_manager = factory.Faker('job')
street = factory.Faker('street_name')
house_number = factory.Faker('building_number')
house_number_addition = factory.Faker('secondary_address')
The Order is linked to a User, but many of its methods call fields of its self.user.profile.tenant
class OrderFactory(factory.DjangoModelFactory):
class Meta:
model = Order
user = factory.SubFactory(UserFactory)
order_date = factory.LazyFunction(timezone.now)
price = factory.LazyFunction(lambda: Decimal(random.uniform(1, 100)))
site_tenant = factory.SubFactory(TenantFactory)
no_tax = fuzzy.FuzzyChoice([True, False])
Again, most of the asserts in the test pass without failing, all separate factories are able to initialize fetch values from their immediate foreignkey relations. However, as soon as factories/models are three steps removed from each other the call will return None instead of the expected Tenant instance.
Since I was unable to find any reference to this behaviour in the FactoryBoy documentation its probably a bug on my side, but so far I've been unable to determine its origin. Does anyone know what I am doing wrong?
post_save method
def create_user_profile(sender, instance, created, **kwargs):
if created:
profile = Profile.objects.create(user=instance)
resume = profile.get_resume()
resume.initialize()
post_save.connect(create_user_profile, sender=User)
As I mentioned in a comment, I've discovered the source of the problem: the post-save method linked to the UserProfile (I've included the code in my post).
This post-save method created a Profile on User creation. I accounted for this signal by using the #factory.django.mute_signals decorater on both the UserFactoryand the ProfileFactory.
I had assumed that any calls on Order.user would trigger the UserFactory which had already been enclosed with the decorator, but this is not assumption proved to be wrong. Only when I applied the decorated to the OrderFactory as well did the tests pass.
Thus the #factory.django.mute_signals decorator should not just be used on factories that are affected by these signals, but also on any factory that is using those factories as a SubFactory!
Hey so I am making a color scheme posting site where people can register and post color schemes they come up with. So far everything is working great, the only thing I have left to do is add a "Like Post" feature. I'm wondering what the best way to implement this would be.
I have two ideas on how this could be done, the first is add an additional field to both the ColorSet (posts) and the User models (for the user model I would set up a new model with a OneToOne relationship to add onto the User model) which would record users that have each single post, and which posts each user has liked to keep track of everything.
So this could look something like this:
from django.db import models
from django.core.urlresolvers import reverse
from django.conf import settings
from django.contrib.auth import get_user_model
User = get_user_model()
# Create your models here.
class ColorSet(models.Model):
user = models.ForeignKey(User,related_name='colorset')
published_date = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=50,blank=False)
color_one = models.CharField(max_length=6,blank=False,default='cccccc')
color_two = models.CharField(max_length=6,blank=False,default='ffffff')
color_three = models.CharField(max_length=6,blank=False,default='e5e5e5')
color_four = models.CharField(max_length=6,blank=False,default='f0f0f0')
color_five = models.CharField(max_length=6,blank=False,default='bababa')
liked_by = models.IntegerField(blank=True)
def publish(self):
self.save()
def get_absolute_url(self):
return reverse('index')
def __str__(self):
return self.name
user model:
from django.db import models
from django.contrib.auth.models import User
# Create your models here.
class UserStats(models.Model):
user = models.OneToOneField(User)
liked_sets = models.IntegerField(blank=True)
def __str__(self):
return self.user.username
In this first option I would have the new model fields (liked_sets and liked_by) be equal to lists containing the ok's of all the Color Sets each user has liked and all the users who have liked each post respectively.
The other way that I'm thinking about would be to just create an entirely new model that tracks the likes for each color set post (not totally sure how this model would look yet exactly).
Aside from which is easier, I wondering which makes more sense from a technical standpoint? Will one of these two options take up more space or create heavier server load?
Thanks for the help.
As far as I understand your problem, it can be broken in two parts.
Maintaining the total number of likes on a model ColorSet.
Keeping the track of all those users who liked a single instance of ColorSet.
Now if I understand your problem correctly(correct me if I'm wrong), when you say:
new model fields (liked_sets and liked_by) be equal to lists containing the ok's of all the Color Sets each user has liked and all the users who have liked each post respectively.
you intend to create a field in your database which would simply store a list of pks of all the people who've liked a ColorSet model instance. Even if you don't intend to do that, still an IntegerField to store such information is(in my humble opinion) somewhat wrong.
Now why you wouldn't want to do that? It's because relational databases are made to recognize the relations between tuples of information and enhance the processing by creating relations. That is why we use relations like OneToOneField and ForeignKey. They make the processing way faster. If we were to simply store the pk values in a Field, further search them in our database to retrieve information, that would be something really slow.
Now I suppose what you are looking for is ManyToManyField.
In your problem, you will simply map the ManyToManyField it to the User model.
It would look something like:
class ColorSet(models.Model):
user = models.ForeignKey(User,related_name='colorset')
published_date = models.DateTimeField(auto_now_add=True)
name = models.CharField(max_length=50,blank=False)
color_one = models.CharField(max_length=6,blank=False,default='cccccc')
color_two = models.CharField(max_length=6,blank=False,default='ffffff')
color_three = models.CharField(max_length=6,blank=False,default='e5e5e5')
color_four = models.CharField(max_length=6,blank=False,default='f0f0f0')
color_five = models.CharField(max_length=6,blank=False,default='bababa')
liked_by = models.IntegerField(blank=True)
#add a simple ManyToManyField which will hold all the users who liked this colorset
likers = models.ManyToManyField(User , related_name = 'liked_colorsets')
def publish(self):
self.save()
def get_absolute_url(self):
return reverse('index')
def __str__(self):
return self.name
and remove your UserStats model to
Now use the following code outline structure to access the information from the database.
1) To get the ColorSets liked by a User:
#obtain any user model object; for example: user_object = User.objects.get(...)
user_object.liked_colorsets.all()
#a queryset with all the liked colorsets is returned.
2) To get the Users who liked a ColorSet:
#obtain any colorset model object; for example: colorset_object = ColorSet.objects.get(...)
colorset_object.likers.all()
#a queryset with all the Users who liked this colorset is returned.
One more thing that I would like to add here. After a User likes a ColorSet, you would obviously want to add this User to the likers field in your ColorSet model(and increment the liked_by field; I assume you'll manage that). To add a User in the likers field:
#obtain any colorset model object; for example: colorset_object = ColorSet.objects.get(...)
#obtain the user model object of the user who liked this colorset in user_object
#and do
colorset_object.likers.add(user_object)
Read more about adding the models in ManyToManyField here in docs.
Hope this helps. Thanks.
I have done a pre_save signal in my django/satchmo inherited model Product called JPiece and I have another model inheritance from satchmo Category called JewelCategory. The pre_save signal makes the JPiece objects get the category list and add those categories that fit the Jpiece description to the relation, that is done in the model, meaning if I manually do
p = Jpiece.objects.get(pk=3)
p.save()
The categories are saved and added to the p.category m2m relation but If i save from the admin it does not do this...
How can I achieve this... to save from the admin a JPiece and to get the categories it belongs too...
Here are the models remember that they both have model inheritance from satchmo product and category classes.
class Pieza(Product):
codacod = models.CharField(_("CODACOD"), max_length=20,
help_text=_("Unique code of the piece. J prefix indicates silver piece, otherwise gold"))
tipocod = models.ForeignKey(Tipo_Pieza, verbose_name=_("Piece Type"),
help_text=_("TIPOCOD"))
tipoenga = models.ForeignKey(Engaste, verbose_name=_("Setting"),
help_text=_("TIPOENGA"))
tipojoya = models.ForeignKey(Estilos, verbose_name=_("Styles"),
help_text=_("TIPOJOYA"))
modelo = models.CharField(_("Model"),max_length=8,
help_text=_("Model No. of casting piece."),
blank=True, null=True)
def autofill(self):
#self.site = Site.objects.get(pk=1)
self.precio = self.unit_price
self.peso_de_piedra = self.stone_weigth
self.cantidades_de_piedra = self.stones_amount
self.for_eda = self.for_eda_pieza
if not self.id:
self.date_added = datetime.date.today()
self.name = str(self.codacod)
self.slug = slugify(self.codacod, instance=self)
cats = []
self.category.clear()
for c in JewelCategory.objects.all():
if not c.parent:
if self.tipocod in c.tipocod_pieza.all():
cats.append(c)
else:
if self.tipocod in c.tipocod_pieza.all() and self.tipojoya in c.estilo.all():
cats.append(c)
self.category.add(*cats)
def pieza_pre_save(sender, **kwargs):
instance = kwargs['instance']
instance.autofill()
# import ipdb;ipdb.set_trace()
pre_save.connect(pieza_pre_save, sender=Pieza)
I know I can be vague with explanations sometimes of what I need so please feel free to ask anything Ill be sure to clarify ASAP since this is a client that needs this urgently.
Thank you all as always...
If you use pre_save, it's called before save(), meaning you can't define m2m relationships since the model doesn't have an ID.
Use post_save.
# this works because the ID does exist
p = Jpiece.objects.get(pk=3)
p.save()
Update, check out the comment here: Django - How to save m2m data via post_save signal?
It looks like the culprit now is that with an admin form, there is a save_m2m() happening AFTER the post_save signal, which could be overwriting your data. Can you exclude the field from the form in your ModelAdmin?
# django.forms.models.py
if commit:
# If we are committing, save the instance and the m2m data immediately.
instance.save()
save_m2m()
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
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).