Skip to content

Commit

Permalink
Added the ability to subscribe to a forum
Browse files Browse the repository at this point in the history
When a new topic is posted, all the users who are subscribed to its forum
will automatically be subscribed to it.
  • Loading branch information
franga2000 committed Nov 30, 2017
1 parent 215eb27 commit 2adf216
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 23 deletions.
14 changes: 14 additions & 0 deletions machina/apps/forum/abstract_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import unicode_literals

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Sum
Expand Down Expand Up @@ -75,6 +76,11 @@ class AbstractForum(MPTTModel, DatedModel):
on_delete=models.SET_NULL, verbose_name=_('Last post'))
last_post_on = models.DateTimeField(verbose_name=_('Last post added on'), blank=True, null=True)

# Many users can subscribe to this topic
subscribers = models.ManyToManyField(
settings.AUTH_USER_MODEL, related_name='forum_subscriptions',
blank=True, verbose_name=_('Subscribers'))

# Display options ; these fields can be used to alter the display of the forums in the list of
# forums.
display_sub_forum_list = models.BooleanField(
Expand Down Expand Up @@ -121,6 +127,14 @@ def is_link(self):
"""
return self.type == self.FORUM_LINK

def has_subscriber(self, user):
"""
Returns True if the given user is a subscriber of this forum.
"""
if not hasattr(self, '_subscribers'):
self._subscribers = list(self.subscribers.all())
return user in self._subscribers

def clean(self):
super(AbstractForum, self).clean()

Expand Down
22 changes: 22 additions & 0 deletions machina/apps/forum/migrations/0010_forum_subscribers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.6 on 2017-11-30 18:26
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('forum', '0009_auto_20170928_2327'),
]

operations = [
migrations.AddField(
model_name='forum',
name='subscribers',
field=models.ManyToManyField(blank=True, related_name='forum_subscriptions', to=settings.AUTH_USER_MODEL, verbose_name='Subscribers'),
),
]
7 changes: 7 additions & 0 deletions machina/apps/forum_member/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ class MemberApp(Application):
topic_subscribe_view = get_class('forum_member.views', 'TopicSubscribeView')
topic_unsubscribe_view = get_class('forum_member.views', 'TopicUnsubscribeView')
topic_subscription_list_view = get_class('forum_member.views', 'TopicSubscribtionListView')
forum_subscribe_view = get_class('forum_member.views', 'ForumSubscribeView')
forum_unsubscribe_view = get_class('forum_member.views', 'ForumUnsubscribeView')

def get_urls(self):
return [
Expand All @@ -36,6 +38,11 @@ def get_urls(self):
self.topic_subscribe_view.as_view(), name='topic_subscribe'),
url(_(r'^topic/(?P<pk>\d+)/unsubscribe/$'),
self.topic_unsubscribe_view.as_view(), name='topic_unsubscribe'),

url(_(r'^forum/(?P<pk>\d+)/subscribe/$'),
self.forum_subscribe_view.as_view(), name='forum_subscribe'),
url(_(r'^forum/(?P<pk>\d+)/unsubscribe/$'),
self.forum_unsubscribe_view.as_view(), name='forum_unsubscribe'),
]


Expand Down
17 changes: 17 additions & 0 deletions machina/apps/forum_member/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from django.db.models import F
from django.db.models.signals import post_delete
from django.db.models.signals import pre_save
from django.db.models.signals import post_save
from django.dispatch import receiver

from machina.core.db.models import get_model
Expand All @@ -15,9 +16,25 @@
User = get_user_model()

Post = get_model('forum_conversation', 'Post')
Topic = get_model('forum_conversation', 'Topic')
ForumProfile = get_model('forum_member', 'ForumProfile')


@receiver(post_save, sender=Topic)
def auto_subscribe(sender, instance, created, **kwargs):
""" When a new topic is posted, subscribes all the subscriber of its forum to it.
This receiver handles automatically subscribing a user to a topic, if they are
subscribed to the forum in which the topic was created
"""
if not created:
# Only new topics should be considered
return

for subscriber in instance.forum.subscribers.all():
instance.subscribers.add(subscriber)


@receiver(pre_save, sender=Post)
def increase_posts_count(sender, instance, **kwargs):
""" Increases the member's post count after a post save.
Expand Down
108 changes: 85 additions & 23 deletions machina/apps/forum_member/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,15 @@ def dispatch(self, request, *args, **kwargs):
return super(ForumProfileUpdateView, self).dispatch(request, *args, **kwargs)


class TopicSubscribeView(
class GenericSubscribeView(
PermissionRequiredMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
"""
Allows a user to subscribe to a specific topic.
Base class for the topic and forums subscription views.
"""
model = Topic
success_message = _('You subscribed to this topic successfully.')
template_name = 'forum_member/topic_subscribe.html'

def get_context_data(self, **kwargs):
context = super(GenericSubscribeView, self).get_context_data(**kwargs)
return context

def subscribe(self, request, *args, **kwargs):
self.object = self.get_object()
Expand All @@ -139,6 +140,37 @@ def subscribe(self, request, *args, **kwargs):
def post(self, request, *args, **kwargs):
return self.subscribe(request, *args, **kwargs)

@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(GenericSubscribeView, self).dispatch(request, *args, **kwargs)

class GenericUnsubscribeView(
PermissionRequiredMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
"""
Base class for the topic and forums unsubscription views.
"""

def unsubscribe(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.subscribers.remove(request.user)
return HttpResponseRedirect(self.get_success_url())

def post(self, request, *args, **kwargs):
return self.unsubscribe(request, *args, **kwargs)

@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(GenericUnsubscribeView, self).dispatch(request, *args, **kwargs)


class TopicSubscribeView(GenericSubscribeView):
"""
Allows a user to subscribe to a specific topic.
"""
model = Topic
success_message = _('You subscribed to this topic successfully.')
template_name = 'forum_member/topic_subscribe.html'

def get_context_data(self, **kwargs):
context = super(TopicSubscribeView, self).get_context_data(**kwargs)
context['topic'] = self.object
Expand All @@ -151,33 +183,20 @@ def get_success_url(self):
'forum_slug': self.object.forum.slug, 'forum_pk': self.object.forum.pk,
'slug': self.object.slug, 'pk': self.object.pk})

@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(TopicSubscribeView, self).dispatch(request, *args, **kwargs)

# Permissions checks

def perform_permissions_check(self, user, obj, perms):
return self.request.forum_permission_handler.can_subscribe_to_topic(obj, user)


class TopicUnsubscribeView(
PermissionRequiredMixin, SingleObjectTemplateResponseMixin, BaseDetailView):
class TopicUnsubscribeView(GenericUnsubscribeView):
"""
Allows a user to unsubscribe from a specific topic.
"""
model = Topic
success_message = _('You unsubscribed from this topic successfully.')
template_name = 'forum_member/topic_unsubscribe.html'

def unsubscribe(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.subscribers.remove(request.user)
return HttpResponseRedirect(self.get_success_url())

def post(self, request, *args, **kwargs):
return self.unsubscribe(request, *args, **kwargs)

def get_context_data(self, **kwargs):
context = super(TopicUnsubscribeView, self).get_context_data(**kwargs)
context['topic'] = self.object
Expand All @@ -190,10 +209,6 @@ def get_success_url(self):
'forum_slug': self.object.forum.slug, 'forum_pk': self.object.forum.pk,
'slug': self.object.slug, 'pk': self.object.pk})

@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(TopicUnsubscribeView, self).dispatch(request, *args, **kwargs)

# Permissions checks

def perform_permissions_check(self, user, obj, perms):
Expand All @@ -214,3 +229,50 @@ def get_queryset(self):
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(TopicSubscribtionListView, self).dispatch(request, *args, **kwargs)

class ForumSubscribeView(GenericSubscribeView):
"""
Allows a user to subscribe to a specific forum.
"""
model = Forum
success_message = _('You subscribed to this forum successfully.')
template_name = 'forum_member/forum_subscribe.html'

def get_context_data(self, **kwargs):
context = super(ForumSubscribeView, self).get_context_data(**kwargs)
context['forum'] = self.object
return context

def get_success_url(self):
messages.success(self.request, self.success_message)
return reverse('forum:forum', kwargs={
'slug': self.object.slug, 'pk': self.object.pk})

# Permissions checks

def perform_permissions_check(self, user, obj, perms):
return self.request.forum_permission_handler.can_subscribe_to_forum(obj, user)


class ForumUnsubscribeView(GenericUnsubscribeView):
"""
Allows a user to unsubscribe from a specific forum.
"""
model = Forum
success_message = _('You unsubscribed from this forum successfully.')
template_name = 'forum_member/forum_unsubscribe.html'

def get_context_data(self, **kwargs):
context = super(ForumUnsubscribeView, self).get_context_data(**kwargs)
context['forum'] = self.object
return context

def get_success_url(self):
messages.success(self.request, self.success_message)
return reverse('forum:forum', kwargs={
'slug': self.object.slug, 'pk': self.object.pk})

# Permissions checks

def perform_permissions_check(self, user, obj, perms):
return self.request.forum_permission_handler.can_unsubscribe_from_forum(obj, user)
22 changes: 22 additions & 0 deletions machina/apps/forum_permission/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,28 @@ def can_unsubscribe_from_topic(self, topic, user):
return user.is_authenticated and topic.has_subscriber(user) \
and self._perform_basic_permission_check(topic.forum, user, 'can_read_forum')

# Forum subscription

def can_subscribe_to_forum(self, forum, user):
"""
Given a forum, checks whether the user can add it to his subscription list.
"""
# A user can subscribe to forums if he is authenticated and if he has the permission to read
# them. Of course a user can subscribe only if he has not already subscribed to
# the considered forum.
return user.is_authenticated() and not forum.has_subscriber(user) \
and self._perform_basic_permission_check(forum, user, 'can_read_forum')

def can_unsubscribe_from_forum(self, forum, user):
"""
Given a forum, checks whether the user can remove it from his subscription list.
"""
# A user can unsubscribe from forums if he is authenticated and if he has the permission to
# read them. Of course a user can unsubscribe only if he is already a subscriber of
# the considered forum.
return user.is_authenticated() and forum.has_subscriber(user) \
and self._perform_basic_permission_check(forum, user, 'can_read_forum')

# Moderation

def get_moderation_queue_forums(self, user):
Expand Down
7 changes: 7 additions & 0 deletions machina/templates/machina/forum/forum_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ <h1>{{ forum.name }}</h1>
{% if user_can_add_topic %}
<a href="{% url 'forum_conversation:topic_create' forum.slug forum.pk %}" class="btn btn-primary btn-sm"><i class="fa fa-comments fa-lg"></i>&nbsp;{% trans "New topic" %}</a>
{% endif %}
{% get_permission 'can_subscribe_to_forum' forum request.user as user_can_subscribe_to_forum %}
{% get_permission 'can_unsubscribe_from_forum' forum request.user as user_can_unsubscribe_from_forum %}
{% if user_can_subscribe_to_forum %}
<a href="{% url 'forum_member:forum_subscribe' forum.pk %}" class="btn btn-info btn-sm btn-subscription"><i class="fa fa-check">&nbsp;</i>{% trans "Subscribe" %}</a>
{% elif user_can_unsubscribe_from_forum %}
<a href="{% url 'forum_member:forum_unsubscribe' forum.pk %}" class="btn btn-info btn-sm btn-subscription"><i class="fa fa-times">&nbsp;</i>{% trans "Unsubscribe" %}</a>
{% endif %}
</div>
<div class="col-xs-12 col-md-8 pagination-block">
{% with "pagination-sm" as pagination_size %}
Expand Down
25 changes: 25 additions & 0 deletions machina/templates/machina/forum_member/forum_subscribe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% extends 'board_base.html' %}
{% load i18n %}
{% load forum_conversation_tags %}

{% block sub_title %}{% trans "Subscribe to forum" %}{% endblock sub_title %}

{% block content %}
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default post-delete">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Subscribe to forum" %}</h3>
</div>
<div class="panel-body">
<div class="warning-message">{% trans "Would you like to subscribe to this forum?" %}</div>
<form action="" method="post">{% csrf_token %}
<input type="hidden" name="post" value="yes" />
<input type="submit" value="{% trans "Yes" %}" class="btn btn-warning" />
<a href="{% url 'forum:forum' forum.slug forum.pk %}" class="btn">{% trans "No" %}</a>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}
25 changes: 25 additions & 0 deletions machina/templates/machina/forum_member/forum_unsubscribe.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{% extends 'board_base.html' %}
{% load i18n %}
{% load forum_conversation_tags %}

{% block sub_title %}{% trans "Unsubscribe from forum" %}{% endblock sub_title %}

{% block content %}
<div class="row">
<div class="col-xs-12">
<div class="panel panel-default post-delete">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Unsubscribe from forum" %}</h3>
</div>
<div class="panel-body">
<div class="warning-message">{% trans "Would you like to unsubscribe from this forum?" %}</div>
<form action="" method="post">{% csrf_token %}
<input type="hidden" name="post" value="yes" />
<input type="submit" value="{% trans "Yes" %}" class="btn btn-warning" />
<a href="{% url 'forum:forum' forum.slug forum.pk %}" class="btn">{% trans "No" %}</a>
</form>
</div>
</div>
</div>
</div>
{% endblock content %}

0 comments on commit 2adf216

Please sign in to comment.