I need to join and pass two queries of category and subcategory as elements of an autocomplete select2 dropdown as my django form field as below:
This is my form:
class CategoriesAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if not self.request.user.is_authenticated:
return Categories.objects.none()
qs = Categories.objects.all()
if self.q:
qs = qs.filter(Q(name__icontains=self.q))
return qs
def get_result_label(self, item):
return format_html( item.name)
class categories_form(forms.ModelForm):
categories = forms.ModelChoiceField(
queryset= Categories.objects.none(),
widget= autocomplete.ModelSelect2(
url='load_categories',
attrs={
'data-placeholder': 'Select a category',
'data-html': True,
'style': 'min-width: 15em !important;',
}
)
)
class Meta:
model = Post
fields = ['categories']
def __init__(self, *args, **kwargs):
super(category_form, self).__init__(*args, **kwargs)
self.fields['categories'].queryset = Categories.objects.all()
and in url:
path('ajax/categories-autocomplete', CategoriesAutocomplete.as_view(), name='load_categories'),
for category model:
class Categories(models.Model):
name = models.CharField(max_length=100)
brief = models.TextField(null=True)
slug = models.SlugField(max_length=200)
date = models.DateTimeField(default=timezone.now)
class Meta:
db_table = "categories"
for sub-category model:
class Sub_Categories(models.Model):
name = models.CharField(max_length=100)
brief = models.TextField(null=True)
slug = models.SlugField(max_length=200)
date = models.DateTimeField(default=timezone.now)
class Meta:
db_table = "sub_categories"
model to connect category with sub-categories:
class Categories_Sub_Categories(models.Model):
category = models.OneToOneField(Categories, primary_key=True, on_delete=models.CASCADE)
sub_cat = models.OneToOneField(Sub_Categories, on_delete=models.CASCADE)
class Meta:
db_table = "categories_sub-categories"
and in Post model:
class Post(models.Model):
title = models.CharField(max_length=50)
descript = HTMLField(blank=True, null=True)
categories = models.ForeignKey(Categories, blank=True, null=True, on_delete=models.CASCADE)
subcategories = models.ForeignKey(Sub_Categories, blank=True, null=True, on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
date = models.DateTimeField(default=timezone.now)
UPDATE:
The answer provided by #ha-neul is working just there is a bug. I show it with an example:
This is what expect in the dropdown:
**Asia**
China
Malaysia
India
Tajikistan
Iran
Qatar
**Europe**
Germany
Italy
Spain
Netherlands
France
**Africa**
Gana
...
But this is what I see:
**Asia**
China
Malaysia
**Europe**
Netherlands
France
Sweden
Norway
**Asia**
India
Tajikistan
Iran
**Europe**
Germany
Italy
**Asia**
Qatar
**Africa**
Gana
...
**America**
....
**Europe**
Spain
in the SubCategory table I have something like:
id ........... category_id
1 1
2 1
3 1
4 1
5 3
6 1
7 2
I am following this package. Any idea to make me even closer to the solution would be appreciated!!
If this is what you want to achieve, then:
The short answer is you should
have your subcategory with ForeignKeyField referring to
Category model.
use Select2GroupQuerySetView instead of Select2QuerySetView.
But implementing it is a bit complicated.
First of all, although django-autocomplete-light's source code has Select2GroupQuerySetView , somehow you cannot just use it as autocomplete.Select2GroupQuerySetView. So, you have to write the same thing in your own views.py. In addition, the source code has a typo, so you need to fix it.
Step 1. In models.py:
class Category(models.Model):
name = models.CharField(max_length=100)
brief = models.TextField(null=True)
slug = models.SlugField(max_length=200)
date = models.DateTimeField(default=timezone.now)
class SubCategory(models.Model):
##################
#You need add this line, so there is a one-to-many relationship
category = models.ForeignKey(Category, on_delete=models.CASCADE,
related_name='subcategories')
###############
name = models.CharField(max_length=100)
brief = models.TextField(null=True)
slug = models.SlugField(max_length=200)
date = models.DateTimeField(default=timezone.now)
Step2.in views.py copy-paste Select2GroupQuerySetView code and fix a typo
# import collections, so Select2GroupQuerySetView can work
import collections
class Select2GroupQuerySetView(autocomplete.Select2QuerySetView):
group_by_related = None
related_field_name = 'name'
def get_results(self, context):
if not self.group_by_related:
raise ImproperlyConfigured("Missing group_by_related.")
groups = collections.OrderedDict()
object_list = context['object_list']
print(object_list)
object_list = object_list.annotate(
group_name=F(f'{self.group_by_related}__{self.related_field_name}'))
for result in object_list:
group_name = getattr(result, 'group_name')
groups.setdefault(group_name, [])
groups[group_name].append(result)
return [{
'id': None,
'text': group,
'children': [{
'id': result.id,
'text': getattr(result, self.related_field_name),
# this is the line I had to comment out
#'title': result.descricao
} for result in results]
} for group, results in groups.items()]
3. write your own view using Select2GroupQuerySetView
class SubCategoryAutocomplete(Select2GroupQuerySetView):
print('under subcategory autocomplete')
group_by_related = 'category' # this is the fieldname of ForeignKey
related_field_name = 'name' # this is the fieldname that you want to show.
def get_queryset(self):
##### Here is what you normally put... I am showing the minimum code.
qs = SubCategory.objects.all()
if self.q:
qs = qs.filter(name__istartswith=self.q)
return qs
Howe to use this view in a your project?
1. Say you have a Post model as below, with subcategory as a ForeignKey.
class Post(models.Model):
title = models.CharField(max_length=100)
subcategory = models.ForeignKey(SubCategory,on_delete=models.CASCADE)
def __str__(self):
return self.title
2. You will generate a PostForm that contains the subcategory autocomplete field.
in forms.py
from django.conf.urls import url
class PostForm(forms.ModelForm):
subcategory = forms.ModelChoiceField(
queryset=Subcategory.objects.all(),
widget=autocomplete.ModelSelect2(url='subcategory-autocomplete')
)
class Meta:
model= Post
fields = ('title','subcategory')
You will generate a CreatePostView using generic CreateView
from django.views.generic.edit import CreateView
class CreatePostView(CreateView):
model=Post
template_name='yourapp/yourtemplate.html'# need to change
form_class=PostForm
success_url = '/'
Now, in your urls.py, one url for CreatePostView another one for autocomplete view.
urlpatterns = [
url(
r'^subcategory-autocomplete/$',
SubCategoryAutocomplete.as_view(),
name='subcategory-autocomplete',
),
path('post/create',CreatePostView.as_view(), name='create_post'),
it's all set, you will go to post/create and see a PostForm with subcategories autocomplete field.
OP had a weird grouping behavior after using the code above. In his comment, he mentioned:
Added a .order_by(category_id')` to the qs and fixed it.
You do not need to create this model to make a relation
Categories_Sub_Categories
just create a one-to-many field (categories) in Sub_Categories model and put Categories model there (foreign), it will do that automatically, then retrieve data like this (in backend)
categories = Categories.objects.all()
you will get all categories with Sub_Categories object here, pass it to frontend and loop through it (in front-end)
for category in categories:
sub_categories = category.sub_categories_set.all()
To make SubCategory you can have ForeignKey to self.
Another point, you'll need to use prefetch_related from main model (Post) to be able to "join" Category/SubCategory there.
Here is an example how this should look like:
# forms.py
from django import forms
from django.db.models import Prefetch
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = [...]
def __init__(self, *args, **kwargs):
super(PostForm, self).__init__(*args, **kwargs)
cats = Category.objects \
.filter(category__isnull=True) \
.order_by('order') \
.prefetch_related(Prefetch('subcategories',
queryset=Category.objects.order_by('order')))
self.fields['subcategory'].choices = \
[("", self.fields['subcategory'].empty_label)] \
+ [(c.name, [
(self.fields['subcategory'].prepare_value(sc),
self.fields['subcategory'].label_from_instance(sc))
for sc in c.subcategories.all()
]) for c in cats]
# models.py
class Category(models.Model):
category = models.ForeignKey('self', null=True, on_delete=models.CASCADE,
related_name='subcategories', related_query_name='subcategory')
class Post(models.Model):
subcategory = models.ForeignKey(Category, on_delete=models.CASCADE,
related_name='posts', related_query_name='post')
Related
I have been stuck on this problem for about 9 hours and am having trouble understanding the Grandparent - Parent - Child relationship with a One to Many Relationship(foreign key) in Django.
I am currently creating an E-Commerce website and am trying to display the information in 4 different ways to fetch from my React frontend.
I have 3 models setup:
Category
Subcategory (Foreign key to Category)
Products (Foreign Key to Subcategory)
This is the result I am attempting to create in my urls/views/serializer files:
For some reason I can successfully retrieve at a granular level for a single product but as soon as I start going up nested models I am running into the following errors:
Desired Result
I have api/ included in the core urls folder already.
I want JSON to display the information like this.
# URLSearch api/Category/
# Category -
# |
# Subcategory 1 -
# |
# Product 1
# Product 2
# Subcategory 2 -
# |
# Product 1
# Proudct 2
I also want JSON to display the information like this.
# URLSearch api/Category/Subcategory/
# Category -
# |
# Subcategory -
# |
# Product 1
# Product 2
Errors
Error 1:
Got AttributeError when attempting to get a value for field slug on serializer SubcategoryListSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the QuerySet instance.
Original exception text was: 'QuerySet' object has no attribute 'slug'.
Error 2:
Returns Wrong Object piggies back off issue above - I deleted my code to clean it up file as I have been attempting this for 10+ hours so there is currently a place holder route there.
Urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework import routers
from api import views
urlpatterns = [
path('search/<slug:category_slug>/<slug:subcategory_slug>/',
views.SubcategoryListView.as_view()), #Error 1
path('search/<slug:category_slug>/', views.AllProductsListView.as_view()), # Error 2
path('all-products/', views.AllProductsListView.as_view()), # Good
path('search/<slug:category_slug>/<slug:subcategory_slug>/<slug:product_slug>',
views.ProductDetail.as_view()), # WORKS
# path('all-products/', views.AllProductsListView.as_view())
]
Models.py
from django.db import models
import uuid
# Create your models here.
class Category(models.Model):
slug = models.SlugField(
primary_key=True, max_length=50, null=False, unique=True)
class Meta:
ordering = ('slug',)
def __str__(self):
return self.slug
def get_absolute_url(self):
return f'/{self.slug}/'
class Subcategory(models.Model):
slug = models.SlugField(
primary_key=True, max_length=50, null=False, unique=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
class Meta:
ordering = ('category',)
def __str__(self):
return self.slug
def get_absolute_url(self):
return f'/{self.category.slug}/{self.slug}/'
class Products(models.Model):
subcategory = models.ForeignKey(Subcategory, on_delete=models.CASCADE)
newAdd = models.BooleanField(default=False)
name = models.CharField(max_length=200)
slug = models.SlugField(
primary_key=True, max_length=50, null=False, unique=True)
imageOne = models.CharField(max_length=100)
imageTwo = models.CharField(max_length=100, null=True)
imageThree = models.CharField(max_length=100, null=True)
colors = models.CharField(max_length=300) # This is going to be a list
price = models.FloatField()
dateCreated = models.DateField(auto_now_add=True)
class Meta:
ordering = ('-dateCreated',)
def __str__(self):
return self.slug
def get_absolute_url(self):
return f'/{self.subcategory.category.slug}/{self.subcategory.slug}/{self.slug}'
Views
from django.shortcuts import render
from .serializers import AllProductsSerializer, CategorySerializer, SubcategoryListSerializer, ProductsSerializer, SubcategorySerializer
from rest_framework import viewsets, generics, views, response
from products.models import Products, Subcategory, Category
# Create your views here.
class ProductDetail(views.APIView):
def get_object(self, category_slug, subcategory_slug, product_slug):
obj = Products.objects.filter(subcategory__category__slug=category_slug).filter(
subcategory=subcategory_slug).get(slug=product_slug)
return obj
def get(self, request, category_slug, subcategory_slug, product_slug):
print(request)
product = self.get_object(
category_slug, subcategory_slug, product_slug)
serializer = ProductsSerializer(product)
return response.Response(serializer.data)
class SubcategoryListView(views.APIView):
def get_object(self, category_slug, subcategory_slug):
obj = Products.objects.filter(
subcategory__category__slug=category_slug)
return obj
def get(self, request, category_slug, subcategory_slug):
products = self.get_object(category_slug, subcategory_slug)
serializer = SubcategoryListSerializer(products)
return response.Response(serializer.data)
class AllProductsListView(generics.ListAPIView):
serializer_class = AllProductsSerializer
queryset = Products.objects.all()
class CategoryListView(generics.ListCreateAPIView):
queryset = Products.objects.all()
serializer_class = Category
Serializers
from rest_framework import serializers
from products.models import Products, Category, Subcategory
class ProductsSerializer(serializers.ModelSerializer):
class Meta:
model = Products
fields = (
'newAdd',
'name',
'slug',
'imageOne',
'price',
'dateCreated',
'get_absolute_url',
)
class SubcategoryListSerializer(serializers.ModelSerializer):
class Meta:
model = Products
fields = ('__all__')
class SubcategorySerializer(serializers.ModelSerializer):
items = SubcategoryListSerializer(many=True, read_only=True)
class Meta:
model = Subcategory
fields = (
'slug',
'items'
'get_absolute_url',
)
class AllProductsSerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ('__all__')
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Products
fields = ('__all__')
Suppose we have this django models:
from django.db import models
# Create your models here.
class Artist(models.Model):
name = models.CharField(max_length=10)
class Album(models.Model):
name = models.CharField(max_length=10)
date = models.DateField()
artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
So I can write:
artist_one = models.Artist.objects.create(name='Santana')
album_one = models.Album.objects.create(name='Abraxas', date = datetime.date.today(), artist=artist_one)
album_two = models.Album.objects.create(name='Supernatural', date = datetime.date.today(), artist=artist_one)
How can I add a constrain to the Album class as to say an artist cannot publish two albums the same year?
You could override the model's save method like this.
def save(self, *args, **kwargs):
year = self.date.year
albums_of_year = Album.objects.filter(artist=self.artist, date__year=year)
if albums_of_year:
# you can raise your error here
else:
super(Album, self).save(*args, **kwargs)
Use validators on model
from django.core.exceptions import ValidationError
def validate_atrist_album(value):
#logic to check if artist has album from same year here
if not user_can_publish:
raise ValidationError('Artist can't publish')
class Album(models.Model):
name = models.CharField(max_length=10)
date = models.DateField()
artist = models.ForeignKey(
Artist,
on_delete=models.CASCADE,
validators=[
validate_artist_album,
],
)
I want to filter Blog Post objects or records based on the Post Category and a User that uploaded the Post record, it gives me an error when I try to do filter, this is the error.
ValueError at /dashboard/filter-post/
The QuerySet value for an exact lookup must be limited to one result using slicing.
Here is my models.py
class Category(models.Model):
cat_name = models.CharField(max_length=100, verbose_name='Category Name')
cat_desc = models.TextField(blank=True, null=True)
def __str__(self):
return self.cat_name
class Meta():
verbose_name_plural='Category'
class Post(models.Model):
pst_title = models.CharField(max_length=150)
pst_image = models.ImageField(blank=True, null=True, upload_to='uploads/')
user = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ManyToManyField(Category)
content = models.TextField()
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.pst_title
#property
def img_url(self):
if self.pst_image:
return self.pst_image.url
on forms.py
class FilterForm(forms.ModelForm):
user = forms.ModelChoiceField(
queryset=User.objects.all(),
widget=forms.Select(attrs={'class': 'form-control'}))
category = forms.ModelMultipleChoiceField(
queryset=Category.objects.all(),
widget=forms.SelectMultiple(attrs={'class': 'form-control js-example-disabled-results'}))
catch_bot = forms.CharField(required=False,
widget=forms.HiddenInput, validators=[validators.MaxLengthValidator(0)])
class Meta():
fields = ['user', 'category' ]
model = Post
on views.py
def filter_post(request):
post = FilterForm(request.GET)
queryset = Post.objects.all()
if post.is_valid():
user=post.cleaned_data.get('user')
category=post.cleaned_data.get('category')
if user and category:
queryset = queryset.filter(user__username=user, category__cat_name=category)
return render(request, 'backend/filter-post.html', {'query':queryset, 'post':post})
I am having challenges properly filtering this in my views any help?
Try this:
instead of this:
queryset = queryset.filter(user__username=user, category__cat_name=category)
use this:
queryset = queryset.filter(user=user, category=category)
Also don't name your model fields after the model name, just use name instead of pst_name or cat_name, you will see that when you will try access these values there will be no confusion.
UPDATE
Ok, maybe try to rewrite your view like this:
def filter_post(request):
posts = Post.objects.all()
form = FilterForm(request.GET) # its best practice to call your form instance `form` in the view so that the next line has better readability
if form.is_valid():
user=post.cleaned_data['user']
category=post.cleaned_data['category']
if user:
posts = posts.filter(user=user)
if category:
posts = posts.filter(category=category)
return render(request, 'backend/filter-post.html', {'posts':posts})
So I have a simple Ad model and a FilterView showing all the ads. The ads can be filtered by different tags stored in a separate model joined by a ManyToManyField.
I'm using django-filter to set up a small ModelMultipleChoiceFilter and let users select different tags to filter the Ads. This is working however it uses the tag__id. I would like it to use the tag__slug field.
Therefore I've added the attribute "to_field_name='slug'" but I get the following;
Field 'id' expected a number but got 'diner'.
The following code does work but only filters by tag__id like:
/?tags=6
and I would rather see something like this;
?tags=diner
models.py
class Ad(models.Model):
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
description = RichTextField()
tags = models.ManyToManyField('Tag')
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, null=True)
class Meta:
ordering = ['-title']
def __str__(self):
return self.title
class Tag(models.Model):
name = models.CharField(max_length=200, help_text='Titel van de tag')
slug = models.SlugField(max_length=200, null=True)
def __str__(self):
return self.name
filters.py
from django import forms
from discovery.grid.models import Ad, Tag
import django_filters
class AdFilter(django_filters.FilterSet):
tags = django_filters.ModelMultipleChoiceFilter(
# to_field_name='slug',
queryset=Tag.objects.all(),
widget=forms.CheckboxSelectMultiple)
class Meta:
model = Ad
fields = [
'tags'
]
How can I achieve filtering based on the model name or slug instead of the id?
With best regards,
Maybe you can try like this:
class AdFilter(django_filters.FilterSet):
tags = CharFilter(method='my_custom_filter')
def my_custom_filter(self, queryset, name, value):
return queryset.filter(**{
'tags__slug__iexact': value,
})
class Meta:
model = Ad
fields = [
'tags'
]
More information can be found in documentation.
i have 3 models:
class Category(models.Model):
name = models.CharField(max_length=200)
slug = models.SlugField(max_length=70, null=True, blank=True)
class SubCategory(models.Model):
category= models.ForeignKey(Category, on_delete=models.CASCADE)
name = models.CharField(max_length=200, )
class Products(models.Model):
user= models.ForeignKey(User, on_delete=models.CASCADE)
category= models.ForeignKey(Category, on_delete=models.CASCADE)
subcategory = models.CharField(max_length=200, null=True, blank=True)
and i have a view which receive request and category.slug
def category_list(request, slug):
category = Category.objects.get(slug=slug)
products = ProductFilter(request.GET, queryset=Products.objects.filter(category=category)
return render(request, 'products/category_list.html', {"products":products, 'category': category})
when rendering i receive a QuerySet filtered to Category
I want to send category.id to ProductsFilter and recive a dynamic Choices from database
class ProductsFilter(django_filters.FilterSet):
subcategory= django_filters.ChoiceFilter(lookup_expr='iexact', choices=TEST, required=False)
class Meta:
model = Products
fields = {
"subcategory",
}
Want to change choices=TEST to choices=list(SubCategory.objects.filter(category_id=category.id)
Is this possible?
The answer from #Sherpa has just two slight problems. First, you should replace fields with filters. Second, you can't use += operator, you have to directly assign to the filter's extra.
Here's my working code in two different ways
class LayoutFilterView(filters.FilterSet):
supplier = filters.ChoiceFilter(
label=_('Supplier'), empty_label=_("All Suppliers"),)
def __init__(self, *args, **kwargs):
super(LayoutFilterView, self).__init__(*args, **kwargs)
# First Method
self.filters['supplier'].extra['choices'] = [
(supplier.id, supplier.name) for supplier in ourSuppliers(request=self.request)
]
# Second Method
self.filters['supplier'].extra.update({
'choices': [(supplier.id, supplier.name) for supplier in ourSuppliers(request=self.request)]
})
Originally posted here
You can handle this in the FilterSet.__init__ method. Something like the below (Note that I haven't tested it, may require some fiddling):
class ProductsFilter(django_filters.FilterSet):
subcategory= django_filters.ChoiceFilter(lookup_expr='iexact', choices=[], required=False)
def __init__(self, category, *args, **kwargs):
super(ProductsFilter, self).__init__(*args, **kwargs)
choices = self.fields['subcategory'].extra['choices']
choices += [
(subcat.name, subcat.name) for subcat
in SubCategory.objects.filter(category=category)
]
class Meta:
model = Products