Skip to content

Commit d497bd5

Browse files
authored
Merge pull request #532 from pythonindia/spam
Add mark as spam
2 parents 0fde515 + 38ec445 commit d497bd5

File tree

13 files changed

+240
-6
lines changed

13 files changed

+240
-6
lines changed

junction/proposals/comments_views.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
from django.conf import settings
55
from django.contrib.auth.decorators import login_required
66
from django.core.urlresolvers import reverse
7-
from django.http.response import HttpResponseRedirect
7+
from django.http.response import (HttpResponseRedirect, HttpResponse,
8+
HttpResponseForbidden)
89
from django.shortcuts import Http404, get_object_or_404
10+
from django.core.exceptions import PermissionDenied
911
from django.views.decorators.http import require_http_methods
1012

1113
# Junction Stuff
@@ -14,7 +16,7 @@
1416
from . import permissions
1517
from .forms import ProposalCommentForm
1618
from .models import Proposal, ProposalComment
17-
from .services import send_mail_for_new_comment
19+
from .services import send_mail_for_new_comment, user_action_for_spam
1820

1921

2022
@login_required
@@ -24,6 +26,10 @@ def create_proposal_comment(request, conference_slug, proposal_slug):
2426
proposal = get_object_or_404(
2527
Proposal, slug=proposal_slug, conference=conference)
2628
form = ProposalCommentForm(request.POST)
29+
30+
if request.user.is_active is False:
31+
raise PermissionDenied()
32+
2733
if form.is_valid():
2834
comment = form.cleaned_data['comment']
2935
private = form.cleaned_data['private']
@@ -58,3 +64,53 @@ def create_proposal_comment(request, conference_slug, proposal_slug):
5864
redirect_url += "#js-comments"
5965

6066
return HttpResponseRedirect(redirect_url)
67+
68+
69+
@login_required
70+
@require_http_methods(['POST'])
71+
def mark_comment_as_spam(request, conference_slug, proposal_slug, proposal_comment_id):
72+
if not request.is_ajax() or request.user.is_active is False:
73+
return HttpResponseForbidden()
74+
75+
conference = get_object_or_404(Conference, slug=conference_slug)
76+
proposal = get_object_or_404(
77+
Proposal, slug=proposal_slug, conference=conference)
78+
proposal_comment = get_object_or_404(ProposalComment, proposal=proposal,
79+
id=proposal_comment_id)
80+
81+
if proposal_comment.is_spam:
82+
return HttpResponse('Already marked as spam')
83+
84+
proposal_comment.is_spam = True
85+
proposal_comment.marked_as_spam_by = request.user
86+
proposal_comment.save()
87+
88+
user_action_for_spam(proposal_comment.commenter,
89+
getattr(settings, 'USER_SPAM_THRESHOLD', 2))
90+
91+
return HttpResponse('Marked as spam')
92+
93+
94+
@login_required
95+
@require_http_methods(['POST'])
96+
def unmark_comment_as_spam(request, conference_slug, proposal_slug, proposal_comment_id):
97+
if not request.is_ajax() or request.user.is_active is False:
98+
return HttpResponseForbidden()
99+
100+
conference = get_object_or_404(Conference, slug=conference_slug)
101+
proposal = get_object_or_404(
102+
Proposal, slug=proposal_slug, conference=conference)
103+
proposal_comment = get_object_or_404(ProposalComment, proposal=proposal,
104+
id=proposal_comment_id)
105+
106+
if proposal_comment.is_spam and proposal_comment.marked_as_spam_by == request.user:
107+
proposal_comment.is_spam = False
108+
proposal_comment.marked_as_spam_by = None
109+
proposal_comment.save()
110+
111+
user_action_for_spam(proposal_comment.commenter,
112+
getattr(settings, 'USER_SPAM_THRESHOLD', 2))
113+
114+
return HttpResponse('Unmarked as spam')
115+
116+
return HttpResponseForbidden()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9 on 2017-06-10 10:30
3+
from __future__ import unicode_literals
4+
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
import django.db.models.deletion
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14+
('proposals', '0021_auto_20160905_0044'),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='SpamComment',
20+
fields=[
21+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22+
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
23+
('modified_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
24+
],
25+
),
26+
migrations.AddField(
27+
model_name='proposalcomment',
28+
name='is_spam',
29+
field=models.BooleanField(default=False),
30+
),
31+
migrations.AddField(
32+
model_name='spamcomment',
33+
name='comment',
34+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='proposals.ProposalComment'),
35+
),
36+
migrations.AddField(
37+
model_name='spamcomment',
38+
name='marked_by',
39+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
40+
),
41+
migrations.AlterUniqueTogether(
42+
name='spamcomment',
43+
unique_together=set([('comment', 'marked_by')]),
44+
),
45+
]
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9 on 2017-06-10 11:03
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('proposals', '0022_auto_20170610_1600'),
12+
]
13+
14+
operations = [
15+
migrations.AlterIndexTogether(
16+
name='spamcomment',
17+
index_together=set([('comment', 'marked_by')]),
18+
),
19+
]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9 on 2017-06-10 13:27
3+
from __future__ import unicode_literals
4+
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
import django.db.models.deletion
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14+
('proposals', '0023_auto_20170610_1633'),
15+
]
16+
17+
operations = [
18+
migrations.AlterUniqueTogether(
19+
name='spamcomment',
20+
unique_together=set([]),
21+
),
22+
migrations.AlterIndexTogether(
23+
name='spamcomment',
24+
index_together=set([]),
25+
),
26+
migrations.RemoveField(
27+
model_name='spamcomment',
28+
name='comment',
29+
),
30+
migrations.RemoveField(
31+
model_name='spamcomment',
32+
name='marked_by',
33+
),
34+
migrations.AddField(
35+
model_name='proposalcomment',
36+
name='marked_as_spam_by',
37+
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='marked_as_spam_by', to=settings.AUTH_USER_MODEL),
38+
),
39+
migrations.AlterIndexTogether(
40+
name='proposalcomment',
41+
index_together=set([('is_spam', 'marked_as_spam_by')]),
42+
),
43+
migrations.DeleteModel(
44+
name='SpamComment',
45+
),
46+
]

junction/proposals/models.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,14 @@ class ProposalComment(TimeAuditModel):
291291
comment_type = models.PositiveSmallIntegerField(
292292
choices=ProposalCommentType.CHOICES, default=ProposalCommentType.GENERAL)
293293
objects = ProposalCommentQuerySet.as_manager()
294+
is_spam = models.BooleanField(default=False, blank=True)
295+
marked_as_spam_by = models.ForeignKey(User, related_name='marked_as_spam_by',
296+
default=None, null=True, blank=True)
294297

295298
class Meta:
296299
ordering = ('created_at', )
300+
index_together = [['is_spam', 'marked_as_spam_by'],
301+
['commenter', 'is_spam']]
297302

298303
def __str__(self):
299304
return "[{} by {}] {}".format(self.comment,
@@ -306,6 +311,14 @@ def get_up_vote_url(self):
306311
def get_down_vote_url(self):
307312
return reverse('proposal-comment-down-vote', args=[self.proposal.conference.slug, self.proposal.slug, self.id])
308313

314+
def get_mark_spam_url(self):
315+
return reverse('comment_mark_spam',
316+
args=[self.proposal.conference.slug, self.proposal.slug, self.id])
317+
318+
def get_unmark_spam_url(self):
319+
return reverse('comment_unmark_spam',
320+
args=[self.proposal.conference.slug, self.proposal.slug, self.id])
321+
309322
def get_votes_count(self):
310323
up_vote_count = ProposalCommentVote.objects.filter(proposal_comment=self, up_vote=True).count()
311324
down_vote_count = ProposalCommentVote.objects.filter(proposal_comment=self, up_vote=False).count()

junction/proposals/services.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
# Third Party Stuff
1010
from django.conf import settings
11+
from django.contrib.auth.models import User
1112
from markdown2 import markdown
1213
from celery import shared_task
1314

@@ -78,6 +79,10 @@ def comment_recipients(proposal_comment):
7879
for comment in proposal.proposalcomment_set
7980
.all().select_related('commenter')}
8081
recipients.add(proposal.author)
82+
ADMINS = getattr(settings, 'SPAM_MODERATION_ADMINS', [])
83+
if ADMINS:
84+
for admin in ADMINS:
85+
recipients.add(User.objects.get(email=admin))
8186

8287
return recipients
8388

@@ -150,3 +155,17 @@ def send_mail_for_proposal_content(conference_id, proposal_id, host):
150155
}
151156
return send_email(to=author, template_dir='proposals/email/upload_content',
152157
context=context)
158+
159+
160+
def user_action_for_spam(user, threshold):
161+
"""When a comment is marked as spam, make appropriate status update to user model
162+
"""
163+
total_spam = ProposalComment.objects.filter(commenter=user, is_spam=True).count()
164+
if total_spam >= threshold:
165+
if user.is_active is True:
166+
user.is_active = False
167+
user.save()
168+
else:
169+
if user.is_active is False:
170+
user.is_active = True
171+
user.save()

junction/proposals/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
votes_views.proposal_comment_up_vote, name='proposal-comment-up-vote'),
1515
url(r'^(?P<proposal_slug>[\w-]+)/comments/(?P<proposal_comment_id>\d+)/down-vote/$',
1616
votes_views.proposal_comment_down_vote, name='proposal-comment-down-vote'),
17+
url(r'^(?P<proposal_slug>[\w-]+)/comments/(?P<proposal_comment_id>\d+)/mark_spam/$',
18+
comments_views.mark_comment_as_spam, name='comment_mark_spam'),
19+
url(r'^(?P<proposal_slug>[\w-]+)/comments/(?P<proposal_comment_id>\d+)/unmark_spam/$',
20+
comments_views.unmark_comment_as_spam, name='comment_unmark_spam'),
1721
]
1822

1923
urlpatterns = [

junction/static/css/app.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8993,3 +8993,6 @@ textarea.review-form select {
89938993
.dropdown {
89948994
margin-left: 10px;
89958995
}
8996+
.spam{
8997+
background-color: #b3a6a6;
8998+
}

junction/templates/proposals/detail/comments.html

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,19 @@ <h5 class="text-muted clear-margin vote-count">
3636

3737
</div>
3838

39-
<div class="comment-description" id="comment-{{comment.id}}">
40-
<span>{{ comment.comment|markdown }}</span>
39+
{% if comment.is_spam %}
40+
<div class="comment-description spam" id="comment-{{comment.id}}">
41+
<span>The comment is marked as spam.</span>
42+
{% if request.user.is_authenticated and request.user == comment.marked_marked_as_spam_by %}
43+
<a href="#" data-url="{{ comment.get_unmark_spam_url }}" class="js-unmark-spam">Unmark as spam</a>
44+
{% endif %}<br/><br/>
45+
{% else %}
46+
<div class="comment-description" id="comment-{{comment.id}}">
47+
<span>{{ comment.comment|markdown }}</span>
48+
{% if request.user.is_authenticated and request.user != comment.commenter %}
49+
<a href="#" data-url="{{ comment.get_mark_spam_url }}" class="js-mark-spam">Mark as spam</a><br/><br/>
50+
{% endif %}
51+
{% endif %}
4152
<b>
4253
{% if comment.private or comment.reviewer %}
4354
{% if comment.commenter == proposal.author %}
@@ -118,6 +129,18 @@ <h5 class="text-muted clear-margin vote-count">
118129
$(_that).siblings('.vote-count').html(result);
119130
});
120131
});
132+
$('.js-mark-spam').click(function(e){
133+
e.preventDefault();
134+
var that = $(this);
135+
var url = that.attr('data-url');
136+
$.ajax({
137+
method: "POST",
138+
url: url})
139+
.done(function( msg ){
140+
location.reload(true);
141+
});
142+
});
143+
121144
{% else %}
122145
$('.js-proposal-upvote, .js-proposal-downvote, .js-proposal-comment-upvote, .js-proposal-comment-downvote').click(function(e){
123146
e.preventDefault();

junction/templates/proposals/email/comment/message.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ <h1 style="padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;color:#
3636
<table class="btn-primary" cellpadding="0" cellspacing="0" border="0" style="font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6em;margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;Margin-bottom:10px;width:auto !important;" >
3737
<tr style="font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;line-height:1.6em;margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;" >
3838
<td style="line-height:1.6em;margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#348eda;border-radius:25px;font-family:'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif;font-size:14px;text-align:center;vertical-align:top;" >
39-
<a href="{{host}}{{login_url}}" style="font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#348eda;border-width:10px 20px;border-style:solid;border-color:#348eda;border-radius:25px;display:inline-block;color:#ffffff;cursor:pointer;font-weight:bold;line-height:2;text-decoration:none;" >Click here to leave a comment</a>
39+
<a href="{{host}}{{login_url}}" style="font-family:'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif;font-size:100%;margin-top:0;margin-bottom:0;margin-right:0;margin-left:0;padding-top:0;padding-bottom:0;padding-right:0;padding-left:0;background-color:#348eda;border-width:10px 20px;border-style:solid;border-color:#348eda;border-radius:25px;display:inline-block;color:#ffffff;cursor:pointer;font-weight:bold;line-height:2;text-decoration:none;" >Click here to leave a comment or mark the comment as spam</a>
4040
</td>
4141
</tr>
4242
</table>

0 commit comments

Comments
 (0)