djangobb

changeset 156:ddfffe696aa8

replacing Read model by PostTracking and changing algorithm for more correct
author slav0nic <slav0nic0@gmail.com>
date Thu Oct 22 15:19:26 2009 +0300 (9 months ago)
parents acfd250e5f8c
children afe6455bc6c0 92fb0cc7f0a6 c38d73dc5013
files apps/forum/admin.py apps/forum/fields.py apps/forum/models.py apps/forum/templates/forum/forum.html apps/forum/templatetags/forum_extras.py apps/forum/unread.py apps/forum/views.py
line diff
     1.1 --- a/apps/forum/admin.py	Wed Oct 21 14:28:14 2009 +0300
     1.2 +++ b/apps/forum/admin.py	Thu Oct 22 15:19:26 2009 +0300
     1.3 @@ -1,8 +1,8 @@
     1.4  # -*- coding: utf-8 -*-
     1.5  from django.contrib import admin
     1.6  
     1.7 -from forum.models import Category, Forum, Topic, Post, Profile, Read,\
     1.8 -    Reputation, Report, Ban
     1.9 +from forum.models import Category, Forum, Topic, Post, Profile, Reputation,\
    1.10 +    Report, Ban
    1.11  
    1.12  
    1.13  class CategoryAdmin(admin.ModelAdmin):
    1.14 @@ -25,10 +25,6 @@
    1.15      list_display = ['user', 'status', 'time_zone', 'location', 'language']
    1.16      raw_id_fields = ['user']
    1.17  
    1.18 -class ReadAdmin(admin.ModelAdmin):
    1.19 -    list_display = ['user', 'topic', 'time']
    1.20 -    raw_id_fields = ['user', 'topic']
    1.21 -
    1.22  class ReputationAdmin(admin.ModelAdmin):
    1.23      list_display = ['from_user', 'to_user', 'topic', 'sign', 'time', 'reason']
    1.24      raw_id_fields = ['from_user', 'to_user', 'topic']
    1.25 @@ -47,7 +43,6 @@
    1.26  admin.site.register(Topic, TopicAdmin)
    1.27  admin.site.register(Post, PostAdmin)
    1.28  admin.site.register(Profile, ProfileAdmin)
    1.29 -admin.site.register(Read, ReadAdmin)
    1.30  admin.site.register(Reputation, ReputationAdmin)
    1.31  admin.site.register(Report, ReportAdmin)
    1.32  admin.site.register(Ban, BanAdmin)
     2.1 --- a/apps/forum/fields.py	Wed Oct 21 14:28:14 2009 +0300
     2.2 +++ b/apps/forum/fields.py	Thu Oct 22 15:19:26 2009 +0300
     2.3 @@ -7,15 +7,13 @@
     2.4  except ImportError:
     2.5      from StringIO import StringIO
     2.6  import logging
     2.7 -try:
     2.8 -    import cPickle as pickle
     2.9 -except ImportError:
    2.10 -    import Pickle as pickle
    2.11  
    2.12  from django.db.models import OneToOneField
    2.13  from django.db.models.fields.related import SingleRelatedObjectDescriptor 
    2.14  from django.db import models
    2.15  from django.core.files.uploadedfile import SimpleUploadedFile
    2.16 +from django.core.serializers.json import DjangoJSONEncoder
    2.17 +from django.utils import simplejson as json
    2.18  
    2.19  
    2.20  class AutoSingleRelatedObjectDescriptor(SingleRelatedObjectDescriptor):
    2.21 @@ -83,19 +81,30 @@
    2.22          return string.getvalue()
    2.23  
    2.24  
    2.25 -class RangesField(models.TextField):
    2.26 +class JSONField(models.TextField):
    2.27      """
    2.28 -    Field for stored pickled data.
    2.29 -    Taken from Cicero forum engine.
    2.30 +    JSONField is a generic textfield that neatly serializes/unserializes
    2.31 +    JSON objects seamlessly.
    2.32 +    Django snippet #1478
    2.33      """
    2.34 +
    2.35      __metaclass__ = models.SubfieldBase
    2.36  
    2.37      def to_python(self, value):
    2.38 -        if isinstance(value, list):
    2.39 -            return value
    2.40 -        if not value:
    2.41 -            return [(0, 0)]
    2.42 -        return pickle.loads(str(value))
    2.43 +        if value == "":
    2.44 +            return None
    2.45  
    2.46 -    def get_db_prep_value(self, value):
    2.47 -        return unicode(pickle.dumps(value))
    2.48 +        try:
    2.49 +            if isinstance(value, basestring):
    2.50 +                return json.loads(value)
    2.51 +        except ValueError:
    2.52 +            pass
    2.53 +        return value
    2.54 +
    2.55 +    def get_db_prep_save(self, value):
    2.56 +        if value == "":
    2.57 +            return None
    2.58 +        if isinstance(value, dict):
    2.59 +            value = json.dumps(value, cls=DjangoJSONEncoder)
    2.60 +        return super(JSONField, self).get_db_prep_save(value)
    2.61 +
     3.1 --- a/apps/forum/models.py	Wed Oct 21 14:28:14 2009 +0300
     3.2 +++ b/apps/forum/models.py	Thu Oct 22 15:19:26 2009 +0300
     3.3 @@ -14,7 +14,7 @@
     3.4  from markdown import Markdown
     3.5  
     3.6  from forum.markups import mypostmarkup 
     3.7 -from forum.fields import AutoOneToOneField, ExtendedImageField, RangesField
     3.8 +from forum.fields import AutoOneToOneField, ExtendedImageField, JSONField
     3.9  from forum.util import urlize, smiles
    3.10  from forum import settings as forum_settings
    3.11  
    3.12 @@ -156,18 +156,24 @@
    3.13          super(Topic, self).save(*args, **kwargs)
    3.14  
    3.15      def update_read(self, user):
    3.16 -        read, new = Read.objects.get_or_create(user=user, topic=self)
    3.17 -        if not new:
    3.18 -            read.time = datetime.now()
    3.19 -            read.save()
    3.20 -
    3.21 -    #def has_unreads(self, user):
    3.22 -        #try:
    3.23 -            #read = Read.objects.get(user=user, topic=self)
    3.24 -        #except Read.DoesNotExist:
    3.25 -            #return True
    3.26 -        #else:
    3.27 -            #return self.updated > read.time
    3.28 +        tracking = user.posttracking
    3.29 +        #if last_read > last_read - don't check topics
    3.30 +        if tracking.last_read and (tracking.last_read > self.last_post.created):
    3.31 +            return
    3.32 +        if isinstance(tracking.topics, dict):
    3.33 +            #clear topics if len > 5Kb and set last_read to current time
    3.34 +            if len(tracking.topics) > 5120:
    3.35 +                tracking.topics = None
    3.36 +                tracking.last_read = datetime.now()
    3.37 +                tracking.save()
    3.38 +            #update topics if exist new post or does't exist in dict
    3.39 +            if self.last_post.id > tracking.topics.get(str(self.id), 0):
    3.40 +                tracking.topics.setdefault(str(self.id), self.last_post.id)
    3.41 +                tracking.save()
    3.42 +        else:
    3.43 +            #initialize topic tracking dict
    3.44 +            tracking.topics = {self.id: self.last_post.id}
    3.45 +            tracking.save()
    3.46  
    3.47  
    3.48  class Post(models.Model):
    3.49 @@ -241,7 +247,6 @@
    3.50          forum.save()
    3.51  
    3.52  
    3.53 -
    3.54  class Reputation(models.Model):
    3.55      from_user = models.ForeignKey(User, related_name='reputations_from', verbose_name=_('From'))
    3.56      to_user = models.ForeignKey(User, related_name='reputations_to', verbose_name=_('To'))
    3.57 @@ -249,7 +254,7 @@
    3.58      time = models.DateTimeField(_('Time'), blank=True)
    3.59      sign = models.IntegerField(_('Sign'), choices=SIGN_CHOICES, default=0)
    3.60      reason = models.TextField(_('Reason'), blank=True, default='', max_length=1000)
    3.61 -    
    3.62 +
    3.63      class Meta:
    3.64          verbose_name = _('Reputation')
    3.65          verbose_name_plural = _('Reputations')
    3.66 @@ -278,7 +283,6 @@
    3.67      privacy_permission = models.IntegerField(_('Privacy permission'), choices=PRIVACY_CHOICES, default=1)
    3.68      markup = models.CharField(_('Default markup'), max_length=15, default=forum_settings.DEFAULT_MARKUP, choices=MARKUP_CHOICES)
    3.69      post_count = models.IntegerField(_('Post count'), blank=True, default=0)
    3.70 -    #read_posts = fields.RangesField(editable=False)
    3.71  
    3.72      class Meta:
    3.73          verbose_name = _('Profile')
    3.74 @@ -298,28 +302,22 @@
    3.75          return Reputation.objects.filter(to_user=self.user, sign=1).count()
    3.76  
    3.77  
    3.78 -class Read(models.Model):
    3.79 +class PostTracking(models.Model):
    3.80      """
    3.81 -    For each topic that user has entered the time 
    3.82 -    is logged to this model.
    3.83 +    Model for tracking read/unread posts.
    3.84 +    In topics stored ids of topics and last_posts as dict.
    3.85      """
    3.86  
    3.87 -    user = models.ForeignKey(User, verbose_name=_('User'))
    3.88 -    topic = models.ForeignKey(Topic, verbose_name=_('Topic'))
    3.89 -    time = models.DateTimeField(_('Time'), blank=True)
    3.90 +    user = AutoOneToOneField(User)
    3.91 +    topics = JSONField(null=True)
    3.92 +    last_read = models.DateTimeField(null=True)
    3.93  
    3.94      class Meta:
    3.95 -        unique_together = [('user', 'topic')]
    3.96 -        verbose_name = _('Read')
    3.97 -        verbose_name_plural = _('Reads')
    3.98 -
    3.99 -    def save(self, *args, **kwargs):
   3.100 -        if self.time is None:
   3.101 -            self.time = datetime.now()
   3.102 -        super(Read, self).save(*args, **kwargs)
   3.103 +        verbose_name = _('Post tracking')
   3.104 +        verbose_name_plural = _('Post tracking')
   3.105  
   3.106      def __unicode__(self):
   3.107 -        return u'T[%d], U[%d]: %s' % (self.topic.id, self.user.id, unicode(self.time))
   3.108 +        return self.user.username
   3.109  
   3.110  
   3.111  class Report(models.Model):
     4.1 --- a/apps/forum/templates/forum/forum.html	Wed Oct 21 14:28:14 2009 +0300
     4.2 +++ b/apps/forum/templates/forum/forum.html	Thu Oct 22 15:19:26 2009 +0300
     4.3 @@ -32,7 +32,7 @@
     4.4  			</thead>
     4.5  			<tbody>
     4.6  			{% if topics %}
     4.7 -			{% for topic in topics|forum_unreads:user %}
     4.8 +			{% for topic in topics %}
     4.9  				<tr>
    4.10  					<td class="tcl">
    4.11  						<div class="intd">
     5.1 --- a/apps/forum/templatetags/forum_extras.py	Wed Oct 21 14:28:14 2009 +0300
     5.2 +++ b/apps/forum/templatetags/forum_extras.py	Thu Oct 22 15:19:26 2009 +0300
     5.3 @@ -14,8 +14,7 @@
     5.4  from django.utils import dateformat
     5.5  from django.utils.hashcompat import md5_constructor
     5.6  
     5.7 -from forum.models import Forum, Topic, Post, Read, PrivateMessage, Report
     5.8 -from forum.unread import cache_unreads
     5.9 +from forum.models import Forum, Topic, Post, PostTracking, PrivateMessage, Report
    5.10  from forum import settings as forum_settings
    5.11  
    5.12  register = template.Library()
    5.13 @@ -100,7 +99,8 @@
    5.14          'page_list': page_list,
    5.15          'per_page': context['per_page'],
    5.16          }
    5.17 -    
    5.18 +
    5.19 +
    5.20  @register.inclusion_tag('forum/lofi/pagination.html',takes_context=True)
    5.21  def lofi_pagination(context):
    5.22      page_list = range(1, context['pages'] + 1)
    5.23 @@ -119,6 +119,7 @@
    5.24              'paginator': paginator,
    5.25              } 
    5.26  
    5.27 +
    5.28  @register.simple_tag
    5.29  def link(object, anchor=u''):
    5.30      """
    5.31 @@ -129,6 +130,7 @@
    5.32      anchor = anchor or smart_unicode(object)
    5.33      return mark_safe('<a href="%s">%s</a>' % (url, escape(anchor)))
    5.34  
    5.35 +
    5.36  @register.simple_tag
    5.37  def lofi_link(object, anchor=u''):
    5.38      """
    5.39 @@ -139,36 +141,23 @@
    5.40      anchor = anchor or smart_unicode(object)
    5.41      return mark_safe('<a href="%slofi">%s</a>' % (url, escape(anchor)))
    5.42  
    5.43 +
    5.44  @register.filter
    5.45  def has_unreads(topic, user):
    5.46      """
    5.47      Check if topic has messages which user didn't read.
    5.48      """
    5.49 -
    5.50 -    now = datetime.now()
    5.51 -    delta = timedelta(seconds=forum_settings.READ_TIMEOUT)
    5.52 -
    5.53 -    if not user.is_authenticated():
    5.54 -        return False
    5.55 +    if not user.is_authenticated() or\
    5.56 +        (user.posttracking.last_read is not None and\
    5.57 +         user.posttracking.last_read > topic.last_post.created):
    5.58 +            return False
    5.59      else:
    5.60 -        if isinstance(topic, Topic):
    5.61 -            if (now - delta > topic.updated):
    5.62 +        if isinstance(user.posttracking.topics, dict):
    5.63 +            if topic.last_post.id > user.posttracking.topics.get(str(topic.id), 0):
    5.64 +                return True
    5.65 +            else:
    5.66                  return False
    5.67 -            else:
    5.68 -                if hasattr(topic, '_read'):
    5.69 -                    read = topic._read
    5.70 -                else:
    5.71 -                    try:
    5.72 -                        read = Read.objects.get(user=user, topic=topic)
    5.73 -                    except Read.DoesNotExist:
    5.74 -                        read = None
    5.75 -
    5.76 -                if read is None:
    5.77 -                    return True
    5.78 -                else:
    5.79 -                    return topic.updated > read.time
    5.80 -        else:
    5.81 -            raise Exception('Object should be a topic')
    5.82 +        return True
    5.83  
    5.84  
    5.85  @register.filter
    5.86 @@ -219,11 +208,6 @@
    5.87  
    5.88  
    5.89  @register.filter
    5.90 -def forum_unreads(qs, user):
    5.91 -    return cache_unreads(qs, user)
    5.92 -
    5.93 -
    5.94 -@register.filter
    5.95  def forum_authority(user):
    5.96      posts = user.forum_profile.post_count
    5.97      if posts >= forum_settings.AUTHORITY_STEP_10: 
     6.1 --- a/apps/forum/unread.py	Wed Oct 21 14:28:14 2009 +0300
     6.2 +++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.3 @@ -1,23 +0,0 @@
     6.4 -from forum.models import Topic, Post, Read
     6.5 -
     6.6 -def cache_unreads(qs, user):
     6.7 -    if not len(qs) or not user.is_authenticated():
     6.8 -        return qs
     6.9 -    if isinstance(qs[0], Topic):
    6.10 -        reads = Read.objects.filter(topic__pk__in=set(x.id for x in qs),
    6.11 -            user=user).select_related()
    6.12 -        read_map = dict((x.topic.id, x) for x in reads)
    6.13 -
    6.14 -        for topic in qs:
    6.15 -            topic._read = read_map.get(topic.id, None)
    6.16 -        return qs
    6.17 -    elif isinstance(qs[0], Post):
    6.18 -        ids = set(x.topic.id for x in qs)
    6.19 -        reads = Read.objects.filter(topic__pk__in=ids, user=user).select_related()
    6.20 -        read_map = dict((x.topic.id, x) for x in reads)
    6.21 -
    6.22 -        for post in qs:
    6.23 -            post.topic._read = read_map.get(post.topic.id, None)
    6.24 -        return qs
    6.25 -    else:
    6.26 -        raise Exception('cache_unreads could process only Post or Topic querysets')
     7.1 --- a/apps/forum/views.py	Wed Oct 21 14:28:14 2009 +0300
     7.2 +++ b/apps/forum/views.py	Thu Oct 22 15:19:26 2009 +0300
     7.3 @@ -1,6 +1,6 @@
     7.4  import math
     7.5  import re
     7.6 -import datetime
     7.7 +from datetime import datetime, timedelta
     7.8  
     7.9  from django.shortcuts import get_object_or_404
    7.10  from django.http import Http404, HttpResponse, HttpResponseRedirect, HttpResponseForbidden
    7.11 @@ -15,8 +15,8 @@
    7.12  
    7.13  
    7.14  from forum.util import render_to, paged, build_form, paginate, set_language
    7.15 -from forum.models import Category, Forum, Topic, Post, Profile, Read,\
    7.16 -    Reputation, Report, PrivateMessage, Attachment
    7.17 +from forum.models import Category, Forum, Topic, Post, Profile, Reputation,\
    7.18 +    Report, PrivateMessage, Attachment, PostTracking
    7.19  from forum.forms import AddPostForm, EditPostForm, UserSearchForm,\
    7.20      PostSearchForm, ReputationForm, MailToForm, EssentialsProfileForm,\
    7.21      PersonalProfileForm, MessagingProfileForm, PersonalityProfileForm,\
    7.22 @@ -129,10 +129,11 @@
    7.23      if 'action' in request.GET:
    7.24          action = request.GET['action']
    7.25          if action == 'show_24h':
    7.26 -            date =  datetime.datetime.today() - datetime.timedelta(1)
    7.27 +            date = datetime.today() - timedelta(1)
    7.28              topics = Topic.objects.filter(created__gte=date).order_by('created')
    7.29          elif action == 'show_new':
    7.30              #TODO: FIXME
    7.31 +            #must be filter topic.last_post > tracking.last_read and exclude tracking.topics
    7.32              topics = Topic.objects.all().order_by('created')
    7.33              topics = [topic for topic in topics if forum_extras.has_unreads(topic, request.user)] 
    7.34          elif action == 'show_unanswered':
    7.35 @@ -224,14 +225,10 @@
    7.36      if 'action' in request.GET:
    7.37          action = request.GET['action']
    7.38          if action =='markread':
    7.39 -            for category in Category.objects.all():
    7.40 -                for topic in category.topics:
    7.41 -                    read, new = Read.objects.get_or_create(user=request.user, topic=topic)
    7.42 -                    if not new:
    7.43 -                        read.time = datetime.datetime.now()
    7.44 -                        read.save()
    7.45 +            user = request.user
    7.46 +            PostTracking.objects.filter(user=user).update(last_read=datetime.now(), topics=None)
    7.47              return HttpResponseRedirect(reverse('index'))
    7.48 -        
    7.49 +
    7.50          elif action == 'report':
    7.51              if request.GET.get('post_id', ''):
    7.52                  post_id = request.GET['post_id']