From a4bf2658ce6474c1b8dc2d4f8997d8a4c508a1a3 Mon Sep 17 00:00:00 2001 From: Aurélien Bompard Date: Mon, 28 Jan 2013 16:48:03 +0100 Subject: fix the favorites system --- hyperkitty/migrations/0002_auto__add_favorite.py | 93 ++++++++++++++++++++++++ hyperkitty/models.py | 12 ++- hyperkitty/static/css/hyperkitty.css | 8 +- hyperkitty/static/js/hyperkitty.js | 55 +++++++++++++- hyperkitty/templates/month_view.html | 2 +- hyperkitty/templates/threads/add_tag_form.html | 15 ---- hyperkitty/templates/threads/right_col.html | 16 ++-- hyperkitty/templates/user_profile.html | 83 +++++++++++++-------- hyperkitty/templatetags/hk_generic.py | 9 ++- hyperkitty/urls.py | 15 ++-- hyperkitty/views/accounts.py | 17 ++++- hyperkitty/views/list.py | 17 ++++- hyperkitty/views/thread.py | 55 ++++++++++++-- 13 files changed, 319 insertions(+), 78 deletions(-) create mode 100644 hyperkitty/migrations/0002_auto__add_favorite.py delete mode 100644 hyperkitty/templates/threads/add_tag_form.html (limited to 'hyperkitty') diff --git a/hyperkitty/migrations/0002_auto__add_favorite.py b/hyperkitty/migrations/0002_auto__add_favorite.py new file mode 100644 index 0000000..6257d06 --- /dev/null +++ b/hyperkitty/migrations/0002_auto__add_favorite.py @@ -0,0 +1,93 @@ +# -*- 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 'Favorite' + db.create_table('hyperkitty_favorite', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('list_address', self.gf('django.db.models.fields.CharField')(max_length=50)), + ('threadid', self.gf('django.db.models.fields.CharField')(max_length=100)), + ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])), + )) + db.send_create_signal('hyperkitty', ['Favorite']) + + + def backwards(self, orm): + # Deleting model 'Favorite' + db.delete_table('hyperkitty_favorite') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + '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': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + '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': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + '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'}), + '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'}) + }, + 'hyperkitty.favorite': { + 'Meta': {'object_name': 'Favorite'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'threadid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + }, + 'hyperkitty.rating': { + 'Meta': {'object_name': 'Rating'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'messageid': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'vote': ('django.db.models.fields.SmallIntegerField', [], {}) + }, + 'hyperkitty.tag': { + 'Meta': {'object_name': 'Tag'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'list_address': ('django.db.models.fields.CharField', [], {'max_length': '50'}), + 'tag': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'threadid': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'hyperkitty.userprofile': { + 'Meta': {'object_name': 'UserProfile'}, + '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': "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 40eb9e3..370908f 100644 --- a/hyperkitty/models.py +++ b/hyperkitty/models.py @@ -67,4 +67,14 @@ class Tag(models.Model): def __unicode__(self): """Unicode representation""" - return u'threadid = %s , tag = %s ' % (unicode(self.list_address), unicode(self.threadid)) + return u'threadid = %s , tag = %s ' % (unicode(self.threadid), unicode(self.tag)) + + +class Favorite(models.Model): + list_address = models.CharField(max_length=50) + threadid = models.CharField(max_length=100) + user = models.ForeignKey(User) + + def __unicode__(self): + """Unicode representation""" + return u'thread %s for user %s' % (unicode(self.threadid), unicode(self.user)) diff --git a/hyperkitty/static/css/hyperkitty.css b/hyperkitty/static/css/hyperkitty.css index 1d9ec2b..eabb4fe 100644 --- a/hyperkitty/static/css/hyperkitty.css +++ b/hyperkitty/static/css/hyperkitty.css @@ -666,9 +666,15 @@ ul.attachments-list li { margin-right: 2em; } -#add_to_fav a{ +.favorite a { color: rgb(167, 169, 172); } +.favorite a.saved, +.favorite a.notsaved { + /* Will be shown via Javascript */ + display: none; +} + #grey { color: rgb(167, 169, 172); diff --git a/hyperkitty/static/js/hyperkitty.js b/hyperkitty/static/js/hyperkitty.js index 7d7c40b..13d3f8b 100644 --- a/hyperkitty/static/js/hyperkitty.js +++ b/hyperkitty/static/js/hyperkitty.js @@ -53,8 +53,8 @@ function vote(elem, value) { function setup_vote() { - $(".voteup").click(function() { vote(this, 1); return false; }); - $(".votedown").click(function() { vote(this, -1); return false; }); + $(".voteup").click(function(e) { e.preventDefault(); vote(this, 1); }); + $(".votedown").click(function(e) { e.preventDefault(); vote(this, -1); }); } @@ -82,6 +82,56 @@ function setup_add_tag() { } +/* + * Favorites + */ + +function setup_favorites() { + $(".favorite input[name='action']").bind("change", function() { + // bind the links' visibilities to the hidden input status + var form = $(this).parents("form").first(); + if ($(this).val() === "add") { + form.find("a.saved").hide(); + form.find("a.notsaved").show(); + } else { + form.find("a.notsaved").hide(); + form.find("a.saved").show(); + } + }).trigger("change"); + $(".favorite a").bind("click", function(e) { + e.preventDefault(); + var form = $(this).parents("form").first(); + var action_field = form.find("input[name='action']"); + var form_data = form.serializeArray(); + var data = {}; + for (input in form_data) { + data[form_data[input].name] = form_data[input].value; + } + $.ajax({ + type: "POST", + url: form.attr("action"), + dataType: "text", + data: data, + success: function(response) { + // Update the UI + if (action_field.val() === "add") { + action_field.val("rm"); + } else { + action_field.val("add"); + } + action_field.trigger("change"); + }, + error: function(jqXHR, textStatus, errorThrown) { + if (jqXHR.status === 403) { + alert(jqXHR.responseText); + } + } + }); + }); +} + + + /* * Recent activity graph */ @@ -179,4 +229,5 @@ $(document).ready(function() { setup_add_tag(); setup_quotes(); setup_months_list(); + setup_favorites(); }); diff --git a/hyperkitty/templates/month_view.html b/hyperkitty/templates/month_view.html index 16bef68..243c8b1 100644 --- a/hyperkitty/templates/month_view.html +++ b/hyperkitty/templates/month_view.html @@ -37,7 +37,7 @@ {% for thread in threads %}
-
+
{{ thread.starting_email.subject|strip_subject:mlist }} diff --git a/hyperkitty/templates/threads/add_tag_form.html b/hyperkitty/templates/threads/add_tag_form.html deleted file mode 100644 index 44b5f76..0000000 --- a/hyperkitty/templates/threads/add_tag_form.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "base.html" %} - -{% block header %} {% endblock %} - -{% block content %} -
- {% csrf_token %} - {{ addtag_form }} - -
-{% endblock %} - -{# vim: set noet: #} diff --git a/hyperkitty/templates/threads/right_col.html b/hyperkitty/templates/threads/right_col.html index 73194df..60bed09 100644 --- a/hyperkitty/templates/threads/right_col.html +++ b/hyperkitty/templates/threads/right_col.html @@ -22,18 +22,22 @@ old
- {% if use_mockups %} -

- Add to favorite discussions -

- {% endif %} +
+ {% csrf_token %} + +

+ Add to favorite discussions + Remove from favorite discussions +

+
{% include 'threads/tags.html' %}
+ action="{% url add_tag mlist_fqdn=list_address, threadid=threadid %}"> {% csrf_token %} {{ addtag_form.as_p }}
diff --git a/hyperkitty/templates/user_profile.html b/hyperkitty/templates/user_profile.html index e39c0b6..a0a86e4 100644 --- a/hyperkitty/templates/user_profile.html +++ b/hyperkitty/templates/user_profile.html @@ -39,39 +39,62 @@ -

Up Votes :

-
    - {% for vote in votes_up %} -
  • - {% if vote.message.content|trimString|length > 0 %} - {{ vote.message.subject }} by {{ vote.message.sender_name }} - ({{ vote.message|viewer_date|date:"l, j F Y H:i:s" }}) - {% else %} - Message is empty - {% endif %} -
  • - {% empty %} + +

    Favorites

    + {% if favorites %} +
      + {% for fav in favorites %} +
    • + {{ fav.thread.starting_email.subject }} by {{ fav.thread.starting_email.sender_name }} + ({{ fav.thread|viewer_date|date:"l, j F Y H:i:s" }}) +
    • + {% endfor %} +
    + {% else %} +

    No favorite yet.

    + {% endif %} + + +

    Votes

    +

    Up Votes

    + {% if votes_up %} +
      + {% for vote in votes_up %} +
    • + {% if vote.message.content|trimString|length > 0 %} + {{ vote.message.subject }} by {{ vote.message.sender_name }} + ({{ vote.message|viewer_date|date:"l, j F Y H:i:s" }}) + {% else %} + Message is empty + {% endif %} +
    • + {% endfor %} +
    + {% else %}

    No up vote yet.

    - {% endfor %} -
+ {% endif %} -

Down Votes :

-
    - {% for vote in votes_down %} -
  • - {% if vote.message.content|trimString|length > 0 %} - {{ vote.message.subject }} by {{ vote.message.sender_name }} - ({{ vote.message|viewer_date|date:"l, j F Y H:i:s" }}) - {% else %} - Message is empty - {% endif %} -
  • - {% empty %} +

    Down Votes

    + {% if votes_down %} +
      + {% for vote in votes_down %} +
    • + {% if vote.message.content|trimString|length > 0 %} + {{ vote.message.subject }} by {{ vote.message.sender_name }} + ({{ vote.message|viewer_date|date:"l, j F Y H:i:s" }}) + {% else %} + Message is empty + {% endif %} +
    • + {% endfor %} +
    + {% else %}

    No down vote yet.

    - {% endfor %} -
+ {% endif %} + {% endblock %} diff --git a/hyperkitty/templatetags/hk_generic.py b/hyperkitty/templatetags/hk_generic.py index f605df3..1eb2da9 100644 --- a/hyperkitty/templatetags/hk_generic.py +++ b/hyperkitty/templatetags/hk_generic.py @@ -134,9 +134,12 @@ def sender_date(email): @register.filter() -def viewer_date(email): - email_date = email.date.replace(tzinfo=tzutc()) - return localtime(email_date) +def viewer_date(email_or_thread): + if hasattr(email_or_thread, 'date'): + date_obj = email_or_thread.date + elif hasattr(email_or_thread, 'date_active'): + date_obj = email_or_thread.date_active + return localtime(date_obj.replace(tzinfo=tzutc())) SNIPPED_RE = re.compile("^(\s*>).*$", re.M) diff --git a/hyperkitty/urls.py b/hyperkitty/urls.py index 45cfaad..10b52ba 100644 --- a/hyperkitty/urls.py +++ b/hyperkitty/urls.py @@ -52,11 +52,6 @@ urlpatterns = patterns('hyperkitty.views', url(r'^archives/(?P.*@.*)/$', 'list.archives', name='archives'), - # Threads - url(r'^thread/(?P.*@.*)/(?P.+)/$', - 'thread.thread_index', name='thread'), - - # Lists url(r'^list/$', 'pages.index'), # Can I remove this URL? url(r'^list/(?P.*@.*)/$', @@ -95,11 +90,15 @@ urlpatterns = patterns('hyperkitty.views', ### THREAD LEVEL VIEWS ### # Thread view page url(r'^thread/(?P.*@.*)/(?P.+)/$', - 'thread.thread_index', name='thread_index'), - + 'thread.thread_index', name='thread'), # Add Tag to a thread - url(r'^addtag/(?P.*@.*)\/(?P.*)/$', + url(r'^thread/(?P.*@.*)\/(?P.*)/addtag$', 'thread.add_tag', name='add_tag'), + # Thread favorites + url(r'^thread/(?P.*@.*)\/(?P.*)/favorite$', + 'thread.favorite', name='favorite'), + + ### THREAD LEVEL VIEW ENDS ### diff --git a/hyperkitty/views/accounts.py b/hyperkitty/views/accounts.py index 69db991..85b6a9a 100644 --- a/hyperkitty/views/accounts.py +++ b/hyperkitty/views/accounts.py @@ -39,7 +39,7 @@ from django.shortcuts import render_to_response, redirect from django.template import Context, loader, RequestContext from django.utils.translation import gettext as _ -from hyperkitty.models import UserProfile, Rating +from hyperkitty.models import UserProfile, Rating, Favorite from hyperkitty.views.forms import RegistrationForm from hyperkitty.lib import get_store @@ -57,6 +57,7 @@ def user_profile(request, user_email=None): if not request.user.is_authenticated(): return redirect('user_login') t = loader.get_template('user_profile.html') + store = get_store(request) # try to render the user profile. try: @@ -65,11 +66,11 @@ def user_profile(request, user_email=None): except: user_profile = UserProfile.objects.create(user=request.user) + # Votes try: votes = Rating.objects.filter(user=request.user) except Rating.DoesNotExist: - votes = {} - store = get_store(request) + votes = [] votes_up = [] votes_down = [] for vote in votes: @@ -84,10 +85,20 @@ def user_profile(request, user_email=None): elif vote.vote == -1: votes_down.append(vote_data) + # Favorites + try: + favorites = Favorite.objects.filter(user=request.user) + except Favorite.DoesNotExist: + favorites = [] + for fav in favorites: + thread = store.get_thread(fav.list_address, fav.threadid) + fav.thread = thread + c = RequestContext(request, { 'user_profile' : user_profile, 'votes_up': votes_up, 'votes_down': votes_down, + 'favorites': favorites, 'use_mockups': settings.USE_MOCKUPS, }) diff --git a/hyperkitty/views/list.py b/hyperkitty/views/list.py index a782ffb..dc846cc 100644 --- a/hyperkitty/views/list.py +++ b/hyperkitty/views/list.py @@ -40,7 +40,7 @@ from django.contrib.auth.decorators import (login_required, permission_required, user_passes_test) -from hyperkitty.models import Rating, Tag +from hyperkitty.models import Rating, Tag, Favorite from hyperkitty.lib import get_months, get_store, get_display_dates from forms import * @@ -127,6 +127,18 @@ def archives(request, mlist_fqdn, year=None, month=None, day=None): #threads[cnt] = thread #cnt = cnt + 1 + # Favorites + thread.favorite = False + if request.user.is_authenticated(): + try: + Favorite.objects.get(list_address=mlist_fqdn, + threadid=thread.thread_id, + user=request.user) + except Favorite.DoesNotExist: + pass + else: + thread.favorite = True + paginator = Paginator(all_threads, 10) pageNo = request.GET.get('page') @@ -191,7 +203,6 @@ def list(request, mlist_fqdn=None): thread = Thread(thread_obj.thread_id, thread_obj.subject, thread_obj.participants, len(thread_obj), thread_obj.date_active) - threads.append(thread) month = thread.date_active.month if month < 10: @@ -207,6 +218,8 @@ def list(request, mlist_fqdn=None): # Statistics on how many participants and threads this month participants.update(thread.participants) + threads.append(thread) + # top threads are the one with the most answers top_threads = sorted(threads, key=lambda t: t.length, reverse=True) diff --git a/hyperkitty/views/thread.py b/hyperkitty/views/thread.py index 397e45d..6769034 100644 --- a/hyperkitty/views/thread.py +++ b/hyperkitty/views/thread.py @@ -34,7 +34,7 @@ from django.contrib.auth.decorators import (login_required, permission_required, user_passes_test) -from hyperkitty.models import Rating, Tag +from hyperkitty.models import Rating, Tag, Favorite #from hyperkitty.lib.mockup import * from forms import * from hyperkitty.lib import get_months, get_store, stripped_subject @@ -99,13 +99,24 @@ def thread_index(request, mlist_fqdn, threadid, month=None, year=None): archives_length = get_months(store, mlist_fqdn) from_url = reverse("thread", kwargs={"mlist_fqdn":mlist_fqdn, "threadid":threadid}) + # Tags tag_form = AddTagForm(initial={'from_url' : from_url}) - try: tags = Tag.objects.filter(threadid=threadid, list_address=mlist_fqdn) except Tag.DoesNotExist: tags = {} + # Favorites + fav_action = "add" + if request.user.is_authenticated(): + try: + Favorite.objects.get(list_address=mlist_fqdn, threadid=threadid, + user=request.user) + except Favorite.DoesNotExist: + pass + else: + fav_action = "rm" + # Extract relative dates today = datetime.date.today() days_old = today - thread.starting_email.date.date() @@ -132,11 +143,12 @@ def thread_index(request, mlist_fqdn, threadid, month=None, year=None): 'days_old': days_old.days, 'use_mockups': settings.USE_MOCKUPS, 'sort_mode': sort_mode, + 'fav_action': fav_action, }) return HttpResponse(t.render(c)) -def add_tag(request, mlist_fqdn, hashid): +def add_tag(request, mlist_fqdn, threadid): """ Add a tag to a given thread. """ if not request.user.is_authenticated(): return HttpResponse('You must be logged in to add a tag', @@ -152,14 +164,14 @@ def add_tag(request, mlist_fqdn, hashid): content_type="text/plain", status=500) tag = form.data['tag'] try: - tag_obj = Tag.objects.get(threadid=hashid, + tag_obj = Tag.objects.get(threadid=threadid, list_address=mlist_fqdn, tag=tag) except Tag.DoesNotExist: - tag_obj = Tag(list_address=mlist_fqdn, threadid=hashid, tag=tag) + tag_obj = Tag(list_address=mlist_fqdn, threadid=threadid, tag=tag) tag_obj.save() # Now refresh the tag list - tags = Tag.objects.filter(threadid=hashid, list_address=mlist_fqdn) + tags = Tag.objects.filter(threadid=threadid, list_address=mlist_fqdn) t = loader.get_template('threads/tags.html') html = t.render(RequestContext(request, { "tags": tags, @@ -169,3 +181,34 @@ def add_tag(request, mlist_fqdn, hashid): return HttpResponse(simplejson.dumps(response), mimetype='application/javascript') + + +def favorite(request, mlist_fqdn, threadid): + """ Add or remove from favorites""" + if not request.user.is_authenticated(): + return HttpResponse('You must be logged in to have favorites', + content_type="text/plain", status=403) + + if request.method != 'POST': + return HttpResponse("Something went wrong here", + content_type="text/plain", status=500) + + props = dict(list_address=mlist_fqdn, threadid=threadid, user=request.user) + if request.POST["action"] == "add": + try: + fav = Favorite.objects.get(**props) + except Favorite.DoesNotExist: + fav = Favorite(**props) + fav.save() + elif request.POST["action"] == "rm": + try: + fav = Favorite.objects.get(**props) + except Favorite.DoesNotExist: + pass + else: + fav.delete() + else: + return HttpResponse("Something went wrong here", + content_type="text/plain", status=500) + return HttpResponse("success", mimetype='text/plain') + -- cgit