Skip to content
Snippets Groups Projects
Commit 0e15739b authored by Tomáš Valenta's avatar Tomáš Valenta
Browse files

add timer

parent 42386cd5
Branches
No related tags found
1 merge request!9Release
Pipeline #14301 passed
Showing with 352 additions and 60 deletions
......@@ -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:
......
......@@ -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"
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"],
}
},
)
......@@ -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"
},
......
......@@ -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",
......
channels[daphne]==4.0.0
Django==4.1.5
django-database-url==1.0.3
django-environ==0.9.0
......
gunicorn==20.1.0
whitenoise==6.3.0
uvicorn==0.23.2
gunicorn==21.2.0
......@@ -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
"""
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))
),
}
)
......@@ -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"}}
......@@ -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")),
]
......@@ -45,6 +45,7 @@
{% block head %}{% endblock %}
</head>
<body>
{% block nav %}
<nav class="navbar navbar--simple __js-root">
<ui-app inline-template>
<ui-navbar inline-template>
......@@ -64,9 +65,11 @@
</ui-navbar>
</ui-app>
</nav>
{% endblock %}
<div class="container container--default py-8 lg:py-24">
{% block content %}{% endblock %}
</div>
{% block footer %}
<footer class="footer bg-grey-700 text-white __js-root hidden lg:block">
<ui-app inline-template>
<div>
......@@ -80,5 +83,6 @@
</div>
</ui-app>
</footer>
{% endblock %}
</body>
</html>
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 ---
}
)
......@@ -3,8 +3,7 @@ const defaultTheme = require("tailwindcss/defaultTheme");
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"*/templates/*/*.html",
"*/templates/*/*/*.html",
"*/templates/**/*.html",
],
theme: {
extend: {
......
from django.contrib import admin
# Register your models here.
from django.apps import AppConfig
class TimerConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "timer"
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"])
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)]
)
# 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",
),
),
],
),
]
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment