summaryrefslogtreecommitdiffstats
path: root/hyperkitty
diff options
context:
space:
mode:
Diffstat (limited to 'hyperkitty')
-rw-r--r--hyperkitty/migrations/0004_auto__add_lastview.py102
-rw-r--r--hyperkitty/models.py14
-rw-r--r--hyperkitty/static/css/hyperkitty-common.css4
-rw-r--r--hyperkitty/static/css/hyperkitty-message.css12
-rw-r--r--hyperkitty/templates/messages/message.html6
-rw-r--r--hyperkitty/templates/thread.html2
-rw-r--r--hyperkitty/templatetags/hk_generic.py10
-rw-r--r--hyperkitty/views/thread.py20
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)