I'm creating Django application, with forum-like thingy. One of the views should display list of Discussions, with last written post beside it.
class Discussion(models.Model):
<snip>
topic = models.CharField(max_length=512)
class DiscussionPost(models.Model):
<snip>
target = models.ForeignKey(Discussion)
author = models.ForeignKey(User)
content = models.TextField(max_length=16000)
creation_date = models.DateTimeField(auto_now_add=True)
With standard Django queries, I would have to fetch ~50 times per page (one for each discussion).
DiscussionPost.objects
.filter(target=some_discussion)
.annotate(last_post=Max('creation_date'))
.filter(creation_date=F('last_post'))
I tried to work this around by adding field last_post = models.ForeignKey(DiscussionPost, null=True) to discussion, and changing 'save' method in DiscussionPost like this:
def save(self, *args, **kwargs):
if self.pk == None:
i_am_new = True
else:
i_am_new = False
super(DiscussionPost, self).save(*args, **kwargs)
if i_am_new:
self.target.last_post=self
self.target.save()
But this makes circular dependency, and simply won't compile.
Does anyone know a way to solve this problem? It seems easy, but I'm stuck...
To solve your circular dependency:
The problem is: DiscussionPost has not been declared yet when you FK it in Discussion.
put the name of the model that has not been declared yet in quotes.
models.ForeignKey('DiscussionPost', null=True)
see: https://stackoverflow.com/a/9606701/884453
Related
I have created this model in Django which also saves date and slug. But even if I am making these changes again the str and the slug still remains the same and not updating as per the new data. How to fix this ?
class EntranceExamination(models.Model):
course = models.CharField(max_length=100, blank=True, default='')
year = models.PositiveIntegerField(choices=year_choices(),default=current_year(), validators=[MinValueValidator(1984), max_value_current_year])
month = models.PositiveIntegerField(choices=month_choices(),validators=[MinValueValidator(1), MaxValueValidator(12)])
day = models.PositiveIntegerField(validators=[MinValueValidator(1), MaxValueValidator(31)])
start_time = models.TimeField(blank=True)
end_time = models.TimeField(blank=True)
slug = models.SlugField(editable=False,max_length=100)
class Meta:
ordering = ['month']
def __str__(self):
return f"{self.course}'s exam on {self.slug}"
def save(self):
if not self.slug:
self.slug = f'{self.day}-{self.month}-{self.year}'
super(EntranceExamination, self).save()
__str__ is evaluated when you can str(…) on the object, so when data on the model changes, then __str__ will return something different.
As for slug, you can each time update the slug when you save the model:
class EntranceExamination(models.Model):
# …
def save(self, *args, **kwargs):
self.slug = f'{self.day}-{self.month}-{self.year}'
super().save(*args, **kwargs)
This will however not work when you update in bulk through the ORM, since ORM calls often circumvent the .save(…) method and triggers.
It is however not a good idea. Slugs are used for URI's, and as the W3 consortium says: "Cool URIs don't change" [w3.org]:
When you change a URI on your server, you can never completely tell
who will have links to the old URI. They might have made links from
regular web pages. They might have bookmarked your page. They might
have scrawled the URI in the margin of a letter to a friend.
When someone follows a link and it breaks, they generally lose
confidence in the owner of the server. They also are frustrated -
emotionally and practically from accomplishing their goal.
Enough people complain all the time about dangling links that I hope
the damage is obvious. I hope it also obvious that the reputation
damage is to the maintainer of the server whose document vanished.
I added a new model to my app named SocialProfile, which is responsible for keeping social-related properties of a user which has a one-to-one relationship with UserProfile model. This is the SocialProfile model in models.py:
class SocialProfile(models.Model):
profile = models.OneToOneField('UserProfile', on_delete=models.CASCADE)
facebook_profiles = models.ManyToManyField('FacebookContact', related_name='synced_profiles', blank=True)
google_profiles = models.ManyToManyField('GoogleContact', related_name='synced_profiles', blank=True)
hash = models.CharField(max_length=30, unique=True, blank=True)
def save(self, *args, **kwargs):
if not self.pk:
hash = gen_hash(self.id, 30)
while SocialProfile.objects.filter(hash=hash).exists():
hash = gen_hash(self.id, 30)
self.hash = hash
def __str__(self):
return str(self.profile)
Right now, I keep a record for synced facebook & google profiles. Now, the problem is that creating new objects does not actually add any record in the database. I cannot create instances with scripts or admin. In case of scripts, the following runs without errors but no record is created:
for profile in UserProfile.objects.all():
sp = SocialProfile.objects.create(profile=profile)
print(profile, sp)
SocialProfile.objects.count()
The prints are done, and look correct and the count() returns 0. I try creating objects in admin, but I get the following error:
"{{socialprofile object}}" needs to have a value for field "socialprofile" before
this many-to-many relationship can be used.
I think that is another problem, because if I comment the Many-to-Many relationships, it is done, without error (still no new records). I mentioned it just if it might help.
I have checked the database, the tables are there, no new migrations are detected either.
Any help and idea about what could be the problem would be appreciated!
You've overwritten the save method so that it never actually saves anything. You need to call the superclass method at the end:
def save(self, *args, **kwargs):
if not self.pk:
...
return super(SocialProfile, self).save(*args, **kwargs)
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.
Let's say I have a model Issue that looks like this:
class Issue(models.Model):
title = models.CharField(max_length=100)
author = models.CharField(max_length=100)
text = models.TextField()
And another one called Comment like so:
class Comment(models.Model):
issue = models.ForeignKey(Issue)
text = models.TextField()
number = models.AutoField()
I want the comment number field to be relative to the issue.
Which means that every issue will have its separate comment#1, comment#2, comment#3...etc.
Can this be done in Django?
If you removed the number field from your Comment model and replaced it with a date_added field, you could still query the DB to pull all comments associated with a certain entry in order.
class Comment(models.Model):
issue = models.ForeignKey(Issue)
text = models.TextField()
date_added = models.DateField(auto_now_add = True)
Then if you wanted to get all of the comments associated with a certain issue:
i = Issue.object.get(pk=1) #pk = primary key id for whatever issue you want
comments_by_issue = i.comment_set.all().order_by('date_added')
Now you have some comments that you can refer to by index location (comments_by_issue[0] would get you the first comment attached to the issue, for instance).
That index location is the closest way I can figure to get what you're looking for. Someone else mentioned comment.id, but this is just going to be an autoincrementing integer that goes up for every comment. The fifth comment added to your system might have comment.id = 5, but it might be the first comment attached to issue 2 - if I'm reading your query right, having the comment ID doesn't help in that context.
This is more of a workaround than a direct answer, but I hope it helps.
I prefer soundeux's solution. That said, here is the idea...
class Comment(models.Model):
# ...
number = models.PositiveIntegerField(default=1)
def save(self, *args, **kwargs):
obj = super(Comment, self).save(*args, **kwargs)
try:
last_comment = self.issue.comment_set.all().orderby('-number')[0]
except KeyError: # I think it's KeyError... for when there is no comments
pass
else:
Comment.objects.filter(id=self.id).update(number=last_comment.number+1)
return obj
Be careful - this will not be executed when Comments are being updated using the ORM's update method. Indeed, that's what I've used to avoid recursion (which is not a very good solution either). You might wanna implement it as a pre_save event instead of the save() (still won't respond to the update(), but will be more robust about potential recursion problems).
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