diff --git a/coffee_scale/apps.py b/coffee_scale/apps.py deleted file mode 100644 index 57c4c5b..0000000 --- a/coffee_scale/apps.py +++ /dev/null @@ -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)) diff --git a/coffee_scale/mqtt.py b/coffee_scale/mqtt.py deleted file mode 100644 index 47a4592..0000000 --- a/coffee_scale/mqtt.py +++ /dev/null @@ -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 diff --git a/coffee_scale/static/css/coffee.css b/coffee_scale/static/css/coffee.css index 2594006..de6d2be 100644 --- a/coffee_scale/static/css/coffee.css +++ b/coffee_scale/static/css/coffee.css @@ -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 { diff --git a/coffee_scale/static/js/coffee.js b/coffee_scale/static/js/coffee.js index 6b9b4a6..f885408 100644 --- a/coffee_scale/static/js/coffee.js +++ b/coffee_scale/static/js/coffee.js @@ -1,130 +1,161 @@ -var len = 0; -var lastBrew = "∞"; -var brewtext = ""; +//Inner state +var lastBrew = new Date(0); +var brewing = false; +var backoff = 2000; -$(document).ready(function(){ - $('#text').bind("DOMSubtreeModified", resize); - updateTime(); - setInterval(updateTime,1000); - formatBrewTime(); - setInterval(formatBrewTime,10000); -}); +//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); +} -$(window).resize(resize); - -function fetchdata(data, status){ - if (typeof status !== 'undefined'){ - if (status == "success"){ - parseData(data); +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); } - 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 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) + var cupsEvent = new CustomEvent("cupsChanged", {'detail': cups}); + window.dispatchEvent(cupsEvent); +} +function updateCups(ev){ + $("#text").text(ev.detail); +} +function updateScale(ev){ + $("#scale2").css({width: Math.min(ev.detail/9*100,100) + '%'}); } -function handleError() { - setData("?", 0, 0, Number.MAX_VALUE, Number.MAX_VALUE); +function tick(){ + var ev = new CustomEvent("tick", {'detail': new Date()}); + window.dispatchEvent(ev); } -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(); +function updateTime(ev){ + var now = ev.detail; $("#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 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(len); - $("#text").css({ top: s*0.16-font/2 + 'px', fontSize: font + 'px', marginLeft: -font*len*3/10 + 'px'}); + 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("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); diff --git a/coffee_scale/templates/coffee.html b/coffee_scale/templates/coffee.html index 1c2fec9..b09a25d 100644 --- a/coffee_scale/templates/coffee.html +++ b/coffee_scale/templates/coffee.html @@ -10,13 +10,16 @@ +
- + + :) ka.dy.fi @@ -26,7 +29,8 @@
-
+
???
+
 +
diff --git a/coffee_scale/tests.py b/coffee_scale/tests.py index b1e5f9b..f9c051b 100644 --- a/coffee_scale/tests.py +++ b/coffee_scale/tests.py @@ -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) diff --git a/coffee_scale/urls.py b/coffee_scale/urls.py index b9c54e6..f750576 100644 --- a/coffee_scale/urls.py +++ b/coffee_scale/urls.py @@ -1,7 +1,7 @@ from django.conf.urls import url from django.views.generic.base import RedirectView -from .views import coffee_view, cups_view +from .views import coffee_view favicon_view = RedirectView.as_view(url='static/img/favicon.ico', permanent=True) @@ -9,6 +9,4 @@ urlpatterns = [ # landing page url(r'^$', coffee_view), - url(r'^cups', cups_view), - ] diff --git a/coffee_scale/views.py b/coffee_scale/views.py index 9b8ae08..b973641 100644 --- a/coffee_scale/views.py +++ b/coffee_scale/views.py @@ -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) diff --git a/infoscreen/hsl_fetcher.py b/infoscreen/hsl_fetcher.py index d4d00ea..6a3835c 100644 --- a/infoscreen/hsl_fetcher.py +++ b/infoscreen/hsl_fetcher.py @@ -1,73 +1,70 @@ """File containing Infoscreen HSL data fetcher classes.""" -import urllib.request +import requests import json import logging +import os +import pytz + from datetime import timedelta, datetime -from django.utils import timezone +from django.utils import timezone, dateparse +from django.utils.dateformat import format from django.conf import settings -from infoscreen.models import HSLDataModel + +with open(os.path.join(settings.BASE_DIR, 'infoscreen', 'hsl_stops.graphql')) as stops_file: + STOPS_QUERY = stops_file.read() + +with open(os.path.join(settings.BASE_DIR, 'infoscreen', 'hsl_stops_variables.json')) as vars_file: + STOPS_VARS = json.loads(vars_file.read()) + +API_URL = 'https://api.digitransit.fi/routing/v1/routers/hsl/index/graphql' +API_HEADERS = {'Content-Type': 'application/json'} -class HSLFetcher: - """Main class of Infoscreen HSL fetcher.""" +def fetch(): + """Fetch data from HSL API.""" - last_fetched = datetime.fromtimestamp(86400) # epoch - INTERVAL = 1 # minutes - # logging.info( - # "Set up scheduled HSL API fetch every {} minutes".format(INTERVAL)) + query_vars = STOPS_VARS.copy() + query_vars['startTime_6'] = format(timezone.now(), 'U') - def fetch_if_needed(self): - """Check if new fetch from HSL API is needed.""" - if (timezone.now() - HSLFetcher.last_fetched > - timedelta(minutes=HSLFetcher.INTERVAL)): - self.fetch() + post_data = json.dumps({ + 'operationName': 'NearestRoutesContainer', + 'query': STOPS_QUERY, + 'variables': query_vars, + }) - def fetch(self): - """Fetch data from HSL API.""" - location_coords = (2545565, 6675319) - src = urllib.request.urlopen( - ("https://api.reittiopas.fi/hsl/prod/?userhash={}" - "&request=stops_area¢er_coordinate={},{}") - .format(settings.HSL_USERHASH, location_coords[0], - location_coords[1]))\ - .read().decode("utf-8") + resp = requests.post(API_URL, data=post_data, headers=API_HEADERS) - data = json.loads(src) + data = resp.json() - arr = [] + items = data['data']['viewer']['_nearest']['edges'] + places = map(lambda item: item['node']['place'], items) - time = (timezone.now() + - timedelta(minutes=settings.HSL_DEPARTURE_THRESHOLD)) - time = "{0:02d}{0:02d}".format(time.hour, time.minute) - for element in data: - src = urllib.request.urlopen( - ("https://api.reittiopas.fi/hsl/prod/?userhash={}" - "&request=stop&code={}&dep_limit=20&time={}") - .format(settings.HSL_USERHASH, element['code'], time) - ).read().decode("utf-8") + schedule = [] + for place in places: + route = place['pattern']['route']['shortName'] + stop_times = place['_stoptimes'] + for stop_time in stop_times: + 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 - parsed = json.loads(src)[0] - arr.append({ - "name": parsed['name_fi'], - "lines": parsed['lines'], - "dist": element['dist'], - "departures": parsed['departures']}) + if time_diff < settings.HSL_DEPARTURE_THRESHOLD: + continue + elif time_diff < settings.HSL_HURRY_THRESHOLD: + time = '{} min'.format(int(time_diff)) + else: + time = pytz.utc.localize(datetime.fromtimestamp(timestamp)).strftime('%H:%M') - model_arr = HSLDataModel.objects.all() - count = len(model_arr) - json_dump = json.dumps(arr) + schedule.append({ + 'route': route, + 'headsign': headsign, + 'timestamp': time, + 'stop': stop_name, + 'utc': time_utc, + }) - if count == 0: - HSLDataModel.objects.create(data=json_dump) - else: - obj = model_arr[count - 1] - obj.data = json_dump - obj.save() - now = timezone.now() - HSLFetcher.last_fetched = now - - logging.info( - "Fetched HSL timetable data with size {} bytes.".format(len(src))) + return schedule diff --git a/infoscreen/hsl_stops.graphql b/infoscreen/hsl_stops.graphql new file mode 100644 index 0000000..6111303 --- /dev/null +++ b/infoscreen/hsl_stops.graphql @@ -0,0 +1,71 @@ +query NearestRoutesContainer($lat_0: Float!, $lon_1: Float!, $maxDistance_2: Int!, $maxResults_3: Int!, $timeRange_7: Int!, $numberOfDepartures_8: Int!, $filterByModes_4: [Mode]!, $filterByPlaceTypes_5: [FilterPlaceType]!, $startTime_6: Long!) { + viewer { + ...F5 + } +} + +fragment F0 on DepartureRow { + _stoptimes4caEfh: stoptimes(startTime: $startTime_6, timeRange: $timeRange_7, numberOfDepartures: $numberOfDepartures_8) { + pickupType + serviceDay + realtimeDeparture + } + id +} + +fragment F1 on DepartureRow { + pattern { + route { + shortName + } + } + _stoptimes: stoptimes(startTime: $startTime_6, timeRange: $timeRange_7, numberOfDepartures: $numberOfDepartures_8) { + realtimeArrival + serviceDay + stopHeadsign + stop { + name + } + } +} + +fragment F2 on BikeRentalStation { + id +} + +fragment F3 on placeAtDistance { + distance + place { + id + __typename + ...F1 + ...F2 + } + id +} + +fragment F4 on placeAtDistanceConnection { + edges { + node { + distance + place { + id + __typename + ...F0 + } + id + ...F3 + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + } +} + +fragment F5 on QueryType { + _nearest: nearest(lat: $lat_0, lon: $lon_1, maxDistance: $maxDistance_2, maxResults: $maxResults_3, first: $maxResults_3, filterByModes: $filterByModes_4, filterByPlaceTypes: $filterByPlaceTypes_5) { + ...F4 + } +} \ No newline at end of file diff --git a/infoscreen/hsl_stops_variables.json b/infoscreen/hsl_stops_variables.json new file mode 100644 index 0000000..bcc8e18 --- /dev/null +++ b/infoscreen/hsl_stops_variables.json @@ -0,0 +1,10 @@ +{ + "lat_0": 60.190480099999995, + "lon_1": 24.8275665, + "maxDistance_2": 1000, + "maxResults_3": 50, + "numberOfDepartures_8": 2, + "timeRange_7": 7200, + "filterByModes_4": ["BUS"], + "filterByPlaceTypes_5": ["DEPARTURE_ROW"] +} diff --git a/infoscreen/migrations/0006_delete_hsldatamodel.py b/infoscreen/migrations/0006_delete_hsldatamodel.py new file mode 100644 index 0000000..c660813 --- /dev/null +++ b/infoscreen/migrations/0006_delete_hsldatamodel.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-10-31 14:56 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('infoscreen', '0005_auto_20170913_1841'), + ] + + operations = [ + migrations.DeleteModel( + name='HSLDataModel', + ), + ] diff --git a/infoscreen/models.py b/infoscreen/models.py index 6805637..ff1613d 100644 --- a/infoscreen/models.py +++ b/infoscreen/models.py @@ -406,9 +406,3 @@ class UploadFileForm(forms.Form): name = forms.CharField() video = forms.FileField() - - -class HSLDataModel(models.Model): - """Model representing HSL data.""" - - data = models.TextField(default="", editable=False) diff --git a/infoscreen/static/css/hsl.css b/infoscreen/static/css/hsl.css index 7f407a1..8753ab1 100644 --- a/infoscreen/static/css/hsl.css +++ b/infoscreen/static/css/hsl.css @@ -1,5 +1,5 @@ table { - font-size: 5vh; + font-size: 4vh; font-family: 'Droid Sans Mono', monospace; } .red { @@ -58,9 +58,9 @@ thead{ margin-left: 0; margin-right: 0; } + .repeat-item.ng-leave { - -webkit-transition:0.5s linear all; - transition:0.5s linear all; + } .repeat-item.ng-leave.ng-leave-active { diff --git a/infoscreen/static/html/hsl.html b/infoscreen/static/html/hsl.html index 48baab6..288fa4c 100644 --- a/infoscreen/static/html/hsl.html +++ b/infoscreen/static/html/hsl.html @@ -1,12 +1,16 @@ +
-
+

{{clock | date:'HH:mm'}}

HSL-Aikataulut
-

{{ clock | date:'HH:mm'}}

+
- +

+ {{error}} +

+
- - - + -
@@ -18,25 +22,19 @@ Pysäkki - Päätepysäkki -
- {{x.timedelta < 10 ?x.timedelta + ' min' : x.time}} +
+ {{x.timestamp}} - {{x.bus}} + {{x.route}}, {{x.headsign}} {{x.stop}} - {{x.laststop}} -
diff --git a/infoscreen/static/js/infoscreen_controllers.js b/infoscreen/static/js/infoscreen_controllers.js index ec1825b..543ed65 100644 --- a/infoscreen/static/js/infoscreen_controllers.js +++ b/infoscreen/static/js/infoscreen_controllers.js @@ -1,4 +1,5 @@ var app = angular.module('infoApp', ['ngAnimate', 'ngRoute']); + app.controller('infoscreen_main', function($scope,$http,$timeout){ var templates = []; $scope.init = function(rot){ @@ -103,13 +104,22 @@ app.controller('EventController', function($scope, $http) { }) }); +app.filter('unixTimeToDifference', function() { + return function(input) { + var date = moment.unix(input); + var now = moment(); + var res = date.diff(now, 'minutes'); + return res; + } +}) + app.controller('timetableCtrl', function($scope, $http, $interval) { - function load(){ + function load() { $http.get('/infoscreen/hsl_data') .then(function(data, status, headers, config) { //eslint-disable-line no-unused-vars - $scope.arr=[]; - parse(data); + $scope.stoptimes = data.data; + $scope.error = data.data.error || null; }); $http.get('/infoscreen/hsl_data/settings') .then(function(data, status, headers, config) { //eslint-disable-line no-unused-vars @@ -117,112 +127,20 @@ app.controller('timetableCtrl', $scope.hurryThreshold = data.data['hurry_threshold']; }); } - $scope.$on('$destroy', function() { - $interval.cancel(inter1); - $interval.cancel(inter2); - $interval.cancel(inter3); - }); - var objects; - $scope.arr=[]; - var dict=[]; - function parse(data){ - objects=data['data']; - for(var objectIndex in objects){ - var stop = objects[objectIndex]; - var lineIndex; - for (lineIndex in stop['lines']){ - var elem=stop['lines'][lineIndex].split(":"); - dict[elem[0]]=elem[1]; - } - for (lineIndex in stop['departures']){ - var line = stop['departures'][lineIndex]; - var time = line['time']; - var date = line['date']; - var hours = Math.floor(time / 100); - var minutes = time % 100; - if (hours >= 24) { - hours -= 24; - date++; - } - var code = line['code'].substring(1, 5); - if (code.charAt(0) == '0') { - code = code.substring(1,4); - } - - var departure = { - "stop": stop['name'].split(",")[0], - "dist": stop['dist'], - "bus": code, - "date": date, - "time": pad(hours, 2) + ":" + pad(minutes, 2), - "laststop": dict[line['code']].split(",")[0].split(" l.")[0], - "hurry": false - }; - if(departure['laststop']=='Otaniemi') - break; - if(departure['stop']=='Alvar Aallon puisto') - departure['stop']="A. A. puisto" - var trigger = true; - for (var arrIndex = $scope.arr.length - 1; arrIndex >= 0; arrIndex--) { - if ($scope.arr[arrIndex]['bus'] == departure['bus'] && - $scope.arr[arrIndex]['laststop'] == departure['laststop']) { - - if ($scope.arr[arrIndex]['dist'] == departure['dist']){ - break; - } - else if ($scope.arr[arrIndex]['dist'] > departure['dist']){ - $scope.arr.splice(arrIndex, 1); - } - else { - trigger = false; - } - } - } - if (trigger) { - $scope.arr.push(departure); - } - } - } - - function pad(num, size) { - var s = num + ""; - while (s.length < size) { - s = "0" + s; - } - return s; - } - - delOld(); - } - function delOld(){ - var tooSoon = typeof($scope.departureThreshold) != 'undefined' ? $scope.departureThreshold: 0; - var hurry = typeof($scope.hurryThreshold) != 'undefined' ? $scope.hurryThreshold : 0; - var f = new Date(); - for (var a=$scope.arr.length-1; a>=0; a--) { - var time=$scope.arr[a]['time'].split(":"); - var date=$scope.arr[a]['date'].toString(); - var d = new Date(f); - d.setFullYear(date.substring(0,4), date.substring(4,6)-1, date.substring(6,8)); - d.setHours(time[0]); - d.setMinutes(time[1]); - var diff=(d.getTime()-f.getTime()); - $scope.arr[a]['timedelta']=Math.floor(diff/60000); - if(diff < tooSoon*60000) { - $scope.arr.splice(a,1); - } - else if (diff < hurry*60000) { - $scope.arr[a]['hurry']=true; - } - } - } - function updateTime(){ + function update_clock() { $scope.clock = Date.now(); } - $scope.clock = Date.now(); + + $scope.$on('$destroy', function() { + $interval.cancel(load_interval); + $interval.cancel(clock_interval); + }); + + var load_interval = $interval(load, 5000); + var clock_interval = $interval(update_clock, 1000); + + update_clock(); load(); - var inter1=$interval(delOld,2000); - var inter2=$interval(load,10000); - var inter3=$interval(updateTime, 1000); } ); diff --git a/infoscreen/views/admin_views.py b/infoscreen/views/admin_views.py index 152859a..ff0bab6 100644 --- a/infoscreen/views/admin_views.py +++ b/infoscreen/views/admin_views.py @@ -20,7 +20,6 @@ from infoscreen.models import (ABBInfoItem, ExternalImageInfoItem, from infoscreen.models import EventInfoItem from infoscreen.models import ExternalWebsiteInfoItem from infoscreen.models import ImageUploadForm -from infoscreen.models import HSLDataModel from infoscreen.models import ApyInfoItem from infoscreen.models import VideoInfoItem diff --git a/infoscreen/views/public_views.py b/infoscreen/views/public_views.py index 461a547..2ee3e34 100644 --- a/infoscreen/views/public_views.py +++ b/infoscreen/views/public_views.py @@ -1,12 +1,14 @@ 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 infoscreen.models import Rotation, InfoItem, InfoInstance -from infoscreen.hsl_fetcher import HSLFetcher +from infoscreen.hsl_fetcher import fetch as hsl_fetch import json import logging +import threading @require_http_methods(["GET"]) @@ -82,22 +84,18 @@ def hsl_timetable_settings(request, *args, **kwargs): """Set HSL timetable settings.""" d = {"departure_threshold": settings.HSL_DEPARTURE_THRESHOLD, "hurry_threshold": settings.HSL_HURRY_THRESHOLD} - resp = json.dumps(d) - return HttpResponse(resp, status=200) + + return JsonResponse(d, status=200) @require_http_methods(["GET"]) def CurrentHSLView(request, *args, **kwargs): """Get HSL data and return it.""" - fetcher = HSLFetcher() - fetcherThread = threading.Thread(target=fetcher.fetch_if_needed, args=[]) - fetcherThread.setDaemon(False) - fetcherThread.start() + try: + api_resp = hsl_fetch() + except Exception as ex: + logging.exception('Failed to fetch HSL timetables.') + error = {'error': 'Aikataulujen haku epäonnistui.'} + return JsonResponse(error, status=200) - data = HSLDataModel.objects.all() - if len(data) < 1: - return HttpResponse( - '{"error" : "Could not find timetables from database."}', - status=500) - - return HttpResponse(data[len(data) - 1].data, status=200) + return JsonResponse(api_resp, status=200, safe=False) diff --git a/requirements.txt b/requirements.txt index bc58902..8ad339e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,8 +25,8 @@ dealer==2.0.5 django-modeltranslation==0.12.1 django-auditlog==0.4.3 django-phonenumber-field==1.3.0 -paho-mqtt==1.3.0 django-autocomplete-light==3.2.10 six==1.10.0 django-suit==0.2.25 telepot==12.3 +django-password-reset==1.0 diff --git a/sikweb/base.py b/sikweb/base.py index c094344..d57903b 100644 --- a/sikweb/base.py +++ b/sikweb/base.py @@ -78,13 +78,14 @@ INSTALLED_APPS = [ 'webapp', 'members', 'infoscreen', - 'coffee_scale.apps.CoffeeScaleConfig', + 'coffee_scale', 'rest_framework', 'django_nose', 'bootstrap3', 'django_tables2', 'auditlog', 'phonenumber_field', + 'password_reset', ] TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' diff --git a/sikweb/urls.py b/sikweb/urls.py index 2e0793c..ade52f0 100644 --- a/sikweb/urls.py +++ b/sikweb/urls.py @@ -26,6 +26,7 @@ import webapp.urls import infoscreen.urls import members.urls import coffee_scale.urls +import password_reset.urls urlpatterns = [ url(r'', include('webapp.urls')), @@ -35,6 +36,7 @@ urlpatterns = [ # admin url(r'^admin/', admin.site.urls), + url(r'^reset/', include('password_reset.urls')), # i18n default view for changing the active language url(r'^i18n/', include('django.conf.urls.i18n')), diff --git a/static/css/footer.css b/static/css/footer.css index bf63819..23fbc04 100644 --- a/static/css/footer.css +++ b/static/css/footer.css @@ -1,18 +1,8 @@ -#footer-div { - height:10vh; -} - footer { - /* position: absolute; */ - bottom: 0; - width: 100%; - height: 60px; /* Set the fixed height of the footer here */ - /* line-height: 60px; /* Vertically center the text there */ */ - margin-top: 2rem; - margin-bottom: 1rem; + background-color: #f5f5f5; } -footer .container .col .nav .nav-item { +/*footer .container .col .nav .nav-item { display: inline-block; margin-right: 3vh; } @@ -31,4 +21,4 @@ footer .container .col .nav .nav-item { footer .lang-form { margin: 1rem auto 0; -} +}*/ diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..5307718 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,36 @@ + + +{% load i18n %} +{% load static %} +{% load staticfiles %} + + + + + + + + + {% trans "Aalto-yliopiston Sähköinsinöörikilta ry" %} + + + + + + + + + + + + + + + {% block body %} + {% endblock %} + + diff --git a/templates/footer.html b/templates/footer.html index cb749a1..6d1ad0a 100644 --- a/templates/footer.html +++ b/templates/footer.html @@ -3,35 +3,69 @@ {% load staticfiles %} -