Merge pull request #48 from IcyNet/show

Show
This commit is contained in:
Evert Prants 2017-11-11 20:48:22 +02:00 committed by GitHub
commit 325af6929f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 527 additions and 1 deletions

12
.editorconfig Normal file
View File

@ -0,0 +1,12 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
[*.py]
charset = utf-8
indent_style = space
indent_size = 4

View File

@ -20,5 +20,6 @@ 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<abbreviation>\w{1,5})/', include('Show.urls')),
url(r'^', include('LandingPage.urls')) url(r'^', include('LandingPage.urls'))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -4,6 +4,38 @@ body {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.button, input[type="submit"], button {
display: inline-block;
padding: 5px 10px;
background-color: #fdfdfd;
border: 1px solid #ddd;
border-radius: 5px;
text-decoration: none;
color: #000;
}
input[type="submit"] {
font-size: 120%;
}
label {
width: 200px;
display: block;
}
input[type=text], input:not([type=submit]) {
padding: 5px;
font-size: 120%;
width: 280px;
border-radius: 5px;
border: 1px solid #b3b3b3;
background-color: #ffffff;
box-shadow: inset 2px 2px 5px #ddd;
}
.helptext {
display: block;
margin-bottom: 10px;
font-style: italic;
font-size: 80%;
cursor: help;
}
.unibar { .unibar {
padding: 20px; padding: 20px;
border-bottom: 1px solid #ddd; border-bottom: 1px solid #ddd;
@ -59,6 +91,117 @@ body {
width: 20%; width: 20%;
float: right; float: right;
} }
section.show-details {
position: relative;
}
.show-details .banner {
position: absolute;
width: 100%;
height: 100%;
filter: blur(4px);
background-position: center center;
background-size: cover;
z-index: -1;
}
.show-details .banner-cover {
overflow: auto;
}
.show-details .artwork {
width: 182px;
margin: 15px;
float: left;
}
.show-details .details {
margin-left: 220px;
margin-right: 10px;
margin-bottom: 10px;
margin-top: 10px;
display: block;
min-height: 260px;
overflow: hidden;
font-size: 150%;
background-color: #dadada38;
padding: 10px;
}
.show-details .details h1 {
margin: 0;
font-size: 200%;
}
.show-details .details .description {
width: 80%;
}
section.seasons, section.submissions {
min-height: 100vh;
padding: 25px;
}
.season-name {
padding: 6px;
font-size: 180%;
background-color: #e8e8e8;
}
a.episode {
padding: 10px;
display: block;
color: #000;
text-decoration: none;
background-color: #f7f7f7;
}
a.episode:nth-child(even) {
background-color: #fbfbfb;
}
a.episode .submission_cnt {
float: right;
color: #949494;
}
.submission-list .submission {
background-color: #f9f9f9;
}
.submission-list .submission:nth-child(even) {
background-color: #ececec;
}
.submission {
padding: 10px;
}
.submission a.link {
font-size: 180%;
text-decoration: none;
color: #191919;
font-style: italic;
}
.vote-btns {
float: right;
}
.vote-positive, .vote-negative {
padding: 10px;
display: inline-block;
min-width: 20px;
text-align: center;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
text-decoration: none;
}
.vote-btns form {
display: inline-block;
}
.vote-positive {
background-color: #a4ffa7;
color: #008005;
}
.vote-negative {
background-color: #ffa6a6;
color: #ab0000;
}
.submission-list .submission.buried {
opacity: 0.3;
}
.message.error {
width: fit-content;
padding: 10px;
background-color: #ffcaca;
border: 1px solid #b10000;
margin: 5px 0;
}
@media all and (max-width: 800px) { @media all and (max-width: 800px) {
.logo { .logo {
font-size: 5vw !important; font-size: 5vw !important;

View File

@ -25,7 +25,7 @@
<div class="content"> <div class="content">
{% if not recent %} Nothing to show {% endif %} {% if not recent %} Nothing to show {% endif %}
{% for show in recent %} {% for show in recent %}
<a class="show-promo" href="#"> <a class="show-promo" href="/show/{{show.abbr}}">
<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>

11
Show/forms.py Normal file
View File

@ -0,0 +1,11 @@
from django import forms
from LandingPage.models import Submission
class SubmissionForm(forms.ModelForm):
class Meta():
model = Submission
fields = ('url','tags',)
help_texts = {
'url': 'URL to your episode',
'tags': 'Describe your link. Comma-separated list of keywords'
}

View File

@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block title %}
Episode "{{episode.name}}" - S{{episode.season.number}}E{{episode.episode}} - {{show.name}} - Episodes.Community
{% endblock %}
{% block meta %}
{{ block.super }}
<meta property="og:title" content="{{episode.name}}">
<meta property="og:type" content="video.episode">
<meta property="og:video:series" content="{{show.name}}">
<meta property="og:image" content="/media/uploaded_resources/{{show.artwork}}">
<meta property="og:url" content="https://{{show.abbr}}.episodes.community/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}">
{% endblock %}
{% block content %}
<section class="show-details">
<div class="banner" style="background-image: url(/media/uploaded_resources/{{show.banner}});"></div>
<div class="banner-cover">
<div class="artwork">
<img src="/media/uploaded_resources/{{show.artwork}}">
</div>
<div class="details">
<h1>{{show.name}}</h1>
<p class="description">
{{show.description}}
</p>
</div>
</div>
</section>
<section class="submissions">
<a href="/show/{{show.abbr}}" class="button"><i class="fa fa-fw fa-home"></i>&nbsp;Show Index</a>
{% if request.session.user_id %}
<a href="/show/{{show.abbr}}/episode/{{episode.season.number}}/{{episode.episode}}/submit" class="button"><i class="fa fa-fw fa-plus"></i>&nbsp;Submit New Link</a>
{% else %}
<span class="fillertext"><a href="/login">Log in</a> to submit a link</span>
{% endif %}
<h1>Watch <q>{{episode.name}}</q> From</h1>
<div class="submission-list">
{% for sbm in submissions %}
<div class="submission{% if sbm.positives < sbm.negatives %} buried{% endif %}">
<a href="{{sbm.url}}" class="link"><i class="fa fa-fw fa-globe"></i>&nbsp;{{sbm.url}}</a>
<div class="vote-btns" data-vote-id="{{sbm.id}}">
<form method="POST" action="/show/{{show.abbr}}/vote/{{sbm.id}}/1">
{% csrf_token %}
<button class="vote-positive">
<i class="fa fa-fw fa-thumbs-up"></i>&nbsp;{{sbm.positives}}
</button>
</form>
<form method="POST" action="/show/{{show.abbr}}/vote/{{sbm.id}}/0">
{% csrf_token %}
<button class="vote-negative">
<i class="fa fa-fw fa-thumbs-down"></i>&nbsp;{{sbm.negatives}}
</button>
</form>
</div>
</div>
{% empty %}
<h3>Nobody has submitted any links yet.</h3>
{% endfor %}
</div>
</section>
{% endblock %}

53
Show/templates/show.html Normal file
View File

@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}
Watch {{show.name}} Now - on Episodes.Community!
{% endblock %}
{% block meta %}
{{ block.super }}
<meta property="og:title" content="{{show.name}}">
<meta property="og:type" content="video.tv_show">
<meta property="og:image" content="/media/uploaded_resources/{{show.artwork}}">
<meta property="og:url" content="https://{{show.abbr}}.episodes.community/">
{% endblock %}
{% block content %}
<section class="show-details">
<div class="banner" style="background-image: url(/media/uploaded_resources/{{show.banner}});"></div>
<div class="banner-cover">
<div class="artwork">
<img src="/media/uploaded_resources/{{show.artwork}}">
</div>
<div class="details">
<h1>{{show.name}}</h1>
<p class="description">
{{show.description}}
</p>
<!--<div class="data">
<a class="imdb" href="{{show.imdb}}" target="_blank">IMDb Page</a>
<div class="stats">
<div class="param">Seasons</div>
<div class="value">{{seasons|length}}</div>
<div class="param">Episodes</div>
<div class="value">{{episodes|length}}</div>
</div>
</div>-->
</div>
</div>
</section>
<section class="seasons">
<h1>Watch Now</h1>
{% for season in seasons %}
<div class="season" data-season="{{season.number}}">
<div class="season-name">{% if season.name %}{{season.number}} - {{season.name}}{% else %}Season {{season.number}}{%endif%}</div>
<div class="episodes">
{% for episode in season.episodes.all %}
<a class="episode" data-season="{{season.number}}" data-number="{{episode.episode}}" data-title="{{episode.name}}" href="episode/{{season.number}}/{{episode.episode}}-{{episode.name|slugify}}">{{episode.episode}} - {{episode.name}}
<span class="submission_cnt">{{episode.submissions.all|length}} Available Links</span>
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</section>
{% endblock %}

View File

@ -0,0 +1,34 @@
{% extends "base.html" %}
{% block title %}
Submit a link - S{{episode.season.number}}E{{episode.episode}} - {{show.name}} - Episodes.Community
{% endblock %}
{% block content %}
<section class="show-details">
<div class="banner" style="background-image: url(/media/uploaded_resources/{{show.banner}});"></div>
<div class="banner-cover">
<div class="artwork">
<img src="/media/uploaded_resources/{{show.artwork}}">
</div>
<div class="details">
<h1>{{show.name}}</h1>
<p class="description">
{{show.description}}
</p>
</div>
</div>
</section>
<section class="submissions">
<a href="/show/{{show.abbr}}" class="button"><i class="fa fa-fw fa-home"></i>&nbsp;Show Index</a>
<a href="/show/{{show.abbr}}/episode/{{episode.season.number}}/{{episode.episode}}-{{episode.name|slugify}}" class="button">Back to Episode</a>
<h1>Submit a link</h1>
{% if error %}
<div class="message error">{{error}}</div>
{% endif %}
<form action="" method="post">
{% csrf_token %}
{{ form }}
<input type="submit" value="Submit" />
</form>
</div>
</section>
{% endblock %}

26
Show/urls.py Normal file
View File

@ -0,0 +1,26 @@
"""Show URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls import url, include
from . import views
urlpatterns = [
url(r'^$', views.IndexView.as_view()),
url(r'^episode/(?P<season>\d{1,4})/(?P<episode>\d{1,4})(-[\w-]+)?/?$', views.EpisodeView.as_view()),
url(r'^episode/(?P<season>\d{1,4})/(?P<episode>\d{1,4})(-[\w-]+)?/submit$', views.SubmissionForm),
url(r'^vote/(?P<subid>\d+)/(?P<positive>[0-1])/?$', views.SubmissionVoteSubmit.as_view())
]

View File

@ -1,3 +1,186 @@
from django.template import RequestContext
from django.shortcuts import render from django.shortcuts import render
from django.views import View
from django.views.generic.base import TemplateView
from django.conf import settings
from django.http import Http404
from django.http import HttpResponse
from django.http import HttpResponseRedirect
from django.db.models import Case, When, Value, IntegerField, Count, F
from LandingPage.models import User
from LandingPage.models import Show
from LandingPage.models import Season
from LandingPage.models import Episode
from LandingPage.models import Submission, SubmissionVote
from . import forms
import datetime
# Create your views here. # Create your views here.
# Index page of a show
class IndexView(TemplateView):
template_name = "show.html"
def get_context_data(self, abbreviation, **kwargs):
ctx = super().get_context_data()
# Get show by abbreviation, add episode count to the show and return only the first object
show = Show.objects.filter(abbr=abbreviation).first()
# 404
if not show:
raise Http404("Show does not exist")
# Get all seasons of the show and annotate episode counts onto them
seasons = show.seasons.all()
# Add fields to context
ctx['show'] = show
ctx['seasons'] = seasons
return ctx
# Episodes page of a show
class EpisodeView(TemplateView):
template_name = "episode.html"
def get_context_data(self, abbreviation, season, episode, **kwargs):
ctx = super().get_context_data()
# Get show by abbreviation
show = Show.objects.filter(abbr=abbreviation).first()
# Get episode by season and episode number
episode = Episode.objects.filter(show=show,season__number=season,episode=episode).first()
# 404's
if not show:
raise Http404("Show does not exist")
if not episode:
raise Http404("Episode does not exist")
# I acknowledge that this is a mess. A functional mess. But a mess nonetheless. Hey, that rhymed!
submissions = episode.submissions.annotate(
positives=Count(
Case(
When(
votes__positive=True,
then=Value(1)
)
)
),
negatives=Count('votes') - F('positives'),
score=F('positives') - F('negatives')
).order_by('-score')
# Add fields to context
ctx['show'] = show
ctx['episode'] = episode
ctx['submissions'] = submissions
return ctx
# Submission form GET and POST
def SubmissionForm(req, abbreviation, season, episode):
show = Show.objects.get(abbr=abbreviation)
episode = Episode.objects.filter(show=show,season__number=season,episode=episode).first()
# Check for login status
if not 'user_id' in req.session:
return HttpResponse('<h1>Error</h1><p>You need to be logged in to submit. Please <a href=/login>log in</a></p>', status=400)
user = User.objects.get(user_id=req.session['user_id'])
# 404's
if not show:
raise Http404("Show does not exist")
if not episode:
raise Http404("Episode does not exist")
form = forms.SubmissionForm()
# Request context
ctx = {
'form': form,
'show': show,
'episode': episode
}
# Handle POST
if req.method == 'POST':
form = forms.SubmissionForm(req.POST)
ctx['form'] = form
if form.is_valid():
form_data = form.cleaned_data
# Check if the URL has already been submitted
if Submission.objects.filter(episode=episode,url=form_data['url']).count() > 0:
ctx['error'] = 'This URL has already been submitted!'
return render(req, "submit.html", ctx)
# Check if there has been a submission by this user for this episode within the last 24 hours
if Submission.objects.filter(user=user,episode=episode,timestamp__gte=datetime.datetime.now() - datetime.timedelta(hours=24)).count() > 0:
ctx['error'] = 'You can only submit one link for an episode in 24 hours!'
return render(req, "submit.html", ctx)
# Have to do this because you can't add fields to a form
# If you know a better way of doing this, be my guest
new_submission = form.save(commit=False)
new_submission.user = user
new_submission.episode = episode
new_submission.save()
return HttpResponseRedirect('/show/%s/episode/%d/%d'%(abbreviation, episode.season.number, episode.episode))
else:
ctx['error'] = 'Invalid fields!'
return render(req, "submit.html", ctx)
# Vote request
# /show/{{abbr}}/vote/{{submission id}}/{{positive == 1}}
class SubmissionVoteSubmit(View):
def post (self, req, abbreviation, subid, positive):
# Convert positive parameter into a boolean
pos_bool = int(positive) == 1
# Check for login status
if not 'user_id' in req.session:
return HttpResponse('<h1>Error</h1><p>You need to be logged in to vote. Please <a href=/login>log in</a></p>', status=400)
user = User.objects.get(user_id=req.session['user_id'])
# Get the submission from the database
submission = Submission.objects.filter(id=subid).first()
# 404
if not submission:
raise Http404("Submission does not exist")
# Prevent voting for own submission
if submission.user == user:
return HttpResponse('<h1>Error</h1><p>You cannot vote for your own submission.</p>', status=400)
# Allow changing a vote from positive to negative or vice-versa. Delete vote if its a re-vote
vote = submission.votes.filter(user=user,submission__id=submission.id).first()
if vote:
if not vote.positive == pos_bool:
vote.positive = pos_bool
vote.save()
else:
vote.delete()
else:
new_vote = SubmissionVote(
user=user,
submission=submission,
positive=pos_bool
)
new_vote.save()
return HttpResponseRedirect('/show/%s/episode/%d/%d'%(abbreviation, submission.episode.season.number, submission.episode.episode))