diff --git a/strolchguru/forms.py b/strolchguru/forms.py index 43e355195d61fc2768df34a59c215ff16837e413..380e19bd258a4639dd9c9fb3eea99a1e37a18f40 100644 --- a/strolchguru/forms.py +++ b/strolchguru/forms.py @@ -1,5 +1,8 @@ from django import forms +from strolchibot.forms import add_classes, BaseModelForm +from .models import Clip + class ClipSearchForm(forms.Form): search = forms.CharField(label="Search", required=False) @@ -9,11 +12,8 @@ class ClipSearchForm(forms.Form): add_classes(self.fields) -def add_classes(fields): - for field_name, field in fields.items(): - if type(field) is forms.fields.BooleanField: - field.widget.attrs['class'] = ' w3-switch ' - field.label_suffix = "" - else: - field.widget.attrs['class'] = ' w3-input' - field.widget.attrs['placeholder'] = field.label +class ClipEditForm(BaseModelForm): + class Meta: + model = Clip + fields = ['custom_title', 'tags'] + widgets = {'tags': forms.widgets.CheckboxSelectMultiple()} diff --git a/strolchguru/models.py b/strolchguru/models.py index 22b7e5190b26112f81f6391b88fe2837b9347d06..bc8f044e147aefba4793f62ccad6f8c75175615f 100644 --- a/strolchguru/models.py +++ b/strolchguru/models.py @@ -3,7 +3,7 @@ from django.db import models class Clip(models.Model): title = models.CharField(max_length=100) - custom_title = models.CharField(max_length=100, null=True) + custom_title = models.CharField(max_length=100, null=True, blank=True) clip_id = models.IntegerField(unique=True) url = models.URLField() embed_url = models.URLField() @@ -27,3 +27,6 @@ class Clip(models.Model): class Tag(models.Model): name = models.CharField(max_length=100) + + def __str__(self): + return self.name diff --git a/strolchguru/templates/clips/edit.html b/strolchguru/templates/clips/edit.html new file mode 100644 index 0000000000000000000000000000000000000000..069817c765cf3c1bfb8bdc58009541e8c876a2b6 --- /dev/null +++ b/strolchguru/templates/clips/edit.html @@ -0,0 +1,22 @@ +{% extends 'layout.html' %} + +{% block content %} + <div class="w3-card w3-white w3-padding w3-padding-16"> + <div style="width: 75%; margin: auto;"> + <video src="/media/clips/{{ clip.clip_id }}.mp4" controls style="width:100%"> + + </video> + </div> + <form method="post"> + {% csrf_token %} + + {% for field in form %} + <div> + {{ field }} + </div> + {% endfor %} + + <button class="w3-button w3-strolchpink" type="submit">Save</button> + </form> + </div> +{% endblock %} diff --git a/strolchguru/templates/clips.html b/strolchguru/templates/clips/list.html similarity index 61% rename from strolchguru/templates/clips.html rename to strolchguru/templates/clips/list.html index e5346e0923b9a390c0d3b9ec7293c9f14bc19e39..493f6222b96f70cfc79c346a098a5112122888f6 100644 --- a/strolchguru/templates/clips.html +++ b/strolchguru/templates/clips/list.html @@ -9,7 +9,7 @@ {% endfor %} <div style="position: absolute; right: -48px; top:-1px;"> <button type="submit" value="" class="w3-btn w3-strolchpink" style="height: 41px;"> - <i class="fas fa-search"></i></button> + <em class="fas fa-search"></em></button> </div> </form> </div> @@ -18,22 +18,32 @@ {% block content %} {% for clip in clips %} - <div class="w3-card clip-card" + <div class="w3-card w3-white clip-card" style="width: 480px; padding: 10px; box-sizing: content-box; float: left; margin-right: 10px; height:400px; overflow: hidden;"> <div style="height: 272px; width: 480px; position: relative"> <a href="{% url "clip" clip.id %}?controls=1"> - <img src="/media/clips/{{ clip.clip_id }}.jpg"/> + <img src="/media/clips/{{ clip.clip_id }}.jpg" + alt="Thumbnail for clip with title {{ clip.display_title }}"/> </a> - <overlay - style="position: absolute; left: 0px; top: 0px; color: white; background: rgba(0,0,0,0.6); border-radius: 0.2rem; padding: 5px; margin: 5px;">{{ clip.created_at|date:"j.m.Y H:i" }}</overlay> - <overlay - style="position: absolute; right: 0px; bottom: 0px; color: white; background: rgba(0,0,0,0.6); border-radius: 0.2rem; padding: 5px; margin: 5px;">{{ clip.duration }} + <span + style="position: absolute; left: 0; top: 0; color: white; background: rgba(0,0,0,0.6); border-radius: 0.2rem; padding: 5px; margin: 5px;">{{ clip.created_at|date:"j.m.Y H:i" }}</span> + <span + style="position: absolute; right: 0; bottom: 0; color: white; background: rgba(0,0,0,0.6); border-radius: 0.2rem; padding: 5px; margin: 5px;">{{ clip.duration }} sec - </overlay> - <overlay - style="position: absolute; left: 0px; bottom: 0px; color: white; background: rgba(0,0,0,0.6); border-radius: 0.2rem; padding: 5px; margin: 5px;">{{ clip.curator }}</overlay> + </span> + <span + style="position: absolute; left: 0; bottom: 0; color: white; background: rgba(0,0,0,0.6); border-radius: 0.2rem; padding: 5px; margin: 5px;">{{ clip.curator }}</span> </div> - <p><b>{{ clip.display_title }}</b></p> + <div class="w3-display-container"> + <p class="w3-bold">{{ clip.display_title }}</p> + + {% if user.is_authenticated %} + <a href="{% url "clip_edit" clip.id %}" + class="w3-button w3-strolchpink w3-display-topright clip-edit"><em + class="fas fa-pencil-alt"></em></a> + {% endif %} + </div> + <p> {% for tag in clip.tags.all %} <a href="{% url "clips" %}?search={{ tag.name }}" style="text-decoration: none;"> diff --git a/strolchguru/urls.py b/strolchguru/urls.py index 445d2879e855d3e09806f49637e8cf3aa153f5db..7d90c07fa7c7cce5c08d5b603ec0148c833d7af4 100644 --- a/strolchguru/urls.py +++ b/strolchguru/urls.py @@ -19,7 +19,11 @@ from strolchguru import views urlpatterns = [ path('', views.home, name="strolchguru"), + path('tag/<str:tag>', views.home_tag, name="home_tag"), path('<int:id>', views.clip, name="clip"), path('<int:id>/json', views.clip_json, name="clip_json"), path('clips', views.clips, name="clips"), + path('clips/edit/<int:clip_id>', views.clip_edit, name="clip_edit"), + path('tags', views.tags, name="tags"), + path('tags/remove/<int:id>', views.tags_remove, name="tags_remove"), ] diff --git a/strolchguru/views.py b/strolchguru/views.py index 894655a5cd6e572cd4b1c3a256fb01176002e5f8..8640b5cf46537f3830daa08b91ed805d913b9ce7 100644 --- a/strolchguru/views.py +++ b/strolchguru/views.py @@ -1,15 +1,18 @@ import random import re +from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator from django.db.models import Q +from django.forms import modelformset_factory from django.forms.models import model_to_dict from django.http import HttpResponse, JsonResponse, Http404 -from django.shortcuts import render, get_object_or_404 +from django.shortcuts import render, get_object_or_404, redirect +from strolchibot.forms import BaseModelForm from strolchibot.models import TwitchUser -from .forms import ClipSearchForm -from .models import Clip +from .forms import ClipSearchForm, ClipEditForm +from .models import Clip, Tag def get_pages(page, num_pages): @@ -32,12 +35,23 @@ def get_pages(page, num_pages): def home(request) -> HttpResponse: controls = True if request.GET.get("controls") == "1" else False - clips = list(Clip.objects.filter(is_published=True, is_downloaded=True)) - clip = random.choice(clips) + clips = Clip.objects.filter(is_published=True, is_downloaded=True) + clip = random.choice(list(clips)) return render(request, 'strolchguru_home.html', context={'clip': clip, 'mode': "random_clips", 'controls': controls}) +def home_tag(request, tag) -> HttpResponse: + controls = True if request.GET.get("controls") == "1" else False + clips = Clip.objects.filter(is_published=True, is_downloaded=True, tags__name=tag) + if clips: + clip = random.choice(list(clips)) + return render(request, 'strolchguru_home.html', + context={'clip': clip, 'mode': "random_clips", 'controls': controls}) + else: + raise Http404 + + def clip(request, id) -> HttpResponse: controls = True if request.GET.get("controls") == "1" else False clip = get_object_or_404(Clip, pk=id) @@ -81,5 +95,51 @@ def clips(request) -> HttpResponse: pages = get_pages(page, paginator.num_pages) page_obj = paginator.get_page(page) - return render(request, "clips.html", + return render(request, "clips/list.html", context={"title": "Clips", "clips": page_obj, "search_form": form, "page": page, "pages": pages}) + + +@login_required(login_url="/login") +def clip_edit(request, clip_id) -> HttpResponse: + clip = get_object_or_404(Clip, pk=clip_id) + + if request.method == "POST": + clip_form = ClipEditForm(request.POST, instance=clip) + + if clip_form.is_valid(): + clip_form.save() + return redirect("clips") + + else: + clip_form = ClipEditForm(instance=clip) + + return render(request, "clips/edit.html", + context={"title": f"Edit Clip {clip_id}", "clip": clip, "form": clip_form}) + + +@login_required(login_url="/login") +def tags(request) -> HttpResponse: + TagFormSet = modelformset_factory(Tag, form=BaseModelForm, fields=("name",)) + if request.method == "POST": + formset = TagFormSet(request.POST, request.FILES) + if formset.is_valid(): + formset.save() + + forms = { + "Tags": { + "display": "list", + "type": "formset", + "name": "tags", + "formset": TagFormSet(), + "remove_url": "tags_remove", + }, + } + + return render(request, "form.html", {"title": "Tags", "forms": forms, "active": "tags"}) + + +@login_required(login_url="/login") +def tags_remove(request, id): + Tag.objects.filter(pk=id).delete() + + return redirect("tags") diff --git a/strolchibot/forms.py b/strolchibot/forms.py index b292a08103c732526e895d0a83a39fceaf61781e..daf9125cb4b122c01be4ddeba7f92ac2e3ea03b0 100644 --- a/strolchibot/forms.py +++ b/strolchibot/forms.py @@ -1,4 +1,5 @@ from django import forms + from .models import Config @@ -39,6 +40,8 @@ def add_classes(fields): if type(field) is forms.fields.BooleanField: field.widget.attrs['class'] = ' w3-switch ' field.label_suffix = "" + elif type(field) is forms.models.ModelMultipleChoiceField: + field.widget.attrs['class'] = ' w3-multiple-choice ' else: field.widget.attrs['class'] = ' w3-input ' field.widget.attrs['placeholder'] = field.label diff --git a/strolchibot/static/css/styles.css b/strolchibot/static/css/styles.css index 15526254400f7ff476d148ea55f19dd7bac106c9..7a5a65213185c42c959ce4d9a5dca803ceae798b 100644 --- a/strolchibot/static/css/styles.css +++ b/strolchibot/static/css/styles.css @@ -7,26 +7,22 @@ color: #e092b8; } -.w3-strolchpink { - background: #e092b8; +.w3-strolchpink, label.w3-multiple-choice-checked, .w3-hover-strolchpink:hover, .w3-hover-strolchpink.active { + background: #e092b8 !important; } -.w3-strolchpink:hover { +.w3-strolchpink:hover, ul.w3-multiple-choice label.w3-multiple-choice-checked:hover { background: #d2619b !important; } -.w3-strolchgray { +.w3-strolchgray, ul.w3-multiple-choice label { background: #ccc; } -.w3-strolchgray:hover { +.w3-strolchgray:hover, ul.w3-multiple-choice label:hover { background: #888 !important; } -.w3-hover-strolchpink:hover, .w3-hover-strolchpink.active { - background: #e092b8 !important; -} - #logo { max-width: 100%; } @@ -41,7 +37,7 @@ .w3-form-bar-item { width: 100%; - padding: 0!important; + padding: 0 !important; } .w3-card, w3-card-4, .w3-ul { @@ -54,57 +50,56 @@ textarea { } - .switch { - position: relative; - display: inline-block; - width: 40px; + position: relative; + display: inline-block; + width: 40px; } /* Hide default HTML checkbox */ .switch input { - opacity: 0; - width: 0; - height: 0; + opacity: 0; + width: 0; + height: 0; } /* The slider */ .slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - -webkit-transition: .4s; - transition: .4s; + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; } .slider:before { - position: absolute; - content: ""; - height: 14.5px; - width: 14.5px; - left: 4px; - bottom: 4px; - background-color: #e092b8; - -webkit-transition: .4s; - transition: .4s; + position: absolute; + content: ""; + height: 14.5px; + width: 14.5px; + left: 4px; + bottom: 4px; + background-color: #e092b8; + -webkit-transition: .4s; + transition: .4s; } input:checked + .slider { - background-color: #d2619b; + background-color: #d2619b; } input:focus + .slider { - box-shadow: 0 0 1px #d2619b; + box-shadow: 0 0 1px #d2619b; } input:checked + .slider:before { - -webkit-transform: translateX(17.5px); - -ms-transform: translateX(17.5px); - transform: translateX(17.5px); + -webkit-transform: translateX(17.5px); + -ms-transform: translateX(17.5px); + transform: translateX(17.5px); } .w3-bottombar { @@ -121,4 +116,39 @@ input:checked + .slider:before { .w3-hide, .w3-show { transition: display 2s; +} + +.w3-bold { + font-weight: bold; +} + +ul.w3-multiple-choice { + display: block; + list-style: none; + padding: 0; +} + +input.w3-multiple-choice { + display: none; +} + +ul.w3-multiple-choice li { + float: left; +} + +ul.w3-multiple-choice li:last-child { + float: none; +} + +ul.w3-multiple-choice label { + padding: 5px 10px; + background: #ccc; + margin: 0 5px; + line-height: 2.5; + border-radius: 10px; + text-decoration: none; +} + +.edit-form { + margin: 5px; } \ No newline at end of file diff --git a/strolchibot/static/js/scripts.js b/strolchibot/static/js/scripts.js index cf0f7fe6e45feec1ac1ead4ca95d942142e885c3..c92f802baf3458d59b44a0cdaa08895955182579 100644 --- a/strolchibot/static/js/scripts.js +++ b/strolchibot/static/js/scripts.js @@ -49,4 +49,29 @@ function getCurrentPage(urlParams) { } else { return parseInt(currentPage); } -} \ No newline at end of file +} + +document.querySelectorAll(".clip-edit").forEach(value => { + value.addEventListener("click", evt => { + const clipEditModal = document.querySelector("#clip-edit-modal"); + clipEditModal.style.display = "block"; + }); +}); + +document.querySelectorAll("input.w3-multiple-choice").forEach(value => { + value.addEventListener("change", evt => { + setLabelClass(evt.target); + }) + + setLabelClass(value); +}) + +function setLabelClass(input) { + let label = input.parentElement; + let isChecked = input.checked; + if (isChecked) { + label.classList.add("w3-multiple-choice-checked") + } else { + label.classList.remove("w3-multiple-choice-checked") + } +} diff --git a/strolchibot/templates/navigation.html b/strolchibot/templates/navigation.html index 9f4ed5316c1a4aa01a8b79385761829bb53ada33..8dc5911a8ee57b597e662ee1a13bfdae134acce9 100644 --- a/strolchibot/templates/navigation.html +++ b/strolchibot/templates/navigation.html @@ -22,6 +22,8 @@ <a href="{% url 'clips' %}" class="w3-bar-item w3-button {% if title == "Clips" %}w3-light-gray{% endif %}">Clips</a> {% if user.is_authenticated %} + <a href="{% url 'tags' %}" + class="w3-bar-item w3-button {% if title == "Tags" %}w3-light-gray{% endif %}">Tags</a> <a href="{% url 'logout' %}" class="w3-bar-item w3-button">Logout</a> {% endif %} </div> \ No newline at end of file