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
|
# 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)))
|
||||||
|
@ -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'
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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):
|
||||||
|
@ -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>
|
||||||
|
@ -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> {{sbm.positives}}
|
<i class="fa fa-fw fa-thumbs-up"></i> {{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> {{sbm.negatives}}
|
<i class="fa fa-fw fa-thumbs-down"></i> {{sbm.negatives}}
|
||||||
@ -93,9 +98,9 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="divider">·</span>
|
<span class="divider">·</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> Change</a>
|
<a href="{{show.url}}/submission/{{sbm.id}}/moderate" class="button modbutton"><i class="fa fa-fw fa-shield"></i> 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> 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 %}
|
{% 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 %}
|
{% 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 %}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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> 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>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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!'
|
||||||
|
|
||||||
|
@ -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/
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user