diff --git a/Makefile b/Makefile index 0aa344656622e2bcda54d6f306af6721624dc697..13b1223cfb36e3becb48de43196b7f990bc1b3ec 100644 --- a/Makefile +++ b/Makefile @@ -29,11 +29,11 @@ venv: .venv/bin/python install: venv ${VENV}/bin/pip install -r requirements/base.txt -r requirements/production.txt - ${VENV}/bin/npm install + npm install build: venv - ${VENV}/bin/npm run build + npm run build ${VENV}/bin/python manage.py collectstatic --noinput --settings=${SETTINGS} install-hooks: diff --git a/instagram_token/apps.py b/instagram_token/apps.py index 135a246c66478b2cdb299307c8b08b1c71354cc1..0b34638f148e695ad5b9c1c82c65d66922acb453 100644 --- a/instagram_token/apps.py +++ b/instagram_token/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class InstagramTokenConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'instagram_token' + default_auto_field = "django.db.models.BigAutoField" + name = "instagram_token" diff --git a/instagram_token/views.py b/instagram_token/views.py index 9f31e77dd1e0d1d82e3dc15538956ae68eaefb1e..0adee66debfe8429549d9c373f51284b517fb833 100644 --- a/instagram_token/views.py +++ b/instagram_token/views.py @@ -1,5 +1,4 @@ import requests - from django.conf import settings from django.shortcuts import render from django.urls import reverse @@ -20,11 +19,7 @@ def index(request): ) return render( - request, - "instagram_token/index.html", - { - "authorization_url": authorization_url - } + request, "instagram_token/index.html", {"authorization_url": authorization_url} ) @@ -41,8 +36,10 @@ def exchange(request): "client_secret": settings.INSTAGRAM_CLIENT_SECRET, "code": code, "grant_type": "authorization_code", - "redirect_uri": request.build_absolute_uri(reverse("instagram_token:exchange")), - } + "redirect_uri": request.build_absolute_uri( + reverse("instagram_token:exchange") + ), + }, ) if not exchange_request.ok: @@ -56,5 +53,5 @@ def exchange(request): { "access_token": exchange_request["access_token"], "user_id": exchange_request["user_id"], - } + }, ) diff --git a/package-lock.json b/package-lock.json index 1ffe84608e5db3d89a6ec42a46c5e2634989b1b0..779f79f3699d4a7d226535f3984d4b2926dffb4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tailwindcss/typography": "^0.4.1", "alertifyjs": "^1.13.1", "css-loader": "^6.7.3", + "easytimer.js": "^4.5.4", "jquery": "^3.6.4", "js-cookie": "^3.0.1", "select2": "^4.1.0-rc.0", @@ -933,6 +934,11 @@ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" }, + "node_modules/easytimer.js": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/easytimer.js/-/easytimer.js-4.5.4.tgz", + "integrity": "sha512-65STy2sW2z6e9XwfJqSa18JVNWuOu2cb/FXaZ/BbiDiPnTvC53njMS3oY1BsAIm/Dzt9c8YUvcgc8FoPttm1Gw==" + }, "node_modules/electron-to-chromium": { "version": "1.4.284", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", @@ -2198,9 +2204,9 @@ "integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A==" }, "node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, diff --git a/package.json b/package.json index a372dd615e06a6bd4475e6b508f24d3e195023bb..8652882bcb0341d72241def4c324a6176d624262 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@tailwindcss/typography": "^0.4.1", "alertifyjs": "^1.13.1", "css-loader": "^6.7.3", + "easytimer.js": "^4.5.4", "jquery": "^3.6.4", "js-cookie": "^3.0.1", "select2": "^4.1.0-rc.0", diff --git a/requirements/base.txt b/requirements/base.txt index 287bf10146a2e892c511e89d51f443ee0c7ffce0..c696c595f69d5c647056a3adb3e217d48a96cf5f 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,3 +1,4 @@ +channels[daphne]==4.0.0 Django==4.1.5 django-database-url==1.0.3 django-environ==0.9.0 diff --git a/requirements/production.txt b/requirements/production.txt index ce5169e4440b67843ee5d28199ed63e0d8323cfc..088e218c836756413516b33d6f4aaffc0971ef33 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -1,2 +1,3 @@ -gunicorn==20.1.0 whitenoise==6.3.0 +uvicorn==0.23.2 +gunicorn==21.2.0 diff --git a/run.sh b/run.sh index 5d9a7ada6bf4106d23769d523da59ae1e68e3720..402790eba0a74f9db9121b8607f176a225e70638 100644 --- a/run.sh +++ b/run.sh @@ -7,4 +7,4 @@ set -e python manage.py migrate # start webserver -exec gunicorn -c gunicorn.conf.py rybicka.wsgi +exec gunicorn -c gunicorn.conf.py rybicka.asgi:application -k uvicorn.workers.UvicornWorker diff --git a/rybicka/asgi.py b/rybicka/asgi.py index 851133e205c80ad01650d5120324f179fcb15f90..59bb6e0abd2b1719eb44521b9e544101b7c45b5e 100644 --- a/rybicka/asgi.py +++ b/rybicka/asgi.py @@ -1,16 +1,22 @@ -""" -ASGI config for rybicka project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ -""" - import os +from channels.auth import AuthMiddlewareStack +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.security.websocket import AllowedHostsOriginValidator from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rybicka.settings") +from timer.routing import websocket_urlpatterns + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "rybicka.settings.production") +# Initialize Django ASGI application early to ensure the AppRegistry +# is populated before importing code that may import ORM models. +django_asgi_app = get_asgi_application() -application = get_asgi_application() +application = ProtocolTypeRouter( + { + "http": django_asgi_app, + "websocket": AllowedHostsOriginValidator( + AuthMiddlewareStack(URLRouter(websocket_urlpatterns)) + ), + } +) diff --git a/rybicka/settings/base.py b/rybicka/settings/base.py index fc2d8af8fd05389189842e1926b717c2e84e5e69..0fe4966f7b484930b42a8c5186c5343cf0cf4f94 100644 --- a/rybicka/settings/base.py +++ b/rybicka/settings/base.py @@ -42,8 +42,11 @@ SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") # Application definition INSTALLED_APPS = [ + "daphne", + "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", + "django.contrib.sites", "django.contrib.messages", "django.contrib.staticfiles", "webpack_loader", @@ -51,6 +54,7 @@ INSTALLED_APPS = [ "member_group_size_calc", "rv_voting_calc", "mail_signature", + "timer", "asset_server_resize", ] @@ -82,7 +86,7 @@ TEMPLATES = [ }, ] -WSGI_APPLICATION = "rybicka.wsgi.application" +ASGI_APPLICATION = "rybicka.asgi.application" # Database @@ -124,3 +128,6 @@ CHOBOTNICE_API_URL = env.str( "CHOBOTNICE_API_URL", "https://chobotnice.pirati.cz/graphql/" ) CHOBOTNICE_RV_GID = env.str("CHOBOTNICE_RV_GID") + +# FIXME +CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} diff --git a/rybicka/urls.py b/rybicka/urls.py index 1ddf91e069fa46fac44d2a45a5cdec2239df044f..634412f8340996778355ac3b9f22a420639e77af 100644 --- a/rybicka/urls.py +++ b/rybicka/urls.py @@ -19,6 +19,7 @@ urlpatterns = [ path("vypocet-skupiny-clenu/", include("member_group_size_calc.urls")), path("hlasovani-rv/", include("rv_voting_calc.urls")), path("emailove-podpisy/", include("mail_signature.urls")), + path("casovace/", include("timer.urls")), path("asset-server/", include("asset_server_resize.urls")), path("", include("shared.urls")), ] diff --git a/shared/templates/shared/base.html b/shared/templates/shared/base.html index 47bb0c82724e93f5478c6ee079a58d462015e4a0..fafea799b6a4c9b7c6b21dba89cce8e9945053bb 100644 --- a/shared/templates/shared/base.html +++ b/shared/templates/shared/base.html @@ -45,40 +45,44 @@ {% block head %}{% endblock %} </head> <body> - <nav class="navbar navbar--simple __js-root"> - <ui-app inline-template> - <ui-navbar inline-template> - <div> - <div class="container container--default navbar__content navbar__content--initialized"> - <div class="navbar__brand flex items-center pr-8 my-4 lg:my-0"> - <a href="{% url "shared:index" %}"> - <img src="https://styleguide.pirati.cz/2.3.x/images/logo-round-white.svg" class="w-8"> - </a> - <div class="pl-4 font-bold text-xl border-r border-grey-300 pr-8"> - <a href="{% url "shared:index" %}">RybiÄŤka</a> + {% block nav %} + <nav class="navbar navbar--simple __js-root"> + <ui-app inline-template> + <ui-navbar inline-template> + <div> + <div class="container container--default navbar__content navbar__content--initialized"> + <div class="navbar__brand flex items-center pr-8 my-4 lg:my-0"> + <a href="{% url "shared:index" %}"> + <img src="https://styleguide.pirati.cz/2.3.x/images/logo-round-white.svg" class="w-8"> + </a> + <div class="pl-4 font-bold text-xl border-r border-grey-300 pr-8"> + <a href="{% url "shared:index" %}">RybiÄŤka</a> + </div> </div> + {% block header_name %}{% endblock %} </div> - {% block header_name %}{% endblock %} </div> - </div> - </ui-navbar> - </ui-app> - </nav> + </ui-navbar> + </ui-app> + </nav> + {% endblock %} <div class="container container--default py-8 lg:py-24"> {% block content %}{% endblock %} </div> - <footer class="footer bg-grey-700 text-white __js-root hidden lg:block"> - <ui-app inline-template> - <div> - <div class="footer__main py-4 lg:py-16 container container--default"> - <section class="footer__brand"> - <p class="para text-grey-200"> - <span class="copyleft inline-block">©</span> {% now "Y" %} Piráti. Všechna práva vyhlazena. SdĂlejte a nechte ostatnĂ sdĂlet za stejnĂ˝ch podmĂnek. - </p> - </section> + {% block footer %} + <footer class="footer bg-grey-700 text-white __js-root hidden lg:block"> + <ui-app inline-template> + <div> + <div class="footer__main py-4 lg:py-16 container container--default"> + <section class="footer__brand"> + <p class="para text-grey-200"> + <span class="copyleft inline-block">©</span> {% now "Y" %} Piráti. Všechna práva vyhlazena. SdĂlejte a nechte ostatnĂ sdĂlet za stejnĂ˝ch podmĂnek. + </p> + </section> + </div> </div> - </div> - </ui-app> - </footer> + </ui-app> + </footer> + {% endblock %} </body> </html> diff --git a/static_src/timer.js b/static_src/timer.js new file mode 100644 index 0000000000000000000000000000000000000000..6717ea4332a8d1adc8cc11452a26c502cc9f3086 --- /dev/null +++ b/static_src/timer.js @@ -0,0 +1,125 @@ +import $ from "jquery" +import Timer from "easytimer.js" + +const assignEventListeners = (timer) => { + timer.addEventListener( + 'secondsUpdated', + (event) => { + $('#timer .timer-values').html(timer.getTimeValues().toString()) + } + ) + + timer.addEventListener( + 'targetAchieved', + (event) => { + $("#is_counting").prop("checked", false) + $('#timer .timer-values').html("Konec") + } + ) + + $('#timer .timer-values').html(timer.getTimeValues().toString()) +} + +$(window).ready( + () => { + // --- BEGIN Timer --- + + let timer = new Timer({ + countdown: true, + startValues: { + hours: window.startingTime.hours, + minutes: window.startingTime.minutes, + seconds: window.startingTime.seconds, + } + }) + + const timerSocket = new WebSocket( + "ws://" + + window.location.host + + "/ws/timer/" + ) + + timerSocket.onmessage = (event) => { + const data = JSON.parse(event.data) + + if ("status" in data) { + if (data["status"] === "playing") { + // Reset if we are playing again. + if (!timer.isRunning()) { + timer = new Timer({ + countdown: true, + startValues: { + hours: window.startingTime.hours, + minutes: window.startingTime.minutes, + seconds: window.startingTime.seconds, + } + }) + + assignEventListeners(timer) + } + + timer.start() + } else if (data["status"] === "paused") { + timer.pause() + } + } else if ("time" in data) { + timer.pause() + + window.startingTime = { + hours: data["time"]["hours"], + minutes: data["time"]["minutes"], + seconds: data["time"]["seconds"] + } + + timer = new Timer({ + countdown: true, + startValues: { + hours: window.startingTime.hours, + minutes: window.startingTime.minutes, + seconds: window.startingTime.seconds, + } + }) + + assignEventListeners(timer) + } + } + + assignEventListeners(timer) + + // --- END Timer --- + + // --- BEGIN Controls --- + + $("#is_counting").on( + "change", + (event) => { + if (event.target.checked) { + timerSocket.send(JSON.stringify({ + "status": "playing" + })) + } else { + timerSocket.send(JSON.stringify({ + "status": "paused" + })) + } + } + ) + + $("#update-time").on( + "click", + (event) => { + timerSocket.send(JSON.stringify({ + "time": { + "hours": Number($("#hours").val()), + "minutes": Number($("#minutes").val()), + "seconds": Number($("#seconds").val()) + } + })) + + $("#is_counting").prop("checked", false) + } + ) + + // --- END Controls --- + } +) diff --git a/tailwind.config.js b/tailwind.config.js index 689576f4a063f1736155aff77c3d3d3f48b9738c..e2101ddc0dc9acc1cce8e705d89c4aea9d585f8f 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -3,8 +3,7 @@ const defaultTheme = require("tailwindcss/defaultTheme"); /** @type {import('tailwindcss').Config} */ module.exports = { content: [ - "*/templates/*/*.html", - "*/templates/*/*/*.html", + "*/templates/**/*.html", ], theme: { extend: { diff --git a/timer/__init__.py b/timer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/timer/admin.py b/timer/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..8c38f3f3dad51e4585f3984282c2a4bec5349c1e --- /dev/null +++ b/timer/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/timer/apps.py b/timer/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..2760d843bca5fc9a0b893942a598eba7aeae8929 --- /dev/null +++ b/timer/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TimerConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "timer" diff --git a/timer/consumers.py b/timer/consumers.py new file mode 100644 index 0000000000000000000000000000000000000000..1946e2ff37b88f38c2b2e812820ed687e84f88e7 --- /dev/null +++ b/timer/consumers.py @@ -0,0 +1,44 @@ +import json + +from asgiref.sync import async_to_sync +from channels.generic.websocket import WebsocketConsumer + + +class TimerConsumer(WebsocketConsumer): + def connect(self): + async_to_sync(self.channel_layer.group_add)("timer", self.channel_name) + + self.accept() + + def disconnect(self, close_code): + pass + + def receive(self, text_data): + json_data = json.loads(text_data) + + response = None + + if "status" in json_data: + if json_data["status"] == "playing": + response = {"status": "playing"} + elif json_data["status"] == "paused": + response = {"status": "paused"} + + if "time" in json_data: + response = { + "time": { + "hours": json_data["time"]["hours"], + "minutes": json_data["time"]["minutes"], + "seconds": json_data["time"]["seconds"], + } + } + + if response is not None: + async_to_sync(self.channel_layer.group_send)( + "timer", {"type": "timer_message", "text": json.dumps(response)} + ) + + self.send(text_data=json.dumps({})) + + def timer_message(self, event): + self.send(text_data=event["text"]) diff --git a/timer/forms.py b/timer/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..7214ca8397053ef9e748bb39ef0435b641f731fa --- /dev/null +++ b/timer/forms.py @@ -0,0 +1,34 @@ +from django import forms +from django.core.validators import ( + MaxValueValidator, + MinLengthValidator, + MinValueValidator, +) + + +class TimeOnlyForm(forms.Form): + hours = forms.IntegerField( + label="Hodiny", validators=[MinValueValidator(limit_value=0)] + ) + + minutes = forms.IntegerField( + label="Minuty", + validators=[ + MinValueValidator(limit_value=0), + MaxValueValidator(limit_value=60), + ], + ) + + seconds = forms.IntegerField( + label="Minuty", + validators=[ + MinValueValidator(limit_value=0), + MaxValueValidator(limit_value=60), + ], + ) + + +class NewTimerForm(TimeOnlyForm): + name = forms.CharField( + label="Název", max_length=64, validators=[MinLengthValidator(limit_value=1)] + ) diff --git a/timer/migrations/0001_initial.py b/timer/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..93e00c45a863e2f6e2f6cb051a5f591145e0c179 --- /dev/null +++ b/timer/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 4.1.5 on 2023-08-24 13:11 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="OngoingTimer", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=64, verbose_name="Název")), + ( + "hours", + models.IntegerField( + validators=[ + django.core.validators.MinValueValidator(limit_value=0) + ], + verbose_name="Hodiny", + ), + ), + ( + "minutes", + models.IntegerField( + validators=[ + django.core.validators.MinValueValidator(limit_value=0), + django.core.validators.MaxValueValidator(limit_value=60), + ], + verbose_name="Minuty", + ), + ), + ( + "seconds", + models.IntegerField( + validators=[ + django.core.validators.MinValueValidator(limit_value=0), + django.core.validators.MaxValueValidator(limit_value=60), + ], + verbose_name="Minuty", + ), + ), + ], + ), + ] diff --git a/timer/migrations/__init__.py b/timer/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/timer/models.py b/timer/models.py new file mode 100644 index 0000000000000000000000000000000000000000..cc97e9bb807c624d8c00d923b4275650d8dcee5f --- /dev/null +++ b/timer/models.py @@ -0,0 +1,36 @@ +from django.core.validators import ( + MaxValueValidator, + MinLengthValidator, + MinValueValidator, +) +from django.db import models + +# Create your models here. + + +class OngoingTimer(models.Model): + name = models.CharField( + max_length=64, + verbose_name="Název", + validators=[MinLengthValidator(limit_value=1)], + ) + + hours = models.IntegerField( + verbose_name="Hodiny", validators=[MinValueValidator(limit_value=0)] + ) + + minutes = models.IntegerField( + verbose_name="Minuty", + validators=[ + MinValueValidator(limit_value=0), + MaxValueValidator(limit_value=60), + ], + ) + + seconds = models.IntegerField( + verbose_name="Minuty", + validators=[ + MinValueValidator(limit_value=0), + MaxValueValidator(limit_value=60), + ], + ) diff --git a/timer/routing.py b/timer/routing.py new file mode 100644 index 0000000000000000000000000000000000000000..98d01b5cabbbee6d586bcf7c3bb89e664cef72f3 --- /dev/null +++ b/timer/routing.py @@ -0,0 +1,7 @@ +from django.urls import path + +from . import consumers + +websocket_urlpatterns = [ + path("ws/timer/", consumers.TimerConsumer.as_asgi()), +] diff --git a/timer/templates/timer/create.html b/timer/templates/timer/create.html new file mode 100644 index 0000000000000000000000000000000000000000..ec2d87b238d2211f13c26f7a6f396df26c6178d2 --- /dev/null +++ b/timer/templates/timer/create.html @@ -0,0 +1,37 @@ +{% extends "shared/base.html" %} + +{% load render_bundle from webpack_loader %} + +{% block title %}NovĂ˝ ÄŤasovaÄŤ{% endblock %} +{% block header_name %}ÄŚasovaÄŤe{% endblock %} +{% block description %}{% endblock %} + +{% block head %} + <link + rel="stylesheet" + href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + > +{% endblock %} + +{% block content %} + <main> + <h1 class="text-6xl font-bebas mb-5">NovĂ˝ ÄŤasovaÄŤ</h1> + + <form + class=" + flex flex-col gap-1 + [&_label]:w-24 + [&_input]:bg-gray-100 [&_input]:border [&_input]:border-gray-100 [&_input]:px-1.5 [&_input]:py-0.5 + " + method="post" + > + {% csrf_token %} + + {{ form.as_div }} + + <button class="btn"> + <div class="btn__body">VytvoĹ™it</div> + </button> + </form> + </main> +{% endblock %} diff --git a/timer/templates/timer/edit_timer.html b/timer/templates/timer/edit_timer.html new file mode 100644 index 0000000000000000000000000000000000000000..d4d75af1639d6c14bd26385bfa9dde5a2e13ace6 --- /dev/null +++ b/timer/templates/timer/edit_timer.html @@ -0,0 +1,76 @@ +{% extends "shared/base.html" %} + +{% load render_bundle from webpack_loader %} + +{% block title %}ÄŚasovaÄŤe{% endblock %} +{% block header_name %}ÄŚasovaÄŤe{% endblock %} +{% block description %}{% endblock %} + +{% block head %} + <link + rel="stylesheet" + href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + > + {% render_bundle "timer" %} +{% endblock %} + +{% block content %} + <script> + window.startingTime = { + hours: {{ timer.hours }}, + minutes: {{ timer.minutes }}, + seconds: {{ timer.seconds }}, + } + </script> + + <main> + <h1 class="text-6xl font-bebas mb">Ăšprava ÄŤasovaÄŤe {{ timer.name }}</h1> + + <div> + <div id="timer"> + <div class="timer-values">{{ timer.hours }}:{{ timer.minutes }}:{{ timer.seconds }}</div> + </div> + </div> + + <div class="flex flex-col gap-2"> + <div> + <input + type="checkbox" + id="is_counting" + name="is_counting" + autocomplete="off" + value="false" + > + <label + for="is_counting" + >OdpoÄŤet spuštÄ›nĂ˝</label> + </div> + <div class="flex gap-2"> + <input + type="number" + id="hours" + name="hours" + placeholder="Hodiny" + autocomplete="off" + > + <input + type="number" + id="minutes" + name="minutes" + placeholder="Minuty" + autocomplete="off" + > + <input + type="number" + id="seconds" + name="seconds" + placeholder="Sekundy" + autocomplete="off" + > + <button + id="update-time" + >Aktualizovat ÄŤas</button> + </div> + </div> + </main> +{% endblock %} diff --git a/timer/templates/timer/index.html b/timer/templates/timer/index.html new file mode 100644 index 0000000000000000000000000000000000000000..dec7857d7c85e0518f325e36d96c81fd43101399 --- /dev/null +++ b/timer/templates/timer/index.html @@ -0,0 +1,42 @@ +{% extends "shared/base.html" %} + +{% load render_bundle from webpack_loader %} + +{% block title %}ÄŚasovaÄŤe{% endblock %} +{% block header_name %}ÄŚasovaÄŤe{% endblock %} +{% block description %}{% endblock %} + +{% block head %} + <link + rel="stylesheet" + href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + > +{% endblock %} + +{% block content %} + <main> + <h1 class="text-6xl font-bebas mb">ÄŚasovaÄŤe</h1> + <h2 class="text-3xl font-bebas mb-5">Seznam aktivnĂch ÄŤasovaÄŤĹŻ</h2> + + <div class="prose max-w-none mb-8"> + <ul> + {% for timer in ongoing_timers %} + <li> + <a + href="{% url 'timer:view_timer' timer.id %}" + >{{ timer.name }}</a> + </li> + {% endfor %} + </ul> + </div> + + <a class="btn btn--icon" href="{% url 'timer:create' %}"> + <div class="btn__body-wrap"> + <div class="btn__body">NovĂ˝ ÄŤasovaÄŤ</div> + <div class="btn__icon"> + <i class="ico--chevron-right"></i> + </div> + </div> + </a> + </main> +{% endblock %} diff --git a/timer/templates/timer/view_timer.html b/timer/templates/timer/view_timer.html new file mode 100644 index 0000000000000000000000000000000000000000..5e11cf3f617a642f2af0f6a206b4758e7e398ced --- /dev/null +++ b/timer/templates/timer/view_timer.html @@ -0,0 +1,47 @@ +{% extends "shared/base.html" %} + +{% load render_bundle from webpack_loader %} + +{% block title %}ÄŚasovaÄŤe{% endblock %} +{% block header_name %}ÄŚasovaÄŤe{% endblock %} +{% block description %}{% endblock %} + +{% block head %} + <link + rel="stylesheet" + href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + > + {% render_bundle "timer" %} +{% endblock %} + +{% block nav %}{% endblock %} + +{% block content %} + <script> + window.startingTime = { + hours: {{ timer.hours }}, + minutes: {{ timer.minutes }}, + seconds: {{ timer.seconds }}, + } + </script> + + <main> + <h1 class="text-6xl font-bebas mb">{{ timer.name }}</h1> + + <div + class="my-5" + style="font-size:15rem" {% comment %}TODO{% endcomment %} + > + <div id="timer"> + <div class="timer-values">{{ timer.hours }}:{{ timer.minutes }}:{{ timer.seconds }}</div> + </div> + </div> + + <a + class="text-gray-400" + href="{% url 'timer:edit_timer' timer.id %}" + >Upravit</a> + </main> +{% endblock %} + +{% block footer %}{% endblock %} diff --git a/timer/tests.py b/timer/tests.py new file mode 100644 index 0000000000000000000000000000000000000000..7ce503c2dd97ba78597f6ff6e4393132753573f6 --- /dev/null +++ b/timer/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/timer/urls.py b/timer/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..7dc44356fc70ccd72dc4dcdba76f3204e79fe8c9 --- /dev/null +++ b/timer/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from . import views + +app_name = "timer" +urlpatterns = [ + path("", views.index, name="index"), + path("novy", views.create, name="create"), + path("<int:id>", views.view_timer, name="view_timer"), + path("<int:id>/uprava", views.edit_timer, name="edit_timer"), +] diff --git a/timer/views.py b/timer/views.py new file mode 100644 index 0000000000000000000000000000000000000000..614acc4f8a0230fbd05ed8709c6a6da1253ba5df --- /dev/null +++ b/timer/views.py @@ -0,0 +1,46 @@ +from django.shortcuts import get_object_or_404, redirect, render + +from .forms import NewTimerForm, TimeOnlyForm +from .models import OngoingTimer + + +def index(request): + return render( + request, + "timer/index.html", + { + "ongoing_timers": OngoingTimer.objects.all(), + }, + ) + + +def create(request): + if request.method == "POST": + form = NewTimerForm(request.POST) + + if form.is_valid(): + timer = OngoingTimer( + hours=form.cleaned_data["hours"], + minutes=form.cleaned_data["minutes"], + seconds=form.cleaned_data["seconds"], + name=form.cleaned_data["name"], + ) + timer.save() + + return redirect("timer:view_timer", id=timer.id) + else: + form = NewTimerForm() + + return render(request, "timer/create.html", {"form": form}) + + +def view_timer(request, id: int): + timer = get_object_or_404(OngoingTimer, id=id) + + return render(request, "timer/view_timer.html", {"timer": timer}) + + +def edit_timer(request, id: int): + timer = get_object_or_404(OngoingTimer, id=id) + + return render(request, "timer/edit_timer.html", {"timer": timer}) diff --git a/webpack.config.js b/webpack.config.js index 64386ccabf56b6fcb90831314851d5ee99c0b244..6e3af748fb555b9e8a168d13c4d8a731a4862f46 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,6 +21,10 @@ module.exports = { import: path.resolve("static_src", "mail_signature.js"), dependOn: "shared", }, + timer: { + import: path.resolve("static_src", "timer.js"), + dependOn: "shared", + }, asset_server_resize: { import: path.resolve("static_src", "asset_server_resize.js"), dependOn: "shared",