diff --git a/Discussions/forms.py b/Discussions/forms.py new file mode 100644 index 0000000..237908b --- /dev/null +++ b/Discussions/forms.py @@ -0,0 +1,42 @@ +# Episodes.Community - Community-Driven TV Show Episode Link Sharing Site +# Copyright (C) 2017 Evert "Diamond" Prants , Taizo "Tsa6" Simpson +# +# 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 . + +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',) diff --git a/Discussions/templates/board.html b/Discussions/templates/board.html new file mode 100644 index 0000000..482b61f --- /dev/null +++ b/Discussions/templates/board.html @@ -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" %} +
+ +
+

{% if board.locked %}{% endif %}{% if board.pinned %}{% endif %}{{board.title}}

+
+
+ {% if board.locked %} +

This board is locked

+ {% else %} + {% if user.is_authenticated %} +  Reply + {% else %} +

Log in to reply

+ {% endif %} + {% endif %} +
+
+
+

Created {{board.timestamp}} by {{board.user.display_name}}

+ {% for reply in replies %} +
+ {% if reply.deleted %} +

This reply has been deleted by a moderator.

+ ID: {{reply.pk}} + {% if "can_moderate_board" in show_perms or board.user == user %} +
{{reply.body}}
+ Restore + Ban + {% endif %} +
+ {% else %} +
+
+ +

{{reply.user.display_name}}

+
+
+

Submitted {{board.timestamp}}

+
{{reply.body|markdown|safe}}
+
+ {% if user.is_authenticated %} + {% if "can_moderate_board" in show_perms %} + Delete Content + Ban + {% elif not user == reply.user %} + + {% endif %} + {% endif %} +
+ {% if not board.locked %} +
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+ {% else %} + +  {{reply.positives}} + + +  {{reply.negatives}} + + {% endif %} +
+ +
+
+
+ {% endif %} +
+ {% empty %} +

Nobody has replied to this board!

+ {% endfor %} + {% if replies.has_other_pages %} + + {% endif %} + {% if user.is_authenticated and not board.locked %} +

Quick Reply

+
+
+ {% include "form.html" %} +
+ +
+ +
+
+ {% endif %} + {% if user.is_authenticated %} +

Board Tools

+ + {% endif %} +
+{% endblock %} diff --git a/Discussions/templates/board_reply.html b/Discussions/templates/board_reply.html new file mode 100644 index 0000000..10c5fbb --- /dev/null +++ b/Discussions/templates/board_reply.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block title %} + Reply - {{show.name}} Discussions - Episodes.Community +{% endblock %} +{% block content %} +
+ +

New Reply

+ {% if error %} +
{{error}}
+ {% endif %} +
+ {% include "form.html" %} +
+ +
+ +
+
+{% endblock %} diff --git a/Discussions/templates/boards.html b/Discussions/templates/boards.html new file mode 100644 index 0000000..dabc7e3 --- /dev/null +++ b/Discussions/templates/boards.html @@ -0,0 +1,93 @@ +{% extends "base.html" %} +{% block title %} + {{show.name}} Discussions - Episodes.Community +{% endblock %} +{% block content %} +
+ +

{{show.name}} Discussion Boards

+

Discuss {{show.name}} with your fellow community members!

+
+ {% if user.is_authenticated %} +  Create New Board + {% else %} +

Log in to create boards

+ {% endif %} +
+
+
Board Name
+
Latest Reply
+
+ {% for board in boards %} +
+
+
+

{% if board.locked %}{% endif %}{% if board.pinned %}{% endif %}{{board.title}}

+ Submitted {{board.timestamp}} by + {% if board.user.is_staff %} + + {% endif %} + {{board.user.display_name}} + +
+
+ {% if board.num_replies > 0 %} +
+
+ by + {{board.latest_reply.user.display_name}} +
+ {{board.latest_reply.timestamp}} +
+ {% else %} + No replies + {% endif %} +
+
+
+ {% empty %} +

Nobody has started any discussions for this show!

+ {% endfor %} + + {% if boards.has_other_pages %} + + {% endif %} +
+{% endblock %} diff --git a/Discussions/templates/boards_new.html b/Discussions/templates/boards_new.html new file mode 100644 index 0000000..db51c55 --- /dev/null +++ b/Discussions/templates/boards_new.html @@ -0,0 +1,28 @@ +{% extends "base.html" %} +{% block title %} + Create a Board - {{show.name}} Discussions - Episodes.Community +{% endblock %} +{% block content %} +
+ +

Create a Board

+ {% if error %} +
{{error}}
+ {% endif %} +
+ {% include "form.html" %} +
+ +
+ +
+
+{% endblock %} diff --git a/Discussions/templates/report_reply.html b/Discussions/templates/report_reply.html new file mode 100644 index 0000000..c15564d --- /dev/null +++ b/Discussions/templates/report_reply.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} +{% block title %} + Report a Post - {{reply.board.title}} - {{show.name}} Discussions - Episodes.Community +{% endblock %} +{% block content %} +
+ +

Report a Post

+ {% if error %} +
{{error}}
+ {% endif %} +
+
+
Posted by {{ reply.user.display_name }}
+
{{ reply.body }}
+ {% if reply.user.is_staff %} +
+ Warning +

This reply is made by a staff member. Unnecessary reporters will be banned.

+
+ {% endif %} +
+ {% include "form.html" %} +
+ +
+
+
+{% endblock %} diff --git a/Discussions/templatetags/markdown.py b/Discussions/templatetags/markdown.py new file mode 100644 index 0000000..4946523 --- /dev/null +++ b/Discussions/templatetags/markdown.py @@ -0,0 +1,28 @@ +# Episodes.Community - Community-Driven TV Show Episode Link Sharing Site +# Copyright (C) 2017 Evert "Diamond" Prants , Taizo "Tsa6" Simpson +# +# 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 . + +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) diff --git a/Discussions/urls.py b/Discussions/urls.py new file mode 100644 index 0000000..a0b8179 --- /dev/null +++ b/Discussions/urls.py @@ -0,0 +1,33 @@ +# Episodes.Community - Community-Driven TV Show Episode Link Sharing Site +# Copyright (C) 2018 Evert "Diamond" Prants , Taizo "Tsa6" Simpson +# +# 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 . + +from django.conf.urls import url + +from . import views + +urlpatterns = [ + url(r'^$', views.Boards.as_view()), + url(r'^vote/(?P\d+)/(?P[0-1])/?$', views.BoardVoteSubmit.as_view()), + url(r'^board/new$', views.BoardForm), + url(r'^board/report/(?P\d{1,4})/?$', views.ReportForm), + url(r'^board/pin/(?P\d{1,4})/?$', views.BoardPin), + url(r'^board/delete/reply/(?P\d{1,4})/?$', views.BoardDeleteReply), + url(r'^board/delete/(?P\d{1,4})/?$', views.BoardDelete), + url(r'^board/lock/(?P\d{1,4})/?$', views.BoardLock), + url(r'^board/reply/(?P\d{1,4})/?$', views.BoardReplyForm), + url(r'^board/(?P\d{1,4})(-[\w-]+)?/?$', views.Board.as_view()), +] + diff --git a/Discussions/views.py b/Discussions/views.py index f368b15..367f9ab 100644 --- a/Discussions/views.py +++ b/Discussions/views.py @@ -13,7 +13,370 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . - +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.
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.
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('

Error

You cannot vote for your own reply.

' + 'Return to board

' + % (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.
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.
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('

Error

You do not have permission to lock this show.

' + 'Return to board

' + % (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))) diff --git a/EpisodesCommunity/settings.py b/EpisodesCommunity/settings.py index a78521c..9620082 100644 --- a/EpisodesCommunity/settings.py +++ b/EpisodesCommunity/settings.py @@ -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' diff --git a/EpisodesCommunity/urls.py b/EpisodesCommunity/urls.py index 38fc82b..4dd5832 100644 --- a/EpisodesCommunity/urls.py +++ b/EpisodesCommunity/urls.py @@ -36,6 +36,7 @@ from django.conf.urls.static import static urlpatterns = [ url(r'^admin/', admin.site.urls), + url(r'^show/(?P\w{1,16})/discuss/', include('Discussions.urls')), url(r'^show/(?P\w{1,16})/', include('Show.urls')), url(r'^', include('LandingPage.urls')) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/LandingPage/models.py b/LandingPage/models.py index 90cc515..78f4e76 100644 --- a/LandingPage/models.py +++ b/LandingPage/models.py @@ -15,6 +15,7 @@ # along with this program. If not, see . 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) diff --git a/LandingPage/static/css/style.css b/LandingPage/static/css/style.css index 5294b35..a928774 100644 --- a/LandingPage/static/css/style.css +++ b/LandingPage/static/css/style.css @@ -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; diff --git a/LandingPage/templates/base.html b/LandingPage/templates/base.html index 4bd1f6e..e798a6d 100644 --- a/LandingPage/templates/base.html +++ b/LandingPage/templates/base.html @@ -5,7 +5,8 @@ - + + diff --git a/LandingPage/templates/landing_page.html b/LandingPage/templates/landing_page.html index 933935e..16bf0c9 100644 --- a/LandingPage/templates/landing_page.html +++ b/LandingPage/templates/landing_page.html @@ -16,7 +16,7 @@ {% if not recent %} Nothing to show {% endif %}
{% for show in recent %} - + {{show.name}} diff --git a/LandingPage/templates/shows.html b/LandingPage/templates/shows.html index 969774e..6880c90 100644 --- a/LandingPage/templates/shows.html +++ b/LandingPage/templates/shows.html @@ -5,7 +5,7 @@
{% for show in shows %} {% if forloop.counter|divisibleby:3 %}
diff --git a/LandingPage/views.py b/LandingPage/views.py index 946f09b..d0a36e2 100644 --- a/LandingPage/views.py +++ b/LandingPage/views.py @@ -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): diff --git a/Show/templates/create_ban.html b/Show/templates/create_ban.html index dfdb19e..57981b2 100644 --- a/Show/templates/create_ban.html +++ b/Show/templates/create_ban.html @@ -7,7 +7,7 @@
diff --git a/Show/templates/episode.html b/Show/templates/episode.html index 9a9d551..9273541 100644 --- a/Show/templates/episode.html +++ b/Show/templates/episode.html @@ -46,7 +46,7 @@
@@ -55,6 +55,11 @@
{% for sbm in submissions %}
+ {% if forloop.counter0 == 0 and sbm.embed and not sbm.positives < sbm.negatives %} +
+ +
+ {% endif %}
@@ -106,13 +111,34 @@
{% if user.is_authenticated %} {% if "can_moderate_show" in show_perms %} -  Add New Link +  Add New Link {% else %} -  Submit New Link +  Submit New Link {% endif %} {% else %} Log in to submit a link {% endif %}
+

Discuss {{episode.name}} on the discussion boards!

+
{% endblock %} diff --git a/Show/templates/episode_add.html b/Show/templates/episode_add.html index 0c5dac9..c2abbfc 100644 --- a/Show/templates/episode_add.html +++ b/Show/templates/episode_add.html @@ -33,7 +33,7 @@
diff --git a/Show/templates/report.html b/Show/templates/report.html index 394d43b..30a01a8 100644 --- a/Show/templates/report.html +++ b/Show/templates/report.html @@ -33,8 +33,8 @@
diff --git a/Show/templates/season_add.html b/Show/templates/season_add.html index f7e250e..a80f1ba 100644 --- a/Show/templates/season_add.html +++ b/Show/templates/season_add.html @@ -7,7 +7,7 @@
diff --git a/Show/templates/show.html b/Show/templates/show.html index 03d484a..0b4ffd9 100644 --- a/Show/templates/show.html +++ b/Show/templates/show.html @@ -21,6 +21,7 @@ +

Discuss {{show.name}} on the discussion boards!

{% for season in seasons %}
@@ -41,14 +42,14 @@
{% if "can_moderate_show" in show_perms %}
- +
{% endif %}
{% endfor %} {% if "can_moderate_show" in show_perms %} {% endif %}
diff --git a/Show/templates/submit.html b/Show/templates/submit.html index 6c01cda..2f3dc18 100644 --- a/Show/templates/submit.html +++ b/Show/templates/submit.html @@ -33,8 +33,8 @@
diff --git a/Show/templates/submit_mod.html b/Show/templates/submit_mod.html index 4ed99f0..5c054bd 100644 --- a/Show/templates/submit_mod.html +++ b/Show/templates/submit_mod.html @@ -35,8 +35,8 @@ {% get_obj_perms request.user for show as "show_perms" %} diff --git a/Show/views.py b/Show/views.py index cbff335..ab093f4 100644 --- a/Show/views.py +++ b/Show/views.py @@ -15,7 +15,7 @@ # along with this program. If not, see . 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!' diff --git a/options_example.ini b/options_example.ini index 516fc94..48c0668 100644 --- a/options_example.ini +++ b/options_example.ini @@ -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/ diff --git a/requirements.txt b/requirements.txt index ea18de1..f861e02 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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