Merge pull request #63 from IcyNet/community
Discussion boards and other fixes
This commit is contained in:
commit
f3bf9a4e44
42
Discussions/forms.py
Normal file
42
Discussions/forms.py
Normal 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',)
|
160
Discussions/templates/board.html
Normal file
160
Discussions/templates/board.html
Normal 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> 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> {{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> {{reply.negatives}}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="#" class="btn btn-link disabled text-success">
|
||||
<i class="fa fa-fw fa-thumbs-up"></i> {{reply.positives}}
|
||||
</a>
|
||||
<a href="#" class="btn btn-link disabled text-danger">
|
||||
<i class="fa fa-fw fa-thumbs-down"></i> {{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 %}
|
29
Discussions/templates/board_reply.html
Normal file
29
Discussions/templates/board_reply.html
Normal 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 %}
|
93
Discussions/templates/boards.html
Normal file
93
Discussions/templates/boards.html
Normal 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> 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 %}
|
28
Discussions/templates/boards_new.html
Normal file
28
Discussions/templates/boards_new.html
Normal 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 %}
|
36
Discussions/templates/report_reply.html
Normal file
36
Discussions/templates/report_reply.html
Normal 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 %}
|
28
Discussions/templatetags/markdown.py
Normal file
28
Discussions/templatetags/markdown.py
Normal 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'\>\;', '>', bleach.clean(value)), output_format="html5")
|
||||
|
||||
register.filter('markdown', markdown)
|
33
Discussions/urls.py
Normal file
33
Discussions/urls.py
Normal 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()),
|
||||
]
|
||||
|
@ -13,7 +13,370 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
# 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)))
|
||||
|
@ -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_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')
|
||||
|
||||
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'
|
||||
|
@ -36,6 +36,7 @@ from django.conf.urls.static import static
|
||||
|
||||
urlpatterns = [
|
||||
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'^', include('LandingPage.urls'))
|
||||
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
|
@ -15,6 +15,7 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.conf import settings
|
||||
@ -92,8 +93,18 @@ class Show(TimestampedModel):
|
||||
permissions = (
|
||||
('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_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):
|
||||
return '%s [%s]'%(self.name,self.abbr)
|
||||
|
||||
@ -252,7 +263,7 @@ class Season(models.Model):
|
||||
help_text="The artwork associated with the season. Should display the name of the show in a movie-poster esque format. Aspect ration should be about 2:3",
|
||||
verbose_name="Artwork",
|
||||
blank=True
|
||||
)
|
||||
)
|
||||
def __str__(self):
|
||||
return self.show.name + " S%d"%self.number
|
||||
|
||||
@ -387,10 +398,6 @@ class DiscussionBoard(TimestampedModel):
|
||||
max_length=100,
|
||||
help_text='The title of the discussion'
|
||||
)
|
||||
body = models.TextField(
|
||||
help_text='The body of the post',
|
||||
verbose_name='Body'
|
||||
)
|
||||
views = models.IntegerField(
|
||||
help_text='The amount of times this board has been viewed',
|
||||
default=0
|
||||
@ -399,6 +406,14 @@ class DiscussionBoard(TimestampedModel):
|
||||
help_text='Whether or not this board is pinned',
|
||||
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):
|
||||
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',
|
||||
verbose_name='Body'
|
||||
)
|
||||
deleted = models.BooleanField(
|
||||
help_text='Whether or not the content has been deleted by a moderator',
|
||||
default=False
|
||||
)
|
||||
def __str__(self):
|
||||
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',
|
||||
help_text='The user which cast this vote'
|
||||
)
|
||||
board = models.ForeignKey(
|
||||
DiscussionBoard,
|
||||
reply = models.ForeignKey(
|
||||
DiscussionReply,
|
||||
on_delete=models.CASCADE,
|
||||
related_name='votes',
|
||||
help_text='The board this vote was cast on'
|
||||
help_text='The reply this vote was cast on'
|
||||
)
|
||||
positive = models.BooleanField(
|
||||
help_text='If true, the vote is an upvote. Otherwise, it is a downvote. Neutral votes are not recorded'
|
||||
)
|
||||
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)
|
||||
|
@ -187,6 +187,35 @@ footer .logo .part1 {
|
||||
text-shadow: 2px 2px 1px #0059a0;
|
||||
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) {
|
||||
.logo {
|
||||
font-size: 5vw !important;
|
||||
|
@ -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" 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/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://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>
|
||||
|
@ -16,7 +16,7 @@
|
||||
{% if not recent %} Nothing to show {% endif %}
|
||||
<div class="text-center">
|
||||
{% 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}}">
|
||||
<span>{{show.name}}</span>
|
||||
</a>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="row">
|
||||
{% for show in shows %}
|
||||
<div class="col">
|
||||
<a href="/show/{{show.abbr}}">{{show.name}}</a>
|
||||
<a href="{{show.url}}">{{show.name}}</a>
|
||||
</div>
|
||||
{% if forloop.counter|divisibleby:3 %}
|
||||
<div class="w-100"></div>
|
||||
|
@ -21,7 +21,7 @@ from django.contrib.auth import login as auth_login, authenticate
|
||||
from django.conf import settings
|
||||
from django.http import HttpResponse
|
||||
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
|
||||
import requests
|
||||
import hashlib
|
||||
@ -31,7 +31,6 @@ from .models import Show
|
||||
from .models import Submission
|
||||
from .models import DiscussionBoard
|
||||
|
||||
# Create your views here.
|
||||
# Redirect url should point to this view
|
||||
class LoginRedirect(View):
|
||||
def get(self, req):
|
||||
|
@ -7,7 +7,7 @@
|
||||
<section class="container mt-2 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.url}}">{{show.name}}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">New Season</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
@ -46,7 +46,7 @@
|
||||
<section class="container mb-5 mt-2">
|
||||
<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.url}}">{{show.name}}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{episode.name}}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
@ -55,6 +55,11 @@
|
||||
<div class="submission-list">
|
||||
{% 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">
|
||||
{% 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="col">
|
||||
<a href="{{sbm.url}}" class="link d-block mb-2">
|
||||
@ -68,13 +73,13 @@
|
||||
</div>
|
||||
<div class="col-md-2 text-md-right">
|
||||
<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 %}
|
||||
<button class="btn btn-success">
|
||||
<i class="fa fa-fw fa-thumbs-up"></i> {{sbm.positives}}
|
||||
</button>
|
||||
</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 %}
|
||||
<button class="btn btn-danger">
|
||||
<i class="fa fa-fw fa-thumbs-down"></i> {{sbm.negatives}}
|
||||
@ -93,9 +98,9 @@
|
||||
</span>
|
||||
<span class="divider">·</span>
|
||||
{% 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> Change</a>
|
||||
<a href="{{show.url}}/submission/{{sbm.id}}/moderate" class="button modbutton"><i class="fa fa-fw fa-shield"></i> Change</a>
|
||||
{% 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 %}
|
||||
</div>
|
||||
</div>
|
||||
@ -106,13 +111,34 @@
|
||||
<div class="d-flex flex-row-reverse mt-4">
|
||||
{% if user.is_authenticated %}
|
||||
{% 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> 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> Add New Link</a>
|
||||
{% 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> 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> Submit New Link</a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="fillertext"><a href="/login">Log in</a> to submit a link</span>
|
||||
{% endif %}
|
||||
</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>
|
||||
{% endblock %}
|
||||
|
@ -33,7 +33,7 @@
|
||||
<section class="container mt-2 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.url}}">{{show.name}}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">New Episode</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
@ -33,8 +33,8 @@
|
||||
<section class="container mb-4 mt-2">
|
||||
<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}}/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}">{{episode.name}}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{show.url}}">{{show.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>
|
||||
</ol>
|
||||
</nav>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<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.url}}">{{show.name}}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">New Season</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
@ -21,6 +21,7 @@
|
||||
<li class="breadcrumb-item active" aria-current="page">{{show.name}}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<p>Discuss {{show.name}} on the <a href="discuss">discussion boards</a>!</p>
|
||||
{% for season in seasons %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2 mb-2">
|
||||
@ -41,14 +42,14 @@
|
||||
</div>
|
||||
{% if "can_moderate_show" in show_perms %}
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if "can_moderate_show" in show_perms %}
|
||||
<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> Add a Season</a>
|
||||
<a href="{{show.url}}/season/new" class="btn btn-warning"><i class="fa fa-fw fa-plus"></i> Add a Season</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
@ -33,8 +33,8 @@
|
||||
<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}}/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}">{{episode.name}}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{show.url}}">{{show.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>
|
||||
</ol>
|
||||
</nav>
|
||||
|
@ -35,8 +35,8 @@
|
||||
{% get_obj_perms request.user for show as "show_perms" %}
|
||||
<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}}/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}">{{episode.name}}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{show.url}}">{{show.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>
|
||||
</ol>
|
||||
</nav>
|
||||
|
@ -15,7 +15,7 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
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.generic.base import TemplateView
|
||||
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 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
|
||||
|
||||
@ -66,6 +66,34 @@ class EpisodeView(TemplateView):
|
||||
|
||||
# Get show by 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)
|
||||
|
||||
# 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['submissions'] = submissions
|
||||
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
|
||||
|
||||
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):
|
||||
show = get_object_or_404(Show, abbr=abbr)
|
||||
submission = int(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
|
||||
@login_required
|
||||
@ -144,7 +180,7 @@ def SubmissionForm(req, abbr, season, episode):
|
||||
new_submission.episode = episode
|
||||
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:
|
||||
ctx['error'] = 'Invalid fields!'
|
||||
|
||||
@ -171,11 +207,11 @@ def SubmissionModForm(req, abbr, submission):
|
||||
if req.method == 'POST':
|
||||
if 'delete' in req.POST:
|
||||
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:
|
||||
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)
|
||||
ctx['form'] = form
|
||||
@ -184,7 +220,7 @@ def SubmissionModForm(req, abbr, submission):
|
||||
form_data = form.cleaned_data
|
||||
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:
|
||||
ctx['error'] = 'Invalid fields!'
|
||||
|
||||
@ -221,7 +257,7 @@ def SeasonSubmitForm(req, abbr):
|
||||
new_season.show = show
|
||||
new_season.save()
|
||||
|
||||
return HttpResponseRedirect('/show/%s'%(abbr))
|
||||
return HttpResponseRedirect(show.url())
|
||||
else:
|
||||
ctx['error'] = 'Invalid fields!'
|
||||
|
||||
@ -261,7 +297,7 @@ def EpisodeSubmitForm(req, abbr, season):
|
||||
new_episode.season = season
|
||||
new_episode.save()
|
||||
|
||||
return HttpResponseRedirect('/show/%s'%(abbr))
|
||||
return HttpResponseRedirect(show.url())
|
||||
else:
|
||||
ctx['error'] = 'Invalid fields!'
|
||||
|
||||
@ -307,7 +343,7 @@ class SubmissionVoteSubmit(LoginRequiredMixin, View):
|
||||
)
|
||||
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
|
||||
@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 = {
|
||||
'form': form,
|
||||
'show': show,
|
||||
'target': banTarget
|
||||
'target': banTarget,
|
||||
'showurl': get_show_url(abbr)
|
||||
}
|
||||
|
||||
# Handle POST
|
||||
@ -361,7 +398,7 @@ def BanFromShowForm(req, abbr):
|
||||
if 'delete' in req.POST:
|
||||
Submission.objects.filter(episode__show=show,user=banTarget).delete()
|
||||
|
||||
return HttpResponseRedirect('/show/%s'%(abbr))
|
||||
return HttpResponseRedirect(show.url())
|
||||
else:
|
||||
ctx['error'] = 'Invalid fields!'
|
||||
|
||||
@ -387,9 +424,12 @@ def ReportSubmission(req, abbr, submission):
|
||||
'form': form,
|
||||
'show': show,
|
||||
'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
|
||||
if req.method == 'POST':
|
||||
form = forms.ReportForm(req.POST)
|
||||
@ -398,13 +438,23 @@ def ReportSubmission(req, abbr, submission):
|
||||
if form.is_valid():
|
||||
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
|
||||
new_report = form.save(commit=False)
|
||||
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()
|
||||
|
||||
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:
|
||||
ctx['error'] = 'Invalid fields!'
|
||||
|
||||
|
@ -9,6 +9,10 @@ secret_key=5up3r s3cr3t k3y
|
||||
#For configuration details
|
||||
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]
|
||||
#The root of the oauth endpoint you are using for oauth settings
|
||||
token_endpoint=https://icynet.eu/oauth/
|
||||
|
@ -4,3 +4,5 @@ dj-database-url==0.4.2
|
||||
requests==2.18.4
|
||||
django-guardian==1.4.9
|
||||
django-widget-tweaks==1.4.1
|
||||
markdown==2.6.11
|
||||
bleach==2.1.2
|
||||
|
Reference in New Issue
Block a user