changeset 181:500e5875a306

Implementing #61: adding a forum topic subscription feature.
author Brian Neal <bgneal@gmail.com>
date Sun, 28 Mar 2010 01:07:47 +0000
parents aef00df91165
children 5c889b587416
files gpp/core/functions.py gpp/forums/admin.py gpp/forums/models.py gpp/forums/signals.py gpp/forums/subscriptions.py gpp/forums/urls.py gpp/forums/views.py gpp/settings.py gpp/templates/forums/manage_subscriptions.html gpp/templates/forums/subscription_status.html gpp/templates/forums/topic.html gpp/templates/forums/topic_notify_email.txt media/icons/email_add.png media/icons/email_delete.png
diffstat 14 files changed, 262 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/gpp/core/functions.py	Sun Mar 21 20:33:33 2010 +0000
+++ b/gpp/core/functions.py	Sun Mar 28 01:07:47 2010 +0000
@@ -5,6 +5,8 @@
 from django.contrib.sites.models import Site
 from django.conf import settings
 
+import mailer
+
 
 def send_mail(subject, message, from_email, recipient_list, 
         fail_silently = False, auth_user = None, auth_password = None):
--- a/gpp/forums/admin.py	Sun Mar 21 20:33:33 2010 +0000
+++ b/gpp/forums/admin.py	Sun Mar 28 01:07:47 2010 +0000
@@ -27,7 +27,7 @@
 class TopicAdmin(admin.ModelAdmin):
     list_display = ('name', 'forum', 'creation_date', 'update_date', 'user', 'sticky', 'locked',
             'post_count')
-    raw_id_fields = ('user', 'last_post', )
+    raw_id_fields = ('user', 'last_post', 'subscribers')
     search_fields = ('name', )
     date_hierarchy = 'creation_date'
     list_filter = ('creation_date', 'update_date', )
--- a/gpp/forums/models.py	Sun Mar 21 20:33:33 2010 +0000
+++ b/gpp/forums/models.py	Sun Mar 28 01:07:47 2010 +0000
@@ -172,6 +172,8 @@
     view_count = models.IntegerField(blank=True, default=0)
     sticky = models.BooleanField(blank=True, default=False)
     locked = models.BooleanField(blank=True, default=False)
+    subscribers = models.ManyToManyField(User, related_name='subscriptions',
+            verbose_name='subscribers', blank=True)
 
     # denormalized fields to reduce database hits
     post_count = models.IntegerField(blank=True, default=0)
--- a/gpp/forums/signals.py	Sun Mar 21 20:33:33 2010 +0000
+++ b/gpp/forums/signals.py	Sun Mar 28 01:07:47 2010 +0000
@@ -3,8 +3,9 @@
 """
 from django.db.models.signals import post_save
 from django.db.models.signals import post_delete
-from models import Topic
-from models import Post
+
+from forums.models import Topic, Post
+from forums.subscriptions import notify_topic_subscribers
 
 
 def on_topic_save(sender, **kwargs):
@@ -32,6 +33,9 @@
         post.topic.forum.post_count_update()
         post.topic.forum.save()
 
+        # send out any email notifications
+        notify_topic_subscribers(post)
+
 
 def on_post_delete(sender, **kwargs):
     post = kwargs['instance']
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/forums/subscriptions.py	Sun Mar 28 01:07:47 2010 +0000
@@ -0,0 +1,114 @@
+"""This module handles the subscriptions of users to forum topics."""
+from django.conf import settings
+from django.contrib.auth.decorators import login_required
+from django.contrib.sites.models import Site
+from django.core.paginator import InvalidPage
+from django.core.urlresolvers import reverse
+from django.http import HttpResponseRedirect
+from django.http import Http404
+from django.template.loader import render_to_string
+from django.shortcuts import get_object_or_404
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+from django.views.decorators.http import require_POST
+
+from forums.models import Topic
+from core.functions import send_mail
+from core.paginator import DiggPaginator
+
+
+def notify_topic_subscribers(post):
+    """The argument post is a newly created post. Send out an email
+    notification to all subscribers of the post's parent Topic."""
+
+    topic = post.topic
+    recipients = topic.subscribers.exclude(
+            id=post.user.id).values_list('email', flat=True)
+
+    if recipients:
+        site = Site.objects.get_current()
+        subject = "[%s] Topic Reply: %s" % (site.name, topic.name)
+        url_prefix = "http://%s" % site.domain
+        post_url = url_prefix + post.get_absolute_url()
+        unsubscribe_url = url_prefix + reverse("forums-manage_subscriptions")
+        msg = render_to_string("forums/topic_notify_email.txt", {
+                'poster': post.user.username,
+                'topic_name': topic.name,
+                'message': post.body,
+                'post_url': post_url,
+                'unsubscribe_url': unsubscribe_url,
+                })
+        for recipient in recipients:
+            send_mail(subject, msg, settings.DEFAULT_FROM_EMAIL, [recipient])
+
+
+@login_required
+@require_POST
+def subscribe_topic(request, topic_id):
+    """Subscribe the user to the requested topic."""
+    topic = get_object_or_404(Topic.objects.select_related(), id=topic_id)
+    if topic.forum.category.can_access(request.user):
+        topic.subscribers.add(request.user)
+        return HttpResponseRedirect(
+            reverse("forums-subscription_status", args=[topic.id]))
+    raise Http404
+
+
+@login_required
+@require_POST
+def unsubscribe_topic(request, topic_id):
+    """Unsubscribe the user to the requested topic."""
+    topic = get_object_or_404(Topic, id=topic_id)
+    topic.subscribers.remove(request.user)
+    return HttpResponseRedirect(
+        reverse("forums-subscription_status", args=[topic.id]))
+
+
+@login_required
+def subscription_status(request, topic_id):
+    """Display the subscription status for the given topic."""
+    topic = get_object_or_404(Topic.objects.select_related(), id=topic_id)
+    is_subscribed = request.user in topic.subscribers.all()
+    return render_to_response('forums/subscription_status.html', {
+        'topic': topic,
+        'is_subscribed': is_subscribed,
+        },
+        context_instance=RequestContext(request))
+
+
+@login_required
+def manage_subscriptions(request):
+    """Display a user's topic subscriptions, and allow them to be deleted."""
+
+    user = request.user
+    if request.method == "POST":
+        if request.POST.get('delete_all'):
+            user.subscriptions.clear()
+        else:
+            delete_ids = request.POST.getlist('delete_ids')
+            try:
+                delete_ids = [int(id) for id in delete_ids]
+            except ValueError:
+                raise Http404
+            for topic in user.subscriptions.filter(id__in=delete_ids):
+                user.subscriptions.remove(topic)
+
+        page_num = request.POST.get('page', 1)
+    else:
+        page_num = request.GET.get('page', 1)
+
+    topics = user.subscriptions.select_related().order_by('-creation_date')
+    paginator = DiggPaginator(topics, 20, body=5, tail=2, margin=3, padding=2)
+    try:
+        page_num = int(page_num)
+    except ValueError:
+        page_num = 1
+    try:
+        page = paginator.page(page_num)
+    except InvalidPage:
+        raise Http404
+
+    return render_to_response('forums/manage_subscriptions.html', {
+        'page': page,
+        },
+        context_instance=RequestContext(request))
--- a/gpp/forums/urls.py	Sun Mar 21 20:33:33 2010 +0000
+++ b/gpp/forums/urls.py	Sun Mar 28 01:07:47 2010 +0000
@@ -28,3 +28,9 @@
     url(r'^unread/$', 'unread_topics', name='forums-unread_topics'),
 )
 
+urlpatterns += patterns('forums.subscriptions',
+    url(r'^subscribe/(\d+)/$', 'subscribe_topic', name='forums-subscribe_topic'),
+    url(r'^subscriptions/$', 'manage_subscriptions', name='forums-manage_subscriptions'),
+    url(r'^subscriptions/(\d+)/$', 'subscription_status', name='forums-subscription_status'),
+    url(r'^unsubscribe/(\d+)/$', 'unsubscribe_topic', name='forums-unsubscribe_topic'),
+)
--- a/gpp/forums/views.py	Sun Mar 21 20:33:33 2010 +0000
+++ b/gpp/forums/views.py	Sun Mar 28 01:07:47 2010 +0000
@@ -193,6 +193,9 @@
     can_reply = request.user.is_authenticated() and (
         not topic.locked or can_moderate)
 
+    is_subscribed = request.user.is_authenticated() and (
+            topic in request.user.subscriptions.all())
+
     return render_to_response('forums/topic.html', {
         'forum': topic.forum,
         'topic': topic,
@@ -202,6 +205,7 @@
         'can_moderate': can_moderate,
         'can_reply': can_reply,
         'form': NewPostForm(initial={'topic_id': topic.id}),
+        'is_subscribed': is_subscribed,
         },
         context_instance=RequestContext(request))
 
@@ -435,6 +439,9 @@
         topic.forum.last_post_pre_delete()
         topic.forum.save()
 
+    # delete subscriptions to this topic
+    topic.subscribers.clear()
+
     # It should be safe to just delete the topic now. This will
     # automatically delete all posts in the topic.
     topic.delete()
--- a/gpp/settings.py	Sun Mar 21 20:33:33 2010 +0000
+++ b/gpp/settings.py	Sun Mar 28 01:07:47 2010 +0000
@@ -59,12 +59,19 @@
 SECRET_KEY = local_settings.SECRET_KEY
 
 # List of Loader classes that know how to import templates from various sources.
-TEMPLATE_LOADERS = (
-    ('django.template.loaders.cached.Loader', (
+
+if DEBUG:
+    TEMPLATE_LOADERS = (
         'django.template.loaders.filesystem.Loader',
         'django.template.loaders.app_directories.Loader',
-    )),
-)
+    )
+else:
+    TEMPLATE_LOADERS = (
+        ('django.template.loaders.cached.Loader', (
+            'django.template.loaders.filesystem.Loader',
+            'django.template.loaders.app_directories.Loader',
+        )),
+    )
 
 MIDDLEWARE_CLASSES = (
     'django.middleware.common.CommonMiddleware',
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/forums/manage_subscriptions.html	Sun Mar 28 01:07:47 2010 +0000
@@ -0,0 +1,39 @@
+{% extends 'base.html' %}
+{% block title %}Forums: Topic Subscriptions{% endblock %}
+{% block content %}
+<h2>Forums: Topic Subscriptions</h2>
+
+<h3>
+   <a href="{% url forums-index %}">SurfGuitar101 Forum Index</a> &raquo; Topic Subscriptions
+</h3>
+<p>The forum topics you are currently subscribed to are listed below.</p>
+{% include 'forums/pagination.html' %}
+<form action="." method="post">
+<table class="forum-topic-table">
+   <thead>
+      <tr>
+         <th>Forum</th>
+         <th>Topic</th>
+         <th>Select</th>
+      </tr>
+   </thead>
+   <tbody>
+      {% for topic in page.object_list %}
+         <tr>
+            <td><a href="{{ topic.forum.get_absolute_url }}">{{ topic.forum.name }}</a></td>
+            <td><a href="{{ topic.get_absolute_url }}">{{ topic.name }}</a></td>
+            <td><input type="checkbox" name="delete_ids" value="{{ topic.id }}" /></td>
+         </tr>
+      {% empty %}
+         <tr><td colspan="3"><em>No Topic Subscriptions</em></td></tr>
+      {% endfor %}
+   </tbody>
+</table>
+{% include 'forums/pagination.html' %}
+{% if page.object_list %}
+<input type="hidden" name="page" value="{{ page.number }}" />
+<input type="submit" name="delete_selected" value="Delete Selected" /> &bull;
+<input type="submit" name="delete_all" value="Delete All" />
+{% endif %}
+</form>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/forums/subscription_status.html	Sun Mar 28 01:07:47 2010 +0000
@@ -0,0 +1,24 @@
+{% extends 'base.html' %}
+{% block title %}Forums: {% if is_subscribed %}S{% else %}Uns{% endif %}ubscribed to Topic{% endblock %}
+{% block content %}
+<h2>Forums: {% if is_subscribed %}S{% else %}Uns{% endif %}ubscribed to Topic</h2>
+
+<h3>
+   <a href="{% url forums-index %}">SurfGuitar101 Forum Index</a> &raquo;
+   <a href="{% url forums-forum_index slug=topic.forum.slug %}">{{ topic.forum.name }}</a> &raquo;
+   <a href="{% url forums-topic_index id=topic.id %}">{{ topic.name }}</a>
+</h3>
+<p>
+{% if is_subscribed %}
+You are now subscribed to the forum topic <a href="{{ topic.get_absolute_url }}">{{ topic.name }}</a>.
+You will receive an email notification whenever a new reply is posted.
+{% else %}
+You have successfully unsubscribed to the forum topic <a href="{{ topic.get_absolute_url }}">{{ topic.name }}</a>.
+You will no longer receive emails when new replies are posted.
+{% endif %}
+</p>
+<p>
+To manage all your forum topic subscriptions, please visit your 
+<a href="{% url forums-manage_subscriptions %}">subscriptions page</a>.
+</p>
+{% endblock %}
--- a/gpp/templates/forums/topic.html	Sun Mar 21 20:33:33 2010 +0000
+++ b/gpp/templates/forums/topic.html	Sun Mar 28 01:07:47 2010 +0000
@@ -57,5 +57,28 @@
 <a name="forum-reply-form"></a>
 {% show_form "Reply to Topic" form "Submit Reply" 1 MEDIA_URL %}
 {% endif %}
+
+{% if user.is_authenticated %}
+<form action={% if is_subscribed %}"{% url forums-unsubscribe_topic topic.id %}"{% else %}"{% url forums-subscribe_topic topic.id %}"{% endif %} method="post">
+<fieldset>
+   <legend>Subscription Options</legend>
+   <p>
+   {% if is_subscribed %}
+      <img src="{{ MEDIA_URL }}icons/email_delete.png" alt="Email" />
+      You are currently subscribed to this topic and will receive an email when new replies are posted.
+      <input type="submit" value="Unsubscribe Me" />
+   {% else %}
+      <img src="{{ MEDIA_URL }}icons/email_add.png" alt="Email" />
+      Would you like to receive an email when someone replies to this topic?
+      <input type="submit" value="Subscribe via Email" />
+   {% endif %}
+   </p>
+   <p>
+   To manage all your forum topic subscriptions, please visit your 
+   <a href="{% url forums-manage_subscriptions %}">subscriptions page</a>.
+   </p>
+</fieldset>
+</form>
+{% endif %}
 </div>
 {% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gpp/templates/forums/topic_notify_email.txt	Sun Mar 28 01:07:47 2010 +0000
@@ -0,0 +1,27 @@
+Hello,
+
+You are receiving this message because you have subscribed to one or
+more forum topics at SurfGuitar101.com.
+
+This is a heads up that {{ poster }} replied to a forum topic you have
+subscribed to:
+
+---
+RE: {{ topic_name }}
+
+{{ message }}
+---
+
+To view this post on the site, please visit:
+{{ post_url }}
+
+If you would like to stop receiving these email notifications, you may
+manage your topic subscriptions at:
+{{ unsubscribe_url }}
+
+Surf's up!
+-The staff at SurfGuitar101.com
+
+P.S. This is an automated message from SurfGuitar101.com. Please do not
+reply to this email unless you wish to report a problem to the staff.
+Thanks!
Binary file media/icons/email_add.png has changed
Binary file media/icons/email_delete.png has changed