Compare commits
175 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e9f91efce | |||
| 3c098a36bc | |||
| bc328995c5 | |||
| eb5659d0da | |||
| 50268b98a9 | |||
| 4a2a5f9d76 | |||
| b1eda70e7a | |||
| f7b00ab3cc | |||
| 9cad12d879 | |||
| d4800c449d | |||
| d3d4ce5e5d | |||
| 9dbc2424b1 | |||
| 5b38065f13 | |||
| 1879c265cd | |||
| 4273b95b70 | |||
| e96698ef2e | |||
| e04b8ddb9b | |||
| 6fe3e69cd9 | |||
| 7d1a8bd284 | |||
| 5b44d2e4c7 | |||
| 8552bde245 | |||
| 9a4d7fc498 | |||
| 6dea241408 | |||
| b3b1bec12c | |||
| 02cc305abf | |||
| ffe0c45eea | |||
| 499ddc0979 | |||
| b154ffb79e | |||
| 87dfab0e57 | |||
| 92ec8b1b4e | |||
| ac8fb0bfe3 | |||
| 0ce6af8f7c | |||
| 053b705cc1 | |||
| 3676f23f65 | |||
| 7e821f277f | |||
| 91cbdef71f | |||
| 0cfb78bc69 | |||
| 01c20b1a6e | |||
| 1711aca5ec | |||
| c536899cc9 | |||
| 60da9d8256 | |||
| a923e225e8 | |||
| 2d7c9d779a | |||
| aea9898563 | |||
| 4e3f71ea43 | |||
| 6acbdbc760 | |||
| 11b6e68fe1 | |||
| cc3aa66e49 | |||
| 563344e8b4 | |||
| 129b8e4601 | |||
| efde69984d | |||
| c66c8e7367 | |||
| c31f454c78 | |||
| 18926d16d1 | |||
| 4e8adebb2d | |||
| 0207bdf22b | |||
| 5caacd8f44 | |||
| d7a3433d2c | |||
| 6e68e106aa | |||
| 9225ff5967 | |||
| 6996bb8015 | |||
| 7aff7c46ee | |||
| 882732d054 | |||
| 9e0d911f7b | |||
| ab6b7d19fb | |||
| b95be67051 | |||
| 48b6ed5b69 | |||
| e25041d38d | |||
| 7bc77ef232 | |||
| 0f8a7d76da | |||
| ecf34d9039 | |||
| a9cf253c83 | |||
| 45e98e0220 | |||
| d8ea26c777 | |||
| c398e53750 | |||
| 9ce0eccfce | |||
| 17e6bb86ed | |||
| a1b85b3c6c | |||
| fafc988a60 | |||
| 55bcc78670 | |||
| 7bc277a978 | |||
| 780e2d6acb | |||
| bbc5743c39 | |||
| 3298e55fb0 | |||
| 2b2d20f796 | |||
| 117802bf10 | |||
| 8bd53ba897 | |||
| 5114b2ef85 | |||
| d7f53b9700 | |||
| 1ab3180c0d | |||
| b8a8cb2c6d | |||
| a56e8ef241 | |||
| d5d11edbe7 | |||
| f7c2516cd7 | |||
| 45b27e3ac9 | |||
| 52613ba7d4 | |||
| f136b34b6c | |||
| 4d90454cf8 | |||
| 82e0c5c995 | |||
| dec3bf5ccf | |||
| 23c00ce167 | |||
| bdc21f79e8 | |||
| 2fc71eabe3 | |||
| 3debd4b1ec | |||
| b765ae37e8 | |||
| 9eae16110c | |||
| ce1f4eb7c9 | |||
| 661dc84973 | |||
| 6b21ea4af8 | |||
| 37ab278086 | |||
| 509b157d65 | |||
| 253f720043 | |||
| bdf6b469ad | |||
| 7dc9fac597 | |||
| 77330dffe9 | |||
| f0ea3505e4 | |||
| 06c2a2b9a6 | |||
| c671206e8a | |||
| cf50050eba | |||
| f8e68acd4d | |||
| 9b6fc5e687 | |||
| 7d17d8a84f | |||
| b2aa6d1a3e | |||
| 5a2c6d9aaa | |||
| 1ecbda0731 | |||
| 9e9049709b | |||
| ac9a5db356 | |||
| 67ed7edefb | |||
| 181be6b80c | |||
| 1e2bf10494 | |||
| a219b930b3 | |||
| 78ba2d7afa | |||
| 6fe01dadf1 | |||
| 9df62a1247 | |||
| a9164f8c6e | |||
| 6e5074f8fe | |||
| efe8808e79 | |||
| 77cdce714a | |||
| 69c1b2dcb2 | |||
| 2a50f7ef43 | |||
| 0c93446b81 | |||
| 0e52efb449 | |||
| 283d5b566e | |||
| a4367bbc9d | |||
| 935f7a38f1 | |||
| 9973057051 | |||
| 9527e6de5f | |||
| a519d51309 | |||
| 9ee4d600a7 | |||
| 2b2d635cb0 | |||
| ec6051d3d6 | |||
| e4f701711c | |||
| 2363362202 | |||
| ab2682a0d3 | |||
| 6678c691dd | |||
| aaf773c600 | |||
| 417083b050 | |||
| 08e675f698 | |||
| efb1ee6182 | |||
| 99788e8d47 | |||
| 356038a622 | |||
| 31e324e478 | |||
| e5c00a47e8 | |||
| 1fdc9e9ff8 | |||
| cf33d81d69 | |||
| 29070165eb | |||
| b9e9cdb2b0 | |||
| e0e73976db | |||
| 755abe5647 | |||
| 2f80159144 | |||
| e709570f22 | |||
| e63f8d5418 | |||
| b8fd237918 | |||
| 6c153e7ab0 | |||
| 8f74c87df5 |
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "Deploying to development."
|
||||
set -e
|
||||
set -x
|
||||
|
||||
pushd deployment
|
||||
|
||||
docker-compose down
|
||||
docker pull "$1"
|
||||
docker-compose up -d
|
||||
|
||||
popd
|
||||
|
||||
set +x
|
||||
set +e
|
||||
@@ -17,4 +17,7 @@ requirements_henu.txt
|
||||
/collected_static/
|
||||
mydatabase
|
||||
settings.json
|
||||
.vscode/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
*.code-workspace
|
||||
sik_test
|
||||
@@ -63,7 +63,7 @@ deploy_dev:
|
||||
image: alpine:latest
|
||||
environment:
|
||||
name: dev
|
||||
url: http://web.sik.party:8080
|
||||
url: http://web.sik.party:8000
|
||||
only:
|
||||
- develop
|
||||
before_script:
|
||||
@@ -76,7 +76,8 @@ deploy_dev:
|
||||
- '[[ -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"
|
||||
- scp .deploy_dev.sh $DEV_SSH_USER@$DEV_SSH_HOST:~/deployment/deploy_dev.sh
|
||||
- ssh $DEV_SSH_USER@$DEV_SSH_HOST "bash ~/deployment/deploy_dev.sh \"$IMAGE_NAME\""
|
||||
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
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))
|
||||
@@ -1,58 +0,0 @@
|
||||
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
|
||||
@@ -16,7 +16,7 @@ body {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom center;
|
||||
background-image: url("/static/img/smokes.png");
|
||||
background-image: url("/static/coffee_scale/img/smokes.png");
|
||||
transform-origin: bottom;
|
||||
animation: smokes 8s ease-in-out 0s infinite;
|
||||
opacity:0;
|
||||
@@ -27,7 +27,7 @@ body {
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: top center;
|
||||
background-image: url("/static/img/coffeecup3.png");
|
||||
background-image: url("/static/coffee_scale/img/coffeecup3.png");
|
||||
height:60%;
|
||||
}
|
||||
#scale{
|
||||
@@ -47,7 +47,7 @@ body {
|
||||
background: green;
|
||||
border-radius: 10px;
|
||||
}
|
||||
#brewtime{
|
||||
.brewtime{
|
||||
text-align:right;
|
||||
position:absolute;
|
||||
right:0px;
|
||||
@@ -62,10 +62,13 @@ body {
|
||||
font-size:4vw;
|
||||
color: #333;
|
||||
}
|
||||
.layertwo{
|
||||
display: None;
|
||||
}
|
||||
noscript{
|
||||
color:red;
|
||||
}
|
||||
#text{
|
||||
.text{
|
||||
color:green;
|
||||
position:absolute;
|
||||
top:50%;
|
||||
@@ -102,9 +105,9 @@ noscript{
|
||||
}
|
||||
@keyframes coffeeready {
|
||||
0% {background-color:white;}
|
||||
25% {background-color:green;}
|
||||
25% {background-color:rgb(100, 255, 100);}
|
||||
50% {background-color:white;}
|
||||
75% {background-color:green;}
|
||||
75% {background-color:rgb(100, 255, 100);}
|
||||
100% {background-color:white;}
|
||||
}
|
||||
@keyframes unknown {
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.0 KiB |
@@ -0,0 +1,183 @@
|
||||
//Inner state
|
||||
var lastBrew = new Date(0);
|
||||
var brewing = false;
|
||||
var backoff = 2000;
|
||||
|
||||
//MQTT client config
|
||||
var username = "coffee-user-"+ Math.random();
|
||||
// eslint-disable-next-line no-undef
|
||||
var client = new Paho.MQTT.Client("sika.sahkoinsinoorikilta.fi", 9001, username);
|
||||
client.onMessageArrived = function (message) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log("Topic: "+message.destinationName+" msg: "+message.payloadString);
|
||||
var ev = new CustomEvent(message.destinationName, {'detail': message.payloadString});
|
||||
window.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
function reconnect(responseObject){
|
||||
if (responseObject.errorCode !== 0) {
|
||||
console.log("connection lost! Reason: "+responseObject.errorMessage); // eslint-disable-line no-console
|
||||
setTimeout(function(){
|
||||
client.connect({onSuccess:onConnect, useSSL:true, onFailure: reconnect});
|
||||
}, backoff);
|
||||
}
|
||||
}
|
||||
|
||||
function onConnect() {
|
||||
console.log("MQTT connected"); // eslint-disable-line no-console
|
||||
//set and reset reconnector
|
||||
client.onConnectionLost = reconnect
|
||||
// subscribe to topics
|
||||
client.subscribe("sik/kiltahuone/kahvivaaka/cups");
|
||||
client.subscribe("sik/kiltahuone/kahvivaaka/brewing");
|
||||
client.subscribe("sik/kiltahuone/kahvivaaka/brewtime");
|
||||
}
|
||||
|
||||
// data update and parse functions
|
||||
function parseCups(ev){
|
||||
var cups = parseFloat(ev.detail).toFixed(1)
|
||||
|
||||
function makeEvent(cups) {
|
||||
return (String(cups) !== '-1.0')
|
||||
? new CustomEvent("cupsChanged", {'detail': cups})
|
||||
: new CustomEvent("cupsError", {'detail': 'Error: unable to fetch cups :('});
|
||||
}
|
||||
|
||||
window.dispatchEvent(makeEvent(cups));
|
||||
}
|
||||
function updateCups(ev){
|
||||
$("#text").text(ev.detail);
|
||||
}
|
||||
function showCupsError(ev) {
|
||||
$('#text').text(ev.detail);
|
||||
$('#text').css({
|
||||
'font-size': '7vh',
|
||||
'left': '0',
|
||||
'top': '40%',
|
||||
'width': '100%',
|
||||
'text-align': 'center',
|
||||
'color': 'red',
|
||||
});
|
||||
$('#lower').css({'background-image': 'none'});
|
||||
}
|
||||
function updateScale(ev){
|
||||
$("#scale2").css({width: Math.min(ev.detail/9*100,100) + '%'});
|
||||
}
|
||||
|
||||
function tick(){
|
||||
var ev = new CustomEvent("tick", {'detail': new Date()});
|
||||
window.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
function updateTime(ev){
|
||||
var now = ev.detail;
|
||||
$("#time").html(formatTime(now.getHours(),now.getMinutes(),now.getSeconds()));
|
||||
}
|
||||
|
||||
function coffeeLowEffect(ev){
|
||||
ev.detail <= 2 ? $("#text").addClass("hurry") : $("#text").removeClass("hurry");
|
||||
}
|
||||
function coffeeReadyEffect(){
|
||||
$("body").addClass("coffeeready");
|
||||
// autoclear animation class in 10s
|
||||
setTimeout(function(){$("body").removeClass("coffeeready");}, 10000);
|
||||
}
|
||||
function hotEffect(ev){
|
||||
var opa = Math.max(100 - ev.detail / 90000,0);
|
||||
$("#upper").css({opacity: opa/100});
|
||||
}
|
||||
function brewAnimStart(){
|
||||
$(".text").addClass("brewing");
|
||||
$(".layerone").hide();
|
||||
$(".layertwo").show();
|
||||
}
|
||||
function brewAnimEnd(){
|
||||
$(".text").removeClass("brewing");
|
||||
$(".layertwo").hide();
|
||||
$(".layerone").show();
|
||||
}
|
||||
function brewNotifier(ev){
|
||||
var new_brewing = parseInt(ev.detail);
|
||||
if (new_brewing == 1 && brewing == 0){
|
||||
window.dispatchEvent(new Event("brewStart"));
|
||||
} else if (new_brewing == 0 && brewing == 1){
|
||||
window.dispatchEvent(new Event("brewEnd"));
|
||||
}
|
||||
brewing = new_brewing;
|
||||
}
|
||||
function brewTimeParser(ev){
|
||||
lastBrew = new Date(parseInt(ev.detail)*1000.0);
|
||||
}
|
||||
function updateBrewDiff(){
|
||||
var now = new Date();
|
||||
var timeDiff = Math.max(now.getTime() - lastBrew.getTime(), 0);
|
||||
var eve = new CustomEvent("dtUpdate", {'detail': timeDiff});
|
||||
window.dispatchEvent(eve);
|
||||
}
|
||||
function updateBrewTime(ev){
|
||||
var timeDiff = ev.detail;
|
||||
var timeStr;
|
||||
if (timeDiff < 3600000){
|
||||
timeStr = Math.round(timeDiff / 60000) + ' min'
|
||||
} else if (timeDiff < 10000* 3600 * 1000){ // 1000h
|
||||
timeStr = '~' + Math.round(timeDiff / 3600000 * 2) / 2 + ' h';
|
||||
} else {
|
||||
timeStr = "???"
|
||||
}
|
||||
$("#brewtime").html(timeStr);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
function nToS(num){
|
||||
return num < 10 ? "0" + num : "" + num;
|
||||
}
|
||||
|
||||
function formatTime(hours, minutes, seconds){
|
||||
return nToS(hours)+":"+nToS(minutes)+":"+nToS(seconds)
|
||||
}
|
||||
|
||||
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(3);
|
||||
$(".text").css({ top: s*0.16-font/2 + 'px',
|
||||
fontSize: font + 'px',
|
||||
marginLeft: -font*3*3/10 + 'px'});
|
||||
}
|
||||
|
||||
// Init everything
|
||||
|
||||
$(document).ready(function(){
|
||||
client.connect({onSuccess:onConnect, useSSL:true, onFailure:reconnect});
|
||||
|
||||
//connect MQTT event listeners
|
||||
window.addEventListener("sik/kiltahuone/kahvivaaka/cups", parseCups);
|
||||
window.addEventListener("sik/kiltahuone/kahvivaaka/brewing", brewNotifier);
|
||||
window.addEventListener("sik/kiltahuone/kahvivaaka/brewtime", brewTimeParser);
|
||||
|
||||
//connect other event listeners
|
||||
window.addEventListener("cupsChanged", updateCups);
|
||||
window.addEventListener("cupsChanged", coffeeLowEffect);
|
||||
window.addEventListener("cupsChanged", updateScale);
|
||||
window.addEventListener("cupsChanged", resize);
|
||||
|
||||
window.addEventListener("cupsError", showCupsError);
|
||||
window.addEventListener("cupsError", coffeeLowEffect);
|
||||
window.addEventListener("cupsError", updateScale);
|
||||
|
||||
window.addEventListener("brewStart", brewAnimStart);
|
||||
window.addEventListener("brewEnd", brewAnimEnd);
|
||||
window.addEventListener("brewEnd", coffeeReadyEffect);
|
||||
window.addEventListener("tick", updateTime);
|
||||
window.addEventListener("tick", updateBrewDiff);
|
||||
window.addEventListener("dtUpdate", updateBrewTime);
|
||||
window.addEventListener("dtUpdate", hotEffect);
|
||||
|
||||
//start time based events
|
||||
setInterval(tick, 100);
|
||||
tick();
|
||||
|
||||
});
|
||||
$(window).resize(resize);
|
||||
@@ -1,130 +0,0 @@
|
||||
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'});
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<title>Coffee Cups @Guild Room - AYY SIK ry</title>
|
||||
@@ -10,13 +13,16 @@
|
||||
<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>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.2/mqttws31.js"
|
||||
type="text/javascript"></script>
|
||||
<link rel="stylesheet" href="{% static "coffee_scale/css/coffee.css" %}">
|
||||
<script src="{% static "coffee_scale/js/coffee.js" %}"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container">
|
||||
<span id="brewtime"></span>
|
||||
<span id="brewtime" class="brewtime layerone"></span>
|
||||
<span class="brewtime layertwo">:)</span>
|
||||
<span id="address">
|
||||
ka.dy.fi
|
||||
<noscript><br>This page uses JavaScript!</noscript>
|
||||
@@ -26,7 +32,8 @@
|
||||
</div>
|
||||
<!--Kahvinkeitin on rikki. Varakeittimellä keitettyä kahvia saattaa olla.-->
|
||||
<div id="lower" class="normal">
|
||||
<div id="text"></div>
|
||||
<div id="text" class="text layerone">???</div>
|
||||
<div class="text layertwo"> +</div>
|
||||
<div id="scale"><div id="scale2"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,30 +1,2 @@
|
||||
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)
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
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)
|
||||
from django.conf import settings
|
||||
from .views import coffee_view
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# landing page
|
||||
url(r'^$', coffee_view),
|
||||
url(r'^cups', cups_view),
|
||||
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
@@ -3,25 +3,9 @@ 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)
|
||||
return render(request, 'coffee_scale:coffee.html')
|
||||
|
||||
@@ -5,9 +5,9 @@ services:
|
||||
image: postgres
|
||||
web:
|
||||
build: .
|
||||
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"]
|
||||
image: git.sahkoinsinoorikilta.fi:4567/vtmk/web2.0
|
||||
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:8000"]
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Ilmotunkki
|
||||
|
||||
## Terms
|
||||
- Signup, Form with collection of questions
|
||||
- Response, One answer to some signup
|
||||
- Quota, Amount of people allowed to respond with some option selected.
|
||||
- In generic case there is no option and quota is just max number of people.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Officials may generate signups forms
|
||||
- Officials may see results from signups
|
||||
- Officials may see some stats from their signups
|
||||
- for example distributions of multiple choice answers
|
||||
- Officials should be able to edit signups wherever possible
|
||||
- Propably not possible to edit after first response
|
||||
- Officials should be able to delete responses
|
||||
- Officials should be able to embed payment information to the signup?
|
||||
- TODO: is there need for unique reference numbers for every response?
|
||||
- Officials should be able to save a signup to a reusable template.
|
||||
- Possibility to save templates?
|
||||
|
||||
- Signup may be attached to an event
|
||||
- Multiple signups to a single event should be possible (FTMK uses for museum visits? Erna asked if it was possible in old web)
|
||||
- Possibility for external service (Google Form, URL will suffice)
|
||||
|
||||
- Signup should support custom quotas
|
||||
- Atleast quotas from multiple choices and checkboxes
|
||||
- Text quotas are risky (typos everywhere!!)
|
||||
- Signup should have start and end times
|
||||
- Signup should support atleast following questiontypes
|
||||
- Text
|
||||
- multiple choice (select one)
|
||||
- checkbox (boolean yes/no)
|
||||
|
||||
- Signup should support reserve slots.
|
||||
- TODO: quota based reserves or generic? or both?
|
||||
|
||||
- Responding should send confirm email
|
||||
- Response should be editable by responder and only by the responder until the closing of the signup
|
||||
- TODO: is there need to custom edit period or disable?
|
||||
|
||||
- Responders should see amount of quotas left.
|
||||
- Responders should see some information about other responses
|
||||
- TODO: names? should this be editable by officials?
|
||||
- Or superadmin can edit and the one signing up within edit period
|
||||
- NOTE: Quota related info is exposed if any info is printed
|
||||
- When quotas need to be hidden? PoTa?
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IlmotunkkiConfig(AppConfig):
|
||||
name = 'ilmotunkki'
|
||||
@@ -0,0 +1,16 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class Signup(models.Model):
|
||||
start = models.DateTimeField()
|
||||
end = models.DateTimeField()
|
||||
|
||||
|
||||
class Question(models.Model):
|
||||
pass
|
||||
|
||||
|
||||
class Answer(models.Model):
|
||||
signup = models.ForeignKey(Signup, on_delete=models.CASCADE)
|
||||
question = models.ForeignKey(Question, on_delete=models.PROTECT)
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@@ -1,10 +1,9 @@
|
||||
"""Admin site registers."""
|
||||
|
||||
from django.contrib import admin
|
||||
from infoscreen.models import Rotation, InfoItem, InfoInstance
|
||||
from infoscreen.models import ImageInfoItem, ExternalImageInfoItem, ABBInfoItem
|
||||
from infoscreen.models import ExternalWebsiteInfoItem
|
||||
from infoscreen.models import VideoInfoItem
|
||||
from infoscreen.models import (
|
||||
Rotation, InfoItem, InfoInstance, ImageInfoItem,
|
||||
ExternalImageInfoItem, ABBInfoItem, ExternalWebsiteInfoItem, VideoInfoItem)
|
||||
|
||||
# Register your models here.
|
||||
admin.site.register(Rotation)
|
||||
|
||||
@@ -47,7 +47,7 @@ def fetch():
|
||||
route = place['pattern']['route']['shortName']
|
||||
stop_times = place['_stoptimes']
|
||||
for stop_time in stop_times:
|
||||
timestamp = stop_time['serviceDay'] + stop_time['realtimeArrival']
|
||||
timestamp = time_utc = stop_time['serviceDay'] + stop_time['realtimeArrival']
|
||||
headsign = stop_time['stopHeadsign']
|
||||
stop_name = stop_time['stop']['name']
|
||||
time_diff = (timestamp - timezone.now().timestamp()) / 60 # minutes
|
||||
@@ -64,6 +64,7 @@ def fetch():
|
||||
'headsign': headsign,
|
||||
'timestamp': time,
|
||||
'stop': stop_name,
|
||||
'utc': time_utc,
|
||||
})
|
||||
|
||||
return schedule
|
||||
|
||||
@@ -98,12 +98,12 @@ class ABBInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return ABB infoitem template url."""
|
||||
return "/static/html/abb.html"
|
||||
return "/static/infoscreen/html/abb.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create ABB infoitem template url command."""
|
||||
return "/static/html/abb_create.html"
|
||||
return "/static/infoscreen/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/html/apy.html"
|
||||
return "/static/infoscreen/html/apy.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create APY infoitem template url command."""
|
||||
return "/static/html/apy_create.html"
|
||||
return "/static/infoscreen/html/apy_create.html"
|
||||
|
||||
|
||||
class ExternalWebsiteInfoItem(InfoItem):
|
||||
@@ -129,12 +129,12 @@ class ExternalWebsiteInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return external website infoitem template url."""
|
||||
return "/static/html/external_website.html?url={}".format(self.name)
|
||||
return "/static/infoscreen/html/external_website.html?url={}".format(self.name)
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create external website infoitem template url command."""
|
||||
return "/static/html/external_website_create.html"
|
||||
return "/static/infoscreen/html/external_website_create.html"
|
||||
|
||||
def get_dict(self):
|
||||
"""Convert django model to dict and return it."""
|
||||
@@ -185,12 +185,12 @@ class SossoInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return Sosso infoitem template url."""
|
||||
return "/static/html/sosso.html"
|
||||
return "/static/infoscreen/html/sosso.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create Sosso infoitem template url command."""
|
||||
return "/static/html/sosso_create.html"
|
||||
return "/static/infoscreen/html/sosso_create.html"
|
||||
|
||||
|
||||
class EventInfoItem(InfoItem):
|
||||
@@ -200,12 +200,12 @@ class EventInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return Event infoitem template url."""
|
||||
return "/static/html/events.html"
|
||||
return "/static/infoscreen/html/events.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create Event infoitem template url command."""
|
||||
return "/static/html/events_create.html"
|
||||
return "/static/infoscreen/html/events_create.html"
|
||||
|
||||
|
||||
class ImageInfoItem(InfoItem):
|
||||
@@ -218,12 +218,12 @@ class ImageInfoItem(InfoItem):
|
||||
"""Return Image infoitem template url."""
|
||||
# get param to avoid angular from optimizing same template
|
||||
# with different options
|
||||
return "/static/html/generic_image.html?img={}".format(self.name)
|
||||
return "/static/infoscreen/html/generic_image.html?img={}".format(self.name)
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create Image infoitem template url command."""
|
||||
return "/static/html/generic_image_create.html"
|
||||
return "/static/infoscreen/html/generic_image_create.html"
|
||||
|
||||
def get_dict(self):
|
||||
"""Convert django model to dict and return it."""
|
||||
@@ -240,12 +240,12 @@ class VideoInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return Video infoitem template url."""
|
||||
return "/static/html/generic_video.html?video={}".format(self.name)
|
||||
return "/static/infoscreen/html/generic_video.html?video={}".format(self.name)
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create Video infoitem template url command."""
|
||||
return "/static/html/generic_video_create.html"
|
||||
return "/static/infoscreen/html/generic_video_create.html"
|
||||
|
||||
def get_dict(self):
|
||||
"""Convert django model to dict and return it."""
|
||||
@@ -261,12 +261,12 @@ class HslInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return HSL infoitem template url."""
|
||||
return "/static/html/hsl.html"
|
||||
return "/static/infoscreen/html/hsl.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create HSL infoitem template url command."""
|
||||
return "/static/html/hsl_create.html"
|
||||
return "/static/infoscreen/html/hsl_create.html"
|
||||
|
||||
|
||||
class ExternalImageInfoItem(InfoItem):
|
||||
@@ -277,12 +277,12 @@ class ExternalImageInfoItem(InfoItem):
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return External Image infoitem template url."""
|
||||
return "/static/html/generic_image.html?img={}".format(self.name)
|
||||
return "/static/infoscreen/html/generic_image.html?img={}".format(self.name)
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create External Image infoitem template url command."""
|
||||
return "/static/html/generic_external_image_create.html"
|
||||
return "/static/infoscreen/html/generic_external_image_create.html"
|
||||
|
||||
def get_dict(self):
|
||||
"""Convert django model to dict and return it."""
|
||||
@@ -321,7 +321,7 @@ class ExternalImageInfoItem(InfoItem):
|
||||
class InfoInstance(models.Model):
|
||||
"""Class for Info instance in Infoscreen."""
|
||||
|
||||
rotation = models.ForeignKey('Rotation', related_name='instances')
|
||||
rotation = models.ForeignKey('Rotation', related_name='instances', on_delete=models.CASCADE)
|
||||
duration = models.FloatField(default=15.0) # seconds
|
||||
# generic relation to some kind of InfoItem
|
||||
item_id = models.PositiveIntegerField()
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
<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>
|
||||
@@ -2,7 +2,10 @@ body {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
}
|
||||
|
||||
#header {
|
||||
#header:after {
|
||||
content: " ";
|
||||
display: block;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
#header-logo {
|
||||
@@ -5,6 +5,7 @@ html {
|
||||
body {
|
||||
padding: 1.5rem;
|
||||
margin: 0.5rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
tbody {
|
||||
@@ -49,7 +50,18 @@ 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: right;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.article-title-col {
|
||||
@@ -1,10 +1,10 @@
|
||||
<link rel="stylesheet" href="/static/css/abb.css">
|
||||
<link rel="stylesheet" href="/static/infoscreen/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/img/ABB_logo.png">
|
||||
<img src="/static/infoscreen/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/img/ABB_uralle.png" style="position:absolute;left:0;top:0;width:100vw;">
|
||||
<img src="/static/infoscreen/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 type="text" ng-model="item.name"></input>
|
||||
<input class="form-control" 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/css/apy.css">
|
||||
<link rel="stylesheet" href="/static/infoscreen/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 apyitem
|
||||
Create new ÄPY statistics item
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
<input class="form-control" 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/css/coffee.css">
|
||||
<link rel="stylesheet" href="/static/infoscreen/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,4 +1,4 @@
|
||||
<link rel="stylesheet" href="/static/css/events.css">
|
||||
<link rel="stylesheet" href="/static/infoscreen/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">
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
<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,4 +1,4 @@
|
||||
<link rel="stylesheet" href="/static/css/external_website.css">
|
||||
<link rel="stylesheet" href="/static/infoscreen/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 "ka.dy.fi".
|
||||
Create new item to show external website. For example "https://ka.dy.fi".
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
<input type="text" class="form-control" ng-model="item.name"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Url:</label>
|
||||
<input type="text" ng-model="item.url"></input>
|
||||
<input type="text" class="form-control" 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" ng-model="item.name"></input>
|
||||
<input type="text" class="form-control" ng-model="item.name"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Url:</label>
|
||||
<input type="text" ng-model="item.url"></input>
|
||||
<input type="text" class="form-control" 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" ng-model="imagename"></input>
|
||||
<input type="text" class="form-control" 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/css/video.css">
|
||||
<link rel="stylesheet" href="/static/infoscreen/css/video.css">
|
||||
|
||||
|
||||
<div class="fullscreen-bg">
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" ng-model="name"></input>
|
||||
<input type="text" class="form-control" ng-model="name"></input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="file" ngf-select ng-model="video" name="file" required>
|
||||
@@ -1,4 +1,4 @@
|
||||
<link rel="stylesheet" href="/static/css/hsl.css">
|
||||
<link rel="stylesheet" href="/static/infoscreen/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">
|
||||
@@ -25,7 +25,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['timestamp'] | limitTo: 11">
|
||||
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['utc'] | limitTo: 11">
|
||||
<td style="min-width: 300px">
|
||||
{{x.timestamp}}
|
||||
</td>
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
<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>
|
||||
@@ -0,0 +1,17 @@
|
||||
<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>
|
||||
@@ -4,7 +4,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<input type="text" ng-model="item.name"></input>
|
||||
<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>
|
||||
|
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 |
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 736 KiB After Width: | Height: | Size: 736 KiB |
|
Before Width: | Height: | Size: 1.8 MiB 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 |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 96 KiB |
@@ -46,6 +46,13 @@ 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");
|
||||
@@ -0,0 +1,43 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load staticfiles %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
|
||||
{% block html %}
|
||||
<html ng-app="{% block appname %}{% endblock appname %}">
|
||||
<head>
|
||||
<meta charset="utf-8" name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1" />
|
||||
<title>
|
||||
{% block title %}
|
||||
{% endblock title %}
|
||||
</title>
|
||||
|
||||
{% block libraries %}
|
||||
<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/moment.js" %}"></script>
|
||||
<script src="{% static "js/lib/angular-route.js" %}"></script>
|
||||
<script src="{% static "js/lib/angular-animate.js" %}"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||
{% endblock libraries %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static "webapp/css/footer.css" %}">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block controllers %}
|
||||
<script src="{% static "infoscreen/js/infoadmin_controllers.js" %}"></script>
|
||||
{% endblock controllers %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
{% endblock body %}
|
||||
</body>
|
||||
</html>
|
||||
{% endblock html %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<div class="header-content">
|
||||
<div class="logo">
|
||||
<a href="/"><img src="{% static "webapp/img/logo_header.png" %}" alt="Aalto-yliopiston Sähköinsinöörikilta ry"></a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,154 +1,45 @@
|
||||
{% extends "infoscreen:base.html" %}
|
||||
|
||||
{% 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>
|
||||
{% block appname %}infoAdmin{% endblock appname %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Infoscreen admin" %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block styles %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static "infoscreen/css/admin.css" %}"></link>
|
||||
{% endblock styles %}
|
||||
|
||||
{% block controllers %}
|
||||
<script src="{% static "infoscreen/js/infoadmin_controllers.js" %}"></script>
|
||||
{% endblock controllers %}
|
||||
|
||||
{% block body %}
|
||||
<div id="header" class="row">
|
||||
<div class="logout-button">
|
||||
<form action="/admin/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">
|
||||
<h1>{% trans "Infoscreen Admin Pane" %}</h1>
|
||||
</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>
|
||||
{% include "infoscreen:nav.html" %}
|
||||
<div class="tab-content" id="tabContent">
|
||||
{% include "infoscreen:tabs/slides.html" %}
|
||||
{% include "infoscreen:tabs/rotations.html" %}
|
||||
{% include "infoscreen:tabs/add_remove.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</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>
|
||||
{% include "webapp:footer.html" %}
|
||||
{% endblock body %}
|
||||
@@ -1,29 +1,25 @@
|
||||
{% extends "infoscreen:base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="infoApp">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{% block appname %}infoApp{% endblock appname %}
|
||||
|
||||
<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>
|
||||
{% block title %}
|
||||
{% trans "Infoscreen" %}
|
||||
{% endblock title %}
|
||||
|
||||
<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>
|
||||
{% block controllers %}
|
||||
<script src="{% static "infoscreen/js/infoscreen_controllers.js" %}"></script>
|
||||
{% endblock controllers %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{% static "infoscreen/css/infoscreen.css" %}"></link>
|
||||
{% endblock styles %}
|
||||
|
||||
{% block 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>
|
||||
{% endblock body %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% load i18n %}
|
||||
|
||||
<ul class="nav nav-tabs" id="tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" role="tab" href="#slides">{% trans "Manage Slides" %}</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" role="button" data-toggle="dropdown" href="#" aria-haspopup="true" aria-expanded="false">
|
||||
{% trans "Manage Rotations" %}<span class="caret"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" role="tab" href="#rotations" ng-repeat="r in rotations" ng-click="selectRotation(r.id)" data-toggle="tab">{$ r.name $}</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" data-toggle="tab" href="#deleterot" role="tab">{% trans "Create/Delete" %}</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,30 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div id="deleterot" class="tab-pane fade" role="tabpanel">
|
||||
<div class="col" 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 class="btn btn-primary" href="/infoscreen/{$ r.id $}">{% trans "Preview" %}</a></td>
|
||||
<td><a class="btn btn-danger" href="#" ng-click="deleteRotation(r.id)">{% trans "Delete" %}</a></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>
|
||||
@@ -0,0 +1,44 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div id="rotations" class="tab-pane fade" role="tabpanel" ng-controller="infoadmin_ctrl">
|
||||
<div class="col">
|
||||
<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">
|
||||
<div class="rotation-title-row">
|
||||
<h2>{% trans "Rotation" %}: {$ selected_rot.name $}</h2>
|
||||
<a class="btn btn-primary" href="/infoscreen/{$ selected_rot.id $}">{% trans "Preview" %}</a>
|
||||
</div>
|
||||
<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>
|
||||
@@ -0,0 +1,37 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div id="slides" class="tab-pane fade show active" role="tabpanel">
|
||||
<div ng-controller="infoadmin_ctrl">
|
||||
<div class="col">
|
||||
<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">
|
||||
<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>
|
||||
@@ -1,6 +1,7 @@
|
||||
"""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
|
||||
@@ -52,5 +53,8 @@ urlpatterns = [
|
||||
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,6 +6,7 @@ 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
|
||||
@@ -14,21 +15,17 @@ import logging
|
||||
import threading
|
||||
import requests
|
||||
|
||||
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
|
||||
from infoscreen.models import (
|
||||
Rotation, InfoItem, InfoInstance, ABBInfoItem, ExternalImageInfoItem,
|
||||
ImageInfoItem, SossoInfoItem, HslInfoItem, EventInfoItem,
|
||||
ExternalWebsiteInfoItem, ImageUploadForm, ApyInfoItem, VideoInfoItem)
|
||||
|
||||
|
||||
@login_required(login_url='/login')
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('infoscreen.change_infoinstance', raise_exception=True)
|
||||
def admin(request, *args, **kwargs):
|
||||
"""Render infoscreen admin page."""
|
||||
return render(request, 'infoscreen_admin.html', {})
|
||||
return render(request, 'infoscreen:infoscreen_admin.html', {})
|
||||
|
||||
|
||||
def create_item_generator(model):
|
||||
@@ -36,12 +33,12 @@ def create_item_generator(model):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["POST"])
|
||||
@login_required(login_url='/login')
|
||||
@login_required(login_url='/admin/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 ValueError:
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponseBadRequest(
|
||||
'{"status":"failure","error":"invalid json supplied"}')
|
||||
try:
|
||||
@@ -58,7 +55,7 @@ def delete_item_generator(model):
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@require_http_methods(["DELETE"])
|
||||
@login_required(login_url='/login')
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('infoscreen.delete_infoinstance', raise_exception=True)
|
||||
def delete_item(request, *args, **kwargs):
|
||||
idx = kwargs.pop("idx", 0)
|
||||
@@ -71,7 +68,7 @@ def delete_item_generator(model):
|
||||
try:
|
||||
item.delete()
|
||||
return HttpResponse('{"status":"success"}')
|
||||
except:
|
||||
except DatabaseError:
|
||||
resp = HttpResponse('{"error" : "could not delete item"}')
|
||||
resp.status_code = 500
|
||||
return resp
|
||||
@@ -80,7 +77,7 @@ def delete_item_generator(model):
|
||||
|
||||
# due to model structure this is little complicated
|
||||
@ensure_csrf_cookie
|
||||
@login_required(login_url='/login')
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('infoscreen.delete_infoinstance', raise_exception=True)
|
||||
@require_http_methods(["DELETE"])
|
||||
def delete_info_item(request, *args, **kwargs):
|
||||
@@ -97,7 +94,7 @@ def delete_info_item(request, *args, **kwargs):
|
||||
try:
|
||||
item.delete()
|
||||
return HttpResponse('{"status":"success"}')
|
||||
except:
|
||||
except DatabaseError:
|
||||
resp = HttpResponse('{"error" : "could not delete item"}')
|
||||
resp.status_code = 500
|
||||
return resp
|
||||
@@ -105,7 +102,7 @@ def delete_info_item(request, *args, **kwargs):
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@ensure_csrf_cookie
|
||||
@login_required(login_url='/login')
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
|
||||
def create_image_item(request, *args, **kwargs):
|
||||
"""Create image Infoscreen item."""
|
||||
@@ -122,7 +119,7 @@ def create_image_item(request, *args, **kwargs):
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@ensure_csrf_cookie
|
||||
@login_required(login_url='/login')
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
|
||||
def create_video_item(request, *args, **kwargs):
|
||||
"""Create video Infoscreen item."""
|
||||
@@ -139,20 +136,20 @@ def create_video_item(request, *args, **kwargs):
|
||||
|
||||
@require_http_methods(["POST"])
|
||||
@ensure_csrf_cookie
|
||||
@login_required(login_url='/login')
|
||||
@login_required(login_url='/admin/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:
|
||||
except json.JSONDecodeError:
|
||||
return HttpResponse('{"error": "bad post body!"}', status=400)
|
||||
|
||||
try:
|
||||
name = data["name"]
|
||||
Rotation.objects.create(name=name)
|
||||
resp = HttpResponse(status=200)
|
||||
except:
|
||||
except DatabaseError:
|
||||
resp = HttpResponse(
|
||||
'{"error" : "could not create rotation!"}', status=400)
|
||||
|
||||
@@ -161,7 +158,7 @@ def create_rotation(request, *args, **kwargs):
|
||||
|
||||
@require_http_methods(["DELETE"])
|
||||
@ensure_csrf_cookie
|
||||
@login_required(login_url='/login')
|
||||
@login_required(login_url='/admin/login')
|
||||
@permission_required('infoscreen.delete_rotation', raise_exception=True)
|
||||
def delete_rotation(request, *args, **kwargs):
|
||||
"""Delete rotation."""
|
||||
@@ -171,7 +168,7 @@ def delete_rotation(request, *args, **kwargs):
|
||||
try:
|
||||
Rotation.objects.filter(id=id).delete()
|
||||
resp = HttpResponse(status=200)
|
||||
except:
|
||||
except DatabaseError:
|
||||
resp = HttpResponse(
|
||||
'{"error" : "could not delete rotation!"}', status=400)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -9,6 +10,7 @@ from infoscreen.hsl_fetcher import fetch as hsl_fetch
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import requests
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@@ -22,7 +24,7 @@ def default(request, *args, **kwargs):
|
||||
"""Try getting first rotation item."""
|
||||
try:
|
||||
first = Rotation.objects.all()[0].id
|
||||
except:
|
||||
except DatabaseError:
|
||||
first = 0
|
||||
return index(request, first, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.contrib import admin
|
||||
from modeltranslation.admin import TranslationAdmin
|
||||
|
||||
from kaehmy.models import Application, Comment, CustomRole, PresetRole, TelegramChannel
|
||||
|
||||
admin.site.register(Application)
|
||||
admin.site.register(Comment)
|
||||
admin.site.register(CustomRole)
|
||||
admin.site.register(PresetRole, TranslationAdmin)
|
||||
admin.site.register(TelegramChannel)
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class KaehmyConfig(AppConfig):
|
||||
name = 'kaehmy'
|
||||
@@ -0,0 +1,89 @@
|
||||
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, KaehmyBaseRole
|
||||
|
||||
|
||||
class CheckboxSelectMultiple(forms.widgets.CheckboxSelectMultiple):
|
||||
option_template_name = '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 KaehmyBaseRole.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']
|
||||
@@ -0,0 +1,93 @@
|
||||
# -*- 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',),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
# Generated by Django 2.0.7 on 2018-09-02 16:43
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('kaehmy', '0002_auto_20180902_1929'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Kaehmylomake',
|
||||
'verbose_name_plural': 'Kaehmylomakkeet',
|
||||
},
|
||||
bases=('kaehmy.commentparent',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomRole',
|
||||
fields=[
|
||||
('kaehmybaserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.KaehmyBaseRole')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Custom kaehmy role',
|
||||
'verbose_name_plural': 'Custom kaehmy roles',
|
||||
},
|
||||
bases=('kaehmy.kaehmybaserole',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PresetRole',
|
||||
fields=[
|
||||
('kaehmybaserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.KaehmyBaseRole')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Preset kaehmy role',
|
||||
'verbose_name_plural': 'Preset kaehmy roles',
|
||||
},
|
||||
bases=('kaehmy.kaehmybaserole',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='custom_roles',
|
||||
field=models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.CustomRole'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='preset_roles',
|
||||
field=models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.PresetRole'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.7 on 2018-10-18 18:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('kaehmy', '0003_auto_20180902_1943'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='kaehmybaserole',
|
||||
name='category',
|
||||
field=models.CharField(choices=[('corporate', 'Corporate affairs'), ('freshman', 'Freshmen'), ('international', 'International'), ('external', 'External affairs'), ('media', 'Media'), ('tech', 'Technology'), ('wellbeing', 'Wellbeing'), ('elepaja', 'Elepaja'), ('ceremonies', 'Ceremonies'), ('studies', 'Studies'), ('sosso', 'Sössö magazine'), ('alumni', 'Alumni relations'), ('others', 'Others')], default='others', max_length=255, verbose_name='Category'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,164 @@
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from datetime import timedelta
|
||||
from webapp.utils import month_from_now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from auditlog.registry import auditlog
|
||||
from phonenumber_field.modelfields import PhoneNumberField
|
||||
|
||||
import logging
|
||||
|
||||
import webapp.models
|
||||
|
||||
VERBOSE_NAME = _('Kaehmy')
|
||||
|
||||
|
||||
class KaehmyBaseRole(webapp.models.BaseRole):
|
||||
|
||||
CATEGORIES = (
|
||||
('corporate', _('Corporate affairs')),
|
||||
('freshman', _('Freshmen')),
|
||||
('international', _('International')),
|
||||
('external', _('External affairs')),
|
||||
('media', _('Media')),
|
||||
('tech', _('Technology')),
|
||||
('wellbeing', _('Wellbeing')),
|
||||
('elepaja', _('Elepaja')),
|
||||
('ceremonies', _('Ceremonies')),
|
||||
('studies', _('Studies')),
|
||||
('sosso', _('Sössö magazine')),
|
||||
('alumni', _('Alumni relations')),
|
||||
('others', _('Others')),
|
||||
)
|
||||
category = models.CharField(_('Category'), choices=CATEGORIES, default='others', max_length=255)
|
||||
|
||||
|
||||
class PresetRole(KaehmyBaseRole):
|
||||
"""Model for kaehmy role."""
|
||||
|
||||
description = models.TextField(_('Description'))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Preset kaehmy role')
|
||||
verbose_name_plural = _('Preset kaehmy roles')
|
||||
|
||||
|
||||
class CustomRole(KaehmyBaseRole):
|
||||
"""Model representing a user-specified custom occupation."""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Custom kaehmy role')
|
||||
verbose_name_plural = _('Custom kaehmy roles')
|
||||
|
||||
|
||||
class CommentParent(models.Model):
|
||||
|
||||
name = models.CharField(_('Name'), max_length=255, default='')
|
||||
email = models.EmailField(_('Email'), default='')
|
||||
timestamp = models.DateTimeField(_('Timestamp'), default=timezone.now)
|
||||
|
||||
def __str__(self):
|
||||
return 'Message parent #{}'.format(self.id)
|
||||
|
||||
|
||||
class Comment(CommentParent):
|
||||
"""
|
||||
Model representing a kaehmymessage.
|
||||
|
||||
Every message relates to certain kaehmyform or parent message.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Kaehmykommentti')
|
||||
verbose_name_plural = _('Kaehmykommentit')
|
||||
|
||||
message = models.TextField(_('Message'))
|
||||
parent = models.ForeignKey('CommentParent', related_name='messages', on_delete=models.CASCADE)
|
||||
|
||||
|
||||
class Application(CommentParent):
|
||||
"""
|
||||
Model representing a form for kaehmy.
|
||||
|
||||
Allows user to choose from existing roles or to create custom ones.
|
||||
"""
|
||||
YEAR_CHOICES = (
|
||||
(1, '1'),
|
||||
(2, '2'),
|
||||
(3, '3'),
|
||||
(4, '4'),
|
||||
(5, 'N'),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Kaehmylomake')
|
||||
verbose_name_plural = _('Kaehmylomakkeet')
|
||||
|
||||
phone_number = models.CharField(
|
||||
_('Phone number'), max_length=10, default="")
|
||||
year = models.IntegerField(_('Year'), choices=YEAR_CHOICES)
|
||||
text = models.TextField(_('Text'), default="", max_length=300)
|
||||
custom_role_name = models.CharField(
|
||||
_('Custom role name'), max_length=255, blank=True)
|
||||
custom_role_is_board = models.BooleanField(
|
||||
_('Board member'), blank=True)
|
||||
custom_roles = models.ManyToManyField(
|
||||
'kaehmy.CustomRole', related_name='forms', blank=True)
|
||||
preset_roles = models.ManyToManyField(
|
||||
'kaehmy.PresetRole', related_name='forms', blank=True)
|
||||
|
||||
def __str__(self):
|
||||
"""Return model info."""
|
||||
return _('Kaehmy application: {}').format(self.name)
|
||||
|
||||
def comment_count(self):
|
||||
"""Count comments for kaehmy."""
|
||||
total = 0
|
||||
|
||||
def recurse(message):
|
||||
count = 0
|
||||
for msg in message.messages.all():
|
||||
count += recurse(msg)
|
||||
|
||||
return count + 1
|
||||
|
||||
for message in self.messages.all():
|
||||
total += recurse(message)
|
||||
|
||||
return total
|
||||
|
||||
def board_roles(self):
|
||||
presets = [r.name.capitalize() for r in self.preset_roles.filter(is_board=True)]
|
||||
customs = [r.name.capitalize() for r in self.custom_roles.filter(is_board=True)]
|
||||
combined = presets + customs
|
||||
return _('Board: {}').format(', '.join(combined)) if len(combined) > 0 else ''
|
||||
|
||||
def official_roles(self):
|
||||
presets = [r.name.capitalize() for r in self.preset_roles.filter(is_board=False)]
|
||||
customs = [r.name.capitalize() for r in self.custom_roles.filter(is_board=False)]
|
||||
combined = presets + customs
|
||||
return _('Official: {}').format(', '.join(combined)) if len(combined) > 0 else ''
|
||||
|
||||
def all_roles(self):
|
||||
presets = [r.name.capitalize() for r in self.preset_roles.all()]
|
||||
customs = [r.name.capitalize() for r in self.custom_roles.all()]
|
||||
combined = presets + customs
|
||||
return ', '.join(combined) if len(combined) > 0 else ''
|
||||
|
||||
def has_any_board_role(self):
|
||||
return self.preset_roles.filter(is_board=True).exists() or self.custom_roles.filter(is_board=True)
|
||||
|
||||
|
||||
# Telegram channel entry for Kaehmys
|
||||
class TelegramChannel(models.Model):
|
||||
"""Model containing the channel id of a Telegram chat"""
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Telegram channel')
|
||||
verbose_name_plural = _('Telegram channels')
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
channel_id = models.CharField(max_length=255, unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return 'Telegram channel: "{}"'.format(self.name)
|
||||
@@ -24,3 +24,24 @@ div.tooltip-inner {
|
||||
width: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.kaehmy-content {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.role-filter-form select {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.card a.comment-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ footer {
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60px; /* Set the fixed height of the footer here */
|
||||
/* line-height: 60px; /* Vertically center the text there */ */
|
||||
/* line-height: 60px; /* Vertically center the text there */
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -7,5 +7,5 @@
|
||||
}
|
||||
|
||||
.navbar-light .navbar-nav .nav-link {
|
||||
color: #102a40;
|
||||
color: black;
|
||||
}
|
||||
|
After Width: | Height: | Size: 192 KiB |
@@ -0,0 +1,13 @@
|
||||
import django_tables2 as tables
|
||||
from django.db.models import Count, Q
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from kaehmy.models import Application
|
||||
|
||||
|
||||
class ExportTable(tables.Table):
|
||||
class Meta:
|
||||
model = Application
|
||||
exclude = ['text', 'messageparent_ptr', 'custom_role_name', 'custom_role_is_board', 'timestamp']
|
||||
|
||||
all_roles = tables.Column(verbose_name=_('Roles'), orderable=False)
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "project.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{% static "kaehmy/css/base.css" %}">
|
||||
<link rel="stylesheet" href="{% static "kaehmy/css/header.css" %}">
|
||||
<link rel="stylesheet" href="{% static "kaehmy/css/nav.css" %}">
|
||||
<link rel="stylesheet" href="{% static "kaehmy/css/footer.css" %}">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
{% block header %}
|
||||
<div class="kaehmy_header">
|
||||
{% include "kaehmy:header.html" %}
|
||||
</div>
|
||||
{% endblock header %}
|
||||
|
||||
{% block navigation %}
|
||||
{% include "kaehmy:navigation.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<div class="kaehmy-content">
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="footer">
|
||||
{% block footer %}
|
||||
{% include "kaehmy:footer.html" %}
|
||||
{% endblock footer %}
|
||||
</div>
|
||||
|
||||
{% endblock body %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "kaehmy_base.html" %}
|
||||
{% extends "kaehmy:base.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
@@ -1,4 +1,4 @@
|
||||
{% extends "kaehmy_base.html" %}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||