diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..efb1e94 --- /dev/null +++ b/.editorconfig @@ -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 diff --git a/EpisodesCommunity/urls.py b/EpisodesCommunity/urls.py index e7a72a8..8676f12 100644 --- a/EpisodesCommunity/urls.py +++ b/EpisodesCommunity/urls.py @@ -20,5 +20,6 @@ from django.conf.urls.static import static urlpatterns = [ url(r'^admin/', admin.site.urls), + url(r'^show/(?P\w{1,5})/', include('Show.urls')), url(r'^', include('LandingPage.urls')) ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/LandingPage/static/css/style.css b/LandingPage/static/css/style.css index af1df3c..045cd4b 100644 --- a/LandingPage/static/css/style.css +++ b/LandingPage/static/css/style.css @@ -4,6 +4,38 @@ body { margin: 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 { padding: 20px; border-bottom: 1px solid #ddd; @@ -59,6 +91,117 @@ body { width: 20%; 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) { .logo { font-size: 5vw !important; diff --git a/LandingPage/templates/landing_page.html b/LandingPage/templates/landing_page.html index de1e5bd..172e3d9 100644 --- a/LandingPage/templates/landing_page.html +++ b/LandingPage/templates/landing_page.html @@ -25,7 +25,7 @@
{% if not recent %} Nothing to show {% endif %} {% for show in recent %} - + {{show.name}} diff --git a/Show/forms.py b/Show/forms.py new file mode 100644 index 0000000..163cff9 --- /dev/null +++ b/Show/forms.py @@ -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' + } diff --git a/Show/templates/episode.html b/Show/templates/episode.html new file mode 100644 index 0000000..9563ae7 --- /dev/null +++ b/Show/templates/episode.html @@ -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 }} + + + + + +{% endblock %} + +{% block content %} +
+ + +
+
+  Show Index + {% if request.session.user_id %} +  Submit New Link + {% else %} + Log in to submit a link + {% endif %} +

Watch {{episode.name}} From

+
+ {% for sbm in submissions %} +
+  {{sbm.url}} +
+
+ {% csrf_token %} + +
+
+ {% csrf_token %} + +
+
+
+ {% empty %} +

Nobody has submitted any links yet.

+ {% endfor %} +
+
+{% endblock %} diff --git a/Show/templates/show.html b/Show/templates/show.html new file mode 100644 index 0000000..a4ed4de --- /dev/null +++ b/Show/templates/show.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% block title %} + Watch {{show.name}} Now - on Episodes.Community! +{% endblock %} + +{% block meta %} + {{ block.super }} + + + + +{% endblock %} + +{% block content %} +
+ + +
+
+

Watch Now

+ {% for season in seasons %} +
+
{% if season.name %}{{season.number}} - {{season.name}}{% else %}Season {{season.number}}{%endif%}
+
+ {% for episode in season.episodes.all %} + {{episode.episode}} - {{episode.name}} + {{episode.submissions.all|length}} Available Links + + {% endfor %} +
+
+ {% endfor %} +
+{% endblock %} diff --git a/Show/templates/submit.html b/Show/templates/submit.html new file mode 100644 index 0000000..98d285c --- /dev/null +++ b/Show/templates/submit.html @@ -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 %} +
+ + +
+
+  Show Index + Back to Episode +

Submit a link

+ {% if error %} +
{{error}}
+ {% endif %} +
+ {% csrf_token %} + {{ form }} + +
+
+ +{% endblock %} diff --git a/Show/urls.py b/Show/urls.py new file mode 100644 index 0000000..10095fa --- /dev/null +++ b/Show/urls.py @@ -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\d{1,4})/(?P\d{1,4})(-[\w-]+)?/?$', views.EpisodeView.as_view()), + url(r'^episode/(?P\d{1,4})/(?P\d{1,4})(-[\w-]+)?/submit$', views.SubmissionForm), + url(r'^vote/(?P\d+)/(?P[0-1])/?$', views.SubmissionVoteSubmit.as_view()) +] diff --git a/Show/views.py b/Show/views.py index 91ea44a..ba23497 100644 --- a/Show/views.py +++ b/Show/views.py @@ -1,3 +1,186 @@ +from django.template import RequestContext 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. + +# 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('

Error

You need to be logged in to submit. Please log in

', 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('

Error

You need to be logged in to vote. Please log in

', 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('

Error

You cannot vote for your own submission.

', 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)) +