Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e83b4d4624 | |||
| 38225cabc8 | |||
| 08bb63ce1f | |||
| 72a93e1dfd | |||
| fa5597f7cf | |||
| 92ea427c53 | |||
| abc2519bc7 | |||
| 87a0c68ef2 | |||
| 612b17960a | |||
| 7f21b7bba2 | |||
| 8c116d58de |
@@ -1,11 +1,3 @@
|
||||
[report]
|
||||
show_missing = True
|
||||
omit =
|
||||
*/migrations/*
|
||||
*/admin.py
|
||||
*/translation.py
|
||||
[run]
|
||||
omit =
|
||||
*/migrations/*
|
||||
*/admin.py
|
||||
*/translation.py
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
.DS_Store
|
||||
.dockerignore
|
||||
.git/
|
||||
.husky/
|
||||
.venv/
|
||||
.vscode/
|
||||
collected_static/
|
||||
logs/
|
||||
!logs/README
|
||||
media/
|
||||
!media/REMOVE_ME
|
||||
misc/
|
||||
node_modules/
|
||||
scripts/
|
||||
.coverage
|
||||
.coveragerc
|
||||
.env*
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
.gitlab-ci.yml
|
||||
.python-version
|
||||
docker-compose.yml
|
||||
!manage.py
|
||||
package*.json
|
||||
!poetry.lock
|
||||
!production_entrypoint.sh
|
||||
pycodestyle.cfg
|
||||
!pyproject.toml
|
||||
pyright.json
|
||||
README.md
|
||||
stack-compose*.yml
|
||||
@@ -1,11 +0,0 @@
|
||||
DEPLOY_ENV=local
|
||||
SENTRY_DSN=
|
||||
HOST=api.dev.sahkoinsinoorikilta.fi
|
||||
DEBUG=True
|
||||
SECRET_KEY=7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(
|
||||
DB_NAME=postgres
|
||||
DB_USER=postgres
|
||||
DB_PASSWD=postgres
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
EMAIL_API_KEY=
|
||||
@@ -1,13 +0,0 @@
|
||||
DEPLOY_ENV=local
|
||||
#SENTRY_DSN=
|
||||
HOST=localhost
|
||||
DEBUG=True
|
||||
SECRET_KEY=7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(
|
||||
DB_NAME=postgres
|
||||
DB_USER=postgres
|
||||
DB_PASSWD=postgres
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
EMAIL_API_KEY=
|
||||
GROUP_KEY=
|
||||
GOOGLE_CREDS_JSON='{}'
|
||||
@@ -3,4 +3,3 @@ infoscreen/static/js/lib
|
||||
webapp/static/js/lib
|
||||
static/js/lib
|
||||
collected_static
|
||||
venv
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
"jquery": true
|
||||
},
|
||||
"globals": {
|
||||
"angular": true,
|
||||
"noty": true,
|
||||
"_": true,
|
||||
"moment": true
|
||||
"angular": 1,
|
||||
"noty": 1,
|
||||
"app": 1,
|
||||
"_": 1,
|
||||
"moment": 1
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"rules": {
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
.DS_Store
|
||||
.env
|
||||
*.swp
|
||||
sikweb/settings.py
|
||||
*~
|
||||
*.pyc
|
||||
/collected_static/
|
||||
/media/
|
||||
logs/
|
||||
*.sqlite3
|
||||
uwsgi.ini
|
||||
uwsgi.log
|
||||
infoscreen/static/js/hsl.json
|
||||
members/logs/*
|
||||
node_modules/
|
||||
.coverage
|
||||
.vscode/
|
||||
.idea/
|
||||
*.code-workspace
|
||||
venv/
|
||||
.venv/
|
||||
logs/
|
||||
/media/
|
||||
node_modules/
|
||||
/.coverage
|
||||
db.sqlite3
|
||||
requirements_henu.txt
|
||||
/collected_static/
|
||||
mydatabase
|
||||
settings.json
|
||||
.vscode/
|
||||
@@ -1,131 +1,100 @@
|
||||
stages:
|
||||
- setup
|
||||
- audit
|
||||
- lint
|
||||
- test
|
||||
- lint
|
||||
- publish
|
||||
- deploy
|
||||
|
||||
install:
|
||||
image: node:14
|
||||
stage: setup
|
||||
script:
|
||||
- npm ci
|
||||
artifacts:
|
||||
paths:
|
||||
- node_modules
|
||||
expire_in: 1 week
|
||||
|
||||
audit:
|
||||
image: python:3.9
|
||||
stage: audit
|
||||
needs: []
|
||||
before_script:
|
||||
- pip install poetry==1.1.13
|
||||
- poetry config virtualenvs.create false
|
||||
- poetry install --no-interaction --no-ansi
|
||||
script:
|
||||
- safety check
|
||||
|
||||
test:
|
||||
image: python:3.9
|
||||
image: python:3.5
|
||||
stage: test
|
||||
needs: []
|
||||
services:
|
||||
- postgres:12
|
||||
- postgres:latest
|
||||
variables:
|
||||
POSTGRES_DB: ci
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
|
||||
DB_HOST: postgres
|
||||
before_script:
|
||||
- pip install poetry==1.1.13
|
||||
- poetry config virtualenvs.create false
|
||||
- poetry install --no-interaction --no-ansi
|
||||
script:
|
||||
- python -V
|
||||
- pip install -r requirements.txt
|
||||
- cp sikweb/settings-sample.py sikweb/default_settings.py
|
||||
- cp sikweb/.ci-settings.py sikweb/settings.py
|
||||
- python manage.py migrate --noinput
|
||||
- python manage.py createdefaultadmin
|
||||
- python manage.py test
|
||||
|
||||
lint:py:
|
||||
image: python:3.9
|
||||
pycodestyle:
|
||||
image: python:3.5
|
||||
stage: lint
|
||||
needs: []
|
||||
script:
|
||||
- pip install black==22.3.0
|
||||
- black --check .
|
||||
- pip install pycodestyle
|
||||
- pycodestyle --config=setup.cfg --count .
|
||||
|
||||
lint:js:
|
||||
image: node:14
|
||||
eslint:
|
||||
image: node:7.10.0
|
||||
stage: lint
|
||||
needs: ["install"]
|
||||
before_script:
|
||||
- npm install
|
||||
script:
|
||||
- npm run lint:js
|
||||
- npm run eslint
|
||||
|
||||
lint:md:
|
||||
image: node:14
|
||||
remark:
|
||||
image: node:7.10.0
|
||||
stage: lint
|
||||
needs: ["install"]
|
||||
before_script:
|
||||
- npm install
|
||||
script:
|
||||
- npm run lint:md
|
||||
- npm run remark
|
||||
|
||||
publish:
|
||||
stage: publish
|
||||
image: docker:stable
|
||||
needs: ["test", "lint:py", "lint:js", "lint:md"]
|
||||
services:
|
||||
- docker:stable-dind
|
||||
image: docker:latest
|
||||
only:
|
||||
- develop
|
||||
- master
|
||||
script:
|
||||
before_script:
|
||||
- docker info
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $REGISTRY_URL
|
||||
script:
|
||||
- docker build . -t "$IMAGE_NAME"
|
||||
- docker push "$IMAGE_NAME"
|
||||
|
||||
deploy:dev:
|
||||
deploy_dev:
|
||||
stage: deploy
|
||||
image: docker:stable
|
||||
only:
|
||||
- develop
|
||||
image: alpine:latest
|
||||
environment:
|
||||
name: dev
|
||||
url: http://api.dev.sahkoinsinoorikilta.fi
|
||||
variables:
|
||||
DOCKER_HOST: $DEV_CI_DOCKER_HOST
|
||||
DOCKER_TLS_VERIFY: 1
|
||||
before_script:
|
||||
- mkdir -p ~/.docker
|
||||
- echo "$DEV_TLSCACERT" > ~/.docker/ca.pem
|
||||
- echo "$DEV_TLSCERT" > ~/.docker/cert.pem
|
||||
- echo "$DEV_TLSKEY" > ~/.docker/key.pem
|
||||
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
|
||||
script:
|
||||
- docker stack deploy --with-registry-auth -c stack-compose-dev.yml "$SERVICE_NAME"
|
||||
after_script:
|
||||
- docker logout "$CI_REGISTRY"
|
||||
|
||||
deploy:production:
|
||||
stage: deploy
|
||||
image: docker:stable
|
||||
url: http://web.sik.party:8080
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
before_script:
|
||||
- pwd
|
||||
- apk add --update openssh
|
||||
- ssh -V
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
|
||||
script:
|
||||
- scp docker-compose.yml $DEV_SSH_USER@$DEV_SSH_HOST:~/deployment/docker-compose.yml
|
||||
- ssh $DEV_SSH_USER@$DEV_SSH_HOST "cd deployment && docker-compose down && docker pull \"$IMAGE_NAME\" && docker-compose up -d && docker image prune -f"
|
||||
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
image: alpine:latest
|
||||
environment:
|
||||
name: production
|
||||
url: https://api.sahkoinsinoorikilta.fi
|
||||
url: https://sika.sahkoinsinoorikilta.fi
|
||||
when: manual
|
||||
variables:
|
||||
DOCKER_HOST: $CI_DOCKER_HOST
|
||||
DOCKER_TLS_VERIFY: 1
|
||||
only:
|
||||
- master
|
||||
before_script:
|
||||
- mkdir -p ~/.docker
|
||||
- echo "$TLSCACERT" > ~/.docker/ca.pem
|
||||
- echo "$TLSCERT" > ~/.docker/cert.pem
|
||||
- echo "$TLSKEY" > ~/.docker/key.pem
|
||||
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
|
||||
- pwd
|
||||
- apk add --update openssh
|
||||
- ssh -V
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
|
||||
script:
|
||||
- docker stack deploy --with-registry-auth -c stack-compose.yml "$SERVICE_NAME"
|
||||
after_script:
|
||||
- docker logout "$CI_REGISTRY"
|
||||
- ssh $PROD_SSH_USER@$PROD_SSH_HOST "zsh ~/deploy.sh"
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
PURPLE='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
source "${VIRTUAL_ENV}/bin/activate"
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
printf "${PURPLE}Failed to find virtualenv. Skipping pre-commit hook.\n${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
npm run lint
|
||||
@@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
PURPLE='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
source "${VIRTUAL_ENV}/bin/activate"
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
printf "${PURPLE}Failed to find virtualenv. Skipping pre-push hook.\n${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
printf "${PURPLE}Running pre-push tests.${NC}\n"
|
||||
printf "${PURPLE}linters...${NC}\n"
|
||||
npm run lint
|
||||
printf "${PURPLE}unit tests...${NC}\n"
|
||||
python -Wall manage.py test --noinput
|
||||
set +e
|
||||
|
||||
printf "${PURPLE}Tests passed.${NC}\n"
|
||||
@@ -1 +0,0 @@
|
||||
3.9
|
||||
@@ -0,0 +1 @@
|
||||
global_static
|
||||
@@ -1,28 +1,8 @@
|
||||
FROM python:3.9-slim-buster as builder
|
||||
FROM python:3
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
COPY . ./
|
||||
|
||||
ENV POETRY_VERSION=1.1.13
|
||||
|
||||
RUN pip install "poetry==$POETRY_VERSION"
|
||||
RUN poetry export --without-hashes > requirements.txt
|
||||
|
||||
FROM python:3.9-slim-buster as server
|
||||
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
COPY --from=builder requirements.txt ./
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
# prevents python creating .pyc files
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
# pip
|
||||
PIP_NO_CACHE_DIR=off \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=on \
|
||||
PIP_DEFAULT_TIMEOUT=100
|
||||
|
||||
RUN apt-get update && apt-get install --no-install-recommends -y build-essential
|
||||
RUN pip install --no-deps -r requirements.txt
|
||||
|
||||
RUN python manage.py collectstatic --noinput
|
||||
CMD ["sh", "-c", "./production_entrypoint.sh"]
|
||||
ENV IS_DOCKER 1
|
||||
RUN mkdir /code
|
||||
WORKDIR /code
|
||||
ADD requirements.txt /code/
|
||||
RUN env
|
||||
ADD . /code/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,23 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from coffee_scale import mqtt
|
||||
|
||||
|
||||
class CoffeeScaleConfig(AppConfig):
|
||||
name = 'coffee_scale'
|
||||
|
||||
def ready(self):
|
||||
if ('makemigrations' in sys.argv or 'migrate' in sys.argv):
|
||||
return
|
||||
|
||||
try:
|
||||
logging.info('Connecting to MQTT (coffee scale) at {}...'.format(mqtt.HOST))
|
||||
logging.info('If there is no confirmation, the MQTT connection has probably failed.')
|
||||
mqtt.client.connect_async(mqtt.HOST, mqtt.PORT, 60)
|
||||
mqtt.client.loop_start()
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
logging.error('Failed to connect to MQTT at {}'.format(mqtt.HOST))
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -0,0 +1,58 @@
|
||||
import paho.mqtt.client as mqtt
|
||||
import logging
|
||||
import datetime
|
||||
from collections import deque
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
HOST = settings.MQTT_SETTINGS['HOST']
|
||||
PORT = settings.MQTT_SETTINGS['PORT']
|
||||
TOPICS = settings.MQTT_SETTINGS['TOPICS']
|
||||
latest = {}
|
||||
|
||||
|
||||
def on_connect(client, userdata, flags, rc):
|
||||
logging.info('Connected successfully to MQTT.')
|
||||
logging.info('Subscribing to all topics on {}.'.format(HOST))
|
||||
client.subscribe('sik/kiltahuone/kahvivaaka/#')
|
||||
|
||||
|
||||
def update_latest(msg):
|
||||
payload = msg.payload.decode('utf-8')
|
||||
if msg.topic == TOPICS['WEIGHT']:
|
||||
weight = float(payload)
|
||||
latest['weight'] = weight
|
||||
elif msg.topic == TOPICS['CUPS']:
|
||||
cups = float(payload)
|
||||
latest['cups'] = cups
|
||||
elif msg.topic == TOPICS['BREWING']:
|
||||
brewing = bool(int(payload))
|
||||
latest['brewing'] = brewing
|
||||
elif msg.topic == TOPICS['BREW_TIME']:
|
||||
brew_time = datetime.datetime.fromtimestamp(float(payload))
|
||||
latest['brew_time'] = brew_time
|
||||
|
||||
|
||||
def on_message(client, userdata, msg):
|
||||
try:
|
||||
update_latest(msg)
|
||||
except Exception as ex:
|
||||
logging.exception('Failed to parse MQTT payload.')
|
||||
|
||||
|
||||
def on_disconnect(client, userdata, rc):
|
||||
if rc != 0:
|
||||
logging.warning('MQTT unexpectedly disconnected.')
|
||||
else:
|
||||
client.loop_stop(force=False)
|
||||
logging.warning('MQTT disconnected.')
|
||||
|
||||
|
||||
def get_latest():
|
||||
return latest
|
||||
|
||||
|
||||
client = mqtt.Client()
|
||||
client.on_connect = on_connect
|
||||
client.on_message = on_message
|
||||
client.on_disconnect = on_disconnect
|
||||
@@ -0,0 +1,121 @@
|
||||
body {
|
||||
background-color: white;
|
||||
font-family: monospace;
|
||||
color: black;
|
||||
}
|
||||
#container{
|
||||
position:relative;
|
||||
width:95%;
|
||||
margin-left:auto;
|
||||
margin-right:auto;
|
||||
height:100%;
|
||||
overflow:hidden;
|
||||
|
||||
}
|
||||
#upper{
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom center;
|
||||
background-image: url("/static/img/smokes.png");
|
||||
transform-origin: bottom;
|
||||
animation: smokes 8s ease-in-out 0s infinite;
|
||||
opacity:0;
|
||||
height:40%;
|
||||
}
|
||||
#lower{
|
||||
position:relative;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: top center;
|
||||
background-image: url("/static/img/coffeecup3.png");
|
||||
height:60%;
|
||||
}
|
||||
#scale{
|
||||
position:absolute;
|
||||
top:80%;
|
||||
width:90%;
|
||||
height:10%;
|
||||
margin: 0% 5% 0% 5%;
|
||||
background: lightgrey;
|
||||
border-radius: 10px;
|
||||
overflow:hidden;
|
||||
}
|
||||
#scale2{
|
||||
width: 0%;
|
||||
transition: width 2s;
|
||||
height:100%;
|
||||
background: green;
|
||||
border-radius: 10px;
|
||||
}
|
||||
#brewtime{
|
||||
text-align:right;
|
||||
position:absolute;
|
||||
right:0px;
|
||||
z-index:5;
|
||||
font-size:10vw;
|
||||
}
|
||||
#address{
|
||||
text-align:left;
|
||||
position:absolute;
|
||||
left:0px;
|
||||
z-index:5;
|
||||
font-size:4vw;
|
||||
color: #333;
|
||||
}
|
||||
noscript{
|
||||
color:red;
|
||||
}
|
||||
#text{
|
||||
color:green;
|
||||
position:absolute;
|
||||
top:50%;
|
||||
left:50%;
|
||||
}
|
||||
.brewing{
|
||||
animation: brewing 5s ease-in-out 0s infinite;
|
||||
}
|
||||
.hurry{
|
||||
color:red !important;
|
||||
}
|
||||
.unknown{
|
||||
color:orange !important;
|
||||
animation: unknown 5s ease-in-out 0s infinite;
|
||||
}
|
||||
.friday{
|
||||
animation: friday 20s ease-in-out 0s infinite;
|
||||
}
|
||||
.normal{
|
||||
animation: normal 1000s ease-in-out 0s infinite;
|
||||
}
|
||||
.coffeeready{
|
||||
animation: coffeeready 10s ease-in-out 0s;
|
||||
}
|
||||
@keyframes smokes {
|
||||
0% {transform: skewX(-10deg);}
|
||||
50% {transform: skewX(10deg);}
|
||||
100% {transform: skewX(-10deg);}
|
||||
}
|
||||
@keyframes brewing {
|
||||
0% {color:green;}
|
||||
50% {color: transparent;}
|
||||
100% {color:green;}
|
||||
}
|
||||
@keyframes coffeeready {
|
||||
0% {background-color:white;}
|
||||
25% {background-color:green;}
|
||||
50% {background-color:white;}
|
||||
75% {background-color:green;}
|
||||
100% {background-color:white;}
|
||||
}
|
||||
@keyframes unknown {
|
||||
0%,40% {transform: rotate(0deg);}
|
||||
60%,100% {transform: rotate(360deg);}
|
||||
}
|
||||
@keyframes friday {
|
||||
0% {transform: rotate(0deg);}
|
||||
100% {transform: rotate(360deg);}
|
||||
}
|
||||
@keyframes normal {
|
||||
0%,49% {transform: rotate(0deg);}
|
||||
50%,100% {transform: rotate(360deg);}
|
||||
}
|
||||
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
@@ -0,0 +1,130 @@
|
||||
var len = 0;
|
||||
var lastBrew = "∞";
|
||||
var brewtext = "";
|
||||
|
||||
$(document).ready(function(){
|
||||
$('#text').bind("DOMSubtreeModified", resize);
|
||||
updateTime();
|
||||
setInterval(updateTime,1000);
|
||||
formatBrewTime();
|
||||
setInterval(formatBrewTime,10000);
|
||||
});
|
||||
|
||||
$(window).resize(resize);
|
||||
|
||||
function fetchdata(data, status){
|
||||
if (typeof status !== 'undefined'){
|
||||
if (status == "success"){
|
||||
parseData(data);
|
||||
}
|
||||
else if (status == "error"){
|
||||
handleError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$.getJSON("/coffee/cups", fetchdata);
|
||||
setInterval(function() {
|
||||
$.getJSON("/coffee/cups", fetchdata);
|
||||
}, 2000);
|
||||
|
||||
function formatBrewTime(){
|
||||
if (!brewtext && lastBrew instanceof Date){
|
||||
var now = new Date();
|
||||
var timeDiff = Math.max(now.getTime() - lastBrew.getTime(), 0);
|
||||
var tmp = (timeDiff < 3600000)
|
||||
? Math.round(timeDiff / 60000) + ' min'
|
||||
: '~' + Math.round(timeDiff / 3600000 * 2) / 2 + ' h';
|
||||
|
||||
$("#brewtime").html(tmp);
|
||||
} else {
|
||||
$("#brewtime").html(brewtext);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError() {
|
||||
setData("?", 0, 0, Number.MAX_VALUE, Number.MAX_VALUE);
|
||||
}
|
||||
|
||||
function parseData(data) {
|
||||
if (data) {
|
||||
var date = new Date(data.date);
|
||||
lastBrew = new Date(data.last_brew);
|
||||
var now = new Date();
|
||||
var cups = data.cups;
|
||||
var brewing = data.brewing;
|
||||
var timeDiff = Math.max(now.getTime() - lastBrew.getTime(),0);
|
||||
var opa = Math.max(100 - timeDiff / 90000,0);
|
||||
setData(cups, data.temp, opa,now.getTime()-date.getTime(), timeDiff, brewing);
|
||||
}
|
||||
else{
|
||||
handleError();
|
||||
}
|
||||
}
|
||||
|
||||
function setData(cups, temp, opa, timeFromUpdate, timeFromBrew, brewing){
|
||||
if (cups == 0) {
|
||||
opa = 0;
|
||||
}
|
||||
brewtext = "";
|
||||
$("#upper").css({opacity: opa/100});
|
||||
$("#scale2").css({width: Math.min(cups/9*100,100) + '%'});
|
||||
$("#text,body").removeClass();
|
||||
|
||||
cups = Number(cups).toFixed(1);
|
||||
|
||||
if(timeFromUpdate > 600000){
|
||||
cups = "?";
|
||||
brewtext = "∞";
|
||||
$("#text").addClass("unknown");
|
||||
}
|
||||
else if(brewing){
|
||||
cups = "+";
|
||||
brewtext = ":)";
|
||||
$("#text").addClass("brewing");
|
||||
}
|
||||
else if(cups <= 2){
|
||||
$("#text").addClass("hurry");
|
||||
}
|
||||
formatBrewTime();
|
||||
if($("#text").html() == "+" && !brewing)
|
||||
$("body").addClass("coffeeready");
|
||||
|
||||
var cupsString = cups.toString();
|
||||
len = cupsString.length;
|
||||
$("#text").html(cups);
|
||||
}
|
||||
|
||||
function updateTime(){
|
||||
var now = new Date();
|
||||
$("#time").html(formatTime(now.getHours(),now.getMinutes(),now.getSeconds()));
|
||||
}
|
||||
|
||||
function formatTime(hours, minutes, seconds){
|
||||
var str = "";
|
||||
|
||||
if(hours < 10)
|
||||
str += "0";
|
||||
str += hours;
|
||||
|
||||
str += ":";
|
||||
|
||||
if(minutes < 10)
|
||||
str += "0";
|
||||
str += minutes;
|
||||
|
||||
str += ":";
|
||||
|
||||
if(seconds < 10)
|
||||
str += "0";
|
||||
str += seconds;
|
||||
|
||||
return str;
|
||||
}
|
||||
function resize(){
|
||||
var w = $("#container").width();
|
||||
var h = $("#container").height();
|
||||
var s = w > h ? h : w;
|
||||
var font = s*0.8*0.38/Math.sqrt(len);
|
||||
$("#text").css({ top: s*0.16-font/2 + 'px', fontSize: font + 'px', marginLeft: -font*len*3/10 + 'px'});
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Coffee Cups @Guild Room - AYY SIK ry</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="3600">
|
||||
<meta http-equiv="cache-control" content="max-age=0" />
|
||||
<meta http-equiv="cache-control" content="no-cache" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
|
||||
<meta http-equiv="pragma" content="no-cache" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/coffee.css" />
|
||||
<script src="/static/js/coffee.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container">
|
||||
<span id="brewtime"></span>
|
||||
<span id="address">
|
||||
ka.dy.fi
|
||||
<noscript><br>This page uses JavaScript!</noscript>
|
||||
<br><span id="time"></span></span>
|
||||
</span>
|
||||
<div id="upper">
|
||||
</div>
|
||||
<!--Kahvinkeitin on rikki. Varakeittimellä keitettyä kahvia saattaa olla.-->
|
||||
<div id="lower" class="normal">
|
||||
<div id="text"></div>
|
||||
<div id="scale"><div id="scale2"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
from django.test import TestCase, Client
|
||||
from django.conf import settings
|
||||
|
||||
from coffee_scale.mqtt import on_message
|
||||
|
||||
HOST = settings.MQTT_SETTINGS['HOST']
|
||||
PORT = settings.MQTT_SETTINGS['PORT']
|
||||
TOPICS = settings.MQTT_SETTINGS['TOPICS']
|
||||
|
||||
|
||||
class MQTTTestCase(TestCase):
|
||||
"""Tests MQTT functionality"""
|
||||
|
||||
class MockMessage:
|
||||
def __init__(self, payload, topic):
|
||||
self.payload = payload
|
||||
self.topic = topic
|
||||
|
||||
def setUp(self):
|
||||
payload = '10'.encode('utf-8')
|
||||
topic = TOPICS['CUPS']
|
||||
msg = MQTTTestCase.MockMessage(payload, topic)
|
||||
|
||||
on_message(None, None, msg)
|
||||
self.c = Client()
|
||||
|
||||
def test_receive_cups(self):
|
||||
response = self.c.get('/coffee/cups')
|
||||
payload = response.json()
|
||||
self.assertEquals(payload['cups'], 10)
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.conf.urls import url
|
||||
from django.views.generic.base import RedirectView
|
||||
|
||||
from .views import coffee_view, cups_view
|
||||
|
||||
favicon_view = RedirectView.as_view(url='static/img/favicon.ico', permanent=True)
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# landing page
|
||||
url(r'^$', coffee_view),
|
||||
url(r'^cups', cups_view),
|
||||
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
from django.shortcuts import render
|
||||
from django.http import JsonResponse
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from .mqtt import get_latest
|
||||
import coffee_scale.mqtt # somehow this is needed
|
||||
|
||||
import logging
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def coffee_view(request):
|
||||
return render(request, 'coffee.html')
|
||||
|
||||
|
||||
def cups_view(request):
|
||||
now = timezone.now()
|
||||
latest = get_latest()
|
||||
data = {
|
||||
'date': now,
|
||||
'cups': latest.get('cups'),
|
||||
'last_brew': latest.get('brew_time'),
|
||||
'brewing': latest.get('brewing'),
|
||||
'weight': latest.get('weight')
|
||||
}
|
||||
return JsonResponse(data)
|
||||
@@ -2,23 +2,12 @@ version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:12
|
||||
volumes:
|
||||
- dbdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=postgres
|
||||
|
||||
image: postgres
|
||||
web:
|
||||
build: .
|
||||
image: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-backend
|
||||
env_file:
|
||||
- .env
|
||||
image: 86.50.143.82:5000/web20
|
||||
command: ["bash", "-c", "cd /code && ./wait-for-it.sh db:5432 -- bash setup.sh --no-input --no-npm && python manage.py runserver 0.0.0.0:8080"]
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
volumes:
|
||||
dbdata:
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
"""Admin site registers."""
|
||||
|
||||
from django.contrib import admin
|
||||
from infoscreen.models import (
|
||||
Rotation,
|
||||
InfoItem,
|
||||
InfoInstance,
|
||||
ImageInfoItem,
|
||||
ExternalImageInfoItem,
|
||||
ABBInfoItem,
|
||||
ExternalWebsiteInfoItem,
|
||||
VideoInfoItem,
|
||||
)
|
||||
from infoscreen.models import Rotation, InfoItem, InfoInstance
|
||||
from infoscreen.models import ImageInfoItem, ExternalImageInfoItem, ABBInfoItem
|
||||
from infoscreen.models import ExternalWebsiteInfoItem
|
||||
from infoscreen.models import VideoInfoItem
|
||||
|
||||
# Register your models here.
|
||||
admin.site.register(Rotation)
|
||||
|
||||
@@ -6,4 +6,4 @@ from django.apps import AppConfig
|
||||
class InfoscreenConfig(AppConfig):
|
||||
"""Infoscreen app configuration."""
|
||||
|
||||
name = "infoscreen"
|
||||
name = 'infoscreen'
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""File containing Infoscreen HSL data fetcher classes."""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pytz
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
from django.utils import timezone, dateparse
|
||||
from django.utils.dateformat import format
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
with open(os.path.join(settings.BASE_DIR, 'infoscreen', 'hsl_stops.graphql')) as stops_file:
|
||||
STOPS_QUERY = stops_file.read()
|
||||
|
||||
with open(os.path.join(settings.BASE_DIR, 'infoscreen', 'hsl_stops_variables.json')) as vars_file:
|
||||
STOPS_VARS = json.loads(vars_file.read())
|
||||
|
||||
API_URL = 'https://api.digitransit.fi/routing/v1/routers/hsl/index/graphql'
|
||||
API_HEADERS = {'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
def fetch():
|
||||
"""Fetch data from HSL API."""
|
||||
|
||||
query_vars = STOPS_VARS.copy()
|
||||
query_vars['startTime_6'] = format(timezone.now(), 'U')
|
||||
|
||||
post_data = json.dumps({
|
||||
'operationName': 'NearestRoutesContainer',
|
||||
'query': STOPS_QUERY,
|
||||
'variables': query_vars,
|
||||
})
|
||||
|
||||
resp = requests.post(API_URL, data=post_data, headers=API_HEADERS)
|
||||
|
||||
data = resp.json()
|
||||
|
||||
items = data['data']['viewer']['_nearest']['edges']
|
||||
places = map(lambda item: item['node']['place'], items)
|
||||
|
||||
schedule = []
|
||||
for place in places:
|
||||
route = place['pattern']['route']['shortName']
|
||||
stop_times = place['_stoptimes']
|
||||
for stop_time in stop_times:
|
||||
timestamp = stop_time['serviceDay'] + stop_time['realtimeArrival']
|
||||
headsign = stop_time['stopHeadsign']
|
||||
stop_name = stop_time['stop']['name']
|
||||
time_diff = (timestamp - timezone.now().timestamp()) / 60 # minutes
|
||||
|
||||
if time_diff < settings.HSL_DEPARTURE_THRESHOLD:
|
||||
continue
|
||||
elif time_diff < settings.HSL_HURRY_THRESHOLD:
|
||||
time = '{} min'.format(int(time_diff))
|
||||
else:
|
||||
time = pytz.utc.localize(datetime.fromtimestamp(timestamp)).strftime('%H:%M')
|
||||
|
||||
schedule.append({
|
||||
'route': route,
|
||||
'headsign': headsign,
|
||||
'timestamp': time,
|
||||
'stop': stop_name,
|
||||
})
|
||||
|
||||
return schedule
|
||||
@@ -0,0 +1,71 @@
|
||||
query NearestRoutesContainer($lat_0: Float!, $lon_1: Float!, $maxDistance_2: Int!, $maxResults_3: Int!, $timeRange_7: Int!, $numberOfDepartures_8: Int!, $filterByModes_4: [Mode]!, $filterByPlaceTypes_5: [FilterPlaceType]!, $startTime_6: Long!) {
|
||||
viewer {
|
||||
...F5
|
||||
}
|
||||
}
|
||||
|
||||
fragment F0 on DepartureRow {
|
||||
_stoptimes4caEfh: stoptimes(startTime: $startTime_6, timeRange: $timeRange_7, numberOfDepartures: $numberOfDepartures_8) {
|
||||
pickupType
|
||||
serviceDay
|
||||
realtimeDeparture
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
fragment F1 on DepartureRow {
|
||||
pattern {
|
||||
route {
|
||||
shortName
|
||||
}
|
||||
}
|
||||
_stoptimes: stoptimes(startTime: $startTime_6, timeRange: $timeRange_7, numberOfDepartures: $numberOfDepartures_8) {
|
||||
realtimeArrival
|
||||
serviceDay
|
||||
stopHeadsign
|
||||
stop {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment F2 on BikeRentalStation {
|
||||
id
|
||||
}
|
||||
|
||||
fragment F3 on placeAtDistance {
|
||||
distance
|
||||
place {
|
||||
id
|
||||
__typename
|
||||
...F1
|
||||
...F2
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
fragment F4 on placeAtDistanceConnection {
|
||||
edges {
|
||||
node {
|
||||
distance
|
||||
place {
|
||||
id
|
||||
__typename
|
||||
...F0
|
||||
}
|
||||
id
|
||||
...F3
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
|
||||
fragment F5 on QueryType {
|
||||
_nearest: nearest(lat: $lat_0, lon: $lon_1, maxDistance: $maxDistance_2, maxResults: $maxResults_3, first: $maxResults_3, filterByModes: $filterByModes_4, filterByPlaceTypes: $filterByPlaceTypes_5) {
|
||||
...F4
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"lat_0": 60.190480099999995,
|
||||
"lon_1": 24.8275665,
|
||||
"maxDistance_2": 1000,
|
||||
"maxResults_3": 50,
|
||||
"numberOfDepartures_8": 2,
|
||||
"timeRange_7": 7200,
|
||||
"filterByModes_4": ["BUS"],
|
||||
"filterByPlaceTypes_5": ["DEPARTURE_ROW"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from infoscreen.hsl_fetcher import HSLFetcher
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Loads HSL timetables and save to json file.'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
fetcher = HSLFetcher()
|
||||
fetcher.fetch()
|
||||
@@ -11,173 +11,81 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="HSLDataModel",
|
||||
name='HSLDataModel',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("data", models.TextField(default="", editable=False)),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('data', models.TextField(default='', editable=False)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="InfoInstance",
|
||||
name='InfoInstance',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("duration", models.FloatField(default=15.0)),
|
||||
("item_id", models.PositiveIntegerField()),
|
||||
(
|
||||
"item_type",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="contenttypes.ContentType",
|
||||
),
|
||||
),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('duration', models.FloatField(default=15.0)),
|
||||
('item_id', models.PositiveIntegerField()),
|
||||
('item_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="InfoItem",
|
||||
name='InfoItem',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("expire_date", models.DateTimeField(blank=True, null=True)),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('expire_date', models.DateTimeField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Rotation",
|
||||
name='Rotation',
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ABBInfoItem",
|
||||
name='ABBInfoItem',
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
bases=('infoscreen.infoitem',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ExternalImageInfoItem",
|
||||
name='ExternalImageInfoItem',
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
("url", models.TextField()),
|
||||
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
|
||||
('url', models.TextField()),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
bases=('infoscreen.infoitem',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="HslInfoItem",
|
||||
name='HslInfoItem',
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
bases=('infoscreen.infoitem',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ImageInfoItem",
|
||||
name='ImageInfoItem',
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
("img", models.ImageField(upload_to="infoimages/")),
|
||||
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
|
||||
('img', models.ImageField(upload_to='infoimages/')),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
bases=('infoscreen.infoitem',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="SossoInfoItem",
|
||||
name='SossoInfoItem',
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
bases=('infoscreen.infoitem',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="infoinstance",
|
||||
name="rotation",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="instances",
|
||||
to="infoscreen.Rotation",
|
||||
),
|
||||
model_name='infoinstance',
|
||||
name='rotation',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='infoscreen.Rotation'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,25 +9,15 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("infoscreen", "0001_initial"),
|
||||
('infoscreen', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CoffeeInfoItem",
|
||||
name='CoffeeInfoItem',
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
bases=('infoscreen.infoitem',),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,63 +9,33 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("infoscreen", "0002_coffeeinfoitem"),
|
||||
('infoscreen', '0002_coffeeinfoitem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ApyInfoItem",
|
||||
name='ApyInfoItem',
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
bases=('infoscreen.infoitem',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="EventInfoItem",
|
||||
name='EventInfoItem',
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
bases=('infoscreen.infoitem',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ExternalWebsiteInfoItem",
|
||||
name='ExternalWebsiteInfoItem',
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
("url", models.TextField()),
|
||||
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
|
||||
('url', models.TextField()),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
bases=('infoscreen.infoitem',),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="CoffeeInfoItem",
|
||||
name='CoffeeInfoItem',
|
||||
),
|
||||
]
|
||||
|
||||
@@ -9,26 +9,16 @@ import django.db.models.deletion
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("infoscreen", "0003_auto_20170329_1857"),
|
||||
('infoscreen', '0003_auto_20170329_1857'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="VideoInfoItem",
|
||||
name='VideoInfoItem',
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
("video", models.FileField(upload_to="infovideos/")),
|
||||
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
|
||||
('video', models.FileField(upload_to='infovideos/')),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
bases=('infoscreen.infoitem',),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -8,18 +8,18 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("infoscreen", "0004_videoinfoitem"),
|
||||
('infoscreen', '0004_videoinfoitem'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="externalimageinfoitem",
|
||||
name="url",
|
||||
model_name='externalimageinfoitem',
|
||||
name='url',
|
||||
field=models.URLField(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="externalwebsiteinfoitem",
|
||||
name="url",
|
||||
model_name='externalwebsiteinfoitem',
|
||||
name='url',
|
||||
field=models.URLField(),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -8,14 +8,11 @@ from django.db import migrations
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("infoscreen", "0005_auto_20170913_1841"),
|
||||
('infoscreen', '0005_auto_20170913_1841'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="HSLDataModel",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="HslInfoItem",
|
||||
name='HSLDataModel',
|
||||
),
|
||||
]
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# Generated by Django 2.1.5 on 2019-03-26 12:49
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("infoscreen", "0006_delete_hsldatamodel"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="LunchItem",
|
||||
fields=[
|
||||
(
|
||||
"infoitem_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="infoscreen.InfoItem",
|
||||
),
|
||||
),
|
||||
],
|
||||
bases=("infoscreen.infoitem",),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +0,0 @@
|
||||
# Generated by Django 3.2.14 on 2022-08-01 19:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("infoscreen", "0007_lunchitem"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="infoinstance",
|
||||
name="id",
|
||||
field=models.AutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="infoitem",
|
||||
name="id",
|
||||
field=models.AutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="rotation",
|
||||
name="id",
|
||||
field=models.AutoField(primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
||||
@@ -16,7 +16,6 @@ class InfoItem(models.Model):
|
||||
class __meta__:
|
||||
abstract = True
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=255)
|
||||
# expire_date = None means never expiring item
|
||||
expire_date = models.DateTimeField(blank=True, null=True)
|
||||
@@ -24,14 +23,14 @@ class InfoItem(models.Model):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Get infoscreen template url."""
|
||||
raise NotImplementedError("inheriting classes must implement get_template_url")
|
||||
raise NotImplementedError(
|
||||
"inheriting classes must implement get_template_url")
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Get create infoscreen template url command."""
|
||||
raise NotImplementedError(
|
||||
"inheriting classes must implement get_create_template_url"
|
||||
)
|
||||
"inheriting classes must implement get_create_template_url")
|
||||
|
||||
@classmethod
|
||||
def create_from_dict(cls, d):
|
||||
@@ -43,13 +42,14 @@ class InfoItem(models.Model):
|
||||
def update_from_dict(self, d):
|
||||
"""Update model based on given dict."""
|
||||
try:
|
||||
expire_date = d.pop("expire_date", None)
|
||||
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S")
|
||||
expire_date = d.pop('expire_date', None)
|
||||
self.expire_date = datetime.strptime(
|
||||
expire_date, "%Y-%m-%d %H:%M:%S")
|
||||
except:
|
||||
pass
|
||||
|
||||
dmap = {
|
||||
"name": "name",
|
||||
'name': 'name',
|
||||
}
|
||||
for k, v in d.items():
|
||||
try:
|
||||
@@ -61,13 +61,13 @@ class InfoItem(models.Model):
|
||||
def get_dict(self):
|
||||
"""Convert django model to dict and return it."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"item_type": ContentType.objects.get_for_model(self).id,
|
||||
"template_url": self.get_template_url(),
|
||||
"display_name": self.display_name,
|
||||
"create_template_url": self.get_create_template_url(),
|
||||
"options": {},
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'item_type': ContentType.objects.get_for_model(self).id,
|
||||
'template_url': self.get_template_url(),
|
||||
'display_name': self.display_name,
|
||||
'create_template_url': self.get_create_template_url(),
|
||||
'options': {}
|
||||
}
|
||||
|
||||
def delete(self):
|
||||
@@ -75,8 +75,8 @@ class InfoItem(models.Model):
|
||||
# since generic foreign keys suck, delete info
|
||||
# items pointing here manually
|
||||
InfoInstance.objects.filter(
|
||||
item_id=self.id, item_type=ContentType.objects.get_for_model(self)
|
||||
).delete()
|
||||
item_id=self.id,
|
||||
item_type=ContentType.objects.get_for_model(self)).delete()
|
||||
super().delete()
|
||||
|
||||
@classmethod
|
||||
@@ -98,12 +98,12 @@ class ABBInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return ABB infoitem template url."""
|
||||
return "/static/infoscreen/html/abb.html"
|
||||
return "/static/html/abb.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create ABB infoitem template url command."""
|
||||
return "/static/infoscreen/html/abb_create.html"
|
||||
return "/static/html/abb_create.html"
|
||||
|
||||
|
||||
class ApyInfoItem(InfoItem):
|
||||
@@ -113,12 +113,12 @@ class ApyInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return APY infoitem template url."""
|
||||
return "/static/infoscreen/html/apy.html"
|
||||
return "/static/html/apy.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create APY infoitem template url command."""
|
||||
return "/static/infoscreen/html/apy_create.html"
|
||||
return "/static/html/apy_create.html"
|
||||
|
||||
|
||||
class ExternalWebsiteInfoItem(InfoItem):
|
||||
@@ -129,17 +129,17 @@ class ExternalWebsiteInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return external website infoitem template url."""
|
||||
return "/static/infoscreen/html/external_website.html?url={}".format(self.name)
|
||||
return "/static/html/external_website.html?url={}".format(self.name)
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create external website infoitem template url command."""
|
||||
return "/static/infoscreen/html/external_website_create.html"
|
||||
return "/static/html/external_website_create.html"
|
||||
|
||||
def get_dict(self):
|
||||
"""Convert django model to dict and return it."""
|
||||
d = super().get_dict()
|
||||
d["options"] = {"url": self.url}
|
||||
d["options"] = {'url': self.url}
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
@@ -152,22 +152,23 @@ class ExternalWebsiteInfoItem(InfoItem):
|
||||
def get_list(self):
|
||||
"""Return list containing infoitem data."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"url": self.url,
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'url': self.url,
|
||||
}
|
||||
|
||||
def update_from_dict(self, d):
|
||||
"""Update model based on given dict."""
|
||||
try:
|
||||
expire_date = d.pop("expire_date", None)
|
||||
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S")
|
||||
expire_date = d.pop('expire_date', None)
|
||||
self.expire_date = datetime.strptime(
|
||||
expire_date, "%Y-%m-%d %H:%M:%S")
|
||||
except:
|
||||
pass
|
||||
|
||||
dmap = {
|
||||
"name": "name",
|
||||
"url": "url",
|
||||
'name': 'name',
|
||||
'url': 'url',
|
||||
}
|
||||
for k, v in d.items():
|
||||
try:
|
||||
@@ -184,25 +185,12 @@ class SossoInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return Sosso infoitem template url."""
|
||||
return "/static/infoscreen/html/sosso.html"
|
||||
return "/static/html/sosso.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create Sosso infoitem template url command."""
|
||||
return "/static/infoscreen/html/sosso_create.html"
|
||||
|
||||
|
||||
class LunchItem(InfoItem):
|
||||
"""Class for Lunch Infoscreen item."""
|
||||
|
||||
display_name = _("Today's lunch")
|
||||
|
||||
def get_template_url(self):
|
||||
return "/static/infoscreen/html/lunch.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
return "/static/infoscreen/html/lunch_create.html"
|
||||
return "/static/html/sosso_create.html"
|
||||
|
||||
|
||||
class EventInfoItem(InfoItem):
|
||||
@@ -212,12 +200,12 @@ class EventInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return Event infoitem template url."""
|
||||
return "/static/infoscreen/html/events.html"
|
||||
return "/static/html/events.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create Event infoitem template url command."""
|
||||
return "/static/infoscreen/html/events_create.html"
|
||||
return "/static/html/events_create.html"
|
||||
|
||||
|
||||
class ImageInfoItem(InfoItem):
|
||||
@@ -230,42 +218,57 @@ class ImageInfoItem(InfoItem):
|
||||
"""Return Image infoitem template url."""
|
||||
# get param to avoid angular from optimizing same template
|
||||
# with different options
|
||||
return "/static/infoscreen/html/generic_image.html?img={}".format(self.name)
|
||||
return "/static/html/generic_image.html?img={}".format(self.name)
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create Image infoitem template url command."""
|
||||
return "/static/infoscreen/html/generic_image_create.html"
|
||||
return "/static/html/generic_image_create.html"
|
||||
|
||||
def get_dict(self):
|
||||
"""Convert django model to dict and return it."""
|
||||
d = super().get_dict()
|
||||
d["options"] = {"img": self.img.url}
|
||||
d["options"] = {'img': self.img.url}
|
||||
return d
|
||||
|
||||
|
||||
class VideoInfoItem(InfoItem):
|
||||
"""Class for Video Infoscreen item."""
|
||||
|
||||
display_name = "Video"
|
||||
display_name = ("Video")
|
||||
video = models.FileField(upload_to="infovideos/")
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return Video infoitem template url."""
|
||||
return "/static/infoscreen/html/generic_video.html?video={}".format(self.name)
|
||||
return "/static/html/generic_video.html?video={}".format(self.name)
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create Video infoitem template url command."""
|
||||
return "/static/infoscreen/html/generic_video_create.html"
|
||||
return "/static/html/generic_video_create.html"
|
||||
|
||||
def get_dict(self):
|
||||
"""Convert django model to dict and return it."""
|
||||
d = super().get_dict()
|
||||
d["options"] = {"video": self.video.url}
|
||||
d["options"] = {'video': self.video.url}
|
||||
return d
|
||||
|
||||
|
||||
class HslInfoItem(InfoItem):
|
||||
"""Class for HSL Infoscreen item."""
|
||||
|
||||
display_name = _("HSL timetables")
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return HSL infoitem template url."""
|
||||
return "/static/html/hsl.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create HSL infoitem template url command."""
|
||||
return "/static/html/hsl_create.html"
|
||||
|
||||
|
||||
class ExternalImageInfoItem(InfoItem):
|
||||
"""Class for External Image Infoscreen item."""
|
||||
|
||||
@@ -274,17 +277,17 @@ class ExternalImageInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return External Image infoitem template url."""
|
||||
return "/static/infoscreen/html/generic_image.html?img={}".format(self.name)
|
||||
return "/static/html/generic_image.html?img={}".format(self.name)
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create External Image infoitem template url command."""
|
||||
return "/static/infoscreen/html/generic_external_image_create.html"
|
||||
return "/static/html/generic_external_image_create.html"
|
||||
|
||||
def get_dict(self):
|
||||
"""Convert django model to dict and return it."""
|
||||
d = super().get_dict()
|
||||
d["options"] = {"img": self.url}
|
||||
d["options"] = {'img': self.url}
|
||||
return d
|
||||
|
||||
@classmethod
|
||||
@@ -297,14 +300,15 @@ class ExternalImageInfoItem(InfoItem):
|
||||
def update_from_dict(self, d):
|
||||
"""Update model based on given dict."""
|
||||
try:
|
||||
expire_date = d.pop("expire_date", None)
|
||||
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S")
|
||||
expire_date = d.pop('expire_date', None)
|
||||
self.expire_date = datetime.strptime(
|
||||
expire_date, "%Y-%m-%d %H:%M:%S")
|
||||
except:
|
||||
pass
|
||||
|
||||
dmap = {
|
||||
"name": "name",
|
||||
"url": "url",
|
||||
'name': 'name',
|
||||
'url': 'url',
|
||||
}
|
||||
for k, v in d.items():
|
||||
try:
|
||||
@@ -317,15 +321,12 @@ class ExternalImageInfoItem(InfoItem):
|
||||
class InfoInstance(models.Model):
|
||||
"""Class for Info instance in Infoscreen."""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
rotation = models.ForeignKey(
|
||||
"Rotation", related_name="instances", on_delete=models.CASCADE
|
||||
)
|
||||
rotation = models.ForeignKey('Rotation', related_name='instances')
|
||||
duration = models.FloatField(default=15.0) # seconds
|
||||
# generic relation to some kind of InfoItem
|
||||
item_id = models.PositiveIntegerField()
|
||||
item_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
item = GenericForeignKey("item_type", "item_id")
|
||||
item = GenericForeignKey('item_type', 'item_id')
|
||||
|
||||
@classmethod
|
||||
def create_from_dict(cls, d):
|
||||
@@ -338,27 +339,31 @@ class InfoInstance(models.Model):
|
||||
except:
|
||||
raise RuntimeError("invalid parameters supplied supplied")
|
||||
try:
|
||||
return cls.objects.create(rotation=rotation, item=item, duration=duration)
|
||||
return cls.objects.create(
|
||||
rotation=rotation,
|
||||
item=item,
|
||||
duration=duration
|
||||
)
|
||||
except:
|
||||
raise RuntimeError("error while adding instance to db")
|
||||
|
||||
def get_dict(self):
|
||||
"""Convert django model to dict and return it."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"item": self.item.get_dict(),
|
||||
"duration": self.duration,
|
||||
'id': self.id,
|
||||
'item': self.item.get_dict(),
|
||||
'duration': self.duration,
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
"""Return model name."""
|
||||
return "{}: {} ({}s)".format(self.rotation.name, self.item.name, self.duration)
|
||||
return "{}: {} ({}s)".format(
|
||||
self.rotation.name, self.item.name, self.duration)
|
||||
|
||||
|
||||
class Rotation(models.Model):
|
||||
"""Class for rotation model."""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = models.CharField(max_length=255)
|
||||
|
||||
def get_dict(self):
|
||||
@@ -367,20 +372,21 @@ class Rotation(models.Model):
|
||||
# to avoid excluding items with no expire_date)
|
||||
now = timezone.now()
|
||||
instances = self.instances.all()
|
||||
filtered = filter(lambda i: (i.item.expire_date or now) >= now, list(instances))
|
||||
filtered = filter(lambda i: (i.item.expire_date or now) >= now,
|
||||
list(instances))
|
||||
instance_list = list(map(lambda i: i.get_dict(), filtered))
|
||||
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"instances": instance_list,
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
'instances': instance_list,
|
||||
}
|
||||
|
||||
def get_list(self):
|
||||
"""Return list containing infoitem data."""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
'id': self.id,
|
||||
'name': self.name,
|
||||
}
|
||||
|
||||
def __str__(self):
|
||||
@@ -391,7 +397,6 @@ class Rotation(models.Model):
|
||||
class ImageUploadForm(forms.Form):
|
||||
"""Form used to handle imageuploads to infoscreen app."""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = forms.CharField()
|
||||
image = forms.ImageField()
|
||||
|
||||
@@ -399,6 +404,5 @@ class ImageUploadForm(forms.Form):
|
||||
class UploadFileForm(forms.Form):
|
||||
"""Form used for uploading file."""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
name = forms.CharField()
|
||||
video = forms.FileField()
|
||||
|
||||
@@ -2,10 +2,7 @@ body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
#header:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: both;
|
||||
#header {
|
||||
}
|
||||
|
||||
#header-logo {
|
||||
@@ -12,7 +12,7 @@ body {
|
||||
.event {
|
||||
font-size: 100px;
|
||||
font-weight: bold;
|
||||
margin-left: 20px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
.event-col{
|
||||
padding-top:1vh;
|
||||
@@ -21,7 +21,7 @@ body {
|
||||
|
||||
.header-row{
|
||||
margin: 30px;
|
||||
margin-left: 20px;
|
||||
margin-left: 20px;
|
||||
font-size: 130px;
|
||||
padding-bottom:20px;
|
||||
color:#24a05f;
|
||||
@@ -0,0 +1,74 @@
|
||||
table {
|
||||
font-size: 4vh;
|
||||
font-family: 'Droid Sans Mono', monospace;
|
||||
}
|
||||
.red {
|
||||
color: red;
|
||||
-webkit-animation-name: blinker;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
|
||||
-moz-animation-name: blinker;
|
||||
-moz-animation-duration: 2s;
|
||||
-moz-animation-timing-function: linear;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
|
||||
animation-name: blinker;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
.black {
|
||||
color: black;
|
||||
}
|
||||
@-moz-keyframes blinker {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0.1; }
|
||||
100% { opacity: 1.0; }
|
||||
}
|
||||
|
||||
@-webkit-keyframes blinker {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0.1; }
|
||||
100% { opacity: 1.0; }
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0.1; }
|
||||
100% { opacity: 1.0; }
|
||||
}
|
||||
thead{
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.header-row{
|
||||
background: #f0f0f0;
|
||||
font-size: 7vh;
|
||||
font-family: 'Droid Sans Mono', monospace;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100vw;
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
|
||||
.container .table {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.repeat-item.ng-leave {
|
||||
|
||||
}
|
||||
|
||||
.repeat-item.ng-leave.ng-leave-active {
|
||||
opacity: 0;
|
||||
font-size: 0vh;
|
||||
}
|
||||
|
||||
.repeat-item.ng-leave{
|
||||
opacity: 1;
|
||||
font-size: 5vh;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
#infocontent {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
z-index: -1; /* Ensure div tag stays behind content; -999 might work, too. */
|
||||
}
|
||||
@@ -5,7 +5,6 @@ html {
|
||||
body {
|
||||
padding: 1.5rem;
|
||||
margin: 0.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
tbody {
|
||||
@@ -50,18 +49,7 @@ td {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#header {
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.rotation-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
.article-thumb-col {
|
||||
max-height: 200px;
|
||||
text-align: left;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.article-title-col {
|
||||
@@ -31,10 +31,10 @@
|
||||
max-height 200px;
|
||||
}
|
||||
|
||||
#sossoimage {
|
||||
#sossoimage {
|
||||
height:300px;
|
||||
position: relative;
|
||||
left: 0px;
|
||||
position: relative;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<link rel="stylesheet" href="/static/infoscreen/css/abb.css">
|
||||
<link rel="stylesheet" href="/static/css/abb.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
|
||||
<div ng-controller="ABBController">
|
||||
<!-- Only show the job listing if there are any jobs, i.e, the jobs list is non-empty -->
|
||||
<div id="header" class="row" ng-if="jobs.length > 0">
|
||||
<div id="header-logo">
|
||||
<img src="/static/infoscreen/img/ABB_logo.png">
|
||||
<img src="/static/img/ABB_logo.png">
|
||||
</div>
|
||||
<div id="header-title">
|
||||
TYÖPAIKAT
|
||||
@@ -28,6 +28,6 @@
|
||||
|
||||
<!-- If there are no jobs, show a static image -->
|
||||
<div class="row" ng-if="jobs.length == 0">
|
||||
<img src="/static/infoscreen/img/ABB_uralle.png" style="position:absolute;left:0;top:0;width:100vw;">
|
||||
<img src="/static/img/ABB_uralle.png" style="position:absolute;left:0;top:0;width:100vw;">
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input class="form-control" type="text" ng-model="item.name"></input>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
</div>
|
||||
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
<link rel="stylesheet" href="/static/infoscreen/css/apy.css">
|
||||
<link rel="stylesheet" href="/static/css/apy.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
|
||||
<div id="bg">
|
||||
<div class="container " ng-controller="ApyController">
|
||||
@@ -1,10 +1,10 @@
|
||||
<div ng-controller="infoadmin_apyitem_create" style="margin-top:20px;">
|
||||
<div>
|
||||
Create new ÄPY statistics item
|
||||
create apyitem
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input class="form-control" type="text" ng-model="item.name"></input>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
</div>
|
||||
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
<link rel="stylesheet" href="/static/css/coffee.css">
|
||||
<iframe src="https://host2.kilta.aalto.fi/kahvi/cups" allowfullscreen=true sandbox="allow-scripts allow-pointer-lock allow-same-origin">
|
||||
<p>Your browser does not support iframes.</p>
|
||||
</iframe>
|
||||
@@ -1,5 +1,5 @@
|
||||
<link rel="stylesheet" href="/static/infoscreen/css/events.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/css/events.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
|
||||
<div class="container" ng-app="myApp" ng-controller="EventController">
|
||||
<div class="header-row row">
|
||||
<div class="col-sm-6">Tapahtuma</div>
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" class="form-control" ng-model="item.name"></input>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
</div>
|
||||
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
<link rel="stylesheet" href="/static/infoscreen/css/external_website.css">
|
||||
<link rel="stylesheet" href="/static/css/external_website.css">
|
||||
<iframe ng-src="{{ url | trusted_url }}" allowfullscreen=true sandbox="allow-scripts allow-pointer-lock allow-same-origin">
|
||||
<p>Your browser does not support iframes.</p>
|
||||
</iframe>
|
||||
@@ -1,14 +1,14 @@
|
||||
<div ng-controller="infoadmin_websiteitem_create" style="margin-top:20px;">
|
||||
<div>
|
||||
Create new item to show external website. For example "https://ka.dy.fi".
|
||||
Create new item to show external website. For example "ka.dy.fi".
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" class="form-control" ng-model="item.name"></input>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Url:</label>
|
||||
<input type="text" class="form-control" ng-model="item.url"></input>
|
||||
<input type="text" ng-model="item.url"></input>
|
||||
</div>
|
||||
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
|
||||
</div>
|
||||
@@ -4,11 +4,11 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" class="form-control" ng-model="item.name"></input>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Url:</label>
|
||||
<input type="text" class="form-control" ng-model="item.url"></input>
|
||||
<input type="text" ng-model="item.url"></input>
|
||||
</div>
|
||||
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
|
||||
</div>
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" class="form-control" ng-model="imagename"></input>
|
||||
<input type="text" ng-model="imagename"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="file" ngf-select ng-model="img" name="file" required>
|
||||
@@ -1,4 +1,4 @@
|
||||
<link rel="stylesheet" href="/static/infoscreen/css/video.css">
|
||||
<link rel="stylesheet" href="/static/css/video.css">
|
||||
|
||||
|
||||
<div class="fullscreen-bg">
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" class="form-control" ng-model="name"></input>
|
||||
<input type="text" ng-model="name"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="file" ngf-select ng-model="video" name="file" required>
|
||||
@@ -0,0 +1,41 @@
|
||||
<link rel="stylesheet" href="/static/css/hsl.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/locale/fi.js"></script>
|
||||
<div class="container" ng-app="myApp" ng-controller="timetableCtrl">
|
||||
<div class="header-row row">
|
||||
<div class="col-sm-2"><p>{{clock | date:'HH:mm'}}</p></div>
|
||||
<div class="col-sm-8">HSL-Aikataulut</div>
|
||||
<div class="col-sm-2 time"></div>
|
||||
</div>
|
||||
<h1 style="font-size: 10vh; text-align: center" ng-if="error">
|
||||
{{error}}
|
||||
</h1>
|
||||
<table ng-if="!error" class="table table-striped row">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Aika
|
||||
</th>
|
||||
<th>
|
||||
Linja
|
||||
</th>
|
||||
<th>
|
||||
Pysäkki
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['timestamp'] | limitTo: 11">
|
||||
<td style="min-width: 300px">
|
||||
{{x.timestamp}}
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{x.route}}</strong>, {{x.headsign}}
|
||||
</td>
|
||||
<td>
|
||||
{{x.stop}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div ng-controller="infoadmin_hslitem_create" style="margin-top:20px;">
|
||||
<div>
|
||||
Create new item to show hsl ttimetables. Name is used only as identifier
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
</div>
|
||||
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
<link rel="stylesheet" href="/static/css/sosso.css">
|
||||
<div ng-controller="SossoController">
|
||||
<div id="header">
|
||||
<img id="header-image" src="/static/img/sossoheader.png" >
|
||||
</div>
|
||||
<div id="container">
|
||||
<div class="article-row row" ng-repeat="post in data.posts">
|
||||
<div class="article-thumb-col col-md-6">
|
||||
<img class="thumbnail" ng-src="{{ post.thumbnail_images['mh-edition-lite-medium'].url }}">
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="col-md-6">
|
||||
<div class="article-title-col">
|
||||
{{ post.title }}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" class="form-control" ng-model="item.name"></input>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
</div>
|
||||
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
|
||||
</div>
|
||||
@@ -0,0 +1 @@
|
||||
<h1>testi2</h1>
|
||||
@@ -0,0 +1 @@
|
||||
<h1>testi3</h1>
|
||||
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 96 KiB |
@@ -1,53 +0,0 @@
|
||||
#header {
|
||||
height: 30%;
|
||||
width: 100%;
|
||||
background-color:#7c1330;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#header-image {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.article-row {
|
||||
min-height: 20vh;
|
||||
margin: 10px 10px 10px 10px;
|
||||
}
|
||||
|
||||
.article-thumb-col {
|
||||
max-height: 200px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.article-title-col {
|
||||
font-size: 3vw;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
max-width: 355px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
#sossoimage {
|
||||
height: 300px;
|
||||
position: relative;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.stretch {
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
#post {
|
||||
height: 540px;
|
||||
border:2px solid black;
|
||||
}
|
||||
|
||||
#container {
|
||||
max-height: 70%;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/static/infoscreen/css/lunch.css">
|
||||
<div ng-controller="LunchController">
|
||||
<div id="container">
|
||||
<div class="restaurant row" ng-repeat="restaurant in data">
|
||||
<div class="lunch-option" ng-repeat="l in lunch">
|
||||
<h3 ng-bind-html="l.title | unsafe"></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,10 +0,0 @@
|
||||
<div ng-controller="infoadmin_lunchitem_create" style="margin-top:20px;">
|
||||
<div>
|
||||
Create new item to show restaurants. Name is used only as identifier
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" class="form-control" ng-model="item.name"></input>
|
||||
</div>
|
||||
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
|
||||
</div>
|
||||
@@ -1,17 +0,0 @@
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="/static/infoscreen/css/sosso.css">
|
||||
<div ng-controller="SossoController">
|
||||
<div id="header">
|
||||
<img id="header-image" src="/static/infoscreen/img/sossoheader.png" >
|
||||
</div>
|
||||
<div id="container">
|
||||
<div class="article-row row" ng-repeat="post in data.posts">
|
||||
<div class="article-thumb-col col-md-4">
|
||||
<img class="thumbnail" ng-src="{{ post.thumbnail_images['mh-edition-lite-medium'].url }}">
|
||||
</div>
|
||||
<div class="col-md-8 article-title-col">
|
||||
<h1 ng-bind-html="post.title | unsafe"></h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -182,8 +182,8 @@ var simple_controllers = [
|
||||
"external_image",
|
||||
"abbitem",
|
||||
"sossoitem",
|
||||
"lunchitem",
|
||||
"eventitem",
|
||||
"hslitem",
|
||||
"websiteitem",
|
||||
"apyitem",
|
||||
];
|
||||
@@ -46,18 +46,10 @@ app.filter('trusted_url', ['$sce', function ($sce) {
|
||||
};
|
||||
}]);
|
||||
|
||||
//Used for special characters in Sosso. This may open up XSS, so we need to trust that sosso.fi doesn't get compromised...
|
||||
app.filter('unsafe', function($sce) {
|
||||
return function(val) {
|
||||
return $sce.trustAsHtml(val);
|
||||
};
|
||||
});
|
||||
|
||||
app.controller('ABBController', function($scope, $http){
|
||||
$scope.jobs = [];
|
||||
var min_date = moment().subtract(30,'days').format("YYYY-MM-DD%20HH:mm:ss");
|
||||
// TODO: FIX, we try to get rid of php, not depend on it!
|
||||
var url = "https://old.sahkoinsinoorikilta.fi/api/news.php";
|
||||
var url = "https://sahkoinsinoorikilta.fi/api/news.php";
|
||||
var params = "?type=11&lang=fi&title_search=ABB&min_date="+min_date
|
||||
$http.get(url+params).then(function(response){
|
||||
$scope.jobs = _.filter(response.data, function(job){
|
||||
@@ -83,21 +75,6 @@ app.controller('SossoController', function($scope, $http) {
|
||||
})
|
||||
});
|
||||
|
||||
app.controller('LunchController', function ($scope, $http) {
|
||||
$scope.data = [];
|
||||
var restaurants = [42];
|
||||
var restaurant_names = ["TUAS"]
|
||||
var cur_date = new Date().toISOString().split("T")[0]
|
||||
$http.get("https://kitchen.kanttiinit.fi/menus?restaurants=" + restaurants.join(",") + "&days=" + cur_date).then(function (response) {
|
||||
$scope.data = restaurant_names.map(function(n, idx) {
|
||||
return {
|
||||
name: n,
|
||||
lunch: response[idx][cur_date],
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
app.controller('ApyController', function($scope, $http) {
|
||||
$scope.items = [];
|
||||
$http.get("/infoscreen/apyjson").then(function(response)
|
||||
@@ -135,3 +112,35 @@ app.filter('unixTimeToDifference', function() {
|
||||
return res;
|
||||
}
|
||||
})
|
||||
|
||||
app.controller('timetableCtrl',
|
||||
function($scope, $http, $interval) {
|
||||
function load() {
|
||||
$http.get('/infoscreen/hsl_data')
|
||||
.then(function(data, status, headers, config) { //eslint-disable-line no-unused-vars
|
||||
$scope.stoptimes = data.data;
|
||||
$scope.error = data.data.error || null;
|
||||
});
|
||||
$http.get('/infoscreen/hsl_data/settings')
|
||||
.then(function(data, status, headers, config) { //eslint-disable-line no-unused-vars
|
||||
$scope.departureThreshold = data.data['departure_threshold'];
|
||||
$scope.hurryThreshold = data.data['hurry_threshold'];
|
||||
});
|
||||
}
|
||||
|
||||
function update_clock() {
|
||||
$scope.clock = Date.now();
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
$interval.cancel(load_interval);
|
||||
$interval.cancel(clock_interval);
|
||||
});
|
||||
|
||||
var load_interval = $interval(load, 5000);
|
||||
var clock_interval = $interval(update_clock, 1000);
|
||||
|
||||
update_clock();
|
||||
load();
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,154 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load staticfiles %}
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="infoAdmin">
|
||||
<head>
|
||||
<meta charset="utf-8" name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1" />
|
||||
<title>Infoscreen admin</title>
|
||||
<script src="{% static "js/lib/angular.js" %}"></script>
|
||||
<script src="{% static "js/lib/ng-file-upload-bower-12.2.11/ng-file-upload-all.js" %}"></script>
|
||||
<script src="{% static "js/lib/jquery-3.1.0.min.js" %}"></script>
|
||||
<script src="{% static "js/lib/bootstrap.min.js" %}"></script>
|
||||
<script src="{% static "js/lib/underscore-min.js" %}"></script>
|
||||
<script src="{% static "js/infoadmin_controllers.js" %}"></script>
|
||||
|
||||
<link rel="stylesheet" href="{% static "css/lib/bootstrap.min.css" %}"></link>
|
||||
<link rel="stylesheet" href="{% static "css/base.css" %}"></link>
|
||||
<link rel="stylesheet" href="{% static "css/infoscreen_admin.css" %}"></link>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header" class="row">
|
||||
<div class="logout-button">
|
||||
<form action="/logout" method="post"> {% csrf_token %}
|
||||
<input type="Submit" value="{% trans "Log out" %}" name="Logout" class="btn btn-danger"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" ng-controller="infoadmin_ctrl">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>{% trans "Infoscreen Admin Pane" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="active"><a data-toggle="tab" href="#slides" role="tab">{% trans "Manage Slides" %}</a></li>
|
||||
<li class="dropdown">
|
||||
<a data-toggle="dropdown" href="#">{% trans "Manage Rotations" %}<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="r in rotations"><a href="#rotations" ng-click="selectRotation(r.id)" data-toggle="tab">{$ r.name $}</a></li>
|
||||
<li class="divider">
|
||||
<li class="nav-item"><a data-toggle="tab" href="#deleterot" role="tab">{% trans "Create/Delete" %}</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
<div class="tab-content row">
|
||||
<div id="slides" class="tab-pane active">
|
||||
<div ng-controller="infoadmin_ctrl">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<h2>{% trans "Create new item" %}</h2>
|
||||
<div>{% trans "Create a new item by type" %}</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<td>{% trans "Item type" %}</td>
|
||||
<td>
|
||||
<select class="form-control form-control-sm" ng-model="createtype", ng-options="t.name for t in infotypes | orderBy: 'name'">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div ng-include="createtype.create_template_url"></div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<h2>{% trans "Info items" %}</h2>
|
||||
<div>{% trans "Infoitems available for rotations" %}</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Item" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="i in infoitems | orderBy:['display_name','name']">
|
||||
<td>{$ i.name $}</td>
|
||||
<td>{$ i.display_name $}</td>
|
||||
<td><input type="button" class="btn btn-danger" ng-click="deleteItem(i.item_type, i.id);" value="{% trans "Delete" %}"></input></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rotations" class="tab-pane" ng-controller="infoadmin_ctrl">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<h2>{% trans "Info items" %}</h2>
|
||||
<div>{% trans "Infoitems available for rotations" %}</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Item" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Set duration" %}</th>
|
||||
<th>{% trans "Add to rotation" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="i in infoitems | orderBy:['display_name','name']">
|
||||
<td>{$ i.name $}</td>
|
||||
<td>{$ i.display_name $}</td>
|
||||
<td><input type="number" min="1" max="60" class="form-control" ng-model="i.duration"></input></td>
|
||||
<td><input type="button" class="btn btn-success" ng-click="createInstance(selected_rot.id, i.id, i.item_type, i.duration);" value="{% trans "Add" %}"></input></td>
|
||||
<td><input type="button" class="btn btn-danger" ng-click="deleteItem(i.item_type, i.id);" value="{% trans "Delete" %}"></input></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<h2>{% trans "Rotation" %}: {$ selected_rot.name $}<a href="/infoscreen/{$ selected_rot.id $}"><input type="button" class="btn btn-primary pull-right" value="{% trans "Preview" %}"></a></h2>
|
||||
<div>{% trans "Instances in currently selected rotation" %}:</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Instance" %}</th>
|
||||
<th>{% trans "Duration" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="i in selected_rot.instances">
|
||||
<td>{$ i.item.name $}</td><td>{$ i.duration $} s</td>
|
||||
<td><input type="button" ng-click="deleteInstance(i.id);" value="{% trans "Delete" %}" class="btn btn-danger"></input></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="deleterot" class="tab-pane">
|
||||
<div class="col-xs-12 " ng-controller="infoadmin_ctrl">
|
||||
<h2>{% trans "Rotations" %}</h2>
|
||||
<div>
|
||||
{% trans "Select rotation to edit" %}:
|
||||
</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Rotation" %}</th>
|
||||
<th>{% trans "id" %}</th>
|
||||
<th>{% trans "Preview" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="r in rotations">
|
||||
<td>{$ r.name $}</td>
|
||||
<td>{$ r.id $}</td>
|
||||
<td><a href="/infoscreen/{$ r.id $}"><input type="button" class="btn btn-primary" value="{% trans "Preview" %}"></a></td>
|
||||
<td><input type="button" class="btn btn-danger" ng-click="deleteRotation(r.id)" value="{% trans "Delete" %}"></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input type="text" class="form-control" ng-model="r.name" placeholder="{% trans "Name" %}"></input></td>
|
||||
<td><input type="button" class="btn btn-success" ng-click="createRotation(r.name)" value="{% trans "Create new" %}"></input></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 100px;">
|
||||
{% include "footer.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,29 @@
|
||||
{% load static %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="infoApp">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>Infoscreen</title>
|
||||
<script src="{% static "js/lib/moment.js" %}"></script>
|
||||
<script src="{% static "js/lib/angular.js" %}"></script>
|
||||
<script src="{% static "js/lib/angular-route.js" %}"></script>
|
||||
<script src="{% static "js/lib/angular-animate.js" %}"></script>
|
||||
<script src="{% static "js/lib/jquery-3.1.0.min.js" %}"></script>
|
||||
<script src="{% static "js/lib/bootstrap.min.js" %}"></script>
|
||||
<script src="{% static "js/lib/underscore-min.js" %}"></script>
|
||||
<script src="{% static "js/infoscreen_controllers.js" %}"></script>
|
||||
|
||||
<link rel="stylesheet" href="{% static "css/lib/bootstrap.min.css" %}"></link>
|
||||
<link rel="stylesheet" href="{% static "css/infoscreen.css" %}"></link>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container ng-scope" ng-controller="infoscreen_main" ng-init="init({{ rotation }})">
|
||||
<div ng-animate-swap="index" class="cell swap-animation">
|
||||
<div id="infocontent" ng-include="active.template" onload="active.onload()"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -35,6 +35,6 @@ class InfoscreenTestCase(TestCase):
|
||||
That would mean that something meaningful has been included
|
||||
in the response.
|
||||
"""
|
||||
resp = self.c.get("/infoscreen/items")
|
||||
resp = self.c.get('/infoscreen/items')
|
||||
content = resp.json()
|
||||
self.assertTrue(len(content) > 0)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""File containing infoscreen urls."""
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.conf import settings
|
||||
|
||||
from infoscreen.views import index
|
||||
from infoscreen.views import admin
|
||||
@@ -18,40 +17,40 @@ from infoscreen.views import create_image_item
|
||||
from infoscreen.views import create_video_item
|
||||
from infoscreen.views import createABBItem
|
||||
from infoscreen.views import createSossoItem
|
||||
from infoscreen.views import createLunchItem
|
||||
from infoscreen.views import createHslItem
|
||||
from infoscreen.views import createEventItem
|
||||
from infoscreen.views import createExternalWebsiteItem
|
||||
from infoscreen.views import create_rotation
|
||||
from infoscreen.views import delete_rotation
|
||||
from infoscreen.views import CurrentHSLView
|
||||
from infoscreen.views import createApyItem
|
||||
from infoscreen.views import hsl_timetable_settings
|
||||
from infoscreen.views import get_apy_json
|
||||
|
||||
urlpatterns = [
|
||||
url(r"^$", default),
|
||||
url(r"^admin$", admin),
|
||||
url(r"^(?P<idx>\d+)$", index),
|
||||
url(r"^items$", info_items),
|
||||
url(r"^rotation/(?P<idx>\d+)$", rotation),
|
||||
url(r"^rotations$", rotations),
|
||||
url(r"^instance$", createInstance),
|
||||
url(r"^instance/(?P<idx>\d+)$", deleteInstance),
|
||||
url(r"^types$", info_types),
|
||||
url(r"^delete_item/(?P<type_id>\d+)/(?P<idx>\d+)$", delete_info_item),
|
||||
url(r"^create_external_image$", createExternalImageInfoItem),
|
||||
url(r"^create_image$", create_image_item),
|
||||
url(r"^create_video$", create_video_item),
|
||||
url(r"^create_abbitem$", createABBItem),
|
||||
url(r"^create_sossoitem$", createSossoItem),
|
||||
url(r"^create_lunchitem$", createLunchItem),
|
||||
url(r"^create_eventitem$", createEventItem),
|
||||
url(r"^create_apyitem$", createApyItem),
|
||||
url(r"^create_websiteitem$", createExternalWebsiteItem),
|
||||
url(r"^create_rotation$", create_rotation),
|
||||
url(r"^delete_rotation/(?P<id>\d+)$", delete_rotation),
|
||||
url(r"^apyjson", get_apy_json),
|
||||
url(r'^$', default),
|
||||
url(r'^admin$', admin),
|
||||
url(r'^(?P<idx>\d+)$', index),
|
||||
url(r'^items$', info_items),
|
||||
url(r'^rotation/(?P<idx>\d+)$', rotation),
|
||||
url(r'^rotations$', rotations),
|
||||
url(r'^instance$', createInstance),
|
||||
url(r'^instance/(?P<idx>\d+)$', deleteInstance),
|
||||
url(r'^types$', info_types),
|
||||
url(r'^delete_item/(?P<type_id>\d+)/(?P<idx>\d+)$', delete_info_item),
|
||||
url(r'^create_external_image$', createExternalImageInfoItem),
|
||||
url(r'^create_image$', create_image_item),
|
||||
url(r'^create_video$', create_video_item),
|
||||
url(r'^create_abbitem$', createABBItem),
|
||||
url(r'^create_sossoitem$', createSossoItem),
|
||||
url(r'^create_eventitem$', createEventItem),
|
||||
url(r'^create_hslitem$', createHslItem),
|
||||
url(r'^create_apyitem$', createApyItem),
|
||||
url(r'^create_websiteitem$', createExternalWebsiteItem),
|
||||
url(r'^create_rotation$', create_rotation),
|
||||
url(r'^delete_rotation/(?P<id>\d+)$', delete_rotation),
|
||||
url(r'^hsl_data$', CurrentHSLView),
|
||||
url(r'^hsl_data/settings$', hsl_timetable_settings),
|
||||
url(r'^apyjson', get_apy_json),
|
||||
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
@@ -6,7 +6,6 @@ from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth.decorators import permission_required, login_required
|
||||
from django.db import DatabaseError
|
||||
from infoscreen.models import UploadFileForm
|
||||
|
||||
import sikweb.settings as settings
|
||||
@@ -15,28 +14,21 @@ import logging
|
||||
import threading
|
||||
import requests
|
||||
|
||||
from infoscreen.models import (
|
||||
Rotation,
|
||||
InfoItem,
|
||||
InfoInstance,
|
||||
ABBInfoItem,
|
||||
ExternalImageInfoItem,
|
||||
ImageInfoItem,
|
||||
SossoInfoItem,
|
||||
LunchItem,
|
||||
EventInfoItem,
|
||||
ExternalWebsiteInfoItem,
|
||||
ImageUploadForm,
|
||||
ApyInfoItem,
|
||||
VideoInfoItem,
|
||||
)
|
||||
from infoscreen.models import Rotation, InfoItem, InfoInstance
|
||||
from infoscreen.models import (ABBInfoItem, ExternalImageInfoItem,
|
||||
ImageInfoItem, SossoInfoItem, HslInfoItem)
|
||||
from infoscreen.models import EventInfoItem
|
||||
from infoscreen.models import ExternalWebsiteInfoItem
|
||||
from infoscreen.models import ImageUploadForm
|
||||
from infoscreen.models import ApyInfoItem
|
||||
from infoscreen.models import VideoInfoItem
|
||||
|
||||
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("infoscreen.change_infoinstance", raise_exception=True)
|
||||
@login_required(login_url='/login')
|
||||
@permission_required('infoscreen.change_infoinstance', raise_exception=True)
|
||||
def admin(request, *args, **kwargs):
|
||||
"""Render infoscreen admin page."""
|
||||
return render(request, "infoscreen/infoscreen_admin.html", {})
|
||||
return render(request, 'infoscreen_admin.html', {})
|
||||
|
||||
|
||||
def create_item_generator(model):
|
||||
@@ -44,23 +36,20 @@ def create_item_generator(model):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["POST"])
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("infoscreen.add_infoinstance", raise_exception=True)
|
||||
@login_required(login_url='/login')
|
||||
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
|
||||
def create_item(request, *args, **kwargs):
|
||||
try:
|
||||
data = json.loads(request.body.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
except ValueError:
|
||||
return HttpResponseBadRequest(
|
||||
'{"status":"failure","error":"invalid json supplied"}'
|
||||
)
|
||||
'{"status":"failure","error":"invalid json supplied"}')
|
||||
try:
|
||||
model.create_from_dict(data)
|
||||
return HttpResponse('{"status":"success"}')
|
||||
except RuntimeError as e:
|
||||
return HttpResponseBadRequest(
|
||||
json.dumps({"status": "failure", "error": str(e)})
|
||||
)
|
||||
|
||||
json.dumps({"status": "failure", "error": str(e)}))
|
||||
return create_item
|
||||
|
||||
|
||||
@@ -69,8 +58,8 @@ def delete_item_generator(model):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["DELETE"])
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("infoscreen.delete_infoinstance", raise_exception=True)
|
||||
@login_required(login_url='/login')
|
||||
@permission_required('infoscreen.delete_infoinstance', raise_exception=True)
|
||||
def delete_item(request, *args, **kwargs):
|
||||
idx = kwargs.pop("idx", 0)
|
||||
try:
|
||||
@@ -82,18 +71,17 @@ def delete_item_generator(model):
|
||||
try:
|
||||
item.delete()
|
||||
return HttpResponse('{"status":"success"}')
|
||||
except DatabaseError:
|
||||
except:
|
||||
resp = HttpResponse('{"error" : "could not delete item"}')
|
||||
resp.status_code = 500
|
||||
return resp
|
||||
|
||||
return delete_item
|
||||
|
||||
|
||||
# due to model structure this is little complicated
|
||||
@ensure_csrf_cookie
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("infoscreen.delete_infoinstance", raise_exception=True)
|
||||
@login_required(login_url='/login')
|
||||
@permission_required('infoscreen.delete_infoinstance', raise_exception=True)
|
||||
@require_http_methods(["DELETE"])
|
||||
def delete_info_item(request, *args, **kwargs):
|
||||
"""Delete info item."""
|
||||
@@ -109,7 +97,7 @@ def delete_info_item(request, *args, **kwargs):
|
||||
try:
|
||||
item.delete()
|
||||
return HttpResponse('{"status":"success"}')
|
||||
except DatabaseError:
|
||||
except:
|
||||
resp = HttpResponse('{"error" : "could not delete item"}')
|
||||
resp.status_code = 500
|
||||
return resp
|
||||
@@ -117,65 +105,64 @@ def delete_info_item(request, *args, **kwargs):
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@ensure_csrf_cookie
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("infoscreen.add_infoinstance", raise_exception=True)
|
||||
@login_required(login_url='/login')
|
||||
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
|
||||
def create_image_item(request, *args, **kwargs):
|
||||
"""Create image Infoscreen item."""
|
||||
form = ImageUploadForm(request.POST, request.FILES)
|
||||
if not form.is_valid():
|
||||
return HttpResponseBadRequest(
|
||||
'{"status": "failure",' '"error": "invalid data supplied"}'
|
||||
)
|
||||
return HttpResponseBadRequest('{"status": "failure",'
|
||||
'"error": "invalid data supplied"}')
|
||||
|
||||
img = form.cleaned_data["image"]
|
||||
name = form.cleaned_data["name"]
|
||||
img = form.cleaned_data['image']
|
||||
name = form.cleaned_data['name']
|
||||
ImageInfoItem.objects.create(img=img, name=name)
|
||||
return HttpResponse('{"status":"success"}')
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@ensure_csrf_cookie
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("infoscreen.add_infoinstance", raise_exception=True)
|
||||
@login_required(login_url='/login')
|
||||
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
|
||||
def create_video_item(request, *args, **kwargs):
|
||||
"""Create video Infoscreen item."""
|
||||
form = UploadFileForm(request.POST, request.FILES)
|
||||
if not form.is_valid():
|
||||
return HttpResponseBadRequest(
|
||||
'{"status": "failure",' '"error": "invalid data supplied"}'
|
||||
)
|
||||
return HttpResponseBadRequest('{"status": "failure",'
|
||||
'"error": "invalid data supplied"}')
|
||||
|
||||
video = form.cleaned_data["video"]
|
||||
name = form.cleaned_data["name"]
|
||||
video = form.cleaned_data['video']
|
||||
name = form.cleaned_data['name']
|
||||
VideoInfoItem.objects.create(video=video, name=name)
|
||||
return HttpResponse('{"status": "success"}')
|
||||
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@ensure_csrf_cookie
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("infoscreen.add_rotation", raise_exception=True)
|
||||
@login_required(login_url='/login')
|
||||
@permission_required('infoscreen.add_rotation', raise_exception=True)
|
||||
def create_rotation(request, *args, **kwargs):
|
||||
"""Create rotation."""
|
||||
try:
|
||||
data = json.loads(request.body.decode("utf-8"))
|
||||
except json.JSONDecodeError:
|
||||
except:
|
||||
return HttpResponse('{"error": "bad post body!"}', status=400)
|
||||
|
||||
try:
|
||||
name = data["name"]
|
||||
Rotation.objects.create(name=name)
|
||||
resp = HttpResponse(status=200)
|
||||
except DatabaseError:
|
||||
resp = HttpResponse('{"error" : "could not create rotation!"}', status=400)
|
||||
except:
|
||||
resp = HttpResponse(
|
||||
'{"error" : "could not create rotation!"}', status=400)
|
||||
|
||||
return resp
|
||||
|
||||
|
||||
@require_http_methods(["DELETE"])
|
||||
@ensure_csrf_cookie
|
||||
@login_required(login_url="/admin/login")
|
||||
@permission_required("infoscreen.delete_rotation", raise_exception=True)
|
||||
@login_required(login_url='/login')
|
||||
@permission_required('infoscreen.delete_rotation', raise_exception=True)
|
||||
def delete_rotation(request, *args, **kwargs):
|
||||
"""Delete rotation."""
|
||||
id = kwargs.pop("id", 0)
|
||||
@@ -184,8 +171,9 @@ def delete_rotation(request, *args, **kwargs):
|
||||
try:
|
||||
Rotation.objects.filter(id=id).delete()
|
||||
resp = HttpResponse(status=200)
|
||||
except DatabaseError:
|
||||
resp = HttpResponse('{"error" : "could not delete rotation!"}', status=400)
|
||||
except:
|
||||
resp = HttpResponse(
|
||||
'{"error" : "could not delete rotation!"}', status=400)
|
||||
|
||||
return resp
|
||||
|
||||
@@ -194,7 +182,7 @@ createInstance = create_item_generator(InfoInstance)
|
||||
deleteInstance = delete_item_generator(InfoInstance)
|
||||
createABBItem = create_item_generator(ABBInfoItem)
|
||||
createSossoItem = create_item_generator(SossoInfoItem)
|
||||
createLunchItem = create_item_generator(LunchItem)
|
||||
createHslItem = create_item_generator(HslInfoItem)
|
||||
createExternalImageInfoItem = create_item_generator(ExternalImageInfoItem)
|
||||
createExternalWebsiteItem = create_item_generator(ExternalWebsiteInfoItem)
|
||||
createEventItem = create_item_generator(EventInfoItem)
|
||||
|
||||
@@ -2,20 +2,19 @@ from django.shortcuts import render
|
||||
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.conf import settings
|
||||
from django.db import DatabaseError
|
||||
|
||||
from infoscreen.models import Rotation, InfoItem, InfoInstance
|
||||
from infoscreen.hsl_fetcher import fetch as hsl_fetch
|
||||
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import requests
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def index(request, idx, *args, **kwargs):
|
||||
"""Render infoscreen index page."""
|
||||
return render(request, "infoscreen/infoscreen_index.html", {"rotation": idx})
|
||||
return render(request, 'infoscreen_index.html', {'rotation': idx})
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@@ -23,7 +22,7 @@ def default(request, *args, **kwargs):
|
||||
"""Try getting first rotation item."""
|
||||
try:
|
||||
first = Rotation.objects.all()[0].id
|
||||
except DatabaseError:
|
||||
except:
|
||||
first = 0
|
||||
return index(request, first, *args, **kwargs)
|
||||
|
||||
@@ -32,8 +31,7 @@ def default(request, *args, **kwargs):
|
||||
def get_apy_json(request):
|
||||
"""Render APY diilikone page."""
|
||||
return HttpResponse(
|
||||
requests.get("https://api-diilikone.apy.fi/deals/top-groups").text
|
||||
)
|
||||
requests.get("https://api-diilikone.apy.fi/deals/top-groups").text)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@@ -62,12 +60,10 @@ def info_types(request, *args, **kwargs):
|
||||
types = []
|
||||
classes = InfoItem.get_subclasses()
|
||||
for c in classes:
|
||||
types.append(
|
||||
{
|
||||
"name": c.display_name,
|
||||
"create_template_url": c.get_create_template_url(),
|
||||
}
|
||||
)
|
||||
types.append({
|
||||
"name": c.display_name,
|
||||
"create_template_url": c.get_create_template_url(),
|
||||
})
|
||||
return HttpResponse(json.dumps(types))
|
||||
|
||||
|
||||
@@ -81,3 +77,25 @@ def info_items(request, *args, **kwargs):
|
||||
items.append(i.get_dict())
|
||||
|
||||
return JsonResponse(items, safe=False)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def hsl_timetable_settings(request, *args, **kwargs):
|
||||
"""Set HSL timetable settings."""
|
||||
d = {"departure_threshold": settings.HSL_DEPARTURE_THRESHOLD,
|
||||
"hurry_threshold": settings.HSL_HURRY_THRESHOLD}
|
||||
|
||||
return JsonResponse(d, status=200)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def CurrentHSLView(request, *args, **kwargs):
|
||||
"""Get HSL data and return it."""
|
||||
try:
|
||||
api_resp = hsl_fetch()
|
||||
except Exception as ex:
|
||||
logging.exception('Failed to fetch HSL timetables.')
|
||||
error = {'error': 'Aikataulujen haku epäonnistui.'}
|
||||
return JsonResponse(error, status=200)
|
||||
|
||||
return JsonResponse(api_resp, status=200, safe=False)
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from django.contrib import admin
|
||||
from modeltranslation.admin import TranslationAdmin
|
||||
|
||||
from kaehmy.models import Application, Comment, CustomRole, PresetRole
|
||||
|
||||
admin.site.register(Application)
|
||||
admin.site.register(Comment)
|
||||
admin.site.register(CustomRole)
|
||||
admin.site.register(PresetRole, TranslationAdmin)
|
||||
@@ -1,5 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class KaehmyConfig(AppConfig):
|
||||
name = "kaehmy"
|
||||
@@ -1,114 +0,0 @@
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from kaehmy.models import PresetRole, CustomRole, Application, Comment, BaseRole
|
||||
|
||||
|
||||
class CheckboxSelectMultiple(forms.widgets.CheckboxSelectMultiple):
|
||||
option_template_name = "kaehmy/checkbox_option.html"
|
||||
|
||||
def create_option(
|
||||
self, name, value, label, selected, index, subindex=None, attrs=None
|
||||
):
|
||||
dic = super(CheckboxSelectMultiple, self).create_option(
|
||||
name, value, label, selected, index, subindex, attrs
|
||||
)
|
||||
description = PresetRole.objects.get(id=value).description
|
||||
dic["description"] = description
|
||||
return dic
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CheckboxSelectMultiple, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class ApplicationForm(forms.ModelForm):
|
||||
"""Class representing Kaehmy form."""
|
||||
|
||||
class Meta:
|
||||
"""Meta for class Application."""
|
||||
|
||||
model = Application
|
||||
fields = [
|
||||
"name",
|
||||
"email",
|
||||
"phone_number",
|
||||
"year",
|
||||
"preset_roles",
|
||||
"custom_roles",
|
||||
"custom_role_name",
|
||||
"custom_role_is_board",
|
||||
"text",
|
||||
]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ApplicationForm, self).__init__(*args, **kwargs)
|
||||
|
||||
self.fields["email"].label = _("Email (not public)")
|
||||
self.fields["phone_number"].label = _("Phone number (not public)")
|
||||
|
||||
custom_roles_exist = CustomRole.objects.all().exists()
|
||||
self.fields["custom_roles"].widget = (
|
||||
forms.widgets.CheckboxSelectMultiple()
|
||||
if custom_roles_exist
|
||||
else forms.HiddenInput()
|
||||
)
|
||||
self.fields["custom_roles"].help_text = ""
|
||||
self.fields["custom_roles"].label = _("Custom roles")
|
||||
self.fields["custom_roles"].queryset = CustomRole.objects.all()
|
||||
|
||||
for cat_id, category in BaseRole.CATEGORIES:
|
||||
key = "preset_roles_{}".format(cat_id)
|
||||
qset = PresetRole.objects.filter(category=cat_id).order_by(
|
||||
"category", "-is_board"
|
||||
)
|
||||
self.fields[key] = forms.ModelMultipleChoiceField(qset)
|
||||
self.fields[key].widget = CheckboxSelectMultiple(
|
||||
attrs={
|
||||
"title": _("Preset roles"),
|
||||
"name": "preset_roles",
|
||||
}
|
||||
)
|
||||
self.fields[key].help_text = ""
|
||||
self.fields[key].queryset = qset
|
||||
self.fields[key].label = _(category)
|
||||
self.fields[key].required = False
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(ApplicationForm, self).clean()
|
||||
for key in cleaned_data.keys():
|
||||
if "preset_roles_" in key:
|
||||
cleaned_data["preset_roles"] = (
|
||||
cleaned_data["preset_roles"] | cleaned_data[key]
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def clean_phone_number(self):
|
||||
"""Clean phone number field."""
|
||||
number = self.cleaned_data.get("phone_number")
|
||||
if number.isdigit():
|
||||
return number
|
||||
else:
|
||||
raise ValidationError(_("Invalid phone number"))
|
||||
|
||||
def clean_custom_role_name(self):
|
||||
"""Check that no other custom role with same name exists."""
|
||||
custom_name = self.cleaned_data.get("custom_role_name")
|
||||
if not CustomRole.objects.filter(name=custom_name).exists():
|
||||
return custom_name
|
||||
else:
|
||||
raise ValidationError(_("Custom role with the same name already exists."))
|
||||
|
||||
def non_role_fields(self):
|
||||
return [
|
||||
self.fields[k]
|
||||
for k in self.fields.keys()
|
||||
if k not in ["preset_roles", "custom_roles"]
|
||||
]
|
||||
|
||||
|
||||
class CommentForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Comment
|
||||
fields = ["name", "email", "message", "parent"]
|
||||
@@ -1,199 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-01-25 22:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("webapp", "0037_auto_20180125_2131"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="CommentParent",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"name",
|
||||
models.CharField(default="", max_length=255, verbose_name="Name"),
|
||||
),
|
||||
(
|
||||
"email",
|
||||
models.EmailField(default="", max_length=254, verbose_name="Email"),
|
||||
),
|
||||
(
|
||||
"timestamp",
|
||||
models.DateTimeField(
|
||||
default=django.utils.timezone.now, verbose_name="Timestamp"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="CustomRole",
|
||||
fields=[
|
||||
(
|
||||
"baserole_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="webapp.BaseRole",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "Custom kaehmy roles",
|
||||
"verbose_name": "Custom kaehmy role",
|
||||
},
|
||||
bases=("webapp.baserole",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="PresetRole",
|
||||
fields=[
|
||||
(
|
||||
"presetrole_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="webapp.PresetRole",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "Preset kaehmy roles",
|
||||
"verbose_name": "Preset kaehmy role",
|
||||
},
|
||||
bases=("webapp.presetrole",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="TelegramChannel",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
("name", models.CharField(max_length=255)),
|
||||
("channel_id", models.CharField(max_length=255, unique=True)),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "Telegram channels",
|
||||
"verbose_name": "Telegram channel",
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Application",
|
||||
fields=[
|
||||
(
|
||||
"commentparent_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="kaehmy.CommentParent",
|
||||
),
|
||||
),
|
||||
(
|
||||
"phone_number",
|
||||
models.CharField(
|
||||
default="", max_length=10, verbose_name="Phone number"
|
||||
),
|
||||
),
|
||||
(
|
||||
"year",
|
||||
models.IntegerField(
|
||||
choices=[(1, "1"), (2, "2"), (3, "3"), (4, "4"), (5, "N")],
|
||||
verbose_name="Year",
|
||||
),
|
||||
),
|
||||
(
|
||||
"text",
|
||||
models.TextField(default="", max_length=300, verbose_name="Text"),
|
||||
),
|
||||
(
|
||||
"custom_role_name",
|
||||
models.CharField(
|
||||
blank=True, max_length=255, verbose_name="Custom role name"
|
||||
),
|
||||
),
|
||||
(
|
||||
"custom_role_is_board",
|
||||
models.BooleanField(verbose_name="Board member"),
|
||||
),
|
||||
(
|
||||
"custom_roles",
|
||||
models.ManyToManyField(
|
||||
blank=True, related_name="forms", to="kaehmy.CustomRole"
|
||||
),
|
||||
),
|
||||
(
|
||||
"preset_roles",
|
||||
models.ManyToManyField(
|
||||
blank=True, related_name="forms", to="kaehmy.PresetRole"
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "Kaehmylomakkeet",
|
||||
"verbose_name": "Kaehmylomake",
|
||||
},
|
||||
bases=("kaehmy.commentparent",),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Comment",
|
||||
fields=[
|
||||
(
|
||||
"commentparent_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="kaehmy.CommentParent",
|
||||
),
|
||||
),
|
||||
("message", models.TextField(verbose_name="Message")),
|
||||
(
|
||||
"parent",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="messages",
|
||||
to="kaehmy.CommentParent",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name_plural": "Kaehmykommentit",
|
||||
"verbose_name": "Kaehmykommentti",
|
||||
},
|
||||
bases=("kaehmy.commentparent",),
|
||||
),
|
||||
]
|
||||
@@ -1,65 +0,0 @@
|
||||
# Generated by Django 2.0.7 on 2018-09-02 16:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("webapp", "0047_auto_20180710_2110"),
|
||||
("kaehmy", "0001_initial"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="KaehmyBaseRole",
|
||||
fields=[
|
||||
(
|
||||
"baserole_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="webapp.BaseRole",
|
||||
),
|
||||
),
|
||||
(
|
||||
"category",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("corporate", "Corporate affairs"),
|
||||
("freshman", "Freshmen"),
|
||||
("international", "International"),
|
||||
("external", "External affairs"),
|
||||
("media", "Media"),
|
||||
("tech", "Technology"),
|
||||
("wellbeing", "Wellbeing"),
|
||||
("elepaja", "Elepaja"),
|
||||
("ceremonies", "Ceremonies"),
|
||||
("culture", "Culture"),
|
||||
("studies", "Studies"),
|
||||
("sosso", "Sössö magazine"),
|
||||
("alumni", "Alumni relations"),
|
||||
("others", "Others"),
|
||||
],
|
||||
default="others",
|
||||
max_length=255,
|
||||
verbose_name="Category",
|
||||
),
|
||||
),
|
||||
],
|
||||
bases=("webapp.baserole",),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="Application",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="customrole",
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name="presetrole",
|
||||
),
|
||||
]
|
||||