This question already has an answer here:
How to perform a join and aggregate count in Django
(1 answer)
Closed 4 years ago.
I am trying to send sub model count information with the main model to HTML template. I have PROJECT and Companies models:
class Projects(models.Model):
name = models.CharField(max_length=255)
note = models.CharField(max_length=255, default='')
def __str__(self):
return self.name
class Companies(models.Model):
project = models.ForeignKey(Projects, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
note = models.CharField(max_length=255, default='')
def __str__(self):
return self.name
I need to show, projects have how many companies(company count):
Project name | company.no
project1 | 3
project2 | 5
Method-1 : Using a GROUP BY statement using Django ORM
from django.db.models import Count
Projects.objects.values('name').annotate(count=Count('companies'))
This is equivalant to the SQL query, SELECT name,Count('companies') as count from Projects. This will return a QuerySet as,
<QuerySet [{'name': 'project_name_1', 'count': 10}, {'name': 'project_name_2', 'count': 6}]>
Method-2 : Use #property decorator (as #Kirollos Morkos mentioned )
Hence your model became
class Projects(models.Model):
name = models.CharField(max_length=255)
note = models.CharField(max_length=255, default='')
#property
def company_count(self):
return self.companies_set.count()
def __str__(self):
return self.name
Then you'll get the comapny count in template by {{ project_obj.company_count}}
NOTE: The Method-1 is more efficent and fast, because it's being done in database level
You can use the property decorator to provider an arbitrary getter on your Project model. In your Company model, add a related_name to the project foreign key so you can get the reverse relationship:
project = models.ForeignKey(Projects, on_delete=models.CASCADE, related_name='companies')
Then, you can add a property on your Project model to get the company count:
class Projects(models.Model):
#property
def company_count(self):
return self.companies.count()
...
# Following the sample counts you gave
print(project1.company_count) # 3
print(project2.company_count) # 5
As JPG says you can use annotate. you dont need to add values queryset. see below:
from django.db.models import Count
Projects.objects.filter(<any filter apply to queryset>).annotate(company_count=Count('companies'))
you can add any field to annotation. then in your template or any code you can use this count by calling:
project.company_count
you also can add in your template
{{ project.companies_set.count }}
to get count of companies. but this method is too slow. because for each project record you have a database hit.
Related
My example:
class Product(models.Model):
name = models.CharField(max_length=50)
category = models.ManyToManyField("wms.ProductCategory", blank=True)
#property
def quantity_in(self):
return self.intodocumentproduct_set.aggregate(total=Sum('quantity_in', default=0))['total']
class IntoDocumentProduct(models.Model):
product = models.ForeignKey("wms.Product", on_delete=models.CASCADE)
quantity_in = models.FloatField(blank=True, null=True)
class ProductListAPIView(ListAPIView):
# queryset = Product.objects.prefetch_related('category').annotate(sum_quantity_in=Sum('intodocumentproduct__quantity_in', default=0)).all()
queryset = Product.objects.prefetch_related('category').all()
serializer_class = ProductModelSerializer
Commented queryset results in 4 queries while other query (using property) results in 6 queries. Probably n+ problem.
I have to use properties like: quantity_ordered,
quantity_reserved,
quantity_out,
quantity_in,
quantity_stock,
quantity_available,
quantity_pending_release and more in many places in web app. Calculating them in every view will be time comsuming and error susceptible.
This solution is not very handy when property is used in many views with many properties.
Is there any other solution to remove extra queries?
I really don't understand all the ways to build the right query.
I have the following models in the code i'm working on. I can't change models.
models/FollowUp:
class FollowUp(BaseModel):
name = models.CharField(max_length=256)
questions = models.ManyToManyField(Question, blank=True, )
models/Survey:
class Survey(BaseModel):
name = models.CharField(max_length=256)
followup = models.ManyToManyField(
FollowUp, blank=True, help_text='questionnaires')
user = models.ManyToManyField(User, blank=True, through='SurveyStatus')
models/SurveyStatus:
class SurveyStatus(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
survey = models.ForeignKey(Survey, on_delete=models.CASCADE)
survey_status = models.CharField(max_length=10,
blank=True,
null=True,
choices=STATUS_SURVEY_CHOICES,
)
models/UserSurvey:
class UserSurvey(BaseModel):
user = models.ForeignKey(User, null=True, blank=True,
on_delete=models.DO_NOTHING)
followups = models.ManyToManyField(FollowUp, blank=True)
surveys = models.ManyToManyField(Survey, blank=True)
questions = models.ManyToManyField(Question, blank=True)
#classmethod
def create(cls, user_id):
user = User.objects.filter(pk=user_id).first()
cu_quest = cls(user=user)
cu_quest.save()
cu_quest._get_all_active_surveys
cu_quest._get_all_followups()
cu_quest._get_all_questions()
return cu_quest
def _get_all_questions(self):
[[self.questions.add(ques) for ques in qstnr.questions.all()]
for qstnr in self.followups.all()]
return
def _get_all_followups(self):
queryset = FollowUp.objects.filter(survey__user=self.user).filter(survey__user__surveystatus_survey_status='active')
# queryset = self._get_all_active_surveys()
[self.followups.add(quest) for quest in queryset]
return
#property
def _get_all_active_surveys(self):
queryset = Survey.objects.filter(user=self.user,
surveystatus__survey_status='active')
[self.surveys.add(quest) for quest in queryset]
return
Now my questions:
my view sends to the create of the UserSurvey model in order to create a questionary.
I need to get all the questions of the followup of the surveys with a survey_status = 'active' for the user (the one who clicks on a button)...
I tried several things:
I wrote the _get_all_active_surveys() function and there I get all the surveys that are with a survey_status = 'active' and then the _get_all_followups() function needs to call it to use the result to build its own one. I have an issue telling me that
a list is not a callable object.
I tried to write directly the right query in _get_all_followups() with
queryset = FollowUp.objects.filter(survey__user=self.user).filter(survey__user__surveystatus_survey_status='active')
but I don't succeed to manage all the M2M relationships. I wrote the query above but issue also
Related Field got invalid lookup: surveystatus_survey_status
i read that a related_name can help to build reverse query but i don't understand why?
it's the first time i see return empty and what it needs to return above. Why this notation?
If you have clear explanations (more than the doc) I will very appreciate.
thanks
Quite a few things to answer here, I've put them into a list:
Your _get_all_active_surveys has the #property decorator but neither of the other two methods do? It isn't actually a property so I would remove it.
You are using a list comprehension to add your queryset objects to the m2m field, this is unnecessary as you don't actually want a list object and can be rewritten as e.g. self.surveys.add(*queryset)
You can comma-separate filter expressions as .filter(expression1, expression2) rather than .filter(expression1).filter(expression2).
You are missing an underscore in surveystatus_survey_status it should be surveystatus__survey_status.
Related name is just another way of reverse-accessing relationships, it doesn't actually change how the relationship exists - by default Django will do something like ModelA.modelb_set.all() - you can do reverse_name="my_model_bs" and then ModelA.my_model_bs.all()
*I'm trying to figure out how to populate fields in my model based on previous field selection.
For example, if FIELD_CHOICES = 3
Create 3x TextField()
class Post(models.Model):
STATUS_CHOICES = (('published','Published'),
('draft','Draft '))
FIELD_CHOICES = (('1','1 Title and body field'),
('2','2 Title and body fields'),
('3','3 Title and body fields'),
('4', '4 Title and body fields'),
('5', '5 Title and body fields'))
author = models.ForeignKey(User,
on_delete=models.CASCADE,
related_name='blog_post')
title = models.CharField(max_length=100)
sub_title = models.TextField(max_length=50,default="")
title_and_body_fields = models.IntegerField(choices=FIELD_CHOICES,
default=1)
**/// create number of title and body Textfields based on what was
/// selected in title_and_body_fields**
created = models.DateField()
publish = models.DateTimeField(default=timezone.now)
slug = models.SlugField(max_length=250,
unique_for_date='created')
status = models.CharField(max_length=250,
choices=STATUS_CHOICES,
default='draft')
object = models.Manager()
postManager = PostManager()
class Meta():
ordering = ('publish',)
def __strd__(self):
return self.title
def get_absolute_url(self):
return reverse('my_blog:post_detail',
args=[self.publish.year,
self.publish.month,
self.publish.day,
self.slug])
In the end i decided to do the following.
I added 5 seperate TextFields for Title and Body and added blank=True
title_5 = models.CharField(max_length=100,null=True,blank=True)
title_5_body = models.TextField(null=True,blank=True)
I'm afraid this is not possible.
I think you misunderstand how Models work. Have a look at the Django docs for models here.
Basically the way Django saves your models, is through an ORM (Object-Relational-Mapping).
This means that the model you write, with the fields contained within it, are transformed into a database query, generating a table.
In the docs for models, you can see this piece of code:
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
is transformed to this SQL query:
CREATE TABLE myapp_person (
"id" serial NOT NULL PRIMARY KEY,
"first_name" varchar(30) NOT NULL,
"last_name" varchar(30) NOT NULL
);
This means that Django, under the hood, creates database tables for each Model you have in your app.
So how does work in Django?
Well, these SQL statements are generated when you run python manage.py makemigrations. The actual tables are created when you run python manage.py migrate.
This means that you only run these commands once (For every change to your model), and there will be only one table created for your model.
So why can't I add extra TextFields to an object?
This is because for each model there is only one table in the database, and your model would be stored like this:
id | first_name | last_name
----------------------------------
1 | John | Doe
2 | Jane | Doe
... | ... | ...
If you were to add an extra field (for instance phone_number) to John Doe, you'd need to add that field to the entire table.
So in your case, your choice is between no extra body and title fields, or a set amount extra fields for each object.
Ok, what now?
Well, there are a few ways to do this. Your best bet would be to create a ManyToMany relationship to a Model called something like PostBody, which would allow you to create an arbitrary amount of bodies for a set post.
You could modify the save method of you Post model to automatically create a set amount of PostBody objects for that object.
You can read read more about ManyToManyField here.
I have 2 models:
1: KW (individual keywords)
2: Project (many keywords can belong to many different projects)
class KW(models.Model):
...
project = models.ManyToManyField('KWproject', blank=True)
class KWproject(models.Model):
ProjectKW = models.CharField('Main Keyword', max_length=1000)
author = models.ForeignKey(User, editable=False)
Now when user is in Admin for KWproject they should be able to see all keywords belonging to selected project in list_display. I achieved this but it doesn't feel like proper way.
class ProjectAdmin(admin.ModelAdmin):
form = ProjectForm
list_display = ('Keywordd', 'author')
def Keywordd(self, obj):
return '%s' % (obj.id, obj.ProjectKW)
Keywordd.allow_tags = True
Keywordd.admin_order_field = 'ProjectKW'
Keywordd.short_description = 'ProjectKW'
Is there better way to link and then list_display all items that have reverse relationship to the model? (via "project" field in my example)
As per the Django Admin docs:
ManyToManyField fields aren’t supported, because that would entail
executing a separate SQL statement for each row in the table. If you
want to do this nonetheless, give your model a custom method, and add
that method’s name to list_display. (See below for more on custom
methods in list_display.)
So, you may opt to implement a custom model method like so:
# models.py
class KW(models.Model):
...
project = models.ManyToManyField('KWproject', blank=True)
class KWproject(models.Model):
ProjectKW = models.CharField('Main Keyword', max_length=1000)
author = models.ForeignKey(User, editable=False)
def all_keywords(self):
# Retrieve your keywords
# KW_set here is the default related name. You can set that in your model definitions.
keywords = self.KW_set.values_list('desired_fieldname', flat=True)
# Do some transformation here
desired_output = ','.join(keywords)
# Return value (in example, csv of keywords)
return desired_output
And then, add that model method to your list_display tuple in your ModelAdmin.
# admin.py
class ProjectAdmin(admin.ModelAdmin):
form = ProjectForm
list_display = ('Keywordd', 'author', 'all_keywords')
def Keywordd(self, obj):
return '%s' % (obj.id, obj.ProjectKW)
Keywordd.allow_tags = True
Keywordd.admin_order_field = 'ProjectKW'
Keywordd.short_description = 'ProjectKW'
Do take note: This can potentially be a VERY EXPENSIVE operation. If you are showing 200 rows in the list, then a request to the page will execute 200 additional SQL queries.
I'm working on an eshop with Satchmo framework.
Does anyone know what steps should i follow in order to filter products according to a custom attribute(type of material) in order to present the products that have the same kind of material in a page(material.html)?
Should i make a material_view function
Should i override get_absolute_url function?
If you want to do this without touching the core code, I would make a local app localsite/product and in models.py:
class Material(models.Model):
product = models.ManyToManyField(Product, blank=True, null=True)
name = models.CharField(_("Name"), max_length=30)
slug = models.SlugField(_("Slug"), help_text=_("Used for URLs, auto-generated from name if blank"), blank=True, unique=True)
description = models.TextField(_("Description"), blank=True, help_text="Optional")
Add this new app to your admin, and to additionally make them available from the product page, add them as inline:
# if you have lots of products, use the nice horizontal filter from django's admin
class MaterialAdmin(admin.ModelAdmin):
filter_horizontal = ('product',)
class Material_Inline(admin.TabularInline):
model = Material.product.through
extra = 1
admin.site.register(Material, MaterialAdmin)
# Add material to the inlines (needs: from product.admin import Product, ProductOptions)
ProductOptions.inlines.append(Material_Inline)
admin.site.unregister(Product)
admin.site.register(Product, ProductOptions)
Then you can adapt your views/urls:
# urls.py
url(r'^material-list/([\w-]+)/$', material_list, {}, name="material_list"),
# view.py
def material_list(request, slug):
products = Product.objects.filter(material__slug='slug')
return render_to_response('localsite/material/list.html', {'products':products}, context_instance=RequestContext(request))
When you say "custom attribute" do you mean that you have modified the product.models.Product code to add another field?
If that is the case you'll probably want to create a custom view.
If your Product code is something like...
class Product(models.Model):
...
matieral_type = models.CharField(max_length=128)
...
...then you can build a view like this...
def material(request,material_type):
prods = Product.objects.filter(material_type=material_type)
return render_to_response('material.html',{'products',prods},RequestContext(request))