changeset 167:cf9f9d4c4d54

Adding a query to the forums to get all the topics with unread posts. This is for ticket #54.
author Brian Neal <bgneal@gmail.com>
date Sun, 24 Jan 2010 22:33:11 +0000
parents 8acf5be27f18
children e6d4dfdfbc64
files gpp/forums/models.py gpp/forums/templatetags/forum_tags.py gpp/forums/unread.py gpp/forums/urls.py gpp/forums/views.py gpp/templates/forums/forum_index.html gpp/templates/forums/index.html gpp/templates/forums/topic_icons_tag.html gpp/templates/forums/topic_list.html gpp/templates/forums/topic_page_range_tag.html media/css/base.css
diffstat 11 files changed, 261 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- a/gpp/forums/models.py	Sun Jan 17 20:24:01 2010 +0000
+++ b/gpp/forums/models.py	Sun Jan 24 22:33:11 2010 +0000
@@ -10,9 +10,6 @@
 from core.markup import site_markup
 
 
-POST_EDIT_DELTA = datetime.timedelta(seconds=3)
-
-
 class Category(models.Model):
     """
     Forums belong to a category, whose access may be assigned to groups.
@@ -57,17 +54,23 @@
         Returns a queryset containing the forums that the given user can
         "see" due to authenticated status, superuser status and group membership.
         """
+        qs = self._for_user(user)
+        return qs.select_related('category', 'last_post', 'last_post__user')
+
+    def forum_ids_for_user(self, user):
+        """Returns a list of forum IDs that the given user can "see"."""
+        qs = self._for_user(user)
+        return qs.values_list('id', flat=True)
+
+    def _for_user(self, user):
+        """Common code for the xxx_for_user() methods."""
         if user.is_superuser:
             qs = self.all()
         else:
-            user_groups = []
-            if user.is_authenticated():
-                user_groups = user.groups.all()
-
-            qs = self.filter(Q(category__groups__isnull=True) | \
+            user_groups = user.groups.all() if user.is_authenticated() else []
+            qs = self.filter(Q(category__groups__isnull=True) |
                     Q(category__groups__in=user_groups))
-
-        return qs.select_related('category', 'last_post', 'last_post__user')
+        return qs
 
 
 class Forum(models.Model):
--- a/gpp/forums/templatetags/forum_tags.py	Sun Jan 17 20:24:01 2010 +0000
+++ b/gpp/forums/templatetags/forum_tags.py	Sun Jan 24 22:33:11 2010 +0000
@@ -172,3 +172,20 @@
         'user_count': user_count,
         'latest_user': latest_user,
     }
+
+
+@register.inclusion_tag('forums/topic_icons_tag.html')
+def topic_icons(topic):
+    """Displays the "unread", "sticky", and "locked" icons for a given topic."""
+    return {
+        'topic': topic,
+        'MEDIA_URL': settings.MEDIA_URL,
+    }
+
+
+@register.inclusion_tag('forums/topic_page_range_tag.html')
+def topic_page_range(topic):
+    """Displays the page range links for a topic."""
+    return {
+        'topic': topic,
+    }
--- a/gpp/forums/unread.py	Sun Jan 17 20:24:01 2010 +0000
+++ b/gpp/forums/unread.py	Sun Jan 24 22:33:11 2010 +0000
@@ -5,7 +5,9 @@
 """
 import datetime
 
-from forums.models import ForumLastVisit, TopicLastVisit, Topic
+from django.db.models import Q
+
+from forums.models import ForumLastVisit, TopicLastVisit, Topic, Forum
 
 
 THRESHOLD = datetime.timedelta(days=7)
@@ -186,3 +188,53 @@
     else:
         for post in posts:
             post.unread = post.creation_date > tlv.last_visit
+
+#######################################################################
+
+def get_unread_topics(user):
+    """Returns a list of topics the user hasn't read yet."""
+    #import pdb; pdb.set_trace()
+
+    # This is only available to authenticated users
+    if not user.is_authenticated():
+        return []
+
+    now = datetime.datetime.now()
+
+    # Obtain list of forums the user can view
+    forums = Forum.objects.forums_for_user(user)
+
+    # Get forum last visit records for the forum ids
+    flvs = ForumLastVisit.objects.filter(user=user,
+            forum__in=forums).select_related()
+    flvs = dict([(flv.forum.id, flv) for flv in flvs])
+
+    unread_topics = []
+    topics = Topic.objects.none()
+    for forum in forums:
+        # if the user hasn't visited the forum, create a last
+        # visit record set to "now"
+        if not forum.id in flvs:
+            flv = ForumLastVisit(user=user, forum=forum, begin_date=now,
+                    end_date=now)
+            flv.save()
+        else:
+            flv = flvs[forum.id]
+            topics |= Topic.objects.filter(forum=forum,
+                update_date__gt=flv.begin_date).order_by('-update_date').select_related()
+
+    if topics is not None:
+        # get all topic last visit records for the topics of interest
+
+        tlvs = TopicLastVisit.objects.filter(user=user, topic__in=topics)
+        tlvs = dict([(tlv.topic.id, tlv) for tlv in tlvs])
+
+        for topic in topics:
+            if topic.id in tlvs:
+                tlv = tlvs[topic.id]
+                if topic.update_date > tlv.last_visit:
+                    unread_topics.append(topic)
+            else:
+                unread_topics.append(topic)
+
+    return unread_topics
--- a/gpp/forums/urls.py	Sun Jan 17 20:24:01 2010 +0000
+++ b/gpp/forums/urls.py	Sun Jan 24 22:33:11 2010 +0000
@@ -22,5 +22,6 @@
     url(r'^post/(\d+)/$', 'goto_post', name='forums-goto_post'),
     url(r'^post/new/(?P<topic_id>\d+)/$', 'new_post', name='forums-new_post'),
     url(r'^quick-reply/$', 'quick_reply_ajax', name='forums-quick_reply'),
+    url(r'^unread/$', 'unread_topics', name='forums-unread_topics'),
 )
 
--- a/gpp/forums/views.py	Sun Jan 17 20:24:01 2010 +0000
+++ b/gpp/forums/views.py	Sun Jan 24 22:33:11 2010 +0000
@@ -25,7 +25,7 @@
 from forums.forms import NewTopicForm, NewPostForm, PostForm, MoveTopicForm, \
         SplitTopicForm
 from forums.unread import get_forum_unread_status, get_topic_unread_status, \
-        get_post_unread_status
+        get_post_unread_status, get_unread_topics
 
 from bio.models import UserProfile
 #######################################################################
@@ -33,18 +33,50 @@
 TOPICS_PER_PAGE = 50
 POSTS_PER_PAGE = 20
 
+
+def get_page_num(request):
+    """Returns the value of the 'page' variable in GET if it exists, or 1
+    if it does not."""
+
+    try:
+        page_num = int(request.GET.get('page', 1))
+    except ValueError:
+        page_num = 1
+
+    return page_num
+
+
 def create_topic_paginator(topics):
    return DiggPaginator(topics, TOPICS_PER_PAGE, body=5, tail=2, margin=3, padding=2)
 
 def create_post_paginator(posts):
    return DiggPaginator(posts, POSTS_PER_PAGE, body=5, tail=2, margin=3, padding=2)
 
+
+def attach_topic_page_ranges(topics):
+    """Attaches a page_range attribute to each topic in the supplied list.
+    This attribute will be None if it is a single page topic. This is used
+    by the templates to generate "goto page x" links.
+    """
+    for topic in topics:
+        if topic.post_count > POSTS_PER_PAGE:
+            pp = DiggPaginator(range(topic.post_count), POSTS_PER_PAGE, 
+                    body=2, tail=3, margin=1)
+            topic.page_range = pp.page(1).page_range
+        else:
+            topic.page_range = None
+
 #######################################################################
 
 def index(request):
     """
     This view displays all the forums available, ordered in each category.
     """
+    # check for special forum queries
+    query = request.GET.get("query")
+    if query == "unread":
+        return HttpResponseRedirect(reverse('forums-unread_topics'))
+
     forums = Forum.objects.forums_for_user(request.user)
     get_forum_unread_status(forums, request.user)
     cats = {}
@@ -77,23 +109,13 @@
     get_topic_unread_status(forum, topics, request.user)
 
     paginator = create_topic_paginator(topics)
-    page_num = int(request.GET.get('page', 1))
+    page_num = get_page_num(request)
     try:
         page = paginator.page(page_num)
     except InvalidPage:
         raise Http404
 
-    # Attach "goto page" navigation links onto those topics that have
-    # more than 1 page:
-
-    for topic in page.object_list:
-        if topic.post_count > POSTS_PER_PAGE:
-            pp = DiggPaginator(range(topic.post_count), POSTS_PER_PAGE, 
-                    body=2, tail=3, margin=1)
-            topic.page_range = pp.page(1).page_range
-        else:
-            topic.page_range = None
-
+    attach_topic_page_ranges(page.object_list)
 
     # we do this for the template since it is rendered twice
     page_nav = render_to_string('forums/pagination.html', {'page': page})
@@ -124,7 +146,7 @@
     posts = topic.posts.select_related()
 
     paginator = create_post_paginator(posts)
-    page_num = int(request.GET.get('page', 1))
+    page_num = get_page_num(request)
     try:
         page = paginator.page(page_num)
     except InvalidPage:
@@ -525,7 +547,7 @@
 
     topics = forum.topics.select_related('user', 'last_post', 'last_post__user')
     paginator = create_topic_paginator(topics)
-    page_num = int(request.REQUEST.get('page', 1))
+    page_num = get_page_num(request)
     try:
         page = paginator.page(page_num)
     except InvalidPage:
@@ -619,6 +641,30 @@
         context_instance=RequestContext(request))
 
 
+@login_required
+def unread_topics(request):
+    topics = get_unread_topics(request.user)
+
+    paginator = create_topic_paginator(topics)
+    page_num = get_page_num(request)
+    try:
+        page = paginator.page(page_num)
+    except InvalidPage:
+        raise Http404
+
+    attach_topic_page_ranges(page.object_list)
+
+    # we do this for the template since it is rendered twice
+    page_nav = render_to_string('forums/pagination.html', {'page': page})
+
+    return render_to_response('forums/topic_list.html', {
+        'title': 'Topics With Unread Posts',
+        'page': page,
+        'page_nav': page_nav,
+        },
+        context_instance=RequestContext(request))
+
+
 def _can_moderate(forum, user):
     """
     Determines if a user has permission to moderate a given forum.
--- a/gpp/templates/forums/forum_index.html	Sun Jan 17 20:24:01 2010 +0000
+++ b/gpp/templates/forums/forum_index.html	Sun Jan 24 22:33:11 2010 +0000
@@ -31,21 +31,10 @@
    {% for topic in page.object_list %}
       <tr class="{% cycle 'odd' 'even' %}">
          <td>
-            {% if topic.has_unread %}<img src="{{ MEDIA_URL }}icons/new.png" alt="New Posts" title="New Posts" class="forums-topic-icon" />{% endif %}
-            {% if topic.sticky %}<img src="{{ MEDIA_URL }}icons/asterisk_orange.png" alt="Sticky" title="Sticky" class="forums-topic-icon" />{% endif %}
-            {% if topic.locked %}<img src="{{ MEDIA_URL }}icons/lock.png" alt="Locked" title="Locked"
-            class="forums-topic-icon" />{% endif %}
+            {% topic_icons topic %}
             <h4><a href="{{ topic.get_absolute_url }}">{{ topic.name }}</a></h4>
             {% if topic.page_range %}
-            <span class="small">Goto page: [
-            {% for n in topic.page_range %}
-               {% if n %}
-               <a href="{{ topic.get_absolute_url }}?page={{ n }}">{{ n }}</a>
-               {% else %}
-               &hellip;
-               {% endif %}
-            {% endfor %}
-            ]</span>
+               {% topic_page_range topic %}
             {% endif %}
          </td>
          <td class="forum-index_replies">{{ topic.reply_count }}</td>
--- a/gpp/templates/forums/index.html	Sun Jan 17 20:24:01 2010 +0000
+++ b/gpp/templates/forums/index.html	Sun Jan 24 22:33:11 2010 +0000
@@ -5,6 +5,14 @@
 {% block content %}
 <h2>Forums</h2>
 
+<form action="." method="get" id="forum-query-form">
+   <select name="query">
+      <option value="unread">Show topics with unread posts</option>
+      <option value="mine">Show my posts</option>
+      <option value="unanswered">Show unanswered posts</option>
+   </select><input type="submit" value="Go" />
+</form>
+
 <div class="forum-block">
 {% for iter in cats %}
    <h3>{{ iter.cat }}</h3>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/forums/topic_icons_tag.html	Sun Jan 24 22:33:11 2010 +0000
@@ -0,0 +1,4 @@
+{% if topic.has_unread %}<img src="{{ MEDIA_URL }}icons/new.png" alt="New Posts" title="New Posts" class="forums-topic-icon" />{% endif %}
+{% if topic.sticky %}<img src="{{ MEDIA_URL }}icons/asterisk_orange.png" alt="Sticky" title="Sticky" class="forums-topic-icon" />{% endif %}
+{% if topic.locked %}<img src="{{ MEDIA_URL }}icons/lock.png" alt="Locked" title="Locked"
+class="forums-topic-icon" />{% endif %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/forums/topic_list.html	Sun Jan 24 22:33:11 2010 +0000
@@ -0,0 +1,57 @@
+{% extends 'base.html' %}
+{% load forum_tags %}
+{% block title %}Forums: {{ title }}{% endblock %}
+{% block content %}
+<h2>Forums: {{ title }}</h2>
+<h3>
+<a href="{% url forums-index %}">SurfGuitar101 Forum Index</a> &raquo; {{ title }}
+</h3>
+<div class="forum-block">
+{{ page_nav }}
+<table class="forum-topic-table">
+   <thead>
+      <th class="col-0">Forum</th>
+      <th class="col-1">Topic</th>
+      <th class="col-2">Author</th>
+      <th class="col-3">Replies</th>
+      <th class="col-4">Views</th>
+      <th class="col-5">Last Post</th>
+   </thead>
+   <tbody>
+   {% for topic in page.object_list %}
+      <tr class="{% cycle 'odd' 'even' %}">
+         <td class="col-0">
+            <h4><a href="{{ topic.forum.get_absolute_url }}">{{ topic.forum.name }}</a></h4>
+         </td>
+         <td class="col-1">
+            {% topic_icons topic %}
+            <h4><a href="{{ topic.get_absolute_url }}">{{ topic.name }}</a></h4>
+            {% if topic.page_range %}
+               {% topic_page_range topic %}
+            {% endif %}
+         </td>
+         <td class="col-2">
+            <a href="{% url bio-view_profile username=topic.user.username %}" title="View profile for {{ topic.user.username }}">{{ topic.user.username }}</a>
+         </td>
+         <td class="col-3">
+            {{ topic.reply_count }}
+         </td>
+         <td class="col-4">
+            {{ topic.view_count }}
+         </td>
+         <td class="col-5">
+            {% last_post_info topic.last_post %}
+         </td>
+      </tr>
+   {% empty %}
+      <tr>
+         <td colspan="6" class="info">
+            <em>No topics meet your search criteria.</em>
+         </td>
+      </tr>
+   {% endfor %}
+   </tbody>
+</table>
+{{ page_nav }}
+</div>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/forums/topic_page_range_tag.html	Sun Jan 24 22:33:11 2010 +0000
@@ -0,0 +1,9 @@
+<span class="small">Goto page: [
+{% for n in topic.page_range %}
+   {% if n %}
+   <a href="{{ topic.get_absolute_url }}?page={{ n }}">{{ n }}</a>
+   {% else %}
+   &hellip;
+   {% endif %}
+{% endfor %}
+]</span>
--- a/media/css/base.css	Sun Jan 17 20:24:01 2010 +0000
+++ b/media/css/base.css	Sun Jan 24 22:33:11 2010 +0000
@@ -273,3 +273,39 @@
  display: inline;
  margin: 0 3px;
 }
+table.forum-topic-table {
+   width:100%;
+}
+table.forum-topic-table thead th {
+   background:teal;
+}
+table.forum-topic-table .col-0 {
+   width:23%; 
+   text-align:center;
+}
+table.forum-topic-table .col-1 {
+   width:37%; 
+   text-align:center;
+}
+table.forum-topic-table .col-2 {
+   width:5%;
+   text-align:center;
+}
+table.forum-topic-table .col-3 {
+   width:5%;
+   text-align:center;
+}
+table.forum-topic-table .col-4 {
+   width:5%;
+   text-align:center;
+}
+table.forum-topic-table .col-5 {
+   width:25%;
+   text-align:center;
+}
+table.forum-topic-table .info {
+   text-align:center;
+}
+#forum-query-form {
+   text-align:right;
+}