Browse Source

Refractored reads tracker and redesigned new/popular threads pages to look like forum threads pages.

Ralfp 12 years ago
parent
commit
a8ac1fa8e8

+ 3 - 2
misago/readstracker/management/commands/cleartracker.py

@@ -2,7 +2,7 @@ from datetime import timedelta
 from django.conf import settings
 from django.conf import settings
 from django.core.management.base import BaseCommand
 from django.core.management.base import BaseCommand
 from django.utils import timezone
 from django.utils import timezone
-from misago.readstracker.models import Record
+from misago.readstracker.models import ForumRecord, ThreadRecord
 
 
 class Command(BaseCommand):
 class Command(BaseCommand):
     """
     """
@@ -10,5 +10,6 @@ class Command(BaseCommand):
     """
     """
     help = 'Clears Reads Tracker memory'
     help = 'Clears Reads Tracker memory'
     def handle(self, *args, **options):
     def handle(self, *args, **options):
-        Record.objects.filter(updated__lte=timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH)).delete()
+        ForumRecord.objects.filter(updated__lte=timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH)).delete()
+        ThreadRecord.objects.filter(updated__lte=timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH)).delete()
         self.stdout.write('Reads tracker has been cleared.\n')        
         self.stdout.write('Reads tracker has been cleared.\n')        

+ 234 - 0
misago/readstracker/migrations/0002_auto__add_threadrecord__del_field_record_threads.py

@@ -0,0 +1,234 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding model 'ThreadRecord'
+        db.create_table(u'readstracker_threadrecord', (
+            (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
+            ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['users.User'])),
+            ('forum', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['forums.Forum'])),
+            ('thread', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['threads.Thread'])),
+            ('updated', self.gf('django.db.models.fields.DateTimeField')()),
+        ))
+        db.send_create_signal(u'readstracker', ['ThreadRecord'])
+        
+        db.create_index('readstracker_threadrecord', ['updated'])
+        db.create_index('readstracker_threadrecord', ['user_id', 'forum_id'])
+        db.create_index('readstracker_threadrecord', ['user_id', 'thread_id', 'updated'])
+
+        # Deleting field 'Record.threads'
+        db.delete_column(u'readstracker_record', 'threads')
+
+
+    def backwards(self, orm):
+        # Deleting model 'ThreadRecord'
+        db.delete_table(u'readstracker_threadrecord')
+
+        # Adding field 'Record.threads'
+        db.add_column(u'readstracker_record', 'threads',
+                      self.gf('django.db.models.fields.TextField')(null=True, blank=True),
+                      keep_default=False)
+
+
+    models = {
+        u'forums.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['users.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['threads.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['forums.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        u'ranks.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'name_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        u'readstracker.record': {
+            'Meta': {'object_name': 'Record'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forums.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['users.User']"})
+        },
+        u'readstracker.threadrecord': {
+            'Meta': {'object_name': 'ThreadRecord'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forums.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['threads.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['users.User']"})
+        },
+        u'roles.role': {
+            'Meta': {'object_name': 'Role'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        u'threads.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['users.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forums.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': u"orm['users.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['threads.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['users.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        u'threads.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forums.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['threads.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['users.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['threads.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['users.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        u'users.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pms': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': u"orm['users.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': u"orm['users.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['ranks.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['roles.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        }
+    }
+
+    complete_apps = ['readstracker']

+ 221 - 0
misago/readstracker/migrations/0003_auto__del_record__add_forumrecord.py

@@ -0,0 +1,221 @@
+# -*- coding: utf-8 -*-
+import datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        db.rename_table('readstracker_record', 'readstracker_forumrecord')
+
+        db.create_index('readstracker_forumrecord', ['updated'])
+        db.create_index('readstracker_forumrecord', ['user_id', 'updated'])
+        db.create_index('readstracker_forumrecord', ['user_id', 'forum_id'])
+
+
+    def backwards(self, orm):
+        db.delete_index('readstracker_forumrecord', ['updated'])
+        db.delete_index('readstracker_forumrecord', ['user_id', 'updated'])
+        db.delete_index('readstracker_forumrecord', ['user_id', 'forum_id'])
+
+        db.rename_table('readstracker_forumrecord', 'readstracker_record')
+
+
+    models = {
+        u'forums.forum': {
+            'Meta': {'object_name': 'Forum'},
+            'attrs': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'description_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['users.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['threads.Thread']"}),
+            'last_thread_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_thread_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_thread_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'level': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'lft': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'parent': ('mptt.fields.TreeForeignKey', [], {'blank': 'True', 'related_name': "'children'", 'null': 'True', 'to': u"orm['forums.Forum']"}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'posts_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'prune_last': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'prune_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirect': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'redirects': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'redirects_delta': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'rght': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'show_details': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'tree_id': ('django.db.models.fields.PositiveIntegerField', [], {'db_index': 'True'}),
+            'type': ('django.db.models.fields.CharField', [], {'max_length': '12'})
+        },
+        u'ranks.rank': {
+            'Meta': {'object_name': 'Rank'},
+            'as_tab': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'criteria': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'name_slug': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'on_index': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'order': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'special': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        u'readstracker.forumrecord': {
+            'Meta': {'object_name': 'ForumRecord'},
+            'cleared': ('django.db.models.fields.DateTimeField', [], {}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forums.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['users.User']"})
+        },
+        u'readstracker.threadrecord': {
+            'Meta': {'object_name': 'ThreadRecord'},
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forums.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['threads.Thread']"}),
+            'updated': ('django.db.models.fields.DateTimeField', [], {}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['users.User']"})
+        },
+        u'roles.role': {
+            'Meta': {'object_name': 'Role'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'permissions': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'})
+        },
+        u'threads.post': {
+            'Meta': {'object_name': 'Post'},
+            'agent': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'checkpoints': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'date': ('django.db.models.fields.DateTimeField', [], {}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'edit_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'edit_reason': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['users.User']"}),
+            'edit_user_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edit_user_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'edits': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forums.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'mentions': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'mention_set'", 'symmetrical': 'False', 'to': u"orm['users.User']"}),
+            'merge': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'post': ('django.db.models.fields.TextField', [], {}),
+            'post_preparsed': ('django.db.models.fields.TextField', [], {}),
+            'protected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'reported': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'thread': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['threads.Thread']"}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['users.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'user_name': ('django.db.models.fields.CharField', [], {'max_length': '255'})
+        },
+        u'threads.thread': {
+            'Meta': {'object_name': 'Thread'},
+            'closed': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'deleted': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'downvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'forum': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['forums.Forum']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'last': ('django.db.models.fields.DateTimeField', [], {}),
+            'last_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['threads.Post']"}),
+            'last_poster': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['users.User']"}),
+            'last_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'last_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'merges': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'moderated': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'replies': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_deleted': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_moderated': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'replies_reported': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'score': ('django.db.models.fields.PositiveIntegerField', [], {'default': '30'}),
+            'slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {}),
+            'start_post': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': u"orm['threads.Post']"}),
+            'start_poster': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['users.User']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'start_poster_name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'start_poster_slug': ('django.db.models.fields.SlugField', [], {'max_length': '255'}),
+            'start_poster_style': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'upvotes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'weight': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        },
+        u'users.user': {
+            'Meta': {'object_name': 'User'},
+            'acl_key': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'activation': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'alerts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'alerts_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'allow_pms': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'avatar_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'avatar_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'avatar_image': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_original': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_temp': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'avatar_type': ('django.db.models.fields.CharField', [], {'max_length': '10', 'null': 'True', 'blank': 'True'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '255'}),
+            'email_hash': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
+            'followers': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'following': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'follows': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'follows_set'", 'symmetrical': 'False', 'to': u"orm['users.User']"}),
+            'hide_activity': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'ignores': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'ignores_set'", 'symmetrical': 'False', 'to': u"orm['users.User']"}),
+            'is_team': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'join_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'join_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'join_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
+            'karma_given_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_given_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_n': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'karma_p': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'last_agent': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'last_date': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
+            'last_post': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_search': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'last_sync': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'password_date': ('django.db.models.fields.DateTimeField', [], {}),
+            'posts': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'rank': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['ranks.Rank']", 'null': 'True', 'on_delete': 'models.SET_NULL', 'blank': 'True'}),
+            'ranking': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'receive_newsletters': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'roles': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['roles.Role']", 'symmetrical': 'False'}),
+            'score': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
+            'signature': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'signature_ban_reason_admin': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_ban_reason_user': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'signature_preparsed': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
+            'subscribe_reply': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'subscribe_start': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'threads': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'}),
+            'timezone': ('django.db.models.fields.CharField', [], {'default': "'utc'", 'max_length': '255'}),
+            'title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}),
+            'token': ('django.db.models.fields.CharField', [], {'max_length': '12', 'null': 'True', 'blank': 'True'}),
+            'username': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'username_slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '255'}),
+            'votes': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
+        }
+    }
+
+    complete_apps = ['readstracker']

+ 15 - 17
misago/readstracker/models.py

@@ -1,25 +1,23 @@
+from datetime import timedelta
+from django.conf import settings
 from django.db import models
 from django.db import models
-import base64
-try:
-    import cPickle as pickle
-except ImportError:
-    import pickle
+from django.utils import timezone
 
 
-class Record(models.Model):
+class ThreadRecord(models.Model):
+    user = models.ForeignKey('users.User')
+    forum = models.ForeignKey('forums.Forum')
+    thread = models.ForeignKey('threads.Thread')
+    updated = models.DateTimeField()
+    
+
+class ForumRecord(models.Model):
     user = models.ForeignKey('users.User')
     user = models.ForeignKey('users.User')
     forum = models.ForeignKey('forums.Forum')
     forum = models.ForeignKey('forums.Forum')
-    threads = models.TextField(null=True,blank=True)
     updated = models.DateTimeField()
     updated = models.DateTimeField()
     cleared = models.DateTimeField()
     cleared = models.DateTimeField()
     
     
     def get_threads(self):
     def get_threads(self):
-        try:
-            return pickle.loads(base64.decodestring(self.threads))
-        except Exception:
-            # ValueError, SuspiciousOperation, unpickling exceptions. If any of
-            # these happen, just return an empty dictionary (an empty permissions list).
-            return {}
-    
-    def set_threads(self, threads):
-        self.threads = base64.encodestring(pickle.dumps(threads, pickle.HIGHEST_PROTOCOL))
-    
+        threads = {}
+        for thread in ThreadRecord.objects.filter(user=self.user, forum=self.forum, updated__gte=(timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH))):
+            threads[thread.thread_id] = thread
+        return threads

+ 27 - 21
misago/readstracker/trackers.py

@@ -1,7 +1,7 @@
 from datetime import timedelta
 from datetime import timedelta
 from django.conf import settings
 from django.conf import settings
 from django.utils import timezone
 from django.utils import timezone
-from misago.readstracker.models import Record
+from misago.readstracker.models import ForumRecord, ThreadRecord
 from misago.threads.models import Thread
 from misago.threads.models import Thread
 
 
 class ForumsTracker(object):
 class ForumsTracker(object):
@@ -12,7 +12,7 @@ class ForumsTracker(object):
         if user.is_authenticated() and settings.READS_TRACKER_LENGTH > 0:
         if user.is_authenticated() and settings.READS_TRACKER_LENGTH > 0:
             if user.join_date > self.cutoff:
             if user.join_date > self.cutoff:
                 self.cutoff = user.join_date
                 self.cutoff = user.join_date
-            for forum in Record.objects.filter(user=user).filter(updated__gte=self.cutoff).values('id', 'forum_id', 'updated', 'cleared'):
+            for forum in ForumRecord.objects.filter(user=user).filter(updated__gte=self.cutoff).values('id', 'forum_id', 'updated', 'cleared'):
                  self.forums[forum['forum_id']] = forum
                  self.forums[forum['forum_id']] = forum
 
 
     def is_read(self, forum):
     def is_read(self, forum):
@@ -27,7 +27,9 @@ class ForumsTracker(object):
 class ThreadsTracker(object):
 class ThreadsTracker(object):
     def __init__(self, request, forum):
     def __init__(self, request, forum):
         self.need_sync = False
         self.need_sync = False
-        self.need_update = False
+        self.need_create = []
+        self.need_update = []
+        self.need_delete = []
         self.request = request
         self.request = request
         self.forum = forum
         self.forum = forum
         self.cutoff = timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH)
         self.cutoff = timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH)
@@ -35,19 +37,19 @@ class ThreadsTracker(object):
             if request.user.join_date > self.cutoff:
             if request.user.join_date > self.cutoff:
                 self.cutoff = request.user.join_date
                 self.cutoff = request.user.join_date
             try:
             try:
-                self.record = Record.objects.get(user=request.user, forum=forum)
+                self.record = ForumRecord.objects.get(user=request.user, forum=forum)
                 if self.record.cleared > self.cutoff:
                 if self.record.cleared > self.cutoff:
                     self.cutoff = self.record.cleared
                     self.cutoff = self.record.cleared
-            except Record.DoesNotExist:
-                self.record = Record(user=request.user, forum=forum, cleared=self.cutoff)
+            except ForumRecord.DoesNotExist:
+                self.record = ForumRecord(user=request.user, forum=forum, cleared=self.cutoff)
             self.threads = self.record.get_threads()
             self.threads = self.record.get_threads()
 
 
     def get_read_date(self, thread):
     def get_read_date(self, thread):
         if not self.request.user.is_authenticated():
         if not self.request.user.is_authenticated():
             return timezone.now()
             return timezone.now()
         try:
         try:
-            if self.threads[thread.pk] > self.cutoff:
-                return self.threads[thread.pk]
+            if self.threads[thread.pk].updated > self.cutoff:
+                return self.threads[thread.pk].updated
         except KeyError:
         except KeyError:
             pass
             pass
         return self.cutoff
         return self.cutoff
@@ -56,34 +58,38 @@ class ThreadsTracker(object):
         if not self.request.user.is_authenticated():
         if not self.request.user.is_authenticated():
             return True
             return True
         try:
         try:
-            if thread.last <= self.cutoff and thread.pk in self.threads:
-                del self.threads[thread.pk]
-                self.need_update = True
-            return thread.last <= self.cutoff or thread.last <= self.threads[thread.pk]
+            return thread.last <= self.cutoff or thread.last <= self.threads[thread.pk].updated
         except KeyError:
         except KeyError:
             return False
             return False
 
 
     def set_read(self, thread, post):
     def set_read(self, thread, post):
-        if self.request.user.is_authenticated():
+        if self.request.user.is_authenticated() and post.date > self.cutoff:
             try:
             try:
-                if self.threads[thread.pk] < post.date:
-                    self.threads[thread.pk] = post.date
-                    self.need_sync = True
+                if self.threads[thread.pk].updated < post.date:
+                    self.need_update.append(thread.pk)
+                self.threads[thread.pk].updated = post.date
             except KeyError:
             except KeyError:
-                self.threads[thread.pk] = post.date
-                self.need_sync = True
+                self.need_create.append(thread)
 
 
     def sync(self):
     def sync(self):
         now = timezone.now()
         now = timezone.now()
-        if self.need_sync:
+        need_sync = False
+        if self.need_create or self.need_delete or self.need_update:
             unread_threads = 0
             unread_threads = 0
             for thread in self.request.acl.threads.filter_threads(self.request, self.forum, self.forum.thread_set.filter(last__gte=self.record.cleared)):
             for thread in self.request.acl.threads.filter_threads(self.request, self.forum, self.forum.thread_set.filter(last__gte=self.record.cleared)):
                 if not self.is_read(thread):
                 if not self.is_read(thread):
                     unread_threads += 1
                     unread_threads += 1
             if not unread_threads:
             if not unread_threads:
                 self.record.cleared = now
                 self.record.cleared = now
+                need_sync = True
 
 
-        if self.need_sync or self.need_update:
+        if self.need_create:
+            ThreadRecord.objects.bulk_create(
+                [ThreadRecord(user=self.request.user, thread=t, forum=self.forum, updated=now) for t in self.need_create])
+
+        if self.need_update:
+            ThreadRecord.objects.filter(user_id=self.request.user.id).filter(thread_id__in=self.need_update).update(updated=now)
+
+        if self.need_create or self.need_delete or self.need_update or need_sync:
             self.record.updated = now
             self.record.updated = now
-            self.record.set_threads(self.threads)
             self.record.save(force_update=self.record.pk)
             self.record.save(force_update=self.record.pk)

+ 1 - 1
misago/threads/fixtures.py

@@ -34,7 +34,7 @@ settings_fixtures = (
                 'description':  _("Number of threads displayed on page in forum view."),
                 'description':  _("Number of threads displayed on page in forum view."),
             }),
             }),
             ('avatars_on_threads_list', {
             ('avatars_on_threads_list', {
-                'value':        True,
+                'value':        False,
                 'type':         "boolean",
                 'type':         "boolean",
                 'input':        "yesno",
                 'input':        "yesno",
                 'name':         _("Display avatars on threads list"),
                 'name':         _("Display avatars on threads list"),

+ 22 - 0
misago/threads/models.py

@@ -1,3 +1,5 @@
+from datetime import timedelta
+from django.conf import settings
 from django.db import models
 from django.db import models
 from django.db.models import F
 from django.db.models import F
 from django.utils import timezone
 from django.utils import timezone
@@ -6,12 +8,32 @@ from misago.forums.signals import move_forum_content
 from misago.threads.signals import move_thread, merge_thread, move_post, merge_post
 from misago.threads.signals import move_thread, merge_thread, move_post, merge_post
 from misago.users.signals import delete_user_content, rename_user
 from misago.users.signals import delete_user_content, rename_user
 from misago.utils import slugify, ugettext_lazy
 from misago.utils import slugify, ugettext_lazy
+from misago.readstracker.models import ThreadRecord
 from misago.watcher.models import ThreadWatch
 from misago.watcher.models import ThreadWatch
 
 
 class ThreadManager(models.Manager):
 class ThreadManager(models.Manager):
     def filter_stats(self, start, end):
     def filter_stats(self, start, end):
         return self.filter(start__gte=start).filter(start__lte=end)
         return self.filter(start__gte=start).filter(start__lte=end)
 
 
+    def with_reads(self, queryset, user):
+        threads = []
+        threads_dict = {}
+        cutoff = timezone.now() - timedelta(days=settings.READS_TRACKER_LENGTH)
+
+        if user.is_authenticated() and user.join_date > cutoff:
+            cutoff = user.join_date
+
+        for thread in queryset:
+            thread.is_read = not user.is_authenticated() or thread.last <= cutoff
+            threads.append(thread)
+            threads_dict[thread.pk] = thread
+
+        if user.is_authenticated:
+            for read in ThreadRecord.objects.filter(user=user).filter(thread__in=threads_dict.keys()):
+                thread.is_read = threads_dict[read.thread_id].last <= cutoff or threads_dict[read.thread_id].last <= read.updated
+
+        return threads
+
 
 
 class Thread(models.Model):
 class Thread(models.Model):
     forum = models.ForeignKey('forums.Forum')
     forum = models.ForeignKey('forums.Forum')

+ 1 - 0
misago/urls.py

@@ -22,6 +22,7 @@ urlpatterns = patterns('',
     url(r'^read/$', 'misago.views.read_all', name="read_all"),
     url(r'^read/$', 'misago.views.read_all', name="read_all"),
     url(r'^forum-map/$', 'misago.views.forum_map', name="forum_map"),
     url(r'^forum-map/$', 'misago.views.forum_map', name="forum_map"),
     url(r'^popular/$', 'misago.views.popular_threads', name="popular_threads"),
     url(r'^popular/$', 'misago.views.popular_threads', name="popular_threads"),
+    url(r'^popular/(?P<page>[0-9]+)/$', 'misago.views.popular_threads', name="popular_threads"),
     url(r'^new/$', 'misago.views.new_threads', name="new_threads"),
     url(r'^new/$', 'misago.views.new_threads', name="new_threads"),
     url(r'^new/(?P<page>[0-9]+)/$', 'misago.views.new_threads', name="new_threads"),
     url(r'^new/(?P<page>[0-9]+)/$', 'misago.views.new_threads', name="new_threads"),
 )
 )

+ 22 - 21
misago/views.py

@@ -9,7 +9,7 @@ from misago.authn.decorators import block_guest
 from misago.csrf.decorators import check_csrf
 from misago.csrf.decorators import check_csrf
 from misago.forums.models import Forum
 from misago.forums.models import Forum
 from misago.messages import Message
 from misago.messages import Message
-from misago.readstracker.models import Record
+from misago.readstracker.models import ForumRecord, ThreadRecord
 from misago.readstracker.trackers import ForumsTracker
 from misago.readstracker.trackers import ForumsTracker
 from misago.ranks.models import Rank
 from misago.ranks.models import Rank
 from misago.sessions.models import Session
 from misago.sessions.models import Session
@@ -120,15 +120,15 @@ def redirection(request, forum, slug):
 @block_guest
 @block_guest
 @check_csrf
 @check_csrf
 def read_all(request):
 def read_all(request):
-    Record.objects.filter(user=request.user).delete()
+    ForumRecord.objects.filter(user=request.user).delete()
+    ThreadRecord.objects.filter(user=request.user).delete()
     now = timezone.now()
     now = timezone.now()
     bulk = []
     bulk = []
     for forum in request.acl.forums.known_forums():
     for forum in request.acl.forums.known_forums():
-        new_record = Record(user=request.user, forum_id=forum, updated=now, cleared=now)
-        new_record.set_threads({})
+        new_record = ForumRecord(user=request.user, forum_id=forum, updated=now, cleared=now)
         bulk.append(new_record)
         bulk.append(new_record)
     if bulk:
     if bulk:
-        Record.objects.bulk_create(bulk)
+        ForumRecord.objects.bulk_create(bulk)
     request.messages.set_flash(Message(_("All forums have been marked as read.")), 'success')
     request.messages.set_flash(Message(_("All forums have been marked as read.")), 'success')
     return redirect(reverse('index'))
     return redirect(reverse('index'))
 
 
@@ -142,36 +142,37 @@ def forum_map(request):
                                             context_instance=RequestContext(request));
                                             context_instance=RequestContext(request));
 
 
 
 
-def popular_threads(request):
+def popular_threads(request, page=0):
+    queryset = Thread.objects.filter(forum_id__in=request.acl.threads.get_readable_forums(request.acl)).filter(deleted=False).filter(moderated=False)
+    items_total = queryset.count();
+    pagination = make_pagination(page, items_total, 30)
+
+    queryset = queryset.order_by('-score').prefetch_related('forum')[pagination['start']:pagination['stop']];
+    if request.settings['avatars_on_threads_list']:
+        queryset = queryset.prefetch_related('start_poster', 'last_poster')
+
     return request.theme.render_to_response('popular_threads.html',
     return request.theme.render_to_response('popular_threads.html',
                                             {
                                             {
-                                             'threads': Thread.objects.filter(forum_id__in=request.acl.threads.get_readable_forums(request.acl)).filter(deleted=False).filter(moderated=False).order_by('-score').prefetch_related('start_poster', 'last_poster', 'forum')[:14],
+                                             'items_total': items_total,
+                                             'threads': Thread.objects.with_reads(queryset, request.user),
+                                             'pagination': pagination,
                                              },
                                              },
                                             context_instance=RequestContext(request));
                                             context_instance=RequestContext(request));
 
 
 
 
 def new_threads(request, page=0):
 def new_threads(request, page=0):
-    threads = []
     queryset = Thread.objects.filter(forum_id__in=request.acl.threads.get_readable_forums(request.acl)).filter(deleted=False).filter(moderated=False).filter(start__gte=(timezone.now() - timedelta(days=2)))
     queryset = Thread.objects.filter(forum_id__in=request.acl.threads.get_readable_forums(request.acl)).filter(deleted=False).filter(moderated=False).filter(start__gte=(timezone.now() - timedelta(days=2)))
     items_total = queryset.count();
     items_total = queryset.count();
-    pagination = None
-    if items_total > 0:
-        pagination = make_pagination(page, items_total, 30)
-        threads_dict = {}
-
-        for thread in Thread.objects.filter(forum_id__in=request.acl.threads.get_readable_forums(request.acl)).filter(deleted=False).filter(moderated=False).order_by('-start').prefetch_related('start_poster', 'forum')[pagination['start']:pagination['stop']]:
-            thread.has_reply = False
-            threads.append(thread)
-            threads_dict[thread.pk] = thread
+    pagination = make_pagination(page, items_total, 30)
 
 
-        if request.user.is_authenticated():
-            for post in Post.objects.values('thread_id').distinct().filter(user=request.user).filter(thread_id__in=threads_dict.keys()):
-                threads_dict[post['thread_id']].has_reply = True
+    queryset = queryset.order_by('-start').prefetch_related('forum')[pagination['start']:pagination['stop']];
+    if request.settings['avatars_on_threads_list']:
+        queryset = queryset.prefetch_related('start_poster', 'last_poster')
 
 
     return request.theme.render_to_response('new_threads.html',
     return request.theme.render_to_response('new_threads.html',
                                             {
                                             {
                                              'items_total': items_total,
                                              'items_total': items_total,
-                                             'threads': threads,
+                                             'threads': Thread.objects.with_reads(queryset, request.user),
                                              'pagination': pagination,
                                              'pagination': pagination,
                                              },
                                              },
                                             context_instance=RequestContext(request));
                                             context_instance=RequestContext(request));

+ 22 - 16
static/cranefly/css/cranefly.css

@@ -1001,15 +1001,17 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .forum-map-category.forum-map-category-important caption{background-color:#cf402e;border:1px solid #a53325;color:#ffffff;text-shadow:0px 1px 0px #672017;}.forum-map-category.forum-map-category-important caption small{color:#280c09;text-shadow:none;}
 .forum-map-category.forum-map-category-important caption{background-color:#cf402e;border:1px solid #a53325;color:#ffffff;text-shadow:0px 1px 0px #672017;}.forum-map-category.forum-map-category-important caption small{color:#280c09;text-shadow:none;}
 .forum-map-category.forum-map-category-inverse caption{background-color:#333333;border:1px solid #1a1a1a;color:#eeeeee;text-shadow:0px 1px 0px #000000;}.forum-map-category.forum-map-category-inverse caption small{color:#b3b3b3;text-shadow:none;}
 .forum-map-category.forum-map-category-inverse caption{background-color:#333333;border:1px solid #1a1a1a;color:#eeeeee;text-shadow:0px 1px 0px #000000;}.forum-map-category.forum-map-category-inverse caption small{color:#b3b3b3;text-shadow:none;}
 .forum-map-category.forum-map-category-info caption{background-color:#3c85a3;border:1px solid #2e677e;color:#ffffff;text-shadow:0px 1px 0px #1a3946;}.forum-map-category.forum-map-category-info caption small{color:#1a3946;text-shadow:none;}
 .forum-map-category.forum-map-category-info caption{background-color:#3c85a3;border:1px solid #2e677e;color:#ffffff;text-shadow:0px 1px 0px #1a3946;}.forum-map-category.forum-map-category-info caption small{color:#1a3946;text-shadow:none;}
-.popular-threads hr{margin:14px 0px;border:none;border-top:1px solid #eeeeee;}
-.popular-threads .popular-thread{overflow:auto;}.popular-threads .popular-thread .popular-thread-warmth{display:block;float:left;margin-top:1px;margin-right:14px;padding:10.5px;}.popular-threads .popular-thread .popular-thread-warmth i{background-image:url("../img/glyphicons-halflings-white.png");}
-.popular-threads .popular-thread .popular-thread-warmth.popular-thread-warmth-0{background-color:#cf402e;border:1px solid #a53325;}
-.popular-threads .popular-thread .popular-thread-warmth.popular-thread-warmth-1{background-color:#a95d54;border:1px solid #874b43;}
-.popular-threads .popular-thread .popular-thread-warmth.popular-thread-warmth-2{background-color:#90716d;border:1px solid #735a57;}
-.popular-threads .popular-thread .popular-thread-details .popular-thread-title:link,.popular-threads .popular-thread .popular-thread-details .popular-thread-title:visited{color:#555555;font-weight:bold;}
-.popular-threads .popular-thread .popular-thread-details .popular-thread-title:hover,.popular-threads .popular-thread .popular-thread-details .popular-thread-title:active{color:#333333;}
-.popular-threads .popular-thread .popular-thread-details .popular-thread-info{margin:0px;margin-left:14px;padding:0px;color:#999999;font-size:11.9px;}.popular-threads .popular-thread .popular-thread-details .popular-thread-info a:link,.popular-threads .popular-thread .popular-thread-details .popular-thread-info a:visited{color:#555555;}
-.popular-threads .popular-thread .popular-thread-details .popular-thread-info a:hover,.popular-threads .popular-thread .popular-thread-details .popular-thread-info a:active{color:#333333;}
+.popular-threads-list{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.popular-threads-list table{margin:0px;}.popular-threads-list table th{background-color:#fbfbfb;border-bottom:1px solid #eeeeee;padding:2px 10px;color:#999999;font-size:11.9px;}
+.popular-threads-list table td{vertical-align:middle;}.popular-threads-list table td.threads-list-empty{padding:11px 19px;font-size:17.5px;text-align:center;}
+.popular-threads-list table td .thread-icon:link,.popular-threads-list table td .thread-icon:active,.popular-threads-list table td .thread-icon:visited,.popular-threads-list table td .thread-icon:hover{background-color:#555555;border:1px solid #2f2f2f;border-radius:3px;margin-right:7px;padding:3px 4px;}.popular-threads-list table td .thread-icon:link.thread-new,.popular-threads-list table td .thread-icon:active.thread-new,.popular-threads-list table td .thread-icon:visited.thread-new,.popular-threads-list table td .thread-icon:hover.thread-new{background-color:#cf402e;border:1px solid #902d20;}
+.popular-threads-list table td .thread-icon i{background-image:url("../img/glyphicons-halflings-white.png");}
+.popular-threads-list table td .thread-name{color:#333333;font-weight:bold;}
+.popular-threads-list table td .thread-details,.popular-threads-list table td .thread-last-reply{color:#999999;}.popular-threads-list table td .thread-details a:link,.popular-threads-list table td .thread-last-reply a:link,.popular-threads-list table td .thread-details a:visited,.popular-threads-list table td .thread-last-reply a:visited{color:#333333;}
+.popular-threads-list table td .thread-details{font-size:10.5px;}
+.popular-threads-list table td .thread-flags{float:right;margin:0px;opacity:0.8;filter:alpha(opacity=80);padding:0px;}.popular-threads-list table td .thread-flags li{margin-left:3px;float:left;}
+.popular-threads-list table td .thread-rating{background-color:#eeeeee;border-radius:3px;padding:4px;color:#999999;font-size:17.5px;font-weight:bold;text-align:center;}.popular-threads-list table td .thread-rating.thread-rating-negative,.popular-threads-list table td .thread-rating.thread-rating-positive{color:#ffffff;}
+.popular-threads-list table td .thread-rating.thread-rating-negative{background-color:#cf402e;}
+.popular-threads-list table td .thread-rating.thread-rating-positive{background-color:#46a546;}
 .watched-threads .table td{vertical-align:middle;}
 .watched-threads .table td{vertical-align:middle;}
 .watched-threads .table .watched-thread-flags{overflow:auto;width:118px;}.watched-threads .table .watched-thread-flags form{display:inline-block;float:left;margin:0px;}.watched-threads .table .watched-thread-flags form .btn{padding:3px 5px;padding-bottom:0px;margin-right:16px;}
 .watched-threads .table .watched-thread-flags{overflow:auto;width:118px;}.watched-threads .table .watched-thread-flags form{display:inline-block;float:left;margin:0px;}.watched-threads .table .watched-thread-flags form .btn{padding:3px 5px;padding-bottom:0px;margin-right:16px;}
 .watched-threads .table .watched-thread-flags .label{border:1px solid #555555;float:left;padding:1px 5px;padding-bottom:2px;line-height:20px;}.watched-threads .table .watched-thread-flags .label i{background-image:url("../img/glyphicons-halflings-white.png");}
 .watched-threads .table .watched-thread-flags .label{border:1px solid #555555;float:left;padding:1px 5px;padding-bottom:2px;line-height:20px;}.watched-threads .table .watched-thread-flags .label i{background-image:url("../img/glyphicons-halflings-white.png");}
@@ -1021,13 +1023,17 @@ a.btn-link:hover,a.btn-link:active,a.btn-link:focus{opacity:0.9;filter:alpha(opa
 .watched-threads .table .thread-replies{color:#999999;text-align:right;}
 .watched-threads .table .thread-replies{color:#999999;text-align:right;}
 .watched-threads .table .thread-forum a:link,.watched-threads .table .thread-forum a:visited{color:#555555;}
 .watched-threads .table .thread-forum a:link,.watched-threads .table .thread-forum a:visited{color:#555555;}
 .watched-threads .table .thread-forum a:active,.watched-threads .table .thread-forum a:hover{color:#333333;}
 .watched-threads .table .thread-forum a:active,.watched-threads .table .thread-forum a:hover{color:#333333;}
-.new-threads table td{vertical-align:middle;}
-.new-threads .thread-reply{background-color:#333333;border:1px solid #1a1a1a;margin-top:1px;margin-right:7px;padding:3px 4px;}.new-threads .thread-reply i{background-image:url("../img/glyphicons-halflings-white.png");}
-.new-threads .thread-reply.has-reply{background-color:#46a546;border:1px solid #378137;}
-.new-threads .thread-title:link,.new-threads .thread-title:visited{color:#555555;font-weight:bold;}
-.new-threads .thread-title:hover,.new-threads .thread-title:active{color:#333333;}
-.new-threads .thread-details{color:#999999;font-size:11.9px;}.new-threads .thread-details a:link,.new-threads .thread-details a:visited{color:#555555;}
-.new-threads .thread-details a:hover,.new-threads .thread-details a:active{color:#333333;}
+.new-threads-list{background-color:#ffffff;border:1px solid #d5d5d5;border-radius:2px;-webkit-box-shadow:0px 0px 0px 3px #eeeeee;-moz-box-shadow:0px 0px 0px 3px #eeeeee;box-shadow:0px 0px 0px 3px #eeeeee;margin-bottom:20px;}.new-threads-list table{margin:0px;}.new-threads-list table th{background-color:#fbfbfb;border-bottom:1px solid #eeeeee;padding:2px 10px;color:#999999;font-size:11.9px;}
+.new-threads-list table td{vertical-align:middle;}.new-threads-list table td.threads-list-empty{padding:11px 19px;font-size:17.5px;text-align:center;}
+.new-threads-list table td .thread-icon:link,.new-threads-list table td .thread-icon:active,.new-threads-list table td .thread-icon:visited,.new-threads-list table td .thread-icon:hover{background-color:#555555;border:1px solid #2f2f2f;border-radius:3px;margin-right:7px;padding:3px 4px;}.new-threads-list table td .thread-icon:link.thread-new,.new-threads-list table td .thread-icon:active.thread-new,.new-threads-list table td .thread-icon:visited.thread-new,.new-threads-list table td .thread-icon:hover.thread-new{background-color:#cf402e;border:1px solid #902d20;}
+.new-threads-list table td .thread-icon i{background-image:url("../img/glyphicons-halflings-white.png");}
+.new-threads-list table td .thread-name{color:#333333;font-weight:bold;}
+.new-threads-list table td .thread-details,.new-threads-list table td .thread-last-reply{color:#999999;}.new-threads-list table td .thread-details a:link,.new-threads-list table td .thread-last-reply a:link,.new-threads-list table td .thread-details a:visited,.new-threads-list table td .thread-last-reply a:visited{color:#333333;}
+.new-threads-list table td .thread-details{font-size:10.5px;}
+.new-threads-list table td .thread-flags{float:right;margin:0px;opacity:0.8;filter:alpha(opacity=80);padding:0px;}.new-threads-list table td .thread-flags li{margin-left:3px;float:left;}
+.new-threads-list table td .thread-rating{background-color:#eeeeee;border-radius:3px;padding:4px;color:#999999;font-size:17.5px;font-weight:bold;text-align:center;}.new-threads-list table td .thread-rating.thread-rating-negative,.new-threads-list table td .thread-rating.thread-rating-positive{color:#ffffff;}
+.new-threads-list table td .thread-rating.thread-rating-negative{background-color:#cf402e;}
+.new-threads-list table td .thread-rating.thread-rating-positive{background-color:#46a546;}
 .user-alerts td{vertical-align:middle;}.user-alerts td.alert-icon .label{background-color:#555555;border:1px solid #2f2f2f;border-radius:3px;padding:4px;padding-top:3px;}.user-alerts td.alert-icon .label i{background-image:url("../img/glyphicons-halflings-white.png");}
 .user-alerts td{vertical-align:middle;}.user-alerts td.alert-icon .label{background-color:#555555;border:1px solid #2f2f2f;border-radius:3px;padding:4px;padding-top:3px;}.user-alerts td.alert-icon .label i{background-image:url("../img/glyphicons-halflings-white.png");}
 .user-alerts td.alert-icon .label.label-warning{background-color:#cf402e;border:1px solid #902d20;}
 .user-alerts td.alert-icon .label.label-warning{background-color:#cf402e;border:1px solid #902d20;}
 .user-alerts td.alert-message{color:#555555;font-size:16.8px;}.user-alerts td.alert-message a:link,.user-alerts td.alert-message a:visited{color:#333333;font-weight:bold;}
 .user-alerts td.alert-message{color:#555555;font-size:16.8px;}.user-alerts td.alert-message a:link,.user-alerts td.alert-message a:visited{color:#333333;font-weight:bold;}

+ 0 - 2
static/cranefly/css/cranefly.less

@@ -80,9 +80,7 @@
 @import "cranefly/signin.less";
 @import "cranefly/signin.less";
 @import "cranefly/usercp.less";
 @import "cranefly/usercp.less";
 @import "cranefly/forummap.less";
 @import "cranefly/forummap.less";
-@import "cranefly/popularthreads.less";
 @import "cranefly/watchedthreads.less";
 @import "cranefly/watchedthreads.less";
-@import "cranefly/newthreads.less";
 @import "cranefly/alerts.less";
 @import "cranefly/alerts.less";
 @import "cranefly/newsfeed.less";
 @import "cranefly/newsfeed.less";
 @import "cranefly/category.less";
 @import "cranefly/category.less";

+ 0 - 51
static/cranefly/css/cranefly/newthreads.less

@@ -1,51 +0,0 @@
-// New threads
-// -------------------------
-
-.new-threads {
-  table {
-    td {
-      vertical-align: middle; 
-    }
-  }
-
-  .thread-reply {
-    background-color: @grayDark;
-    border: 1px solid darken(@grayDark, 10%);
-    margin-top: 1px;
-    margin-right: @baseFontSize / 2;
-    padding: 3px 4px;
-
-    i {
-      background-image: url("@{iconWhiteSpritePath}");
-    }
-
-    &.has-reply {
-      background-color: @green;
-      border: 1px solid darken(@green, 10%);
-    }
-  }
-
-  .thread-title {
-    &:link, &:visited {
-      color: @gray;
-      font-weight: bold;
-    }
-
-    &:hover, &:active {
-      color: @textColor;
-    }
-  }
-
-  .thread-details {
-    color: @grayLight;
-    font-size: @fontSizeSmall;
-
-    a:link, a:visited {
-      color: @gray;
-    }
-
-    a:hover, a:active {
-      color: @textColor;
-    }
-  }
-}

+ 0 - 71
static/cranefly/css/cranefly/popularthreads.less

@@ -1,71 +0,0 @@
-// Popular threads list
-// -------------------------
-
-.popular-threads {
-  hr {
-    margin: @baseFontSize 0px;
-    border: none;
-    border-top: 1px solid @grayLighter;
-  }
-
-  .popular-thread {
-    overflow: auto;
-
-    .popular-thread-warmth {
-      display: block;
-      float: left;
-      margin-top: 1px;
-      margin-right: @baseFontSize;
-      padding: @fontSizeMini;
-
-      i {
-        background-image: url("@{iconWhiteSpritePath}");
-      }
-
-      &.popular-thread-warmth-0 {
-        background-color: @red;
-        border: 1px solid darken(@red, 10%);
-      }
-
-      &.popular-thread-warmth-1 {
-        background-color: desaturate(@red, 30%);
-        border: 1px solid darken(desaturate(@red, 30%), 10%);
-      }
-
-      &.popular-thread-warmth-2 {
-        background-color: desaturate(@red, 50%);
-        border: 1px solid darken(desaturate(@red, 50%), 10%);
-      }
-    }
-
-    .popular-thread-details {
-      .popular-thread-title {
-        &:link, &:visited {
-          color: @gray;
-          font-weight: bold;
-        }
-
-        &:hover, &:active {
-          color: @textColor;
-        }
-      }
-
-      .popular-thread-info {
-        margin: 0px;
-        margin-left: @baseFontSize;
-        padding: 0px;
-
-        color: @grayLight;
-        font-size: @fontSizeSmall;
-
-        a:link, a:visited {
-          color: @gray;
-        }
-
-        a:hover, a:active {
-          color: @textColor;
-        }
-      }
-    }
-  }
-}

+ 51 - 14
templates/cranefly/new_threads.html

@@ -13,23 +13,52 @@
 
 
 <div class="container container-primary">
 <div class="container container-primary">
   {% if threads %}
   {% if threads %}
-  <div class="new-threads">
+  <div class="forum-threads-list">
     <table class="table">
     <table class="table">
       <thead>
       <thead>
         <tr>
         <tr>
           <th>{% trans %}Thread{% endtrans %}</th>
           <th>{% trans %}Thread{% endtrans %}</th>
-          <th class="span6">{% trans %}Started{% endtrans %}</th>
+          <th class="span1">{% trans %}Rating{% endtrans %}</th>
+          <th class="span5">{% trans %}Activity{% endtrans %}</th>
+          {% if user.is_authenticated() and list_form %}
+          <th class="check-cell"><label class="checkbox"><input type="checkbox" class="checkbox-master"></label></th>
+          {% endif %}
         </tr>
         </tr>
       </thead>
       </thead>
       <tbody>
       <tbody>
         {% for thread in threads %}
         {% for thread in threads %}
         <tr>
         <tr>
           <td>
           <td>
-            <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="label thread-reply{% if thread.has_reply %} has-reply{% endif %} tooltip-top" title="{% if thread.has_reply %}{% trans %}You've replied to this thread{% endtrans %}{% else %}{% trans %}This thread waits for your voice{% endtrans %}{% endif %}"><i class="icon-comment"></i></a>
-            <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="thread-title">{{ thread.name }}</a>
+            <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon{% if not thread.is_read %} thread-new{% endif %} tooltip-top" title="{% if not thread.is_read -%}
+            {% trans %}Click to see first unread post{% endtrans %}
+            {%- else -%}
+            {% trans %}Click to see last post{% endtrans %}
+            {%- endif %}"><i class="icon-comment"></i></a>
+            <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
+            <span class="thread-details">
+              {% trans user=thread_starter(thread), forum=thread_forum(thread), start=thread.start|reldate|low %}by {{ user }} in {{ forum }} {{ start }}{% endtrans %}
+            </span>
+            <ul class="unstyled thread-flags">
+              {% if thread.weight == 2 %}
+              <li><i class="icon-star tooltip-top" title="{% trans %}This thread is an annoucement{% endtrans %}"></i></li>
+              {% endif %}
+              {% if thread.weight == 1 %}
+              <li><i class="icon-star-empty tooltip-top" title="{% trans %}This thread is sticky{% endtrans %}"></i></li>
+              {% endif %}
+              {% if thread.closed %}
+              <li><i class="icon-lock tooltip-top" title="{% trans %}This thread is closed{% endtrans %}"></i></li>
+              {% endif %}
+            </ul>
           </td>
           </td>
-          <td class="thread-details">
-            {% trans forum=forum(thread.forum), starter=username(thread.start_poster_id, thread.start_poster_name, thread.start_poster_slug), start=thread.start|reltimesince %}{{ start }} - by {{ starter }} - {{ forum }}{% endtrans %}
+          <td>
+            <div class="thread-rating{% if (thread.upvotes-thread.downvotes) > 0 %} thread-rating-positive{% elif (thread.upvotes-thread.downvotes) < 0 %} thread-rating-negative{% endif %}">
+              {% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}{{ (thread.upvotes-thread.downvotes)|abs|intcomma }}
+            </div>
+          </td>
+          <td>
+            <div class="thread-last-reply">
+              {{ replies(thread.replies) }} - {% trans user=thread_reply(thread), last=thread.last|reldate|low %}last by {{ user }} {{ last }}{% endtrans %}
+            </div>
           </td>
           </td>
         </tr>
         </tr>
         {% endfor %}
         {% endfor %}
@@ -44,16 +73,24 @@
 {% endblock %}
 {% endblock %}
 
 
 
 
-{% macro forum(forum) -%}
-<a href="{% url 'forum' forum=forum.pk, slug=forum.slug %}">{{ forum.name }}</a>
+{% macro replies(thread_replies) -%}
+{% trans count=thread_replies, replies=('<strong>' ~ (thread_replies|intcomma) ~ '</strong>')|safe -%}
+{{ replies }} reply
+{%- pluralize -%}
+{{ replies }} replies
+{%- endtrans %}
+{%- endmacro %}
+
+{% macro thread_starter(thread) -%}
+{% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}" class="user-link">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}
+{%- endmacro %}
+
+{% macro thread_forum(thread) -%}
+<a href="{% url 'forum' forum=thread.forum_id, slug=thread.forum.slug %}" class="forum-link">{{ thread.forum.name }}</a>
 {%- endmacro %}
 {%- endmacro %}
 
 
-{% macro username(id, username, slug) -%}
-{%- if id -%}
-<a href="{% url 'user' user=id, username=slug %}">{{ username }}</a>
-{%- else -%}
-{{ username }}
-{%- endif -%}
+{% macro thread_reply(thread) -%}
+{% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}" class="user-link">{{ thread.last_poster_name }}</a>{% else %}{{ thread.last_poster_name }}{% endif %}
 {%- endmacro %}
 {%- endmacro %}
 
 
 {% macro pager() -%}
 {% macro pager() -%}

+ 73 - 36
templates/cranefly/popular_threads.html

@@ -13,28 +13,57 @@
 
 
 <div class="container container-primary">
 <div class="container container-primary">
   {% if threads %}
   {% if threads %}
-  <div class="popular-threads">
-  {% for thread in threads %}
-  {% do thread.__dict__.update({'warmth': loop.index}) %}
-  {% endfor %}
-  {% for row in threads|batch(2, '') %}
-    <div class="row">
-      {% for thread in row %}{% if thread %}
-      <div class="span6">
-        <div class="popular-thread">
-          <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="label popular-thread-warmth popular-thread-warmth-{{ warmth(thread.warmth) }} tooltip-top" title="{% trans %}Jump to latest reply{% endtrans %}"><i class="icon-fire"></i></a>
-          <div class="popular-thread-details">
-            <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="popular-thread-title tooltip-top" title="{% trans %}Jump to thread start{% endtrans %}">{{ thread.name }}</a>
-            <p class="popular-thread-info">
-              {% trans forum=forum(thread.forum), starter=username(thread.start_poster_id, thread.start_poster_name, thread.start_poster_slug), last=thread.last|reldate, replies=replies(thread.replies) %}By {{ starter }} - {{ forum }} - {{ replies }} - last on {{ last }}{% endtrans %}
-            </p>
-          </div>
-        </div>
-      </div>
-      {% endif %}{% endfor %}
-    </div>
-    <hr>
-  {% endfor %}
+  <div class="forum-threads-list">
+    <table class="table">
+      <thead>
+        <tr>
+          <th>{% trans %}Thread{% endtrans %}</th>
+          <th class="span1">{% trans %}Rating{% endtrans %}</th>
+          <th class="span5">{% trans %}Activity{% endtrans %}</th>
+          {% if user.is_authenticated() and list_form %}
+          <th class="check-cell"><label class="checkbox"><input type="checkbox" class="checkbox-master"></label></th>
+          {% endif %}
+        </tr>
+      </thead>
+      <tbody>
+        {% for thread in threads %}
+        <tr>
+          <td>
+            <a href="{% url 'thread_new' thread=thread.pk, slug=thread.slug %}" class="thread-icon{% if not thread.is_read %} thread-new{% endif %} tooltip-top" title="{% if not thread.is_read -%}
+            {% trans %}Click to see first unread post{% endtrans %}
+            {%- else -%}
+            {% trans %}Click to see last post{% endtrans %}
+            {%- endif %}"><i class="icon-comment"></i></a>
+            <a href="{% url 'thread' thread=thread.pk, slug=thread.slug %}" class="thread-name">{{ thread.name }}</a>
+            <span class="thread-details">
+              {% trans user=thread_starter(thread), forum=thread_forum(thread), start=thread.start|reldate|low %}by {{ user }} in {{ forum }} {{ start }}{% endtrans %}
+            </span>
+            <ul class="unstyled thread-flags">
+              {% if thread.weight == 2 %}
+              <li><i class="icon-star tooltip-top" title="{% trans %}This thread is an annoucement{% endtrans %}"></i></li>
+              {% endif %}
+              {% if thread.weight == 1 %}
+              <li><i class="icon-star-empty tooltip-top" title="{% trans %}This thread is sticky{% endtrans %}"></i></li>
+              {% endif %}
+              {% if thread.closed %}
+              <li><i class="icon-lock tooltip-top" title="{% trans %}This thread is closed{% endtrans %}"></i></li>
+              {% endif %}
+            </ul>
+          </td>
+          <td>
+            <div class="thread-rating{% if (thread.upvotes-thread.downvotes) > 0 %} thread-rating-positive{% elif (thread.upvotes-thread.downvotes) < 0 %} thread-rating-negative{% endif %}">
+              {% if (thread.upvotes-thread.downvotes) > 0 %}+{% elif (thread.upvotes-thread.downvotes) < 0 %}-{% endif %}{{ (thread.upvotes-thread.downvotes)|abs|intcomma }}
+            </div>
+          </td>
+          <td>
+            <div class="thread-last-reply">
+              {{ replies(thread.replies) }} - {% trans user=thread_reply(thread), last=thread.last|reldate|low %}last by {{ user }} {{ last }}{% endtrans %}
+            </div>
+          </td>
+        </tr>
+        {% endfor %}
+      </tbody>
+    </table>
   </div>
   </div>
   {% else %}
   {% else %}
   <p class="lead">{% trans %}Looks like there are no popular threads... yet!{% endtrans %}</p>
   <p class="lead">{% trans %}Looks like there are no popular threads... yet!{% endtrans %}</p>
@@ -43,26 +72,34 @@
 {% endblock %}
 {% endblock %}
 
 
 
 
-{% macro warmth(loop_index) -%}
-{% if loop_index < 5 %}0{% elif loop_index < 11 %}1{% else %}2{% endif %}
-{%- endmacro %}
-
 {% macro replies(thread_replies) -%}
 {% macro replies(thread_replies) -%}
-{% trans count=thread_replies, replies=thread_replies|intcomma -%}
-One reply
+{% trans count=thread_replies, replies=('<strong>' ~ (thread_replies|intcomma) ~ '</strong>')|safe -%}
+{{ replies }} reply
 {%- pluralize -%}
 {%- pluralize -%}
 {{ replies }} replies
 {{ replies }} replies
 {%- endtrans %}
 {%- endtrans %}
 {%- endmacro %}
 {%- endmacro %}
 
 
-{% macro forum(forum) -%}
-<a href="{% url 'forum' forum=forum.pk, slug=forum.slug %}">{{ forum.name }}</a>
+{% macro thread_starter(thread) -%}
+{% if thread.start_poster_id %}<a href="{% url 'user' user=thread.start_poster_id, username=thread.start_poster_slug %}" class="user-link">{{ thread.start_poster_name }}</a>{% else %}{{ thread.start_poster_name }}{% endif %}
+{%- endmacro %}
+
+{% macro thread_forum(thread) -%}
+<a href="{% url 'forum' forum=thread.forum_id, slug=thread.forum.slug %}" class="forum-link">{{ thread.forum.name }}</a>
 {%- endmacro %}
 {%- endmacro %}
 
 
-{% macro username(id, username, slug) -%}
-{%- if id -%}
-<a href="{% url 'user' user=id, username=slug %}">{{ username }}</a>
-{%- else -%}
-{{ username }}
-{%- endif -%}
+{% macro thread_reply(thread) -%}
+{% if thread.last_poster_id %}<a href="{% url 'user' user=thread.last_poster_id, username=thread.last_poster_slug %}" class="user-link">{{ thread.last_poster_name }}</a>{% else %}{{ thread.last_poster_name }}{% endif %}
+{%- endmacro %}
+
+{% macro pager() -%}
+{% if items_total > 0 and pagination['total'] > 1 %}
+<div class="pagination">
+  <ul>
+    <li class="count">{{ macros.pager_label(pagination) }}</li>
+    {%- if pagination['prev'] > 0 %}<li><a href="{%- if pagination['prev'] > 1 %}{% url 'popular_threads' page=pagination['prev'] %}{% else %}{% url 'popular_threads' %}{% endif %}" class="tooltip-top" title="{% trans %}Previous Page{% endtrans %}"><i class="icon-chevron-left"></i></a></li>{% endif -%}
+    {%- if pagination['next'] > 0 %}<li><a href="{% url 'popular_threads' page=pagination['next'] %}" class="tooltip-top" title="{% trans %}Next Page{% endtrans %}"><i class="icon-chevron-right"></i></a></li>{% endif -%}
+  </ul>
+</div>
+{% endif %}
 {%- endmacro %}
 {%- endmacro %}