404 lines
16 KiB
Python
404 lines
16 KiB
Python
# Episodes.Community - Community-Driven TV Show Episode Link Sharing Site
|
|
# Copyright (C) 2017 Evert "Diamond" Prants <evert@lunasqu.ee>, Taizo "Tsa6" Simpson <taizo@tsa6.net>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
from django.template import RequestContext
|
|
from django.shortcuts import render, redirect, get_list_or_404, get_object_or_404
|
|
from django.views import View
|
|
from django.views.generic.base import TemplateView
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.conf import settings
|
|
from django.http import Http404, HttpResponseForbidden, HttpResponse, HttpResponseRedirect
|
|
from django.db.models import Case, When, Value, IntegerField, Count, F, Q, Max
|
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
|
from django.shortcuts import render
|
|
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
|
from django.utils.text import slugify
|
|
|
|
from guardian.decorators import permission_required_or_403
|
|
|
|
from LandingPage.models import Show, DiscussionBoard, DiscussionReply, DiscussionVote, Ban, Report, UserAction
|
|
from . import forms
|
|
|
|
import datetime
|
|
import re
|
|
|
|
class Boards(TemplateView):
|
|
|
|
template_name = "boards.html"
|
|
|
|
def get_context_data(self, abbr, **kwargs):
|
|
ctx = super().get_context_data()
|
|
|
|
show = get_object_or_404(Show, abbr=abbr)
|
|
|
|
page = self.request.GET.get('page', 1)
|
|
|
|
boards_list = DiscussionBoard.objects.filter(show=show).annotate(
|
|
num_replies=Count('replies'),
|
|
recency=Case(
|
|
When(
|
|
num_replies=0,
|
|
then=Max('timestamp')
|
|
),
|
|
When(
|
|
num_replies__gt=0,
|
|
then=Max('replies__timestamp')
|
|
)
|
|
),
|
|
).order_by('-pinned','-recency')
|
|
paginator = Paginator(boards_list, getattr(settings, "DISCUSSIONS_PER_PAGE", 26))
|
|
|
|
try:
|
|
boards = paginator.page(page)
|
|
except PageNotAnInteger:
|
|
boards = paginator.page(1)
|
|
except EmptyPage:
|
|
boards = paginator.page(paginator.num_pages)
|
|
|
|
ctx['boards'] = boards
|
|
ctx['show'] = show
|
|
|
|
return ctx
|
|
|
|
class Board(TemplateView):
|
|
|
|
template_name = "board.html"
|
|
|
|
def get_context_data(self, abbr, bid, **kwargs):
|
|
ctx = super().get_context_data()
|
|
show = get_object_or_404(Show, abbr=abbr)
|
|
board = get_object_or_404(DiscussionBoard, pk=bid)
|
|
|
|
page = self.request.GET.get('page', 1)
|
|
find = self.request.GET.get('findReply', None)
|
|
|
|
reply_list = DiscussionReply.objects.filter(board=board).order_by('timestamp').annotate(
|
|
positives=Count(
|
|
Case(
|
|
When(
|
|
votes__positive=True,
|
|
then=Value(1)
|
|
)
|
|
)
|
|
),
|
|
negatives=Count('votes') - F('positives'),
|
|
score=F('positives') - F('negatives')
|
|
)
|
|
perpage = getattr(settings, "DISCUSSIONS_REPLIES_PER_PAGE", 10)
|
|
paginator = Paginator(reply_list, perpage)
|
|
|
|
if find and find.isnumeric():
|
|
item = get_object_or_404(DiscussionReply, pk=find)
|
|
if item.board == board:
|
|
found = DiscussionReply.objects.filter(timestamp__lt=item.timestamp,board=board).count()
|
|
page = int(found / perpage) + 1
|
|
index = int(found % perpage) + 1
|
|
ctx['url'] = show.url() + '/discuss/board/%d-%s?page=%d#reply-%d'%(board.pk, slugify(board.title), page, index)
|
|
|
|
return ctx
|
|
|
|
try:
|
|
replies = paginator.page(page)
|
|
except PageNotAnInteger:
|
|
replies = paginator.page(1)
|
|
except EmptyPage:
|
|
replies = paginator.page(paginator.num_pages)
|
|
|
|
ctx['board'] = board
|
|
ctx['replies'] = replies
|
|
ctx['show'] = show
|
|
ctx['form'] = forms.ReplyForm()
|
|
|
|
return ctx
|
|
|
|
def render_to_response(self, context):
|
|
if 'url' in context:
|
|
return redirect(context['url'])
|
|
|
|
return super(Board, self).render_to_response(context)
|
|
|
|
# Board form GET and POST
|
|
@login_required
|
|
def BoardForm(req, abbr):
|
|
show = get_object_or_404(Show, abbr=abbr)
|
|
user = req.user
|
|
|
|
form = forms.BoardForm()
|
|
|
|
# Request context
|
|
ctx = {
|
|
'form': form,
|
|
'show': show
|
|
}
|
|
|
|
# Get bans for this user regarding this show
|
|
bans = Ban.objects.filter(Q(scope=show) | Q(site_wide=True), Q(expiration__gte=datetime.datetime.now()) | Q(permanent=True), user=user)
|
|
|
|
if bans.count() > 0:
|
|
return HttpResponseForbidden('You are banned from discussing this show.<br>Reason: %s'%(bans.first().reason))
|
|
|
|
# Handle POST
|
|
if req.method == 'POST':
|
|
form = forms.BoardForm(req.POST)
|
|
ctx['form'] = form
|
|
|
|
if form.is_valid():
|
|
form_data = form.cleaned_data
|
|
|
|
# Check if the Title has already been posted
|
|
if DiscussionBoard.objects.filter(show=show,title=form_data['title']).count() > 0:
|
|
ctx['error'] = 'A board with this title already exists!'
|
|
return render(req, "boards_new.html", ctx)
|
|
|
|
if not user.has_perm('LandingPage.can_moderate_board', show):
|
|
# Check if there has been a board by this user for this show within the last 24 hours
|
|
if DiscussionBoard.objects.filter(user=user,show=show,timestamp__gte=datetime.datetime.now() - datetime.timedelta(hours=24)).count() > 8:
|
|
ctx['error'] = 'You can only create 8 boards for a show in 24 hours!'
|
|
return render(req, "boards_new.html", ctx)
|
|
|
|
new_board = form.save(commit=False)
|
|
new_board.user = user
|
|
new_board.show = show
|
|
new_board.save()
|
|
|
|
new_post = DiscussionReply(user=user,board=new_board,body=form_data['body'])
|
|
new_post.save()
|
|
|
|
act = UserAction(user=user,show=show,act_type=0,url='%s/discuss/board/%d-%s'%(show.url(), new_board.pk, slugify(form_data['title'])))
|
|
act.save()
|
|
|
|
return HttpResponseRedirect(show.url() + '/discuss/board/%d-%s'%(new_board.pk, slugify(form_data['title'])))
|
|
else:
|
|
ctx['error'] = 'Invalid fields!'
|
|
|
|
return render(req, "boards_new.html", ctx)
|
|
|
|
# Reply form GET and POST
|
|
@login_required
|
|
def BoardReplyForm(req, abbr, bid):
|
|
show = get_object_or_404(Show, abbr=abbr)
|
|
board = get_object_or_404(DiscussionBoard, pk=bid)
|
|
user = req.user
|
|
|
|
form = forms.ReplyForm()
|
|
|
|
# Request context
|
|
ctx = {
|
|
'form': form,
|
|
'board': board,
|
|
'show': show
|
|
}
|
|
|
|
# Get bans for this user regarding this show
|
|
bans = Ban.objects.filter(Q(scope=show) | Q(site_wide=True), Q(expiration__gte=datetime.datetime.now()) | Q(permanent=True), user=user)
|
|
|
|
if bans.count() > 0:
|
|
return HttpResponseForbidden('You are banned from discussing this show.<br>Reason: %s'%(bans.first().reason))
|
|
|
|
# Handle POST
|
|
if req.method == 'POST':
|
|
form = forms.ReplyForm(req.POST)
|
|
ctx['form'] = form
|
|
|
|
if form.is_valid():
|
|
form_data = form.cleaned_data
|
|
|
|
# Body Content Filter
|
|
real_content = re.sub(r'[\s\W]+', '', form_data['body'])
|
|
err_res = False
|
|
if len(real_content) < 10:
|
|
ctx['error'] = 'The content is too small! Please write more meaningful replies.'
|
|
err_res = True
|
|
elif len(real_content) > 4000:
|
|
ctx['error'] = 'The content body is too large! Please write less in a single reply.'
|
|
err_res = True
|
|
|
|
# TODO: Apply word filtering here
|
|
# TODO: Apply markdown
|
|
|
|
if err_res:
|
|
return render(req, "board_reply.html", ctx)
|
|
|
|
new_reply = form.save(commit=False)
|
|
new_reply.user = user
|
|
new_reply.board = board
|
|
new_reply.save()
|
|
|
|
act = UserAction(user=user,show=show,act_type=1,url='%s/discuss/board/%d-%s?findReply=%d'%(show.url(), new_board.pk, slugify(form_data['title']), new_reply.pk))
|
|
act.save()
|
|
|
|
return HttpResponseRedirect(show.url() + '/discuss/board/%d-%s?findReply=%d'%(board.pk, slugify(board.title), new_reply.pk))
|
|
else:
|
|
ctx['error'] = 'Invalid fields!'
|
|
|
|
return render(req, "board_reply.html", ctx)
|
|
|
|
# Vote request
|
|
# /show/{{abbr}}/vote/{{submission id}}/{{positive == 1}}
|
|
class BoardVoteSubmit(LoginRequiredMixin, View):
|
|
def post (self, req, abbr, replyid, positive):
|
|
# Convert positive parameter into a boolean
|
|
pos_bool = int(positive) == 1
|
|
|
|
user = req.user
|
|
|
|
# Get the reply from the database
|
|
reply = get_object_or_404(DiscussionReply, id=replyid)
|
|
showurl = reply.board.show.url()
|
|
|
|
# Prevent voting for own reply
|
|
if reply.user == user:
|
|
return HttpResponse('<h1>Error</h1><p>You cannot vote for your own reply.</p><p>'
|
|
'<a href="%s/discuss/board/%d-%s">Return to board</a></p>'
|
|
% (showurl, reply.board.pk, slugify(reply.board.title)), status=400)
|
|
|
|
show = reply.board.show
|
|
|
|
# Get bans for this user regarding this show
|
|
bans = Ban.objects.filter(Q(scope=show) | Q(site_wide=True), Q(expiration__gte=datetime.datetime.now()) | Q(permanent=True), user=user)
|
|
|
|
if bans.count() > 0:
|
|
return HttpResponseForbidden('You are banned from voting on this show\'s discussion boards.<br>Reason: %s'%(bans.first().reason))
|
|
|
|
# Allow changing a vote from positive to negative or vice-versa. Delete vote if its a re-vote
|
|
vote = reply.votes.filter(user=user,reply=reply).first()
|
|
if vote:
|
|
if not vote.positive == pos_bool:
|
|
vote.positive = pos_bool
|
|
vote.save()
|
|
|
|
act = UserAction(user=user,show=show,act_type=6 + int(positive),url='%s/discuss/board/%d-%s?findReply=%d'%(showurl,
|
|
reply.board.pk, slugify(reply.board.title), reply.pk))
|
|
act.save()
|
|
else:
|
|
vote.delete()
|
|
else:
|
|
new_vote = DiscussionVote(
|
|
user=user,
|
|
reply=reply,
|
|
positive=pos_bool
|
|
)
|
|
new_vote.save()
|
|
|
|
act = UserAction(user=user,show=show,act_type=6 + int(positive),url='%s/discuss/board/%d-%s?findReply=%d'%(showurl, reply.board.pk,
|
|
slugify(reply.board.title), reply.pk))
|
|
act.save()
|
|
|
|
return HttpResponseRedirect('%s/discuss/board/%d-%s?findReply=%d'%(showurl, reply.board.pk, slugify(reply.board.title), reply.pk))
|
|
|
|
@login_required
|
|
def ReportForm(req, abbr, rid):
|
|
show = get_object_or_404(Show, abbr=abbr)
|
|
reply = get_object_or_404(DiscussionReply, pk=rid,board__show=show)
|
|
user = req.user
|
|
|
|
form = forms.ReportForm()
|
|
|
|
# Get bans for this user regarding this show
|
|
bans = Ban.objects.filter(Q(expiration__gte=datetime.datetime.now()) | Q(permanent=True), user=user, site_wide=True)
|
|
|
|
if bans.count() > 0:
|
|
return HttpResponseForbidden('You are banned from the site and therefor not allowed to create reports.<br>Reason: %s'%(bans.first().reason))
|
|
|
|
# Request context
|
|
ctx = {
|
|
'form': form,
|
|
'show': show,
|
|
'reply': reply
|
|
}
|
|
|
|
url = '%s/discuss/board/%d-%s?findReply=%d'%(show.url(), reply.board.pk, slugify(reply.board.title), reply.pk)
|
|
|
|
# Handle POST
|
|
if req.method == 'POST':
|
|
form = forms.ReportForm(req.POST)
|
|
ctx['form'] = form
|
|
|
|
if form.is_valid():
|
|
form_data = form.cleaned_data
|
|
|
|
if not user.has_perm('LandingPage.can_moderate_board', show):
|
|
# Check if there have been many reports by this user within the last 12 hours
|
|
if Report.objects.filter(reporter=user,timestamp__gte=datetime.datetime.now() - datetime.timedelta(hours=12)).count() > 5:
|
|
ctx['error'] = 'You\'ve created too many reports recently!'
|
|
return render(req, "report_reply.html", ctx)
|
|
|
|
if Report.objects.filter(url=url).count() > 1:
|
|
ctx['error'] = 'This reply has already been brought to our attention! Thank you for reporting.'
|
|
return render(req, "report_reply.html", ctx)
|
|
|
|
# Save
|
|
new_report = form.save(commit=False)
|
|
new_report.reporter = user
|
|
new_report.url = url
|
|
new_report.show = show
|
|
new_report.save()
|
|
|
|
return HttpResponseRedirect('%s/discuss/board/%d-%s'%(show.url(), reply.board.pk, slugify(reply.board.title)))
|
|
else:
|
|
ctx['error'] = 'Invalid fields!'
|
|
|
|
return render(req, "report_reply.html", ctx)
|
|
|
|
@login_required
|
|
def BoardLock(req, abbr, bid):
|
|
user = req.user
|
|
board = get_object_or_404(DiscussionBoard, pk=bid)
|
|
|
|
if not user.has_perm('LandingPage.can_moderate_board', board.show) and not board.user == user:
|
|
return HttpResponse('<h1>Error</h1><p>You do not have permission to lock this show.</p><p>'
|
|
'<a href="%s/discuss/board/%d-%s">Return to board</a></p>'
|
|
% (board.show.url(), board.pk, slugify(board.title)), status=400)
|
|
|
|
lock = not board.locked
|
|
DiscussionBoard.objects.filter(pk=board.pk).update(locked=lock)
|
|
return HttpResponseRedirect('%s/discuss/board/%d-%s'%(board.show.url(), board.pk, slugify(board.title)))
|
|
|
|
|
|
@permission_required_or_403('LandingPage.can_moderate_board', (Show, 'abbr', 'abbr'), accept_global_perms=True)
|
|
def BoardPin(req, abbr, bid):
|
|
board = get_object_or_404(DiscussionBoard, pk=bid)
|
|
|
|
pin = not board.pinned
|
|
|
|
DiscussionBoard.objects.filter(pk=board.pk).update(pinned=pin)
|
|
return HttpResponseRedirect('%s/discuss/board/%d-%s'%(board.show.url(), board.pk, slugify(board.title)))
|
|
|
|
@permission_required_or_403('LandingPage.can_moderate_board', (Show, 'abbr', 'abbr'), accept_global_perms=True)
|
|
def BoardDelete(req, abbr, bid):
|
|
board = get_object_or_404(DiscussionBoard, pk=bid)
|
|
showurl = get_show_url(abbr)
|
|
|
|
act = UserAction(user=req.user,show=board.show,act_type=4,url='#%s by %s'%(board.title, board.user.display_name))
|
|
act.save()
|
|
|
|
DiscussionBoard.objects.filter(pk=board.pk).delete()
|
|
|
|
return HttpResponseRedirect('%s/discuss' % (board.show.url()))
|
|
|
|
@permission_required_or_403('LandingPage.can_moderate_board', (Show, 'abbr', 'abbr'), accept_global_perms=True)
|
|
def BoardDeleteReply(req, abbr, rid):
|
|
reply = get_object_or_404(DiscussionReply, pk=rid)
|
|
|
|
act = UserAction(user=req.user,show=reply.board.show,act_type=4,url='%s/discuss/board/%d-%s?findReply=%d'%(reply.board.show.url(),
|
|
reply.board.pk, slugify(reply.board.title), reply.pk))
|
|
act.save()
|
|
|
|
delete = not reply.deleted
|
|
|
|
DiscussionReply.objects.filter(pk=reply.pk).update(deleted=delete)
|
|
|
|
return HttpResponseRedirect('%s/discuss/board/%d-%s'%(reply.board.show.url(), reply.board.pk, slugify(reply.board.title)))
|