Boards, Replies and Votes
This commit is contained in:
parent
22ca7de96b
commit
8230e83528
37
Discussions/forms.py
Normal file
37
Discussions/forms.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# 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 import forms
|
||||||
|
from LandingPage.models import DiscussionBoard, DiscussionReply
|
||||||
|
|
||||||
|
class BoardForm(forms.ModelForm):
|
||||||
|
body = forms.CharField(widget=forms.Textarea)
|
||||||
|
|
||||||
|
class Meta():
|
||||||
|
model = DiscussionBoard
|
||||||
|
fields = ('title','body',)
|
||||||
|
help_texts = {
|
||||||
|
'title': 'Name of the board',
|
||||||
|
'body': 'Enter your message here'
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReplyForm(forms.ModelForm):
|
||||||
|
class Meta():
|
||||||
|
model = DiscussionReply
|
||||||
|
fields = ('body',)
|
||||||
|
help_texts = {
|
||||||
|
'body': 'Enter your message here'
|
||||||
|
}
|
113
Discussions/templates/board.html
Normal file
113
Discussions/templates/board.html
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
{{board.title}} - {{show.name}} Discussions - Episodes.Community
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mb-5 mt-5">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}/discuss">Discussions</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{{board.title}}</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<div class="row">
|
||||||
|
<h1 class="col">{{board.title}}</h1>
|
||||||
|
<div class="col-2">
|
||||||
|
<div class="d-flex flex-row-reverse mt-2">
|
||||||
|
{% if board.locked %}
|
||||||
|
<p>This board is locked</p>
|
||||||
|
{% else %}
|
||||||
|
{% if user.is_authenticated %}
|
||||||
|
<a href="/show/{{show.abbr}}/discuss/board/reply/{{board.pk}}" class="btn btn-primary"><i class="fa fa-fw fa-pencil"></i> Reply</a>
|
||||||
|
{% else %}
|
||||||
|
<p><a href="/login">Log in</a> to reply</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="timestamp text-muted font-weight-light">Created {{board.timestamp}} by {{board.user.display_name}}</p>
|
||||||
|
{% for reply in replies %}
|
||||||
|
<div class="reply border-bottom mb-4" id="reply-{{forloop.counter}}">
|
||||||
|
<div class="row">
|
||||||
|
<div class="avatar">
|
||||||
|
<img src="https://icynet.eu/api/avatar/{{reply.user.icy_id}}" class="m-auto d-block">
|
||||||
|
<p class="text-center font-weight-bold">{{reply.user.display_name}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="col border-left d-flex flex-column">
|
||||||
|
<p class="timestamp text-muted font-weight-light">Submitted {{board.timestamp}}</p>
|
||||||
|
<div class="user-content mb-auto">
|
||||||
|
{{reply.body}}
|
||||||
|
</div>
|
||||||
|
<div class="actions d-flex flex-row-reverse">
|
||||||
|
<div class="vote-btns">
|
||||||
|
<form method="POST" class="d-inline" action="/show/{{show.abbr}}/discuss/vote/{{reply.id}}/1">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button href="#" class="btn btn-link text-success">
|
||||||
|
<i class="fa fa-fw fa-thumbs-up"></i> {{reply.positives}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" class="d-inline" action="/show/{{show.abbr}}/discuss/vote/{{reply.id}}/0">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button href="#" class="btn btn-link text-danger">
|
||||||
|
<i class="fa fa-fw fa-thumbs-down"></i> {{reply.negatives}}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!--<a href="#" class="btn btn-secondary mr-1">Quote</a>-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<h3>Nobody has replied to this board!</h3>
|
||||||
|
{% endfor %}
|
||||||
|
{% if replies.has_other_pages %}
|
||||||
|
<nav aria-label="Boards navigation">
|
||||||
|
<ul class="pagination">
|
||||||
|
{% if replies.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a href="?page={{ replies.previous_page_number }}" class="page-link">Previous</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for i in replies.paginator.page_range %}
|
||||||
|
{% if replies.number == i %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ i }} <span class="sr-only">(current)</span></span>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ i }}">{{ i }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if replies.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a href="?page={{ replies.next_page_number }}" class="page-link">Next</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% if user.is_authenticated and not board.locked %}
|
||||||
|
<h2>Quick Reply</h2>
|
||||||
|
<div class="reply-box">
|
||||||
|
<form class="form-horizontal" role="form" action="/show/{{show.abbr}}/discuss/board/reply/{{board.pk}}" method="post">
|
||||||
|
{% include "form.html" %}
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Reply</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
26
Discussions/templates/board_reply.html
Normal file
26
Discussions/templates/board_reply.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Reply - {{show.name}} Discussions - Episodes.Community
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="container mt-5 mb-5">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}/discuss">Discussions</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}/discuss/board/{{board.pk}}-{{board.title|slugify}}">{{board.title}}</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Reply</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<h1>New Reply</h1>
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger">{{error}}</div>
|
||||||
|
{% endif %}
|
||||||
|
<form class="form-horizontal" role="form" action="" method="post">
|
||||||
|
{% include "form.html" %}
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Submit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
@ -4,12 +4,19 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container mb-5 mt-5">
|
<div class="container mb-5 mt-5">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Discussions</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
<h1>{{show.name}} Discussion Boards</h1>
|
<h1>{{show.name}} Discussion Boards</h1>
|
||||||
|
<p>Discuss {{show.name}} with your fellow community members!</p>
|
||||||
<div class="d-flex flex-row-reverse mb-4">
|
<div class="d-flex flex-row-reverse mb-4">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<a href="/show/{{show.abbr}}/discuss/board/new" class="btn btn-primary"><i class="fa fa-fw fa-pencil"></i> Create New Board</a>
|
<a href="/show/{{show.abbr}}/discuss/board/new" class="btn btn-primary"><i class="fa fa-fw fa-pencil"></i> Create New Board</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="/login">Log in</a> to create boards
|
<p><a href="/login">Log in</a> to create boards</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="bg-light rounded p-2 row">
|
<div class="bg-light rounded p-2 row">
|
||||||
@ -29,7 +36,17 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-2">
|
<div class="col-2">
|
||||||
|
{% if board.num_replies > 0 %}
|
||||||
|
<div class="reply mt-2">
|
||||||
|
<div class="user">
|
||||||
|
<small class="text-muted font-weight-light">by </small>
|
||||||
|
{{board.latest_reply.user.display_name}}
|
||||||
|
</div>
|
||||||
|
<small class="timestamp text-muted font-weight-light">{{board.latest_reply.timestamp}}</small>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
<span class="text-muted font-weight-light">No replies</span>
|
<span class="text-muted font-weight-light">No replies</span>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -56,11 +73,11 @@
|
|||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<span class="page-link" href="?page={{ i }}">{{ i }}</span>
|
<a class="page-link" href="?page={{ i }}">{{ i }}</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if users.has_next %}
|
{% if boards.has_next %}
|
||||||
<li class="page-item">
|
<li class="page-item">
|
||||||
<a href="?page={{ boards.next_page_number }}" class="page-link">Next</a>
|
<a href="?page={{ boards.next_page_number }}" class="page-link">Next</a>
|
||||||
</li>
|
</li>
|
||||||
|
25
Discussions/templates/boards_new.html
Normal file
25
Discussions/templates/boards_new.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
Create a Board - {{show.name}} Discussions - Episodes.Community
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="container mt-5 mb-5">
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li>
|
||||||
|
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}/discuss">Discussions</a></li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">New Board</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<h1>Create a Board</h1>
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger">{{error}}</div>
|
||||||
|
{% endif %}
|
||||||
|
<form class="form-horizontal" role="form" action="" method="post">
|
||||||
|
{% include "form.html" %}
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="submit" class="btn btn-primary">Create</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
@ -20,5 +20,9 @@ from . import views
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.Boards.as_view()),
|
url(r'^$', views.Boards.as_view()),
|
||||||
|
url(r'^vote/(?P<replyid>\d+)/(?P<positive>[0-1])/?$', views.BoardVoteSubmit.as_view()),
|
||||||
|
url(r'^board/new$', views.BoardForm),
|
||||||
|
url(r'^board/reply/(?P<bid>\d{1,4})/?$', views.BoardReplyForm),
|
||||||
|
url(r'^board/(?P<bid>\d{1,4})(-[\w-]+)?/?$', views.Board.as_view()),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -14,20 +14,25 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
from django.shortcuts import render, get_list_or_404, get_object_or_404
|
from django.shortcuts import render, redirect, get_list_or_404, get_object_or_404
|
||||||
from django.views import View
|
from django.views import View
|
||||||
from django.views.generic.base import TemplateView
|
from django.views.generic.base import TemplateView
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.http import Http404, HttpResponseForbidden, HttpResponse, HttpResponseRedirect
|
from django.http import Http404, HttpResponseForbidden, HttpResponse, HttpResponseRedirect
|
||||||
from django.db.models import Case, When, Value, IntegerField, Count, F, Q
|
from django.db.models import Case, When, Value, IntegerField, Count, F, Q, Max
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from guardian.decorators import permission_required_or_403
|
from guardian.decorators import permission_required_or_403
|
||||||
|
|
||||||
from LandingPage.models import Show, DiscussionBoard, DiscussionReply, DiscussionVote
|
from LandingPage.models import Show, DiscussionBoard, DiscussionReply, DiscussionVote, Ban
|
||||||
|
from . import forms
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
class Boards(TemplateView):
|
class Boards(TemplateView):
|
||||||
|
|
||||||
@ -39,8 +44,21 @@ class Boards(TemplateView):
|
|||||||
|
|
||||||
page = self.request.GET.get('page', 1)
|
page = self.request.GET.get('page', 1)
|
||||||
|
|
||||||
boards_list = DiscussionBoard.objects.filter(show=show)
|
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('-recency')
|
||||||
paginator = Paginator(boards_list, getattr(settings, "DISCUSSIONS_PER_PAGE", 26))
|
paginator = Paginator(boards_list, getattr(settings, "DISCUSSIONS_PER_PAGE", 26))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
boards = paginator.page(page)
|
boards = paginator.page(page)
|
||||||
except PageNotAnInteger:
|
except PageNotAnInteger:
|
||||||
@ -52,3 +70,214 @@ class Boards(TemplateView):
|
|||||||
ctx['show'] = show
|
ctx['show'] = show
|
||||||
|
|
||||||
return ctx
|
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/%s/discuss/board/%d-%s?page=%d#reply-%d'%(abbr, 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()
|
||||||
|
|
||||||
|
return HttpResponseRedirect('/show/%s/discuss/board/%d-%s'%(abbr, 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()
|
||||||
|
|
||||||
|
return HttpResponseRedirect('/show/%s/discuss/board/%d-%s?findReply=%d'%(abbr, 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)
|
||||||
|
|
||||||
|
# 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="/show/%s/discuss/board/%d-%s">Return to board</a></p>'
|
||||||
|
% (abbr, 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()
|
||||||
|
else:
|
||||||
|
vote.delete()
|
||||||
|
else:
|
||||||
|
new_vote = DiscussionVote(
|
||||||
|
user=user,
|
||||||
|
reply=reply,
|
||||||
|
positive=pos_bool
|
||||||
|
)
|
||||||
|
new_vote.save()
|
||||||
|
|
||||||
|
return HttpResponseRedirect('/show/%s/discuss/board/%d-%s?findReply=%d'%(abbr, reply.board.pk, slugify(reply.board.title), reply.pk))
|
||||||
|
@ -173,3 +173,4 @@ AUTH_B64 = base64.b64encode(bytearray('%s:%s'%(AUTH_CLIENT_ID,oauth_options.get(
|
|||||||
AUTH_REDIRECT_URL = oauth_options.get('redirect_url')
|
AUTH_REDIRECT_URL = oauth_options.get('redirect_url')
|
||||||
|
|
||||||
DISCUSSIONS_PER_PAGE = 26
|
DISCUSSIONS_PER_PAGE = 26
|
||||||
|
DISCUSSIONS_REPLIES_PER_PAGE = 10
|
||||||
|
@ -36,7 +36,7 @@ from django.conf.urls.static import static
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^admin/', admin.site.urls),
|
url(r'^admin/', admin.site.urls),
|
||||||
url(r'^show/(?P<abbr>\w{1,16})/discuss', include('Discussions.urls')),
|
url(r'^show/(?P<abbr>\w{1,16})/discuss/', include('Discussions.urls')),
|
||||||
url(r'^show/(?P<abbr>\w{1,16})/', include('Show.urls')),
|
url(r'^show/(?P<abbr>\w{1,16})/', include('Show.urls')),
|
||||||
url(r'^', include('LandingPage.urls'))
|
url(r'^', include('LandingPage.urls'))
|
||||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||||
|
@ -92,6 +92,7 @@ class Show(TimestampedModel):
|
|||||||
permissions = (
|
permissions = (
|
||||||
('can_create_show_ban', 'Can ban an user from submitting to this show'),
|
('can_create_show_ban', 'Can ban an user from submitting to this show'),
|
||||||
('can_moderate_show', 'Can add episodes, seasons and unrestricted submissions'),
|
('can_moderate_show', 'Can add episodes, seasons and unrestricted submissions'),
|
||||||
|
('can_moderate_board', 'Can delete and edit boards and replies of this show'),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -387,10 +388,6 @@ class DiscussionBoard(TimestampedModel):
|
|||||||
max_length=100,
|
max_length=100,
|
||||||
help_text='The title of the discussion'
|
help_text='The title of the discussion'
|
||||||
)
|
)
|
||||||
body = models.TextField(
|
|
||||||
help_text='The body of the post',
|
|
||||||
verbose_name='Body'
|
|
||||||
)
|
|
||||||
views = models.IntegerField(
|
views = models.IntegerField(
|
||||||
help_text='The amount of times this board has been viewed',
|
help_text='The amount of times this board has been viewed',
|
||||||
default=0
|
default=0
|
||||||
@ -399,6 +396,14 @@ class DiscussionBoard(TimestampedModel):
|
|||||||
help_text='Whether or not this board is pinned',
|
help_text='Whether or not this board is pinned',
|
||||||
default=False
|
default=False
|
||||||
)
|
)
|
||||||
|
locked = models.BooleanField(
|
||||||
|
help_text='Whether or not this board is locked for further replies',
|
||||||
|
default=False
|
||||||
|
)
|
||||||
|
|
||||||
|
def latest_reply(self):
|
||||||
|
return self.replies.latest('timestamp')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '[%s] "%s" by %s'%(self.show.abbr, self.title, self.user)
|
return '[%s] "%s" by %s'%(self.show.abbr, self.title, self.user)
|
||||||
|
|
||||||
@ -420,6 +425,10 @@ class DiscussionReply(TimestampedModel):
|
|||||||
help_text='The body of the response',
|
help_text='The body of the response',
|
||||||
verbose_name='Body'
|
verbose_name='Body'
|
||||||
)
|
)
|
||||||
|
deleted = models.BooleanField(
|
||||||
|
help_text='Whether or not the content has been deleted by a moderator',
|
||||||
|
default=False
|
||||||
|
)
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return '[%s] %s\'s response to "%s"'%(self.board.show.abbr,self.user, self.board.title)
|
return '[%s] %s\'s response to "%s"'%(self.board.show.abbr,self.user, self.board.title)
|
||||||
|
|
||||||
@ -430,11 +439,11 @@ class DiscussionVote(TimestampedModel):
|
|||||||
related_name='discussion_votes',
|
related_name='discussion_votes',
|
||||||
help_text='The user which cast this vote'
|
help_text='The user which cast this vote'
|
||||||
)
|
)
|
||||||
board = models.ForeignKey(
|
reply = models.ForeignKey(
|
||||||
DiscussionBoard,
|
DiscussionReply,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.CASCADE,
|
||||||
related_name='votes',
|
related_name='votes',
|
||||||
help_text='The board this vote was cast on'
|
help_text='The reply this vote was cast on'
|
||||||
)
|
)
|
||||||
positive = models.BooleanField(
|
positive = models.BooleanField(
|
||||||
help_text='If true, the vote is an upvote. Otherwise, it is a downvote. Neutral votes are not recorded'
|
help_text='If true, the vote is an upvote. Otherwise, it is a downvote. Neutral votes are not recorded'
|
||||||
|
@ -187,6 +187,30 @@ footer .logo .part1 {
|
|||||||
text-shadow: 2px 2px 1px #0059a0;
|
text-shadow: 2px 2px 1px #0059a0;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
.avatar {
|
||||||
|
-webkit-box-flex: 0;
|
||||||
|
-ms-flex: 0 0 180px;
|
||||||
|
flex: 0 0 180px;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
.avatar img {
|
||||||
|
max-width: 150px;
|
||||||
|
max-height: 150px;
|
||||||
|
}
|
||||||
|
.mini_avatar {
|
||||||
|
-webkit-box-flex: 0;
|
||||||
|
-ms-flex: 0 0 50px;
|
||||||
|
flex: 0 0 45px;
|
||||||
|
max-width: 45px;
|
||||||
|
}
|
||||||
|
.mini_avatar img {
|
||||||
|
max-width: 45px;
|
||||||
|
max-height: 45px;
|
||||||
|
}
|
||||||
|
.user-content {
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
@media all and (max-width: 800px) {
|
@media all and (max-width: 800px) {
|
||||||
.logo {
|
.logo {
|
||||||
font-size: 5vw !important;
|
font-size: 5vw !important;
|
||||||
|
Reference in New Issue
Block a user