Merge pull request #63 from IcyNet/community

Discussion boards and other fixes
This commit is contained in:
Evert Prants 2018-03-02 16:39:54 +02:00 committed by GitHub
commit f3bf9a4e44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1002 additions and 49 deletions

42
Discussions/forms.py Normal file
View File

@ -0,0 +1,42 @@
# 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, Report
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'
}
class ReportForm(forms.ModelForm):
class Meta():
model = Report
fields = ('title','details',)

View File

@ -0,0 +1,160 @@
{% extends "base.html" %}
{% block title %}
{{board.title}} - {{show.name}} Discussions - Episodes.Community
{% endblock %}
{% load markdown %}
{% load guardian_tags %}
{% block content %}
{% get_obj_perms request.user for show as "show_perms" %}
<div class="container mb-5 mt-5">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{show.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item"><a href="{{show.url}}/discuss">Discussions</a></li>
<li class="breadcrumb-item active" aria-current="page">{{board.title}}</li>
</ol>
</nav>
<div class="row">
<h1 class="col">{% if board.locked %}<i class="fa fa-fw fa-lock"></i>{% endif %}{% if board.pinned %}<i class="fa fa-fw fa-thumb-tack"></i>{% endif %}{{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.url}}/discuss/board/reply/{{board.pk}}" class="btn btn-primary"><i class="fa fa-fw fa-pencil"></i>&nbsp;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}}">
{% if reply.deleted %}
<h3 class="font-weight-light">This reply has been deleted by a moderator.</h1>
<span class="font-weight-light text-muted">ID: {{reply.pk}}</span>
{% if "can_moderate_board" in show_perms or board.user == user %}
<div class="alert alert-warning"><div class="font-weight-light">{{reply.body}}</div></div>
<a href="{{show.url}}/discuss/board/delete/reply/{{reply.id}}" class="btn btn-warning ml-1">Restore</a>
<a href="{{show.url}}/create_ban?user={{reply.user.username}}" class="btn btn-warning ml-1">Ban</a>
{% endif %}
<div class="w-100 mb-4"></div>
{% else %}
<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|markdown|safe}}</div>
<div class="actions d-flex flex-row-reverse">
{% if user.is_authenticated %}
{% if "can_moderate_board" in show_perms %}
<a href="{{show.url}}/discuss/board/delete/reply/{{reply.id}}" class="btn btn-warning ml-1">Delete Content</a>
<a href="{{show.url}}/create_ban?user={{reply.user.username}}" class="btn btn-warning ml-1">Ban</a>
{% elif not user == reply.user %}
<a href="{{show.url}}/discuss/board/report/{{reply.id}}" class="btn btn-secondary ml-1" title="Report" aria-label="Report"><i class="fa fa-fw fa-flag"></i></a>
{% endif %}
{% endif %}
<div class="vote-btns">
{% if not board.locked %}
<form method="POST" class="d-inline" action="{{show.url}}/discuss/vote/{{reply.id}}/1">
{% csrf_token %}
<button href="#" class="btn btn-link text-success">
<i class="fa fa-fw fa-thumbs-up"></i>&nbsp;{{reply.positives}}
</button>
</form>
<form method="POST" class="d-inline" action="{{show.url}}/discuss/vote/{{reply.id}}/0">
{% csrf_token %}
<button href="#" class="btn btn-link text-danger">
<i class="fa fa-fw fa-thumbs-down"></i>&nbsp;{{reply.negatives}}
</button>
</form>
{% else %}
<a href="#" class="btn btn-link disabled text-success">
<i class="fa fa-fw fa-thumbs-up"></i>&nbsp;{{reply.positives}}
</a>
<a href="#" class="btn btn-link disabled text-danger">
<i class="fa fa-fw fa-thumbs-down"></i>&nbsp;{{reply.negatives}}
</a>
{% endif %}
</div>
<!--<a href="#" class="btn btn-secondary mr-1">Quote</a>-->
</div>
</div>
</div>
{% endif %}
</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.url}}/discuss/board/reply/{{board.pk}}" method="post">
{% include "form.html" %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Reply</button>
</div>
<script type="text/javascript">
var simplemde = new SimpleMDE({ element: document.getElementById("id_body"), forceSync: true });
</script>
</form>
</div>
{% endif %}
{% if user.is_authenticated %}
<h4>Board Tools</h4>
<ul>
{% if not board.locked %}
<li><a href="{{show.url}}/discuss/board/reply/{{board.pk}}">Reply to the board</a></li>
{% endif %}
{% if "can_moderate_board" in show_perms or board.user == user and not board.locked %}
<li><a href="{{show.url}}/discuss/board/lock/{{board.pk}}">Lock the board from further replies</a></li>
{% endif %}
{% if "can_moderate_board" in show_perms %}
<li><a href="{{show.url}}/discuss/board/pin/{{board.pk}}">{% if board.pinned %}Unpin the board{% else %}Pin the board at the top{% endif %}</a></li>
<li><a href="{{show.url}}/discuss/board/delete/{{board.pk}}">Delete the board</a></li>
{% endif %}
</ul>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% 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.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item"><a href="{{show.url}}/discuss">Discussions</a></li>
<li class="breadcrumb-item"><a href="{{show.url}}/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>
<script type="text/javascript">
var simplemde = new SimpleMDE({ element: document.getElementById("id_body"), forceSync: true });
</script>
</form>
</section>
{% endblock %}

View File

@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block 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.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item active" aria-current="page">Discussions</li>
</ol>
</nav>
<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">
{% if user.is_authenticated %}
<a href="{{show.url}}/discuss/board/new" class="btn btn-primary"><i class="fa fa-fw fa-pencil"></i>&nbsp;Create New Board</a>
{% else %}
<p><a href="/login">Log in</a> to create boards</p>
{% endif %}
</div>
<div class="bg-light rounded p-2 row">
<div class="col">Board Name</div>
<div class="col-2">Latest Reply</div>
</div>
{% for board in boards %}
<div class="board border-bottom">
<div class="row">
<div class="col">
<h2><a href="{{show.url}}/discuss/board/{{board.pk}}-{{board.title|slugify}}">{% if board.locked %}<i class="fa fa-fw fa-lock"></i>{% endif %}{% if board.pinned %}<i class="fa fa-fw fa-thumb-tack"></i>{% endif %}{{board.title}}</a></h2>
<span class="text-muted font-weight-light">Submitted {{board.timestamp}} by
{% if board.user.is_staff %}
<span class="mod"><i class="fa fa-fw fa-shield"></i></span>
{% endif %}
<span class="display_name">{{board.user.display_name}}</span>
</span>
</div>
<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>
{% endif %}
</div>
</div>
</div>
{% empty %}
<h3>Nobody has started any discussions for this show!</h3>
{% endfor %}
{% if boards.has_other_pages %}
<nav aria-label="Boards navigation">
<ul class="pagination">
{% if boards.has_previous %}
<li class="page-item">
<a href="?page={{ boards.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 boards.paginator.page_range %}
{% if boards.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 boards.has_next %}
<li class="page-item">
<a href="?page={{ boards.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 %}
</div>
{% endblock %}

View File

@ -0,0 +1,28 @@
{% 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.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item"><a href="{{show.url}}/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>
<script type="text/javascript">
var simplemde = new SimpleMDE({ element: document.getElementById("id_body"), forceSync: true });
</script>
</form>
</section>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends "base.html" %}
{% block title %}
Report a Post - {{reply.board.title}} - {{show.name}} Discussions - Episodes.Community
{% endblock %}
{% block content %}
<section class="container mb-5 mt-5">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{show.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item"><a href="{{show.url}}/discuss">Discussions</a></li>
<li class="breadcrumb-item"><a href="{{show.url}}/discuss/board/{{reply.board.pk}}-{{reply.board.title|slugify}}">{{reply.board.title}}</a></li>
<li class="breadcrumb-item active" aria-current="page">Report</li>
</ol>
</nav>
<h1>Report a Post</h1>
{% if error %}
<div class="alert alert-danger">{{error}}</div>
{% endif %}
<form action="" method="post">
<div class="bg-light p-4 mb-4">
<div class="submitter font-weight-bold">Posted by {{ reply.user.display_name }}</div>
<div class="body font-weight-light text-muted">{{ reply.body }}</div>
{% if reply.user.is_staff %}
<div class="alert alert-danger">
<b>Warning</b>
<p>This reply is made by a staff member. Unnecessary reporters <b>will</b> be banned.</p>
</div>
{% endif %}
</div>
{% include "form.html" %}
<div class="form-group">
<button type="submit" class="btn btn-primary">Submit</button>
</div>
</form>
</section>
{% endblock %}

View File

@ -0,0 +1,28 @@
# 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 template
import bleach
import markdown as md
import re
register = template.Library()
def markdown(value):
return md.markdown(re.sub(r'\&gt\;', '>', bleach.clean(value)), output_format="html5")
register.filter('markdown', markdown)

33
Discussions/urls.py Normal file
View File

@ -0,0 +1,33 @@
# Episodes.Community - Community-Driven TV Show Episode Link Sharing Site
# Copyright (C) 2018 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.conf.urls import url
from . import views
urlpatterns = [
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/report/(?P<rid>\d{1,4})/?$', views.ReportForm),
url(r'^board/pin/(?P<bid>\d{1,4})/?$', views.BoardPin),
url(r'^board/delete/reply/(?P<rid>\d{1,4})/?$', views.BoardDeleteReply),
url(r'^board/delete/(?P<bid>\d{1,4})/?$', views.BoardDelete),
url(r'^board/lock/(?P<bid>\d{1,4})/?$', views.BoardLock),
url(r'^board/reply/(?P<bid>\d{1,4})/?$', views.BoardReplyForm),
url(r'^board/(?P<bid>\d{1,4})(-[\w-]+)?/?$', views.Board.as_view()),
]

View File

@ -13,7 +13,370 @@
# #
# 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.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.shortcuts import render
from django.core.paginator import Paginator, PageNotAnInteger, EmptyPage
from django.utils.text import slugify
# Create your views here. from guardian.decorators import permission_required_or_403
from LandingPage.models import Show, DiscussionBoard, DiscussionReply, DiscussionVote, Ban, Report
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()
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)
print(form_data['body'])
new_reply = form.save(commit=False)
new_reply.user = user
new_reply.board = board
new_reply.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()
else:
vote.delete()
else:
new_vote = DiscussionVote(
user=user,
reply=reply,
positive=pos_bool
)
new_vote.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(user=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.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)
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)
delete = not reply.deleted
DiscussionReply.objects.filter(pk=reply.pk).update(deleted=delete)
return HttpResponseRedirect('%s/discuss/board/%d-%s'%(reply.show.url(), reply.board.pk, slugify(reply.board.title)))

View File

@ -171,3 +171,12 @@ AUTH_TOKEN_ENDPOINT = oauth_options.get('token_endpoint','https://icynet.eu/oaut
AUTH_CLIENT_ID = oauth_options.get('client_id') AUTH_CLIENT_ID = oauth_options.get('client_id')
AUTH_B64 = base64.b64encode(bytearray('%s:%s'%(AUTH_CLIENT_ID,oauth_options.get('client_secret')),'utf-8')).decode("utf-8") AUTH_B64 = base64.b64encode(bytearray('%s:%s'%(AUTH_CLIENT_ID,oauth_options.get('client_secret')),'utf-8')).decode("utf-8")
AUTH_REDIRECT_URL = oauth_options.get('redirect_url') AUTH_REDIRECT_URL = oauth_options.get('redirect_url')
DISCUSSIONS_PER_PAGE = 26
DISCUSSIONS_REPLIES_PER_PAGE = 10
# Domain of this app
DOMAIN=options.get('domain')
# Use subdomains for each show
DOMAIN_SUBDOMAIN_SHOWS=options.get('use_subdomain_paths') == 'true'

View File

@ -36,6 +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})/', 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)

View File

@ -15,6 +15,7 @@
# 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.db import models from django.db import models
from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.files.storage import FileSystemStorage from django.core.files.storage import FileSystemStorage
from django.conf import settings from django.conf import settings
@ -92,8 +93,18 @@ 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 url(self):
use_sdms = getattr(settings, "DOMAIN_SUBDOMAIN_SHOWS", False)
domain = getattr(settings, "DOMAIN", 'localhost')
if use_sdms:
return domain.format(sub=self.abbr + '.',path='')
return '/show/%s' % (self.abbr)
def __str__(self): def __str__(self):
return '%s [%s]'%(self.name,self.abbr) return '%s [%s]'%(self.name,self.abbr)
@ -387,10 +398,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 +406,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 +435,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,14 +449,14 @@ 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'
) )
def __str__(self): def __str__(self):
return "%s %s %s"%(self.user, '\U0001f592' if self.positive else '\U0001f44e', self.board.title) return "%s %s reply %d"%(self.user, '\U0001f592' if self.positive else '\U0001f44e', self.reply.pk)

View File

@ -187,6 +187,35 @@ 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;
white-space: pre;
}
blockquote {
padding-left: 16px;
border-left: 5px solid #ddd;
}
@media all and (max-width: 800px) { @media all and (max-width: 800px) {
.logo { .logo {
font-size: 5vw !important; font-size: 5vw !important;

View File

@ -5,7 +5,8 @@
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"> <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel='stylesheet' type='text/css' href='{% static 'css/style.css' %}'> <link rel='stylesheet' type='text/css' href='{% static 'css/style.css' %}'>
<link rel='stylesheet' type='text/css' href='{% static 'css/footer.css' %}'> <link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>

View File

@ -16,7 +16,7 @@
{% if not recent %} Nothing to show {% endif %} {% if not recent %} Nothing to show {% endif %}
<div class="text-center"> <div class="text-center">
{% for show in recent %} {% for show in recent %}
<a class="show-promo" href="/show/{{show.abbr}}"> <a class="show-promo" href="{{show.url}}">
<img class="artwork" src="/media/uploaded_resources/{{show.artwork}}"> <img class="artwork" src="/media/uploaded_resources/{{show.artwork}}">
<span>{{show.name}}</span> <span>{{show.name}}</span>
</a> </a>

View File

@ -5,7 +5,7 @@
<div class="row"> <div class="row">
{% for show in shows %} {% for show in shows %}
<div class="col"> <div class="col">
<a href="/show/{{show.abbr}}">{{show.name}}</a> <a href="{{show.url}}">{{show.name}}</a>
</div> </div>
{% if forloop.counter|divisibleby:3 %} {% if forloop.counter|divisibleby:3 %}
<div class="w-100"></div> <div class="w-100"></div>

View File

@ -21,7 +21,7 @@ from django.contrib.auth import login as auth_login, authenticate
from django.conf import settings from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.db.models import Max from django.db.models import Max, F
from django.contrib.auth.views import logout from django.contrib.auth.views import logout
import requests import requests
import hashlib import hashlib
@ -31,7 +31,6 @@ from .models import Show
from .models import Submission from .models import Submission
from .models import DiscussionBoard from .models import DiscussionBoard
# Create your views here.
# Redirect url should point to this view # Redirect url should point to this view
class LoginRedirect(View): class LoginRedirect(View):
def get(self, req): def get(self, req):

View File

@ -7,7 +7,7 @@
<section class="container mt-2 mb-5"> <section class="container mt-2 mb-5">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li> <li class="breadcrumb-item"><a href="{{show.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item active" aria-current="page">New Season</li> <li class="breadcrumb-item active" aria-current="page">New Season</li>
</ol> </ol>
</nav> </nav>

View File

@ -46,7 +46,7 @@
<section class="container mb-5 mt-2"> <section class="container mb-5 mt-2">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li> <li class="breadcrumb-item"><a href="{{show.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item active" aria-current="page">{{episode.name}}</li> <li class="breadcrumb-item active" aria-current="page">{{episode.name}}</li>
</ol> </ol>
</nav> </nav>
@ -55,6 +55,11 @@
<div class="submission-list"> <div class="submission-list">
{% for sbm in submissions %} {% for sbm in submissions %}
<div class="submission{% if sbm.positives < sbm.negatives %} buried{% endif %}{% if sbm.pinned %} pinned{% endif %}{% if highlight and highlight == sbm.id %} highlighted{% endif %} mb-2"> <div class="submission{% if sbm.positives < sbm.negatives %} buried{% endif %}{% if sbm.pinned %} pinned{% endif %}{% if highlight and highlight == sbm.id %} highlighted{% endif %} mb-2">
{% if forloop.counter0 == 0 and sbm.embed and not sbm.positives < sbm.negatives %}
<div class="onsite-player d-flex justify-content-center mb-2">
<iframe src="{{sbm.embed}}" width="1024" height="640"></iframe>
</div>
{% endif %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<a href="{{sbm.url}}" class="link d-block mb-2"> <a href="{{sbm.url}}" class="link d-block mb-2">
@ -68,13 +73,13 @@
</div> </div>
<div class="col-md-2 text-md-right"> <div class="col-md-2 text-md-right">
<div class="vote-btns" data-vote-id="{{sbm.id}}"> <div class="vote-btns" data-vote-id="{{sbm.id}}">
<form method="POST" class="d-inline" action="/show/{{show.abbr}}/vote/{{sbm.id}}/1"> <form method="POST" class="d-inline" action="{{show.url}}/vote/{{sbm.id}}/1">
{% csrf_token %} {% csrf_token %}
<button class="btn btn-success"> <button class="btn btn-success">
<i class="fa fa-fw fa-thumbs-up"></i>&nbsp;{{sbm.positives}} <i class="fa fa-fw fa-thumbs-up"></i>&nbsp;{{sbm.positives}}
</button> </button>
</form> </form>
<form method="POST" class="d-inline" action="/show/{{show.abbr}}/vote/{{sbm.id}}/0"> <form method="POST" class="d-inline" action="{{show.url}}/vote/{{sbm.id}}/0">
{% csrf_token %} {% csrf_token %}
<button class="btn btn-danger"> <button class="btn btn-danger">
<i class="fa fa-fw fa-thumbs-down"></i>&nbsp;{{sbm.negatives}} <i class="fa fa-fw fa-thumbs-down"></i>&nbsp;{{sbm.negatives}}
@ -93,9 +98,9 @@
</span> </span>
<span class="divider">&middot;</span> <span class="divider">&middot;</span>
{% if "can_moderate_show" in show_perms %} {% if "can_moderate_show" in show_perms %}
<a href="/show/{{show.abbr}}/submission/{{sbm.id}}/moderate" class="button modbutton"><i class="fa fa-fw fa-shield"></i>&nbsp;Change</a> <a href="{{show.url}}/submission/{{sbm.id}}/moderate" class="button modbutton"><i class="fa fa-fw fa-shield"></i>&nbsp;Change</a>
{% else %} {% else %}
<a href="/show/{{show.abbr}}/submission/{{sbm.id}}/report" class="report">Report Invalid or Spam</a> <a href="{{show.url}}/submission/{{sbm.id}}/report" class="report">Report Invalid or Spam</a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -106,13 +111,34 @@
<div class="d-flex flex-row-reverse mt-4"> <div class="d-flex flex-row-reverse mt-4">
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% if "can_moderate_show" in show_perms %} {% if "can_moderate_show" in show_perms %}
<a href="/show/{{show.abbr}}/episode/{{episode.season.number}}/{{episode.episode}}/submit" class="btn btn-warning"><i class="fa fa-fw fa-plus"></i>&nbsp;Add New Link</a> <a href="{{show.url}}/episode/{{episode.season.number}}/{{episode.episode}}/submit" class="btn btn-warning"><i class="fa fa-fw fa-plus"></i>&nbsp;Add New Link</a>
{% else %} {% else %}
<a href="/show/{{show.abbr}}/episode/{{episode.season.number}}/{{episode.episode}}/submit" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>&nbsp;Submit New Link</a> <a href="{{show.url}}/episode/{{episode.season.number}}/{{episode.episode}}/submit" class="btn btn-primary"><i class="fa fa-fw fa-plus"></i>&nbsp;Submit New Link</a>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="fillertext"><a href="/login">Log in</a> to submit a link</span> <span class="fillertext"><a href="/login">Log in</a> to submit a link</span>
{% endif %} {% endif %}
</div> </div>
<p>Discuss <q>{{episode.name}}</q> on the <a href="{{show.url}}/discuss">discussion boards</a>!</p>
<ul class="nav fixed-bottom d-flex justify-content-center border-top bg-light">
{% if has_previous %}
<li class="nav-item">
<a class="nav-link" href="{{show.url}}/episode/{{episode.season.number}}/{{episode.episode|add:'-1'}}">Previous Episode</a>
</li>
{% else %}
<li class="nav-item">
<a href="#" class="nav-link disabled">Previous Episode</a>
</li>
{% endif %}
{% if has_next %}
<li class="nav-item">
<a class="nav-link" href="{{show.url}}/episode/{{episode.season.number}}/{{episode.episode|add:'1'}}">Next Episode</a>
</li>
{% else %}
<li class="nav-item">
<a href="#" class="nav-link disabled">Next Episode</a>
</li>
{% endif %}
</ul>
</section> </section>
{% endblock %} {% endblock %}

View File

@ -33,7 +33,7 @@
<section class="container mt-2 mb-5"> <section class="container mt-2 mb-5">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li> <li class="breadcrumb-item"><a href="{{show.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item active" aria-current="page">New Episode</li> <li class="breadcrumb-item active" aria-current="page">New Episode</li>
</ol> </ol>
</nav> </nav>

View File

@ -33,8 +33,8 @@
<section class="container mb-4 mt-2"> <section class="container mb-4 mt-2">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li> <li class="breadcrumb-item"><a href="{{show.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}">{{episode.name}}</a></li> <li class="breadcrumb-item"><a href="{{show.url}}/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}">{{episode.name}}</a></li>
<li class="breadcrumb-item active" aria-current="page">Report</li> <li class="breadcrumb-item active" aria-current="page">Report</li>
</ol> </ol>
</nav> </nav>

View File

@ -7,7 +7,7 @@
<section class="container mt-5 mb-5"> <section class="container mt-5 mb-5">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li> <li class="breadcrumb-item"><a href="{{show.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item active" aria-current="page">New Season</li> <li class="breadcrumb-item active" aria-current="page">New Season</li>
</ol> </ol>
</nav> </nav>

View File

@ -21,6 +21,7 @@
<li class="breadcrumb-item active" aria-current="page">{{show.name}}</li> <li class="breadcrumb-item active" aria-current="page">{{show.name}}</li>
</ol> </ol>
</nav> </nav>
<p>Discuss {{show.name}} on the <a href="discuss">discussion boards</a>!</p>
{% for season in seasons %} {% for season in seasons %}
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-2 mb-2"> <div class="col-md-2 mb-2">
@ -41,14 +42,14 @@
</div> </div>
{% if "can_moderate_show" in show_perms %} {% if "can_moderate_show" in show_perms %}
<div class="col-md-1 text-md-right"> <div class="col-md-1 text-md-right">
<a href="/show/{{show.abbr}}/season/{{season.number}}/append" class="action"><i class="fa fa-fw fa-plus"></i></a> <a href="{{show.url}}/season/{{season.number}}/append" class="action"><i class="fa fa-fw fa-plus"></i></a>
</div> </div>
{% endif %} {% endif %}
</div> </div>
{% endfor %} {% endfor %}
{% if "can_moderate_show" in show_perms %} {% if "can_moderate_show" in show_perms %}
<div class="d-flex flex-row-reverse"> <div class="d-flex flex-row-reverse">
<a href="/show/{{show.abbr}}/season/new" class="btn btn-warning"><i class="fa fa-fw fa-plus"></i>&nbsp;Add a Season</a> <a href="{{show.url}}/season/new" class="btn btn-warning"><i class="fa fa-fw fa-plus"></i>&nbsp;Add a Season</a>
</div> </div>
{% endif %} {% endif %}
</section> </section>

View File

@ -33,8 +33,8 @@
<section class="container mt-5 mb-5"> <section class="container mt-5 mb-5">
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li> <li class="breadcrumb-item"><a href="{{show.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}">{{episode.name}}</a></li> <li class="breadcrumb-item"><a href="{{show.url}}/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}">{{episode.name}}</a></li>
<li class="breadcrumb-item active" aria-current="page">Submit</li> <li class="breadcrumb-item active" aria-current="page">Submit</li>
</ol> </ol>
</nav> </nav>

View File

@ -35,8 +35,8 @@
{% get_obj_perms request.user for show as "show_perms" %} {% get_obj_perms request.user for show as "show_perms" %}
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}">{{show.name}}</a></li> <li class="breadcrumb-item"><a href="{{show.url}}">{{show.name}}</a></li>
<li class="breadcrumb-item"><a href="/show/{{show.abbr}}/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}">{{episode.name}}</a></li> <li class="breadcrumb-item"><a href="{{show.url}}/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}">{{episode.name}}</a></li>
<li class="breadcrumb-item active" aria-current="page">Edit Submission</li> <li class="breadcrumb-item active" aria-current="page">Edit Submission</li>
</ol> </ol>
</nav> </nav>

View File

@ -15,7 +15,7 @@
# 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, get_list_or_404, get_object_or_404, redirect
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
@ -26,7 +26,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from guardian.decorators import permission_required_or_403 from guardian.decorators import permission_required_or_403
from LandingPage.models import User, Show, Season, Episode, Submission, SubmissionVote, Ban from LandingPage.models import User, Show, Season, Episode, Submission, SubmissionVote, Ban, Report
from . import forms from . import forms
@ -66,6 +66,34 @@ class EpisodeView(TemplateView):
# Get show by abbr # Get show by abbr
show = get_object_or_404(Show, abbr=abbr) show = get_object_or_404(Show, abbr=abbr)
# Check next or previous
season_number = int(season)
episode_number = int(episode)
lastep = Episode.objects.filter(season__number=season_number,show=show).order_by('episode').last()
season_count = Season.objects.filter(show=show).count()
if season_count == 0:
raise Http404('This show has no episodes.')
if episode_number == 0 and season_number > 1:
season_number -= 1
epobj = Episode.objects.filter(season__number=season_number,show=show).order_by('episode').last()
if not epobj:
raise Http404('No Episode matches the given query.')
episode_number = int(epobj.episode)
ctx['url'] = '%s/episode/%d/%d'%(show.url(), season_number, episode_number)
elif episode_number > int(lastep.episode):
season_number += 1
episode_number = 1
ctx['url'] = '%s/episode/%d/%d'%(show.url(), season_number, episode_number)
if 'url' in ctx:
return ctx
episode = get_object_or_404(Episode, show=show,season__number=season,episode=episode) episode = get_object_or_404(Episode, show=show,season__number=season,episode=episode)
# I acknowledge that this is a mess. A functional mess. But a mess nonetheless. Hey, that rhymed! # I acknowledge that this is a mess. A functional mess. But a mess nonetheless. Hey, that rhymed!
@ -87,16 +115,24 @@ class EpisodeView(TemplateView):
ctx['episode'] = episode ctx['episode'] = episode
ctx['submissions'] = submissions ctx['submissions'] = submissions
ctx['highlight'] = highlight ctx['highlight'] = highlight
ctx['has_previous'] = episode_number > 1 or season_number > 1
ctx['has_next'] = episode_number < int(lastep.episode) or season_number < season_count
return ctx return ctx
def render_to_response(self, context):
if 'url' in context:
return redirect(context['url'])
return super(EpisodeView, self).render_to_response(context)
def EpisodeFindSubmission(req, abbr, submission): def EpisodeFindSubmission(req, abbr, submission):
show = get_object_or_404(Show, abbr=abbr) show = get_object_or_404(Show, abbr=abbr)
submission = int(submission) submission = int(submission)
episode = get_object_or_404(Episode, submissions__id=submission) episode = get_object_or_404(Episode, submissions__id=submission)
return HttpResponseRedirect('/show/%s/episode/%d/%d?submission=%d'%(abbr, episode.season.number, episode.episode, submission)) return HttpResponseRedirect('%s/episode/%d/%d?submission=%d'%(show.url(), episode.season.number, episode.episode, submission))
# Submission form GET and POST # Submission form GET and POST
@login_required @login_required
@ -144,7 +180,7 @@ def SubmissionForm(req, abbr, season, episode):
new_submission.episode = episode new_submission.episode = episode
new_submission.save() new_submission.save()
return HttpResponseRedirect('/show/%s/episode/%d/%d'%(abbr, episode.season.number, episode.episode)) return HttpResponseRedirect('%s/episode/%d/%d'%(show.url(), episode.season.number, episode.episode))
else: else:
ctx['error'] = 'Invalid fields!' ctx['error'] = 'Invalid fields!'
@ -171,11 +207,11 @@ def SubmissionModForm(req, abbr, submission):
if req.method == 'POST': if req.method == 'POST':
if 'delete' in req.POST: if 'delete' in req.POST:
submission.delete() submission.delete()
return HttpResponseRedirect('/show/%s/episode/%d/%d'%(abbr, episode.season.number, episode.episode)) return HttpResponseRedirect('%s/episode/%d/%d'%(show.url(), episode.season.number, episode.episode))
if 'delete_ban' in req.POST: if 'delete_ban' in req.POST:
submission.delete() submission.delete()
return HttpResponseRedirect('/show/%s/create_ban?user=%s'%(abbr,submission.user.username)) return HttpResponseRedirect('%s/create_ban?user=%s'%(show.url(),submission.user.username))
form = forms.SubmissionFormAdmin(req.POST, instance=submission) form = forms.SubmissionFormAdmin(req.POST, instance=submission)
ctx['form'] = form ctx['form'] = form
@ -184,7 +220,7 @@ def SubmissionModForm(req, abbr, submission):
form_data = form.cleaned_data form_data = form.cleaned_data
form.save() form.save()
return HttpResponseRedirect('/show/%s/episode/%d/%d'%(abbr, episode.season.number, episode.episode)) return HttpResponseRedirect('%s/episode/%d/%d'%(show.url(), episode.season.number, episode.episode))
else: else:
ctx['error'] = 'Invalid fields!' ctx['error'] = 'Invalid fields!'
@ -221,7 +257,7 @@ def SeasonSubmitForm(req, abbr):
new_season.show = show new_season.show = show
new_season.save() new_season.save()
return HttpResponseRedirect('/show/%s'%(abbr)) return HttpResponseRedirect(show.url())
else: else:
ctx['error'] = 'Invalid fields!' ctx['error'] = 'Invalid fields!'
@ -261,7 +297,7 @@ def EpisodeSubmitForm(req, abbr, season):
new_episode.season = season new_episode.season = season
new_episode.save() new_episode.save()
return HttpResponseRedirect('/show/%s'%(abbr)) return HttpResponseRedirect(show.url())
else: else:
ctx['error'] = 'Invalid fields!' ctx['error'] = 'Invalid fields!'
@ -307,7 +343,7 @@ class SubmissionVoteSubmit(LoginRequiredMixin, View):
) )
new_vote.save() new_vote.save()
return HttpResponseRedirect('/show/%s/episode/%d/%d'%(abbr, submission.episode.season.number, submission.episode.episode)) return HttpResponseRedirect('%s/episode/%d/%d'%(show.url(), submission.episode.season.number, submission.episode.episode))
# Episode form GET and POST # Episode form GET and POST
@permission_required_or_403('LandingPage.can_create_show_ban', (Show, 'abbr', 'abbr'), accept_global_perms=True) @permission_required_or_403('LandingPage.can_create_show_ban', (Show, 'abbr', 'abbr'), accept_global_perms=True)
@ -332,7 +368,8 @@ def BanFromShowForm(req, abbr):
ctx = { ctx = {
'form': form, 'form': form,
'show': show, 'show': show,
'target': banTarget 'target': banTarget,
'showurl': get_show_url(abbr)
} }
# Handle POST # Handle POST
@ -361,7 +398,7 @@ def BanFromShowForm(req, abbr):
if 'delete' in req.POST: if 'delete' in req.POST:
Submission.objects.filter(episode__show=show,user=banTarget).delete() Submission.objects.filter(episode__show=show,user=banTarget).delete()
return HttpResponseRedirect('/show/%s'%(abbr)) return HttpResponseRedirect(show.url())
else: else:
ctx['error'] = 'Invalid fields!' ctx['error'] = 'Invalid fields!'
@ -387,9 +424,12 @@ def ReportSubmission(req, abbr, submission):
'form': form, 'form': form,
'show': show, 'show': show,
'episode': episode, 'episode': episode,
'submission': submission 'submission': submission,
'showurl': get_show_url(abbr)
} }
url = '%s/episode/%d/%d?submission=%s'%(show.url(), episode.season.number, episode.episode, submission.pk)
# Handle POST # Handle POST
if req.method == 'POST': if req.method == 'POST':
form = forms.ReportForm(req.POST) form = forms.ReportForm(req.POST)
@ -398,13 +438,23 @@ def ReportSubmission(req, abbr, submission):
if form.is_valid(): if form.is_valid():
form_data = form.cleaned_data form_data = form.cleaned_data
if not user.has_perm('LandingPage.can_moderate_show', show):
# Check if there have been many reports by this user within the last 12 hours
if Report.objects.filter(user=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 submission has already been brought to our attention! Thank you for reporting.'
return render(req, "report_reply.html", ctx)
# Save # Save
new_report = form.save(commit=False) new_report = form.save(commit=False)
new_report.reporter = user new_report.reporter = user
new_report.url = '/show/%s/episode/%d/%d?submission=%s'%(abbr, episode.season.number, episode.episode,submission.pk) new_report.url = url
new_report.save() new_report.save()
return HttpResponseRedirect('/show/%s/episode/%d/%d'%(abbr, episode.season.number, episode.episode)) return HttpResponseRedirect('%s/episode/%d/%d'%(show.url(), episode.season.number, episode.episode))
else: else:
ctx['error'] = 'Invalid fields!' ctx['error'] = 'Invalid fields!'

View File

@ -9,6 +9,10 @@ secret_key=5up3r s3cr3t k3y
#For configuration details #For configuration details
database=sqlite:///database.sqlite3 database=sqlite:///database.sqlite3
#Domain of this website to use in show path manufacturing
domain=http://{sub}localhost:8000{path}
use_subdomain_paths=false
[OAuth] [OAuth]
#The root of the oauth endpoint you are using for oauth settings #The root of the oauth endpoint you are using for oauth settings
token_endpoint=https://icynet.eu/oauth/ token_endpoint=https://icynet.eu/oauth/

View File

@ -4,3 +4,5 @@ dj-database-url==0.4.2
requests==2.18.4 requests==2.18.4
django-guardian==1.4.9 django-guardian==1.4.9
django-widget-tweaks==1.4.1 django-widget-tweaks==1.4.1
markdown==2.6.11
bleach==2.1.2