mirror of
https://gitlab.science.ru.nl/technicie/MarietjeDjango.git
synced 2026-03-22 12:14:44 +01:00
Merge branch 'marietje-zuid' into feature/marietje-4.1-in-header
This commit is contained in:
0
marietje/announcements/__init__.py
Normal file
0
marietje/announcements/__init__.py
Normal file
16
marietje/announcements/admin.py
Normal file
16
marietje/announcements/admin.py
Normal file
@ -0,0 +1,16 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from announcements.models import Announcement
|
||||
|
||||
|
||||
@admin.register(Announcement)
|
||||
class AnnouncementAdmin(admin.ModelAdmin):
|
||||
"""Manage the admin pages for the announcements."""
|
||||
|
||||
list_display = ("title", "since", "until", "visible")
|
||||
|
||||
def visible(self, obj):
|
||||
"""Is the object visible."""
|
||||
return obj.is_visible
|
||||
|
||||
visible.boolean = True
|
||||
8
marietje/announcements/apps.py
Normal file
8
marietje/announcements/apps.py
Normal file
@ -0,0 +1,8 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AnnouncementsConfig(AppConfig):
|
||||
"""Announcements AppConfig."""
|
||||
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "announcements"
|
||||
43
marietje/announcements/middleware.py
Normal file
43
marietje/announcements/middleware.py
Normal file
@ -0,0 +1,43 @@
|
||||
from announcements.services import (
|
||||
validate_closed_announcements,
|
||||
sanitize_closed_announcements,
|
||||
encode_closed_announcements,
|
||||
)
|
||||
from django.apps import apps
|
||||
|
||||
|
||||
class ClosedAnnouncementsMiddleware:
|
||||
"""Closed Announcements Middleware."""
|
||||
|
||||
def __init__(self, get_response):
|
||||
"""Initialize."""
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
"""Update the closed announcements' cookie."""
|
||||
response = self.get_response(request)
|
||||
|
||||
closed_announcements = validate_closed_announcements(
|
||||
sanitize_closed_announcements(request.COOKIES.get("closed-announcements", None))
|
||||
)
|
||||
response.set_cookie("closed-announcements", encode_closed_announcements(closed_announcements))
|
||||
|
||||
return response
|
||||
|
||||
|
||||
class AppAnnouncementMiddleware:
|
||||
"""Announcement Middleware."""
|
||||
|
||||
def __init__(self, get_response):
|
||||
"""Initialize Announcement Middleware."""
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
"""Call app announcement function if they exist for gathering all announcements."""
|
||||
announcements = []
|
||||
for app in apps.get_app_configs():
|
||||
if hasattr(app, "announcements") and hasattr(app.announcements, "__call__"):
|
||||
announcements += app.announcements(request)
|
||||
setattr(request, "_app_announcements", announcements)
|
||||
|
||||
return self.get_response(request)
|
||||
30
marietje/announcements/migrations/0001_initial.py
Normal file
30
marietje/announcements/migrations/0001_initial.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Generated by Django 3.2.16 on 2022-12-09 19:50
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
import tinymce.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Announcement',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('title', models.CharField(help_text='This is not shown on the announcement but can be used as an identifier in the admin area.', max_length=100)),
|
||||
('content', tinymce.models.HTMLField(max_length=500)),
|
||||
('since', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('until', models.DateTimeField(blank=True, null=True)),
|
||||
('icon', models.CharField(default='bullhorn', help_text='Font Awesome 6 abbreviation for icon to use.', max_length=150, verbose_name='Font Awesome 6 icon')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-since',),
|
||||
},
|
||||
),
|
||||
]
|
||||
0
marietje/announcements/migrations/__init__.py
Normal file
0
marietje/announcements/migrations/__init__.py
Normal file
47
marietje/announcements/models.py
Normal file
47
marietje/announcements/models.py
Normal file
@ -0,0 +1,47 @@
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
from tinymce.models import HTMLField
|
||||
|
||||
|
||||
class AnnouncementManager(models.Manager):
|
||||
"""Announcement Manager."""
|
||||
|
||||
def visible(self):
|
||||
"""Get only visible announcements."""
|
||||
return self.get_queryset().filter((Q(until__gt=timezone.now()) | Q(until=None)) & Q(since__lte=timezone.now()))
|
||||
|
||||
|
||||
class Announcement(models.Model):
|
||||
"""Announcement model."""
|
||||
|
||||
title = models.CharField(
|
||||
max_length=100,
|
||||
help_text="This is not shown on the announcement but can be used as an identifier in the admin area.",
|
||||
)
|
||||
content = HTMLField(blank=False, max_length=500)
|
||||
since = models.DateTimeField(default=timezone.now)
|
||||
until = models.DateTimeField(blank=True, null=True)
|
||||
icon = models.CharField(
|
||||
verbose_name="Font Awesome 6 icon",
|
||||
help_text="Font Awesome 6 abbreviation for icon to use.",
|
||||
max_length=150,
|
||||
default="bullhorn",
|
||||
)
|
||||
|
||||
objects = AnnouncementManager()
|
||||
|
||||
class Meta:
|
||||
"""Meta class."""
|
||||
|
||||
ordering = ("-since",)
|
||||
|
||||
def __str__(self):
|
||||
"""Cast this object to string."""
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def is_visible(self):
|
||||
"""Is this announcement currently visible."""
|
||||
return (self.until is None or self.until > timezone.now()) and self.since <= timezone.now()
|
||||
33
marietje/announcements/services.py
Normal file
33
marietje/announcements/services.py
Normal file
@ -0,0 +1,33 @@
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
from announcements.models import Announcement
|
||||
|
||||
|
||||
def sanitize_closed_announcements(closed_announcements) -> list:
|
||||
"""Convert a cookie (closed_announcements) to a list of id's of closed announcements."""
|
||||
if closed_announcements is None or not isinstance(closed_announcements, str):
|
||||
return []
|
||||
try:
|
||||
closed_announcements_list = json.loads(urllib.parse.unquote(closed_announcements))
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
|
||||
if not isinstance(closed_announcements_list, list):
|
||||
return []
|
||||
|
||||
closed_announcements_list_ints = []
|
||||
for closed_announcement in closed_announcements_list:
|
||||
if isinstance(closed_announcement, int):
|
||||
closed_announcements_list_ints.append(closed_announcement)
|
||||
return closed_announcements_list_ints
|
||||
|
||||
|
||||
def validate_closed_announcements(closed_announcements) -> list:
|
||||
"""Verify the integers in the list such that the ID's that in the database exist only remain."""
|
||||
return list(Announcement.objects.filter(id__in=closed_announcements).values_list("id", flat=True))
|
||||
|
||||
|
||||
def encode_closed_announcements(closed_announcements: list) -> str:
|
||||
"""Encode the announcement list in URL encoding."""
|
||||
return urllib.parse.quote(json.dumps(closed_announcements))
|
||||
@ -0,0 +1,34 @@
|
||||
.announcement {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--primary-shade);
|
||||
color: var(--primary-contrast);
|
||||
text-align: center;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.announcement .btn-close {
|
||||
color: var(--primary-contrast);
|
||||
}
|
||||
|
||||
.announcement p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
color: var(--primary-contrast);
|
||||
font-size: 0.9rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.announcement a {
|
||||
color: var(--primary-contrast);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.announcement a:hover {
|
||||
color: var(--primary-contrast-hover);
|
||||
}
|
||||
|
||||
.announcement button {
|
||||
background-size: .6rem;
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
const ANNOUNCEMENT_COOKIE = "closed-announcements";
|
||||
|
||||
function safeGetCookie() {
|
||||
try {
|
||||
return getCookie(ANNOUNCEMENT_COOKIE);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeAnnouncementsArray(closedAnnouncements) {
|
||||
if (closedAnnouncements === null || typeof closedAnnouncements !== 'string') {
|
||||
return [];
|
||||
}
|
||||
else {
|
||||
try {
|
||||
closedAnnouncements = JSON.parse(closedAnnouncements);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
if (! Array.isArray(closedAnnouncements)) {
|
||||
closedAnnouncements = [];
|
||||
}
|
||||
|
||||
for (let i = 0; i < closedAnnouncements.length; i++) {
|
||||
if (!Number.isInteger(closedAnnouncements[i])) {
|
||||
closedAnnouncements.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return closedAnnouncements;
|
||||
}
|
||||
}
|
||||
function close_announcement(announcementId) {
|
||||
let announcementElement = document.getElementById("announcement-" + announcementId);
|
||||
|
||||
if (announcementElement !== null) {
|
||||
announcementElement.remove();
|
||||
}
|
||||
|
||||
let closedAnnouncements = safeGetCookie();
|
||||
|
||||
if (closedAnnouncements === null) {
|
||||
closedAnnouncements = [];
|
||||
} else {
|
||||
closedAnnouncements = sanitizeAnnouncementsArray(closedAnnouncements);
|
||||
}
|
||||
|
||||
if (!closedAnnouncements.includes(announcementId)) {
|
||||
closedAnnouncements.push(announcementId);
|
||||
}
|
||||
|
||||
setListCookie(ANNOUNCEMENT_COOKIE, closedAnnouncements, 31);
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
{% load bleach_tags %}
|
||||
|
||||
{% for announcement in announcements %}
|
||||
<div class="announcement" id="announcement-{{ announcement.id }}">
|
||||
<i class="fas fa-{{ announcement.icon }} me-3"></i> {{ announcement.content|bleach }}
|
||||
<button type="button" class="btn-close ms-3" aria-label="Close"
|
||||
data-announcement-id="{{ announcement.pk }}" onclick="close_announcement({{ announcement.id }})"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% for announcement in app_announcements %}
|
||||
<div class="announcement">
|
||||
{{ announcement|bleach }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
0
marietje/announcements/templatetags/__init__.py
Normal file
0
marietje/announcements/templatetags/__init__.py
Normal file
20
marietje/announcements/templatetags/announcements.py
Normal file
20
marietje/announcements/templatetags/announcements.py
Normal file
@ -0,0 +1,20 @@
|
||||
from django import template
|
||||
from django.db.models import Q
|
||||
|
||||
from announcements.models import Announcement
|
||||
from announcements.services import sanitize_closed_announcements
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag("announcements/announcements.html", takes_context=True)
|
||||
def render_announcements(context):
|
||||
"""Render all active announcements."""
|
||||
request = context.get("request")
|
||||
closed_announcements = sanitize_closed_announcements(request.COOKIES.get("closed-announcements", None))
|
||||
|
||||
return {
|
||||
"announcements": Announcement.objects.visible().filter(~Q(id__in=closed_announcements)),
|
||||
"app_announcements": getattr(request, "_app_announcements", []),
|
||||
"closed_announcements": closed_announcements,
|
||||
}
|
||||
41
marietje/announcements/templatetags/bleach_tags.py
Normal file
41
marietje/announcements/templatetags/bleach_tags.py
Normal file
@ -0,0 +1,41 @@
|
||||
from bleach.css_sanitizer import CSSSanitizer
|
||||
from django import template
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django.utils.safestring import mark_safe
|
||||
from bleach import clean
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(is_safe=True)
|
||||
@stringfilter
|
||||
def bleach(value):
|
||||
"""Bleach dangerous html from the input."""
|
||||
css_sanitizer = CSSSanitizer(allowed_css_properties=["text-decoration"])
|
||||
return mark_safe(
|
||||
clean(
|
||||
value,
|
||||
tags=[
|
||||
"h2",
|
||||
"h3",
|
||||
"p",
|
||||
"a",
|
||||
"div",
|
||||
"strong",
|
||||
"em",
|
||||
"i",
|
||||
"b",
|
||||
"ul",
|
||||
"li",
|
||||
"br",
|
||||
"ol",
|
||||
"span",
|
||||
],
|
||||
attributes={
|
||||
"*": ["class", "style"],
|
||||
"a": ["href", "rel", "target", "title"],
|
||||
},
|
||||
css_sanitizer=css_sanitizer,
|
||||
strip=True,
|
||||
)
|
||||
)
|
||||
0
marietje/announcements/tests/__init__.py
Normal file
0
marietje/announcements/tests/__init__.py
Normal file
43
marietje/announcements/tests/test_services.py
Normal file
43
marietje/announcements/tests/test_services.py
Normal file
@ -0,0 +1,43 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from announcements import models
|
||||
from announcements.services import sanitize_closed_announcements, validate_closed_announcements
|
||||
|
||||
|
||||
class OrderServicesTests(TestCase):
|
||||
def test_sanitize_closed_announcements_none(self):
|
||||
self.assertEquals([], sanitize_closed_announcements(None))
|
||||
|
||||
def test_sanitize_closed_announcements_non_string(self):
|
||||
self.assertEquals([], sanitize_closed_announcements(5))
|
||||
|
||||
def test_sanitize_closed_announcements_non_json(self):
|
||||
self.assertEquals([], sanitize_closed_announcements("this,is,not,json"))
|
||||
|
||||
def test_sanitize_closed_announcements_non_list(self):
|
||||
self.assertEquals([], sanitize_closed_announcements("{}"))
|
||||
|
||||
def test_sanitize_closed_announcements_list_of_ints(self):
|
||||
self.assertEquals([1, 8, 4], sanitize_closed_announcements("[1, 8, 4]"))
|
||||
|
||||
def test_sanitize_closed_announcements_list_of_different_types(self):
|
||||
self.assertEquals(
|
||||
[1, 8, 4], sanitize_closed_announcements('[1, "bla", 8, 4, 123.4, {"a": "b"}, ["This is also a list"]]')
|
||||
)
|
||||
|
||||
def test_validate_closed_announcements_all_exist(self):
|
||||
announcement_1 = models.Announcement.objects.create(title="Announcement 1", content="blablabla")
|
||||
announcement_2 = models.Announcement.objects.create(title="Announcement 2", content="blablabla")
|
||||
announcement_3 = models.Announcement.objects.create(title="Announcement 3", content="blablabla")
|
||||
self.assertEqual(
|
||||
{announcement_1.id, announcement_2.id, announcement_3.id}, set(validate_closed_announcements([1, 2, 3]))
|
||||
)
|
||||
|
||||
def test_validate_closed_announcements_some_do_not_exist(self):
|
||||
announcement_1 = models.Announcement.objects.create(title="Announcement 1", content="blablabla")
|
||||
announcement_2 = models.Announcement.objects.create(title="Announcement 2", content="blablabla")
|
||||
announcement_3 = models.Announcement.objects.create(title="Announcement 3", content="blablabla")
|
||||
self.assertEqual(
|
||||
{announcement_1.id, announcement_2.id, announcement_3.id},
|
||||
set(validate_closed_announcements([1, 2, 3, 4, 5, 6])),
|
||||
)
|
||||
@ -3,7 +3,13 @@ import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "marietje.settings.settings")
|
||||
try:
|
||||
import marietje.settings.production # noqa
|
||||
except ModuleNotFoundError:
|
||||
# Use the development settings if the production settings are not available (so we're on a dev machine)
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "marietje.settings.development")
|
||||
else:
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "marietje.settings.production")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
"""
|
||||
Django settings for marietje project.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
SECRET_KEY = 'sae2hahHao1soo0Ocoz5Ieh1Ushae6feJe4mooliooj0Ula8'
|
||||
DEBUG = False
|
||||
ALLOWED_HOSTS = ['*']
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
@ -20,6 +13,8 @@ INSTALLED_APPS = [
|
||||
'django_bootstrap5',
|
||||
'fontawesomefree',
|
||||
'rest_framework',
|
||||
'tinymce',
|
||||
'announcements',
|
||||
'marietje',
|
||||
'queues',
|
||||
'songs',
|
||||
@ -64,18 +59,6 @@ TEMPLATES = [
|
||||
|
||||
WSGI_APPLICATION = 'marietje.wsgi.application'
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'marietje',
|
||||
'USER': 'marietje',
|
||||
'PASSWORD': 'v8TzZwdAdSi7Tk5I',
|
||||
'HOST': 'localhost',
|
||||
'PORT': '3306',
|
||||
'OPTIONS': {'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"},
|
||||
}
|
||||
}
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
@ -147,12 +130,6 @@ OAUTH2_PROVIDER = {
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# zc files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.10/howto/static-files/
|
||||
|
||||
BASE_URL = 'https://marietje-zuid.science.ru.nl'
|
||||
|
||||
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
|
||||
STATIC_URL = '/static/'
|
||||
LOGIN_URL = '/login/'
|
||||
@ -164,10 +141,8 @@ LOGOUT_REDIRECT_URL = '/'
|
||||
|
||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||
|
||||
# App specific settings
|
||||
|
||||
BERTHA_HOST = ('bach.science.ru.nl', 1234)
|
||||
MAIL_FROM = 'marietje@marietje.science.ru.nl'
|
||||
|
||||
MAX_MINUTES_IN_A_ROW = 45
|
||||
|
||||
# Time range (dependent on timezone specified) when MAX_MINUTES_IN_A_ROW is in effect.
|
||||
20
marietje/marietje/settings/development.py
Normal file
20
marietje/marietje/settings/development.py
Normal file
@ -0,0 +1,20 @@
|
||||
import os
|
||||
|
||||
from .base import *
|
||||
|
||||
SECRET_KEY = 'sae2hahHao1soo0Ocoz5Ieh1Ushae6feJe4mooliooj0Ula8'
|
||||
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
DATABASES = {
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
|
||||
}
|
||||
}
|
||||
|
||||
BERTHA_HOST = ('localhost', 1234)
|
||||
|
||||
BASE_URL = 'http://localhost:8000'
|
||||
23
marietje/marietje/settings/production.py.example
Normal file
23
marietje/marietje/settings/production.py.example
Normal file
@ -0,0 +1,23 @@
|
||||
from .base import *
|
||||
|
||||
SECRET_KEY = '******'
|
||||
|
||||
DEBUG = False
|
||||
|
||||
ALLOWED_HOSTS = ['marietje-zuid.nl']
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'marietje',
|
||||
'USER': 'marietje',
|
||||
'PASSWORD': '******',
|
||||
'HOST': 'localhost',
|
||||
'PORT': '3306',
|
||||
'OPTIONS': {'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"},
|
||||
}
|
||||
}
|
||||
|
||||
BASE_URL = 'https://marietje-zuid.science.ru.nl'
|
||||
|
||||
BERTHA_HOST = ('bach.science.ru.nl', 1234)
|
||||
@ -1,4 +1,9 @@
|
||||
:root {
|
||||
--pimary: #0d6efd;
|
||||
--primary-shade: #0d54bd;
|
||||
--primary-contrast: #ffffff;
|
||||
--primary-contrast-hover: rgba(255, 255, 255, 0.7);
|
||||
|
||||
--background-color: #ffffff;
|
||||
--background-shade: #dddddd;
|
||||
--background-shade-light: #f7f7f7;
|
||||
|
||||
@ -36,4 +36,37 @@ Number.prototype.timestampToHHMMSS = function () {
|
||||
seconds = '0' + seconds;
|
||||
}
|
||||
return hours + ':' + minutes + ':' + seconds;
|
||||
};
|
||||
};
|
||||
|
||||
function setCookie(name,value,days) {
|
||||
let expires = "";
|
||||
value = encodeURI(value);
|
||||
if (days) {
|
||||
let date = new Date();
|
||||
date.setTime(date.getTime() + (days*24*60*60*1000));
|
||||
expires = "; expires=" + date.toUTCString();
|
||||
}
|
||||
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
||||
}
|
||||
|
||||
function getCookie(name) {
|
||||
let nameEQ = name + "=";
|
||||
let ca = document.cookie.split(';');
|
||||
for(let i=0;i < ca.length;i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0)===' ') c = c.substring(1,c.length);
|
||||
if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length,c.length));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setListCookie(name, list, days) {
|
||||
try {
|
||||
let string = JSON.stringify(list);
|
||||
setCookie(name, string, days);
|
||||
return true;
|
||||
}
|
||||
catch(error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
{% load static django_bootstrap5 %}
|
||||
{% load static django_bootstrap5 announcements %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@ -25,8 +25,19 @@
|
||||
|
||||
<!-- Base JavaScript -->
|
||||
<script type="text/javascript" src="{% static "marietje/js/base.js" %}"></script>
|
||||
|
||||
<!-- Announcements static files -->
|
||||
<link rel="stylesheet" href="{% static 'announcements/css/announcements.css' %}" type="text/css">
|
||||
<script src="{% static 'announcements/js/announcements.js' %}"></script>
|
||||
|
||||
<script>
|
||||
const CSRF_TOKEN = "{{ csrf_token }}";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<section id="announcements-alerts">
|
||||
{% render_announcements %}
|
||||
</section>
|
||||
<nav class="navbar navbar-expand-lg sticky-top navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a class="navbar-brand d-block d-lg-none" href="{% url "index" %}">Marietje 4.1</a>
|
||||
@ -113,9 +124,6 @@
|
||||
</footer>
|
||||
{% endif %}
|
||||
{% bootstrap_javascript %}
|
||||
<script>
|
||||
const CSRF_TOKEN = "{{ csrf_token }}";
|
||||
</script>
|
||||
{% block js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -59,7 +59,13 @@
|
||||
return response.json();
|
||||
})
|
||||
.then(json => {
|
||||
updateScreen(json.current_song.user.name, json.current_song.song.artist, json.current_song.song.title);
|
||||
let requestor;
|
||||
if(json.current_song.user === null) {
|
||||
requestor = "Marietje";
|
||||
} else {
|
||||
requestor = json.current_song.user.name;
|
||||
}
|
||||
updateScreen(requestor, json.current_song.song.artist, json.current_song.song.title);
|
||||
setTimeout(fetchCurrentQueue, 1000);
|
||||
}).catch(err => {
|
||||
if(err.name == "NotLoggedIn") {
|
||||
|
||||
@ -31,7 +31,7 @@ urlpatterns = [
|
||||
path("register/", RegisterView.as_view(), name="register"),
|
||||
path("activate/<int:user_id>/<str:token>/", ActivateView.as_view(), name="activate"),
|
||||
path("forgotpassword/", ForgotPasswordView.as_view(), name="forgotpassword"),
|
||||
path("resetpassword/<int:user_id>/<str:token>/", ResetPasswordView.as_view, name="resetpassword"),
|
||||
path("resetpassword/<int:user_id>/<str:token>/", ResetPasswordView.as_view(), name="resetpassword"),
|
||||
path("admin/", admin.site.urls),
|
||||
path("privacy/", PrivacyView.as_view(), name="privacy"),
|
||||
path("metrics/", metrics, name="metrics"),
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
<td class="col-md-2 d-sm-table-cell d-none">Requested By</td>
|
||||
<td class="col-md-1 text-info d-sm-table-cell d-none" style="cursor: pointer;">
|
||||
<span v-if="playsIn" id="timeswitch" class="btn btn-link p-0" v-on:click="playsIn = false">Plays In</span>
|
||||
<span v-else class="btn btn-link p-0" v-on:click="playsIn = true">Plays at</span>
|
||||
<span v-else class="btn btn-link p-0" v-on:click="playsIn = true">Plays At</span>
|
||||
</td>
|
||||
<td class="col-md-1 control-icons">Control</td>
|
||||
</tr>
|
||||
@ -93,17 +93,22 @@
|
||||
</template>
|
||||
</td>
|
||||
<td>
|
||||
<a href="#" v-if="song.can_move_up" v-on:click="move_down(queue[index-1].id)"><i
|
||||
class="fa-solid fa-arrow-up"></i></a>
|
||||
<a href="#" v-else class="invisible"><i class="fa-solid fa-arrow-up"></i></a>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex flex-row">
|
||||
<button v-if="song.can_move_up" v-on:click="move_down(queue[index-1].id)" class="btn btn-link"><i
|
||||
class="fa-solid fa-arrow-up"></i></button>
|
||||
<button v-else class="btn btn-link invisible"><i class="fa-solid fa-arrow-up"></i></button>
|
||||
|
||||
<a href="#" v-if="song.can_move_down" v-on:click="move_down(song.id)"><i
|
||||
class="fa-solid fa-arrow-down"></i></a>
|
||||
<a href="#" v-else class="invisible"><i class="fa-solid fa-arrow-down"></i></a>
|
||||
|
||||
<a href="#" v-if="song.can_delete" v-on:click="cancel_song(song.id)"><i
|
||||
class="fa-solid fa-trash-can"></i></a>
|
||||
<a href="#" v-else class="invisible"><i class="fa-solid fa-trash-can"></i></a>
|
||||
<button v-if="song.can_move_down" v-on:click="move_down(song.id)" class="btn btn-link"><i
|
||||
class="fa-solid fa-arrow-down"></i></button>
|
||||
<button v-else class="btn btn-link invisible"><i class="fa-solid fa-arrow-down"></i></button>
|
||||
</div>
|
||||
<div class="d-flex flex-row">
|
||||
<button v-if="song.can_delete" v-on:click="cancel_song(song.id)" class="btn btn-link"><i
|
||||
class="fa-solid fa-trash-can"></i></button>
|
||||
<button v-else class="btn btn-link invisible"><i class="fa-solid fa-trash-can"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@ -353,7 +358,11 @@
|
||||
"Content-Type": 'application/json',
|
||||
},
|
||||
}
|
||||
).finally(() => {
|
||||
).then(() => {
|
||||
tata.success("", "Removed song from the queue.");
|
||||
}).catch(() => {
|
||||
tata.error("", "An error occurred while removing the song, please try again.");
|
||||
}).finally(() => {
|
||||
this.refresh();
|
||||
});
|
||||
},
|
||||
@ -368,7 +377,11 @@
|
||||
"Content-Type": 'application/json',
|
||||
},
|
||||
}
|
||||
).finally(() => {
|
||||
).then(() => {
|
||||
tata.success("", "Song was moved successfully.");
|
||||
}).catch(() => {
|
||||
tata.error("", "An error occurred while moving the song, please try again.");
|
||||
}).finally(() => {
|
||||
this.refresh();
|
||||
});
|
||||
}
|
||||
@ -414,6 +427,7 @@
|
||||
this.page_size = 10;
|
||||
}
|
||||
this.page_number = 1;
|
||||
setCookie("REQUEST_PAGE_SIZE", this.page_size, 14);
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
@ -425,7 +439,7 @@
|
||||
},
|
||||
created() {
|
||||
fetch(
|
||||
`/api/v1/songs/?ordering=title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}`
|
||||
`/api/v1/songs/?ordering=artist,title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}`
|
||||
).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
@ -444,11 +458,15 @@
|
||||
tata.error("", "An unknown error occurred, please try again.")
|
||||
}
|
||||
});
|
||||
const stored_page_size = parseInt(getCookie("REQUEST_PAGE_SIZE"));
|
||||
if (stored_page_size !== Number.NaN && stored_page_size > 0) {
|
||||
this.page_size = stored_page_size;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search() {
|
||||
fetch(
|
||||
`/api/v1/songs/?ordering=title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
|
||||
`/api/v1/songs/?ordering=artist,title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
|
||||
{
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
|
||||
@ -102,6 +102,7 @@
|
||||
this.page_size = 10;
|
||||
}
|
||||
this.page_number = 1;
|
||||
setCookie("MANAGE_PAGE_SIZE", this.page_size, 14);
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
@ -113,7 +114,7 @@
|
||||
},
|
||||
created() {
|
||||
fetch(
|
||||
`/api/v1/songs/?ordering=title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}`
|
||||
`/api/v1/songs/?ordering=artist,title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}`
|
||||
).then(response => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
@ -132,6 +133,10 @@
|
||||
tata.error("", "An unknown error occurred, please try again.")
|
||||
}
|
||||
});
|
||||
const stored_page_size = parseInt(getCookie("MANAGE_PAGE_SIZE"));
|
||||
if (stored_page_size !== Number.NaN && stored_page_size > 0) {
|
||||
this.page_size = stored_page_size;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search() {
|
||||
@ -140,7 +145,7 @@
|
||||
},
|
||||
refresh() {
|
||||
fetch(
|
||||
`/api/v1/songs/?ordering=title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
|
||||
`/api/v1/songs/?ordering=artist,title&limit=${this.page_size}&offset=${this.page_size * (this.page_number - 1)}&search=${this.search_input}`,
|
||||
{
|
||||
headers: {
|
||||
"X-CSRFToken": CSRF_TOKEN,
|
||||
|
||||
Reference in New Issue
Block a user