diff options
Diffstat (limited to 'hyperkitty')
-rw-r--r-- | hyperkitty/migrations/0004_auto__add_lastview.py | 102 | ||||
-rw-r--r-- | hyperkitty/models.py | 14 | ||||
-rw-r--r-- | hyperkitty/static/css/hyperkitty-common.css | 4 | ||||
-rw-r--r-- | hyperkitty/static/css/hyperkitty-message.css | 12 | ||||
-rw-r--r-- | hyperkitty/templates/messages/message.html | 6 | ||||
-rw-r--r-- | hyperkitty/templates/thread.html | 2 | ||||
-rw-r--r-- | hyperkitty/templatetags/hk_generic.py | 10 | ||||
-rw-r--r-- | hyperkitty/views/thread.py | 20 |
8 files changed, 163 insertions, 7 deletions
diff --git a/hyperkitty/migrations/0004_auto__add_lastview.py b/hyperkitty/migrations/0004_auto__add_lastview.py new file mode 100644 index 0000000..8e206dc --- /dev/null +++ b/hyperkitty/migrations/0004_auto__add_lastview.py @@ -0,0 +1,102 @@ +# -*- 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 'LastView' + db.create_table(u'hyperkitty_lastview', ( + (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('list_address', self.gf('django.db.models.fields.CharField')(max_length=255)), + ('threadid', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + ('view_date', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)), + )) + db.send_create_signal(u'hyperkitty', ['LastView']) + + + def backwards(self, orm): + # Deleting model 'LastView' + db.delete_table(u'hyperkitty_lastview') + + + models = { + u'auth.group': { + 'Meta': {'object_name': 'Group'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + u'auth.permission': { + 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + u'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + u'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'hyperkitty.favorite': { + 'Meta': {'object_name': 'Favorite'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'threadid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}) + }, + u'hyperkitty.lastview': { + 'Meta': {'object_name': 'LastView'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'threadid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'view_date': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + u'hyperkitty.rating': { + 'Meta': {'object_name': 'Rating'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'messageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {}) + }, + u'hyperkitty.tag': { + 'Meta': {'object_name': 'Tag'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'tag': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'threadid': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + u'hyperkitty.userprofile': { + 'Meta': {'object_name': 'UserProfile'}, + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'karma': ('django.db.models.fields.IntegerField', [], {'default': '1'}), + 'user': ('django.db.models.fields.related.OneToOneField', [], {'to': u"orm['auth.User']", 'unique': 'True'}) + } + } + + complete_apps = ['hyperkitty']
\ No newline at end of file diff --git a/hyperkitty/models.py b/hyperkitty/models.py index fff5637..00882cc 100644 --- a/hyperkitty/models.py +++ b/hyperkitty/models.py @@ -87,3 +87,17 @@ class Favorite(models.Model): unicode(self.user)) admin.site.register(Favorite) + + +class LastView(models.Model): + list_address = models.CharField(max_length=255) + threadid = models.CharField(max_length=100) + user = models.ForeignKey(User) + view_date = models.DateTimeField(auto_now=True) + + def __unicode__(self): + """Unicode representation""" + return u"Last view of %s by user %s was %s" % (unicode(self.threadid), + unicode(self.user), self.view_date.isoformat()) + +admin.site.register(LastView) diff --git a/hyperkitty/static/css/hyperkitty-common.css b/hyperkitty/static/css/hyperkitty-common.css index 4309136..ee591c5 100644 --- a/hyperkitty/static/css/hyperkitty-common.css +++ b/hyperkitty/static/css/hyperkitty-common.css @@ -16,6 +16,10 @@ list-style-type: none; } +i.new-message { + float: right; + margin-right: 2em; +} /* from Bootstrap's alert class */ .errorlist { diff --git a/hyperkitty/static/css/hyperkitty-message.css b/hyperkitty/static/css/hyperkitty-message.css index 3978015..92c8778 100644 --- a/hyperkitty/static/css/hyperkitty-message.css +++ b/hyperkitty/static/css/hyperkitty-message.css @@ -194,21 +194,25 @@ } /* The email thread */ -.even, .odd, .temporary { +.replies .email { border-top: 1px solid rgb(179, 179, 179); padding: 1em; margin: 20px 0px 20px 0px; } -.even { +.even .email { background-color: rgb(246, 246, 246); } -.odd { +.odd .email { background-color: rgb(238, 238, 238); } -.temporary { +.temporary .email { background-color: rgb(215, 215, 229); display: none; } +.replies .email.new-message { + border-top: 1px solid rgb(100, 100, 100); + background-color: rgb(214, 214, 214); +} .email-body { white-space: pre; diff --git a/hyperkitty/templates/messages/message.html b/hyperkitty/templates/messages/message.html index 9b072ef..44ffc9f 100644 --- a/hyperkitty/templates/messages/message.html +++ b/hyperkitty/templates/messages/message.html @@ -3,7 +3,8 @@ {% load gravatar %} {% load hk_generic %} -<div class="email{% if unfolded %} email-first{% endif %}"> +{% is_message_new as is_new %} +<div class="email{% if unfolded %} email-first{% endif %}{% if is_new %} new-message{% endif %}"> <div class="email-header" id="{{email.message_id_hash}}"> <div class="email-date inline-block pull-right"> @@ -18,6 +19,9 @@ {% if unfolded %}<br />{% endif %} <span class="time" title="Sender's time: {{email|sender_date|date:"l, j F H:i:s"}}">{{email|viewer_date|date:"H:i:s"}}</span> </div> + {% if is_new %} + <i class="new-message icon-eye-close"></i> + {% endif %} <div class="gravatar{% if use_mockups %} pull-left{% endif %}"> {% gravatar email.sender_email 40 %} </div> diff --git a/hyperkitty/templates/thread.html b/hyperkitty/templates/thread.html index a198014..78ef424 100644 --- a/hyperkitty/templates/thread.html +++ b/hyperkitty/templates/thread.html @@ -80,7 +80,7 @@ // Hide quotes by default in the thread view fold_quotes() // Load the replies - update_thread_replies("{% url 'thread_replies' threadid=threadid mlist_fqdn=mlist.name %}?sort={{sort_mode}}"); + update_thread_replies("{% url 'thread_replies' threadid=threadid mlist_fqdn=mlist.name %}?sort={{sort_mode}}&last_view={{last_view|date:'U'}}"); }); </script> diff --git a/hyperkitty/templatetags/hk_generic.py b/hyperkitty/templatetags/hk_generic.py index 1eb2da9..c98c3e2 100644 --- a/hyperkitty/templatetags/hk_generic.py +++ b/hyperkitty/templatetags/hk_generic.py @@ -183,3 +183,13 @@ def multiply(num1, num2): else: num2 = float(num2) return num1 * num2 + + +def is_message_new(context): + user = context["user"] + last_view = context.get("last_view") + email = context["email"] + return (user.is_authenticated() and + (last_view is None or email.date > last_view) + ) +register.assignment_tag(takes_context=True)(is_message_new) diff --git a/hyperkitty/views/thread.py b/hyperkitty/views/thread.py index 3694ae6..5c3c7a1 100644 --- a/hyperkitty/views/thread.py +++ b/hyperkitty/views/thread.py @@ -32,7 +32,7 @@ from django.core.urlresolvers import reverse from django.core.exceptions import SuspiciousOperation import robot_detection -from hyperkitty.models import Tag, Favorite +from hyperkitty.models import Tag, Favorite, LastView from forms import SearchForm, AddTagForm, ReplyForm from hyperkitty.lib import get_months, get_store, stripped_subject from hyperkitty.lib.voting import set_message_votes @@ -111,6 +111,15 @@ def thread_index(request, mlist_fqdn, threadid, month=None, year=None): mlist = store.get_list(mlist_fqdn) subject = stripped_subject(mlist, thread.starting_email.subject) + # Last view + last_view = None + if request.user.is_authenticated(): + last_view_obj, created = LastView.objects.get_or_create( + list_address=mlist_fqdn, threadid=threadid, user=request.user) + if not created: + last_view = last_view_obj.view_date + last_view_obj.save() # update timestamp + # TODO: eventually move to a middleware ? # http://djangosnippets.org/snippets/1865/ is_bot = True @@ -136,6 +145,7 @@ def thread_index(request, mlist_fqdn, threadid, month=None, year=None): 'reply_form': ReplyForm(), 'is_bot': is_bot, 'participants': thread.participants, + 'last_view': last_view, } context["participants"].sort(key=lambda x: x[0].lower()) @@ -153,10 +163,18 @@ def replies(request, mlist_fqdn, threadid): store = get_store(request) thread = store.get_thread(mlist_fqdn, threadid) mlist = store.get_list(mlist_fqdn) + # Last view + last_view = request.GET.get("last_view") + if last_view: + try: + last_view = datetime.datetime.fromtimestamp(int(last_view)) + except ValueError: + last_view = None context = { 'mlist': mlist, 'threadid': threadid, 'reply_form': ReplyForm(), + 'last_view': last_view, } context["replies"] = _get_thread_replies(request, thread, offset=offset, limit=chunk_size) |