views:

1915

answers:

4

Hello, I'm in the progress of learning Django at the moment but I can't figure out how to solve this problem on my own. I'm reading the book Developers Library - Python Web Development With Django and in one chapter you build a simple CMS system with two models (Story and Category), some generic and custom views together with templates for the views.

The book only contains code for listing stories, story details and search. I wanted to expand on that and build a page with nested lists for categories and stories.

- Category1
-- Story1
-- Story2
- Category2
- Story3 etc.

I managed to figure out how to add my own generic object_list view for the category listing. My problem is that the Story model have STATUS_CHOICES if the Story is public or not and a custom manager that'll only fetch the public Stories per default. I can't figure out how to tell my generic Category list view to also use a custom manager and only fetch the public Stories. Everything works except that small problem. I'm able to create a list for all categories with a sub list for all stories in that category on a single page, the only problem is that the list contains non public Stories.

I don't know if I'm on the right track here. My urls.py contains a generic view that fetches all Category objects and in my template I'm using the *category.story_set.all* to get all Story objects for that category, wich I then loop over.

I think it would be possible to add a if statement in the template and use the VIEWABLE_STATUS from my model file to check if it should be listed or not. The problem with that solution is that it's not very DRY compatible.

Is it possible to add some kind of manager for the Category model too that only will fetch in public Story objects when using the story_set on a category?

Or is this the wrong way to attack my problem?

Related code

urls.py (only category list view):

urlpatterns += patterns('django.views.generic.list_detail',
    url(r'^categories/$', 'object_list', {'queryset': Category.objects.all(),
                                          'template_object_name': 'category'
                                         }, name='cms-categories'),

models.py:

from markdown import markdown
import datetime
from django.db import models
from django.db.models import permalink
from django.contrib.auth.models import User

VIEWABLE_STATUS = [3, 4]

class ViewableManager(models.Manager):
    def get_query_set(self):
        default_queryset = super(ViewableManager, self).get_query_set()
        return default_queryset.filter(status__in=VIEWABLE_STATUS)

class Category(models.Model):
    """A content category"""
    label = models.CharField(blank=True, max_length=50)
    slug = models.SlugField()

    class Meta:
        verbose_name_plural = "categories"

    def __unicode__(self):
        return self.label

    @permalink
    def get_absolute_url(self):
        return ('cms-category', (), {'slug': self.slug})

class Story(models.Model):
    """A hunk of content for our site, generally corresponding to a page"""

    STATUS_CHOICES = (
        (1, "Needs Edit"),
        (2, "Needs Approval"),
        (3, "Published"),
        (4, "Archived"),
    )

    title = models.CharField(max_length=100)
    slug = models.SlugField()
    category = models.ForeignKey(Category)
    markdown_content = models.TextField()
    html_content = models.TextField(editable=False)
    owner = models.ForeignKey(User)
    status = models.IntegerField(choices=STATUS_CHOICES, default=1)
    created = models.DateTimeField(default=datetime.datetime.now)
    modified = models.DateTimeField(default=datetime.datetime.now)

    class Meta:
        ordering = ['modified']
        verbose_name_plural = "stories"

    def __unicode__(self):
        return self.title

    @permalink
    def get_absolute_url(self):
        return ("cms-story", (), {'slug': self.slug})

    def save(self):
        self.html_content = markdown(self.markdown_content)
        self.modified = datetime.datetime.now()
        super(Story, self).save()

    admin_objects = models.Manager()
    objects = ViewableManager()

category_list.html (related template):

{% extends "cms/base.html" %}
{% block content %}
    <h1>Categories</h1>
    {% if category_list %}
        <ul id="category-list">
        {% for category in category_list %}
            <li><a href="{{ category.get_absolute_url }}">{{ category.label }}</a></li>
            {% if category.story_set %}
                <ul>
                    {% for story in category.story_set.all %}
                        <li><a href="{{ story.get_absolute_url }}">{{ story.title }}</a></li>
                    {% endfor %}
                </ul>
            {% endif %}
        {% endfor %}
        </ul>
    {% else %}
        <p>
            Sorry, no categories at the moment.
        </p>
    {% endif %}
{% endblock %}
+5  A: 

I find the documentation slightly unclear, but the use_for_related_fields Manager attribute may be what you're looking for. When you're trying it, note that this functionality uses _default_manager, which will always point at the first Manager declared in a Model class, regardless of what attribute name it's been given. You can always customise which items are editable in your admin site by overriding the queryset method in the appropriate ModelAdmin class instead.

Failing that...

class Category(models.Model):
    def public_stories(self):
        return Story.objects.filter(category=self)

...and...

{% for story in category.public_stories %}

This at least avoids duplicating the logic which determines which stories are viewable.

insin
Thank you. I wasn't able to figure out how the use_for_related_fields works. Tried to add it on my ViewableManager but no difference. The second solution worked fine. I just have to remember to use category.public_stories instead of category.story_set.
Daniel Johansson
+1  A: 

To elaborate on @insin's answer, you should add use_for_related_fields = True to the ViewableManager class. That way, Category.story_set will return an instance of the custom manager, which has the get_query_set method you need. Your template tag will look like

{% category.story_set.get_query_set %}

Edit: @insin's answer is much better, I'd use that.

Justin Voss
Thank you for the reply but I wasn't able to figure this out. I added use_for_related_fields = True in my ViewableManager and tried the category.story_set.get_query_set but it still returned non public stories.
Daniel Johansson
+1  A: 

If you want a template specific solution you can create a custom filter. This way you can avoid touching your model class

This will be your filter:

from django import template

register = template.Library()

@register.filter
def viewable(queryset):
    return queryset.filter(status__in=VIEWABLE_STATUS)

And you can use it in your template

{% category.story_set|viewable %}
Saurabh
A: 

Just change the positions of your manager declarations.

From

admin_objects = models.Manager()
objects = ViewableManager()

To

objects = ViewableManager()
admin_objects = models.Manager()

According to the documentation:

The default manager on a class is either the first manager declared on the class, if that exists, or the default manager of the first abstract base class in the parent hierarchy, if that exists. If no default manager is explicitly declared, Django's normal default manager is used.

Edward