简体   繁体   中英

django - CBV - pass in multiple values from url

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.

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM