# HG changeset patch # User Brian Neal # Date 1255288254 0 # Node ID d97ceb95ce02056f8be0a843b2bc6ddfad1e399e # Parent d1b0b86441c07aad34194bd028792c078874dc7f Forums: ForumLastVisit logic in place. Need to add code for topics and posts next. diff -r d1b0b86441c0 -r d97ceb95ce02 gpp/forums/admin.py --- a/gpp/forums/admin.py Wed Sep 30 00:42:13 2009 +0000 +++ b/gpp/forums/admin.py Sun Oct 11 19:10:54 2009 +0000 @@ -8,6 +8,7 @@ from forums.models import Topic from forums.models import Post from forums.models import FlaggedPost +from forums.models import TopicLastVisit class CategoryAdmin(admin.ModelAdmin): @@ -48,8 +49,14 @@ list_display = ('__unicode__', 'flag_date', 'get_post_url') +class TopicLastVisitAdmin(admin.ModelAdmin): + raw_id_fields = ('user', 'topic') + list_display = ('user', 'topic', 'last_visit') + + admin.site.register(Category, CategoryAdmin) admin.site.register(Forum, ForumAdmin) admin.site.register(Topic, TopicAdmin) admin.site.register(Post, PostAdmin) admin.site.register(FlaggedPost, FlaggedPostAdmin) +admin.site.register(TopicLastVisit, TopicLastVisitAdmin) diff -r d1b0b86441c0 -r d97ceb95ce02 gpp/forums/models.py --- a/gpp/forums/models.py Wed Sep 30 00:42:13 2009 +0000 +++ b/gpp/forums/models.py Sun Oct 11 19:10:54 2009 +0000 @@ -9,6 +9,9 @@ from django.template.loader import render_to_string +POST_EDIT_DELTA = datetime.timedelta(seconds=3) + + class Category(models.Model): """ Forums belong to a category, whose access may be assigned to groups. @@ -128,6 +131,23 @@ except Post.DoesNotExist: self.last_post = None + def catchup(self, user, flv=None): + """ + Call to mark this forum all caught up for the given user (i.e. mark all topics + read for this user). + """ + TopicLastVisit.objects.filter(user=user, topic__forum=self).delete() + if flv is None: + try: + flv = ForumLastVisit.objects.get(user=user, forum=self) + except ForumLastVisit.DoesNotExist: + flv = ForumLastVisit(user=user, forum=self) + + now = datetime.datetime.now() + flv.begin_date = now + flv.end_date = now + flv.save() + class Topic(models.Model): """ @@ -241,6 +261,9 @@ if self.id == first_post_id: self.topic.delete() + def has_been_edited(self): + return (self.update_date - self.creation_date) > POST_EDIT_DELTA + class FlaggedPost(models.Model): """This model represents a user flagging a post as inappropriate.""" @@ -258,4 +281,55 @@ return 'Post' % self.post.get_absolute_url() get_post_url.allow_tags = True -# TODO: A "read" table + +class ForumLastVisit(models.Model): + """ + This model records the last time a user visited a forum. + It is used to compute if a user has unread topics in a forum. + We keep track of a window of time, delimited by begin_date and end_date. + Topics updated within this window are tracked, and may have TopicLastVisit + objects. + Marking a forum as all read sets the begin_date equal to the end_date. + """ + user = models.ForeignKey(User) + forum = models.ForeignKey(Forum) + begin_date = models.DateTimeField() + end_date = models.DateTimeField() + + class Meta: + unique_together = ('user', 'forum') + ordering = ('-end_date', ) + + def __unicode__(self): + return u'Forum: %d User: %d Date: %s' % (self.forum.id, self.user.id, + self.end_date.strftime('%Y-%m-%d %H:%M:%S')) + + def is_caught_up(self): + return self.begin_date == self.end_date + + +class TopicLastVisit(models.Model): + """ + This model records the last time a user read a topic. + Objects of this class exist for the window specified in the + corresponding ForumLastVisit object. + """ + user = models.ForeignKey(User) + topic = models.ForeignKey(Topic) + last_visit = models.DateTimeField() + + class Meta: + unique_together = ('user', 'topic') + ordering = ('-last_visit', ) + + def __unicode__(self): + return u'Topic: %d User: %d Date: %s' % (self.topic.id, self.user.id, + self.last_visit.strftime('%Y-%m-%d %H:%M:%S')) + + def save(self, *args, **kwargs): + if self.id is None: + self.touch() + super(TopicLastVisit, self).save(*args, **kwargs) + + def touch(self): + self.last_visit = datetime.datetime.now() diff -r d1b0b86441c0 -r d97ceb95ce02 gpp/forums/unread.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gpp/forums/unread.py Sun Oct 11 19:10:54 2009 +0000 @@ -0,0 +1,99 @@ +""" +This file contains routines for implementing the "has unread" feature. +Forums, topics, and posts are displayed with a visual indication if they have +been read or not. +""" +import datetime + +from forums.models import ForumLastVisit, TopicLastVisit, Topic + + +THRESHOLD = datetime.timedelta(days=7) + + +def get_forum_unread_status(qs, user): + if not user.is_authenticated(): + for forum in qs: + forum.has_unread = False + return + + now = datetime.datetime.now() + min_date = now - THRESHOLD + + # retrieve ForumLastVisit records in one SQL query + forum_ids = [forum.id for forum in qs] + flvs = ForumLastVisit.objects.filter(user=user, + forum__in=forum_ids).select_related() + flvs = dict([(flv.forum.id, flv) for flv in flvs]) + + for forum in qs: + # Edge case: forum has no posts + if forum.last_post is None: + forum.has_unread = False + continue + + # Get the ForumLastVisit record + if forum.id in flvs: + flv = flvs[forum.id] + else: + # One doesn't exist, create a default one for next time, + # mark it as having no unread topics, and bail. + flv = ForumLastVisit(user=user, forum=forum) + flv.begin_date = now + flv.end_date = now + flv.save() + forum.has_unread = False + continue + + # If the last visit record was too far in the past, + # catch that user up and mark as no unreads. + if now - flv.end_date > THRESHOLD: + forum.catchup(user, flv) + forum.has_unread = False + continue + + # Check the easy cases first. Check the last_post in the + # forum. If created after the end_date in our window, there + # are new posts. Likewise, if before the begin_date in our window, + # there are no new posts. + if forum.last_post.creation_date > flv.end_date: + forum.has_unread = True + elif forum.last_post.creation_date < flv.begin_date: + if not flv.is_caught_up(): + forum.catchup(user, flv) + forum.has_unread = False + else: + # Going to have to examine the topics in our window. + # First adjust our window if it is too old. + if now - flv.begin_date > THRESHOLD: + flv.begin_date = min_date + flv.save() + TopicLastVisit.objects.filter(user=user, topic__forum=forum, + last_visit__lt=min_date).delete() + + topics = Topic.objects.filter(creation_date__gt=flv.begin_date) + tracked_topics = TopicLastVisit.objects.filter(user=user, + topic__forum=forum, last_visit__gt=flv.begin_date) + + # If the number of topics created since our window was started + # is greater than the tracked topic records, then there are new + # posts. + if topics.count() > tracked_topics.count(): + forum.has_unread = True + continue + + tracked_dict = dict([(t.id, t) for t in tracked_topics]) + + for topic in topics: + if topic.id in tracked_dict: + if topic.update_date > tracked_dict[topic.id].last_visit: + forum.has_unread = True + continue + else: + forum.has_unread = True + continue + + # If we made it through the above loop without continuing, then + # we are all caught up. + forum.catchup(user, flv) + forum.has_unread = False diff -r d1b0b86441c0 -r d97ceb95ce02 gpp/forums/urls.py --- a/gpp/forums/urls.py Wed Sep 30 00:42:13 2009 +0000 +++ b/gpp/forums/urls.py Sun Oct 11 19:10:54 2009 +0000 @@ -11,6 +11,7 @@ url(r'^edit/(?P\d+)/$', 'edit_post', name='forums-edit_post'), url(r'^flag-post/$', 'flag_post', name='forums-flag_post'), url(r'^forum/(?P[\w\d-]+)/$', 'forum_index', name='forums-forum_index'), + url(r'^forum/(?P[\w\d-]+)/catchup/$', 'forum_catchup', name='forums-catchup'), url(r'^forum/(?P[\w\d-]+)/new-topic/$', 'new_topic', name='forums-new_topic'), url(r'^mod/forum/(?P[\w\d-]+)/$', 'mod_forum', name='forums-mod_forum'), url(r'^mod/topic/delete/(\d+)/$', 'mod_topic_delete', name='forums-mod_topic_delete'), diff -r d1b0b86441c0 -r d97ceb95ce02 gpp/forums/views.py --- a/gpp/forums/views.py Wed Sep 30 00:42:13 2009 +0000 +++ b/gpp/forums/views.py Sun Oct 11 19:10:54 2009 +0000 @@ -1,6 +1,8 @@ """ Views for the forums application. """ +import datetime + from django.contrib.auth.decorators import login_required from django.http import Http404 from django.http import HttpResponse @@ -18,16 +20,15 @@ from core.paginator import DiggPaginator from core.functions import email_admins -from forums.models import Forum -from forums.models import Topic -from forums.models import Post -from forums.models import FlaggedPost +from forums.models import Forum, Topic, Post, FlaggedPost, TopicLastVisit, \ + ForumLastVisit from forums.forms import NewTopicForm, NewPostForm, PostForm, MoveTopicForm +from forums.unread import get_forum_unread_status ####################################################################### TOPICS_PER_PAGE = 50 -POSTS_PER_PAGE = 2 +POSTS_PER_PAGE = 20 def create_topic_paginator(topics): return DiggPaginator(topics, TOPICS_PER_PAGE, body=5, tail=2, margin=3, padding=2) @@ -42,6 +43,7 @@ This view displays all the forums available, ordered in each category. """ forums = Forum.objects.forums_for_user(request.user) + get_forum_unread_status(forums, request.user) cats = {} for forum in forums: cat = cats.setdefault(forum.category.id, { @@ -112,6 +114,9 @@ last_page = page_num == paginator.num_pages + if request.user.is_authenticated() and last_page: + _update_last_visit(request.user, topic) + # we do this for the template since it is rendered twice page_nav = render_to_string('forums/pagination.html', {'page': page}) @@ -191,8 +196,10 @@ post = form.save(request.user, request.META.get("REMOTE_ADDR", "")) _bump_post_count(request.user) + _update_last_visit(request.user, form.topic) return render_to_response('forums/display_post.html', { 'post': post, + 'can_moderate': _can_moderate(form.topic.forum, request.user), }, context_instance=RequestContext(request)) @@ -368,6 +375,7 @@ post.user_ip = request.META.get("REMOTE_ADDR", "") post.save() _bump_post_count(request.user) + _update_last_visit(request.user, topic) return HttpResponseRedirect(post.get_absolute_url()) else: quote_id = request.GET.get('quote') @@ -515,6 +523,21 @@ context_instance=RequestContext(request)) +@login_required +@require_POST +def forum_catchup(request, slug): + """ + This view marks all the topics in the forum as being read. + """ + forum = get_object_or_404(Forum.objects.select_related(), slug=slug) + + if not forum.category.can_access(request.user): + return HttpResponseForbidden() + + forum.catchup(request.user) + return HttpResponseRedirect(forum.get_absolute_url()) + + def _can_moderate(forum, user): """ Determines if a user has permission to moderate a given forum. @@ -605,3 +628,28 @@ if topic.forum == old_forum: _move_topic(topic, old_forum, new_forum) + +def _update_last_visit(user, topic): + """ + Does the bookkeeping for the last visit status for the user to the + topic/forum. + """ + now = datetime.datetime.now() + try: + flv = ForumLastVisit.objects.get(user=user, forum=topic.forum) + except ForumLastVisit.DoesNotExist: + flv = ForumLastVisit(user=user, forum=topic.forum) + flv.begin_date = now + + flv.end_date = now + flv.save() + + if topic.update_date > flv.begin_date: + try: + tlv = TopicLastVisit.objects.get(user=user, topic=topic) + except TopicLastVisit.DoesNotExist: + tlv = TopicLastVisit(user=user, topic=topic) + + tlv.touch() + tlv.save() + diff -r d1b0b86441c0 -r d97ceb95ce02 gpp/templates/forums/display_post.html --- a/gpp/templates/forums/display_post.html Wed Sep 30 00:42:13 2009 +0000 +++ b/gpp/templates/forums/display_post.html Sun Oct 11 19:10:54 2009 +0000 @@ -17,9 +17,10 @@
{{ post.html|safe }} - {% ifnotequal post.creation_date post.update_date %} + {% if post.has_been_edited %} +

{{ post.creation_date|date:"M d, Y H:i:s" }} {{ post.update_date|date:"M d, Y H:i:s" }}

Last edited: {{ post.update_date|date:"M d, Y H:i:s" }}

- {% endifnotequal %} + {% endif %}