django - CBV - pass in multiple values from url - python

I am utterly confused and mortified by CBVs, seeking for help.
So I've designed the model structure and decided the url patterns as following, but simply can't write a valid CBV to facilitate the urls:
models.py
class Category(models.Model):
'''Category for men's and women's items'''
men = models.BooleanField()
women = models.BooleanField()
name = models.CharField(max_length=100)
description = models.CharField(max_length=300, blank=True)
uploaded_date = models.DateTimeField(
auto_now_add=True, null=True, blank=True)
class Meta():
verbose_name_plural = 'Categories'
def __str__(self):
return ("Men's " + self.name) if self.men else ("Women's " + self.name)
class SubCategory(models.Model):
'''Sub-category for the categories (not mandatory)'''
category = models.ForeignKey(Category, on_delete=models.CASCADE)
name = models.CharField(max_length=100)
description = models.CharField(max_length=300, blank=True)
uploaded_date = models.DateTimeField(
auto_now_add=True, null=True, blank=True)
class Meta():
verbose_name = 'Sub-category'
verbose_name_plural = 'Sub-categories'
def __str__(self):
return ("Men's " + self.name) if self.category.men else ("Women's " + self.name)
class Item(models.Model):
'''Each item represents a product'''
category = models.ForeignKey(Category, on_delete=models.CASCADE)
subcategory = models.ForeignKey(
SubCategory, on_delete=models.CASCADE, null=True, blank=True)
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
price = models.IntegerField(default='0')
discount = models.IntegerField(null=True, blank=True)
uploaded_date = models.DateTimeField(
auto_now_add=True, null=True, blank=True)
class Meta:
ordering = ['-uploaded_date']
def __str__(self):
return self.name
def discounted_price(self):
'''to calculate the price after discount'''
return int(self.price * (100 - self.discount) * 0.01)
class ItemImage(models.Model):
item = models.ForeignKey(Item, on_delete=models.CASCADE)
image = models.ImageField(upload_to='itemimages', null=True, blank=True)
urls.py
app_name = 'boutique'
urlpatterns = [
# show index page
path('', views.IndexView.as_view(), name='index'),
# show categories of products for men or women
path('<slug:gender>/', views.ItemListView.as_view(), name='show-all'),
# show a specific category for men or women
path('<slug:gender>/cat_<int:category_pk>/', views.ItemListView.as_view(), name='category'),
# show a specific subcategory under a specific category for men or women
path('<slug:gender>/cat_<int:category_pk>/subcat_<int:subcategory_pk>/', views.ItemListView.as_view(), name='subcategory'),
# show a specific item
path('item_<int:item_pk>/', views.ItemDetailView.as_view(), name='item'),
]
views.py
class IndexView(ListView):
'''landing page'''
model = Category
template_name = 'boutique/index.html'
context_object_name = 'categories'
class ItemListView(ListView):
'''display a list of items'''
# model = Category ??? what's the point of declaring model when get_context_data() ???
template_name = 'boutique/items.html'
context_object_name = 'categories'
paginate_by = 12
def get_object(self):
obj = get_object_or_404(Category, pk=self.kwargs.get('category_pk'))
return obj
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all() # for rendering nav bar data
return context
class ItemDetailView(DetailView):
'''display an individual item'''
# model = Item
template_name = 'boutique/item.html'
context_object_name = 'item'
def get_object(self):
return get_object_or_404(Item, pk=self.kwargs['item_pk'])
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = Category.objects.all()
return context
items.html
<a href="{% url 'boutique:show-all' 'women' %}> link to categories of product for women </a>
<a href="{% url 'boutique:category' 'women' category.pk %}> link to a cat for women </a>
<a href="{% url 'boutique:subcategory' 'women' category.pk subcategory.pk %}> link to a subcat under a specific cat for women </a>
Essentially, as you can see, I'd like the ItemListView to render multiple url paths depending on what value passed into the CBV... I can do it (pass multiple values) in a FBV, however, utterly confused by the mechanism of the CBVs...
So if anyone could write an example ItemListView and according anchor url template tags (if mine are incorrect), it would be tremendulent!!! Thanks!!!
EDIT 1
class ItemListView(ListView):
'''display a list of items'''
model = Item
template_name = 'boutique/items.html'
# paginate_by = 12
def get_queryset(self):
# get original queryset: Item.objects.all()
qs = super().get_queryset()
# filter items: men/women
if self.kwargs['gender'] == 'women':
qs = qs.filter(category__women=True)
elif self.kwargs['gender'] == 'men':
qs = qs.filter(category__men=True)
if self.kwargs.get('category_pk'):
qs = qs.filter(category=self.kwargs.get('category_pk'))
if self.kwargs.get('subcategory_pk'):
qs = qs.filter(subcategory=self.kwargs.get('subcategory_pk'))
# print(qs)
return qs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# add categories for navbar link texts
context['categories'] = Category.objects.all()
if self.kwargs.get('gender') == 'women':
context['category_shown'] = Category.objects.filter(women=True)
if self.kwargs.get('gender') == 'men':
context['category_shown'] = Category.objects.filter(men=True)
if self.kwargs.get('category_pk'):
context['category_shown']=get_object_or_404(Category, pk=self.kwargs.get('category_pk'))
if self.kwargs.get('subcategory_pk'):
context['subcategory_shown']=get_object_or_404(SubCategory, pk=self.kwargs.get('subcategory_pk'))
# print(context)
return context
After FiddleStix' answer (I haven't gone thu bluegrounds thread yet), I tried and managed to make everything work except ItemDetailView.
The urls are working fine and the filtering of get_queryset function is working fine, however,
Question 1: I wonder this might not be DRY enough?! Nevertheless, it's working. so thanks!! But could it be dryer??
Question 2: when ItemDetailView runs, the urls appear to be correct, however, the page redirect to a page rendering all items from all categories...
class ItemDetailView(DetailView):
'''display an individual item'''
# model = Item
template_name = 'boutique/item.html'
def get_object(self):
print(get_object_or_404(Item, pk=self.kwargs.get('item_pk')))
return get_object_or_404(Item, pk=self.kwargs.get('item_pk'))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# add categories for navbar link texts
# context['categories'] = Category.objects.all()
print(context)
return context
Neither is the object nor the context being printed out... and it doesn't prompt any error, either. I must have made a silly mistake somewhere!!
Answer to Question 2:
view.py
class ItemDetailView(DetailView):
'''display an individual item'''
model = Item
template_name = 'boutique/item.html'
The DetailView class should be simple if no customisation is needed, the problem is in the urlpatterns:
url.py
urlpatterns = [
path('item_<int:pk>', view.ItemDetailView.as_view(), name='item'),
]
Always use <pk> as the value passed in DetailView as it's the default. I used item_<int:item_pk> as the path url. That's why I had to use get_object() to manually get the item object (to override the default get_object()). As #bluegrounds answer suggests that the reason Class-based Views work well is they save time, for the default functions they possess.
If you wish to use item_<int:item_pk>, CBV offers the flexibility as well: simply override pk_url_kwargs = 'item_pk' in the View class - Feel free to check my other question: DetailView - get_object function confusion for clarification. #neverwalkaloner 's answer is very straightforward.

So first things first, CBVs...
They work by having "default" behaviours built into each one of them. For the ListView class, take this example view:
class ArticleListView(ListView):
model = Article
along with an example urlpattern:
path('all-articles/', ArticleListView.as_view())
and that's it. This is all that is essential for a ListView to work. This ArticleListView view would look for a template called article_list.html and in it you can use the context variable object_list to access all the Article objects that the class gets for you without you having to write the QuerySet explicitly.
Of course you can change these values, and customize the QuerySet, and do all kinds of things, but for that you'll have to study the docs. I personally find ccbv much easier to read than the docs. So for example you can see in ccbv's page about ListViews that the context_object_name = None which defaults to object_list, as mentioned above. You can change that to, for example context_object_name = 'my_articles'. You could also set the template_name = 'my_articles.html' and that will override the default template name pattern of <model>_list.html.
Now, about your code,
If you're sure that you want your URL structure to stay like it is, you could have your class view as follows to get the functionality you need:
class ItemListView(ListView):
template_name = 'boutique/items.html'
context_object_name = 'categories'
paginate_by = 12
def get_queryset(self):
# This method should return a queryset that represents the items to be listed in the view.
# I think you intend on listing categories in your view, in which case consider changing the view's name to CategoryListView. Just sayin'...
# An instance of this view has a dictionary called `kwargs` that has the url parameters, so you can do the following:
# You need some null assertions here because of the way you've setup your URLs
qs = Categories.objects.filter(men=self.kwargs['gender'], pk=self.kwargs['category_pk'])
return qs
As you can see, we didn't set many things in this class view for it to work. Namely, we didn't set the model variable as we did previously. That's because we wouldn't need it. The part that uses that variable was in the default get_queryset() method and we've overridden that method. See CCBV for more info on the default implementation of get_queryset().
Now the template will be supplied with the objects from get_queryset(), under the name categories, because that's what we set context_object_name's value to be.
NOTE: The variable model is used in other places other than get_queryset() such as the default template_name. The default template name is derived from the model name and the template_name_suffix. So if you don't set the model variable, make sure to set the template_name manually.
I'm not sure about your application's logic but I think you should change the Category model to have only one Boolean field that denotes gender. For example men if it is True and women if it's False. That way a category can't be for both men and women at the same time (unless that is something you need), and it also can't be for neither, because currently you can have a category be false for the both gender fields, which doesn't really make sense.
I would actually suggest a completely different solution that involves CHOICES, as such:
gender = models.IntegerField(null=False, CHOICES=[(1,'Men'), (2,'Women'), (3,'Other'), (4,'Unisex')], default=3)
This would store a number in the database that denotes the gender, and in your app you'll only see the correlating string (gender) to that number.
I have not tried this code on my machine so I might have missed a few things, but I hope I clarified the overall workings of CBVs.

To answer you main question, your ItemList should set model=models.Item, as alluded to in the error message, because it is meant to be a list of items.
I would set up your urls.py so that /items/ or /item_list/ goes to ItemListView.as_view(). If you then want to filter your list of items, I would not do it by changing the URL to /items/women/. Instead, I would use a URL query string like /items/?gender=women. How to do that is explained here but basically:
class ItemListView(ListView):
model = Item
template_name = "item_list.html"
paginate_by = 100
def get_queryset(self):
filter_val = self.request.GET.get('gender', 'some_default')
queryset = Item.objects.filter(
... # You'll have to work this bit out
)
return queryset
def get_context_data(self, **kwargs):
context = super(MyView, self).get_context_data(**kwargs)
context['gender'] = self.request.GET.get('gender', 'some_default')
return context

Related

How to make seperate comment section for each item

I am making django app I have a problem I dont have idea how to make seperate comment section to each Item. I do not want to have same comments for every Item on a page.
models.py
class Comment(models.Model):
comment_user = models.OneToOneField(User, on_delete=CASCADE)
item = models.OneToOneField(Item, on_delete=CASCADE)
content = models.TextField(default='')
views.py
class ShopDetailView(DetailView):
model = Item
template_name = 'shop/detail.html'
context_object_name = 'item'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['comments'] = Comment.objects.all()
return context
For a DetailView, you can obtain the related comments with:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['comments'] = Comment.objects.filter(item=self.object)
return context
It makes however not much sense to work with a OneToOneField here, since that means that each item can only have at most one comment. You probably want a many-to-one relation, and thus work with a ForeignKey [Django-doc]:
class Comment(models.Model):
comment_user = models.OneToOneField(User, on_delete=CASCADE)
item = models.ForeignKey(Item, on_delete=CASCADE)
content = models.TextField(default='')

Finding the current name of the model being used in a class based view, then using it as one of two filter parameters

My problem is that I am trying to find the name of the model currently being used in the Class based Detail View, then using that name as one of two search parameters (The Model used in the class based view is a foreign key of another model I'm trying to filter)
I can't figure out how to find out how to filter by the current model being used.
Here is my models.py
class MyProjects(models.Model):
title = models.CharField(max_length=30)
description = models.CharField(max_length=140)
class Post(models.Model):
title = models.CharField(max_length=50)
content = models.TextField()
project = models.ForeignKey(MyProjects, on_delete=models.CASCADE, null=True, default=None)
And here is my views.py
class ProjectView(DetailView):
model = MyProjects
def get_context_data(self, **kwargs):
CurrentProject = get_object_or_404(MyProjects, title=self.kwargs['MyProjects'])
completed = Post.objects.filter(status='Completed', project= CurrentProject)
inProgress = Post.objects.filter(status='InProgress', project= CurrentProject)
posts = Post.objects.filter(project= CurrentProject)
Features = Post.objects.filter(ticket_type='Features', project= CurrentProject)
context = super().get_context_data(**kwargs)
context['posts '] = posts
context['Features '] = Features
context['completed '] = completed
context['inProgress '] = inProgress
context['projects'] = projects
return context
Thanks
I think you are not using DetailView correctly, but you are in the good path.
It is important to remember that self.object references the object that the view is operating on. In your code you name it CurrentProject, so no need to query for the object, you already have it. In the general case, you pass the id of the object to the view as a parameter of the url.
Concluding...
I think the view should look something like this,
class ProjectView(DetailView):
model = MyProjects
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
completed = Post.objects.filter(status='Completed', project=self.object)
in_progress = Post.objects.filter(status='InProgress', project=self.object)
posts = Post.objects.filter(project=self.object)
features = Post.objects.filter(ticket_type='Features', project=self.object)
context['posts'] = posts
context['features'] = features
context['completed'] = completed
context['inProgress'] = in_progress
context['projects'] = projects
return context
And, the url of the view should something like this,
path('<int:pk>/', views.ProjectView.as_view(), name='project-detail')

How to add multiple tags to URL in Django and Taggit

I have this model here, where I pass the tags using TaggableManager.
class Post(models.Model):
title = models.CharField(max_length=120)
content = models.TextField()
date_published = models.DateField(auto_now_add=True)
tags = TaggableManager()
slug = models.SlugField(max_length=40, blank=True)
And this is how I view the data:
class TagView(ListView):
model = Post
template_name = 'tagview.html'
paginate_by = 10
def get_queryset(self):
return get_list_or_404(Post, tags__slug = self.kwargs.get('tag'))
def get_context_data(self, *, object_list=None, **kwargs):
context = super().get_context_data(**kwargs)
context['tag'] = self.kwargs.get('tag')
return context
This works as per 1 tag, but I want to include multiple tags, I've searched a lot but couldn't think of how to implement it. This is my urls.py - path('tag/<slug:tag>/', views.TagView.as_view(), name='tagview'),
I want something like /tags/[django][python]/ etc..
a alternative approach witch will accomplish the same end result will be to
use query_params this means you will leave the url open to receive any arguments witch will follow the base url to this path, and you will check to see if the query_params has the 'tag' key's we are looking for.
the URL request would look something like this:
http://127.0.0.1:8000/blog?tag=python&tag=django
#urls.py
path('', views.TagView.as_view(), name='tagview'),
#views.py
class TagView()
def get(self, request):
posts = Post.objects.all()
is_there_a_tag = request.query_params.get('tag')
if is_there_a_tag is not None:
tag_list = request.query_params.getlist('tag')
matches = self.posts.filter(tags__name__in=tag_list).distinct()

Django Model set #property with filter and show it on template

I have case that need to count and show user that doesn't have a blog.
here my views.py
class Blog(models.Model):
desc = models.TextField(blank=True, null=True)
user = models.ForeignKey(Employee, null=True, on_delete=models.CASCADE, related_name='blogs')
#property
def DOESN_HAVE_BLOG(self):
blog = Self.Blog.all().values_list('user', flat=True)
value = Self.User.exclude(id__in=blog)
return value
here mytemplate.html
{{ DOESN_HAVE_BLOG.count }}
but its doesn't work
This is not something that you would do as a model property. A property relates to an actual instance of Blog, but you are looking for Employees that have no instances at all.
Instead, you should do the query separately in the view - a good place for this is in get_context_data.
class MyView(ListView):
...
def get_context_data(self, *args, **kwargs):
data = super().get_context_data(*args, **kwargs)
data['DOESN_HAVE_BLOG'] = Employee.objects.filter(blog=None)
return data

Django pagination - how to redirect back to a ListView and a page number

In a ListView I render a table using the Paginator with paginate_by = 5.
In each row I have a Button that opens the UpdateView.
After a successful update I'm back on my ListView but always on the first page.
How can I change the success_url so that I'm back on the page number, from where I opened the UpdateView?
Is there a proper way in Django to solve this?
#views.py
class Orders_list(ListView):
model = Order
context_object_name = "orders"
queryset = Order.objects.all()
paginate_by = 5
template_name = "orders/orders_list.html"
#http://127.0.0.1:8000/orders/?page=4
class Orders_update(UpdateView):
model = Order
form_class = Order_UpdateForm
template_name = "orders/orders_update.html"
#success_url = '../../../orders/'
success_url = reverse_lazy('orders')
#urls.py
urlpatterns = [
url(r'^orders/$', Orders_list.as_view()),
url(r'^orders/detail/(?P<pk>\d+)/$', Orders_detail.as_view()),
]
#forms.py
class Order_UpdateForm(forms.ModelForm):
class Meta:
model = Order
fields = ['order_text', 'customer', 'cyclist']
def __init__(self, *args, **kwargs):
super(Order_UpdateForm, self).__init__(*args, **kwargs)
self.fields['cyclist'].queryset = Cyclist.objects.filter(active=True)
#models.py
class Order(models.Model):
customer = models.ForeignKey('Customer')
cyclist = models.ForeignKey('Cyclist')
order_text = models.CharField(max_length=200)
pick_up = models.CharField(max_length=200)
destination = models.CharField(max_length=200)
created_date = models.DateTimeField(default=timezone.now)
changed_date = models.DateTimeField(blank=True, null=True)
def created(self):
self.changed_date = timezone.now()
self.save()
def __str__(self):
return self.order_text
class Customer(models.Model):
company_name = models.CharField(max_length=200)
created_date = models.DateTimeField(default=timezone.now)
changed_date = models.DateTimeField(blank=True, null=True)
def created(self):
self.changed_date = timezone.now()
self.save()
def __str__(self): return self.company_name
class Cyclist(models.Model):
lastname = models.CharField(max_length=20)
firstname = models.CharField(max_length=20)
created_date = models.DateTimeField(default=timezone.now)
changed_date = models.DateTimeField(blank=True, null=True)
active = models.BooleanField(default=True)
def created(self):
self.changed_date = timezone.now()
self.save()
def __str__(self):
return self.lastname
Update
I found a way to solve this (may be). But I still miss something.
What I did so far:
In the ListView template I extended the Url...
window.open("{{ request.path }}update/" + id + "/?page=" + {{ page_obj.number }});
...then in the view I overwrote the get_context_data method...
#views.py
class Orders_update(UpdateView):
model = Order
form_class = Order_UpdateForm
template_name = "orders/orders_update.html"
page = "1" # Assigned
def get_context_data(self, **kwargs):
context = super(Orders_update, self).get_context_data(**kwargs)
page = self.request.GET.get('page') # Modified
return context
success_url = '../../../orders/?page=' + str(page) # Should be used here to redirect to a specific page on a ListView
...but now...how can I access the variable page in views.py in the success_url?
I think this is something very basic (hopefully) and I think I don't understand how scope works in Python.
This is what finally worked for me. Basically I just overwrote the get_success_url to have access to the page parameter from the request.
class Orders_update(UpdateView):
model = Order
form_class = Order_UpdateForm
template_name = "orders/orders_update.html"
def get_success_url(self):
global page
page = self.request.GET.get('page')
return reverse_lazy('orders')
I just got finished pulling my hair out with this same problem. Here is what I did. Start in views.py in your ListView, Order_list, and add context data:
def get_context_data(self, **kwargs):
kwargs['current_page'] = self.request.GET.get('page', 1)
return super().get_context_data(**kwargs)
The above code passes only the current page number into the ListView template.
Then go to the corresponding list view template and add the following in the object list loop:
{% if is_paginated %}
<a href="{% url 'order_update' pk=order.pk %}?next={{current_page}}">Update
</a>
{% else %}
<a href="{% url 'order_update' pk=order.pk %}">Update
{% endif %}
The if statement allows the page to work without pagination. Note the ?next={{current_page}} variable. The "current_page" comes from the context_data in the view. This bit passes the current page number to the "next" variable in the url of the page in the anchor tag. This is what my update view url looks like with this variable in it: http://localhost:8000/food/406/update?next=1. See the ?next=1 in the url.
Next, go back to views.py to your UpdateView, Order_update, and change the success url with this code:
def get_success_url(self, **kwargs):
if self.request.GET.get('next'):
return f"/orders?page={self.request.GET.get('next', 1)}"
else:
return '/orders'
You'll have to remove the success_url declaration in this view. The def get_success_url function wants a string. Reverse seems to work well, but a formatted string will not work in a redirect() statement. It needs to be a plain string. See TypeError: quote_from_bytes() expected bytes after redirect for more on that.
If I broke that down too much, I'm sorry. I don't mean to insult your knowledge. I'm still learning and I wrote this the way I would have needed it earlier today before I figured this out.

Categories

Resources