11 Commits

Author SHA1 Message Date
Aarni Halinen e83b4d4624 Add translations to password reset 2017-10-31 22:34:27 +02:00
Aarni Halinen 38225cabc8 Write password recovery emails in html 2017-10-31 22:34:27 +02:00
henu 08bb63ce1f Add margin to footer 2017-10-31 22:27:51 +02:00
henu 72a93e1dfd Merge branch 'develop' of sika.sahkoinsinoorikilta.fi:vtmk/web2.0 into feature-refactor-webapp-styles 2017-10-31 22:24:37 +02:00
henu fa5597f7cf Add centering back to navigation 2017-10-31 22:24:15 +02:00
henu 92ea427c53 Add copyright icon to footer 2017-10-31 22:23:36 +02:00
henu abc2519bc7 Add collapse property for navbar 2017-10-31 22:18:30 +02:00
henu 87a0c68ef2 Update meta tag 2017-10-31 22:17:44 +02:00
henu 612b17960a Make fa-icons larger and add padding between them 2017-10-31 22:16:52 +02:00
Aarni Halinen 7f21b7bba2 Add bootstrap4 to password recovery html 2017-10-31 21:37:19 +02:00
henu 8c116d58de Remove unnecessary comments 2017-10-31 21:17:36 +02:00
71 changed files with 1213 additions and 1604 deletions
-16
View File
@@ -1,16 +0,0 @@
#!/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
+1 -2
View File
@@ -76,8 +76,7 @@ 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
- 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\""
- ssh $DEV_SSH_USER@$DEV_SSH_HOST "cd deployment && docker-compose down && docker pull \"$IMAGE_NAME\" && docker-compose up -d && docker image prune -f"
deploy_production:
stage: deploy
+23
View File
@@ -0,0 +1,23 @@
from django.apps import AppConfig
import logging
import sys
from coffee_scale import mqtt
class CoffeeScaleConfig(AppConfig):
name = 'coffee_scale'
def ready(self):
if ('makemigrations' in sys.argv or 'migrate' in sys.argv):
return
try:
logging.info('Connecting to MQTT (coffee scale) at {}...'.format(mqtt.HOST))
logging.info('If there is no confirmation, the MQTT connection has probably failed.')
mqtt.client.connect_async(mqtt.HOST, mqtt.PORT, 60)
mqtt.client.loop_start()
except Exception as ex:
logging.error(ex)
logging.error('Failed to connect to MQTT at {}'.format(mqtt.HOST))
@@ -1,74 +0,0 @@
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from django.conf import settings
from coffee_scale.models import Brewing
import paho.mqtt.client as mqtt
import random
brewing = False
lastbrew = timezone.now()
lastcups = 0
def on_connect(client, userdata, flags, rc):
if rc != 0:
print("Failed to connect with result code: {}".format(rc))
print("Connected with result code: {}".format(rc))
client.subscribe(settings.MQTT_SETTINGS.HOST.TOPICS.CUPS)
client.subscribe(settings.MQTT_SETTINGS.HOST.TOPICS.BREWING)
client.subscribe(settings.MQTT_SETTINGS.HOST.TOPICS.BREW_TIME)
def on_message(client, userdata, message):
print("%s %s".format(message.topic, message.payload.decode()))
def on_message_cups(client, userdata, message):
cups = int(message.payload.decode())
print("cups: {}".format(cups))
print("{}".format(timezone.now()))
# checks if new coffee was brewed so we don't add the same brewing again to db
global lastcups # ;/ have to use global to store state instead of class
if cups > lastcups:
new_brew = Brewing(cups=cups, time=timezone.now())
print(new_brew.time)
new_brew.save()
lastcups = cups
def on_message_brewtime(client, userdata, message):
brewtime = datetime.fromtimestamp(int(message.payload.decode()))
print("brewtime: {}".format(brewtime))
def on_message_brewing(client, userdata, message):
brewing = bool(int(message.payload.decode()))
print("brewing: {}".format(brewing))
class Command(BaseCommand):
help = "Fetches coffee mqtt messages"
def add_arquments(self, parser):
pass
def handle(self, *args, **options):
self.username = "coffee-user-%d".format(random.randint(0, 100))
self.client = mqtt.Client("coffee")
self.client.username_pw_set(self.username, password=None)
self.client.tls_set()
# callbacks for different topics
self.client.message_callback_add(settings.MQTT_SETTINGS.HOST.TOPICS.BREW_TIME, on_message_brewtime)
self.client.message_callback_add(settings.MQTT_SETTINGS.HOST.TOPICS.CUPS, on_message_cups)
self.client.message_callback_add(settings.MQTT_SETTINGS.HOST.TOPICS.BREWING, on_message_brewing)
# self.client.connect("localhost", port=1883) # used for local testing
self.client.connect(settings.MQTT_SETTINGS.HOST, port=settings.MQTT_SETTINGS.PORT)
self.client.on_message = on_message
self.client.on_connect = on_connect
while True:
self.client.loop()
-24
View File
@@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-11-19 10:55
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Brewing',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cups', models.PositiveSmallIntegerField()),
('time', models.DateTimeField()),
],
),
]
+1 -4
View File
@@ -1,6 +1,3 @@
from django.db import models
class Brewing(models.Model):
cups = models.PositiveSmallIntegerField()
time = models.DateTimeField()
# Create your models here.
+58
View File
@@ -0,0 +1,58 @@
import paho.mqtt.client as mqtt
import logging
import datetime
from collections import deque
from django.conf import settings
HOST = settings.MQTT_SETTINGS['HOST']
PORT = settings.MQTT_SETTINGS['PORT']
TOPICS = settings.MQTT_SETTINGS['TOPICS']
latest = {}
def on_connect(client, userdata, flags, rc):
logging.info('Connected successfully to MQTT.')
logging.info('Subscribing to all topics on {}.'.format(HOST))
client.subscribe('sik/kiltahuone/kahvivaaka/#')
def update_latest(msg):
payload = msg.payload.decode('utf-8')
if msg.topic == TOPICS['WEIGHT']:
weight = float(payload)
latest['weight'] = weight
elif msg.topic == TOPICS['CUPS']:
cups = float(payload)
latest['cups'] = cups
elif msg.topic == TOPICS['BREWING']:
brewing = bool(int(payload))
latest['brewing'] = brewing
elif msg.topic == TOPICS['BREW_TIME']:
brew_time = datetime.datetime.fromtimestamp(float(payload))
latest['brew_time'] = brew_time
def on_message(client, userdata, msg):
try:
update_latest(msg)
except Exception as ex:
logging.exception('Failed to parse MQTT payload.')
def on_disconnect(client, userdata, rc):
if rc != 0:
logging.warning('MQTT unexpectedly disconnected.')
else:
client.loop_stop(force=False)
logging.warning('MQTT disconnected.')
def get_latest():
return latest
client = mqtt.Client()
client.on_connect = on_connect
client.on_message = on_message
client.on_disconnect = on_disconnect
+4 -7
View File
@@ -47,7 +47,7 @@ body {
background: green;
border-radius: 10px;
}
.brewtime{
#brewtime{
text-align:right;
position:absolute;
right:0px;
@@ -62,13 +62,10 @@ body {
font-size:4vw;
color: #333;
}
.layertwo{
display: None;
}
noscript{
color:red;
}
.text{
#text{
color:green;
position:absolute;
top:50%;
@@ -105,9 +102,9 @@ noscript{
}
@keyframes coffeeready {
0% {background-color:white;}
25% {background-color:rgb(100, 255, 100);}
25% {background-color:green;}
50% {background-color:white;}
75% {background-color:rgb(100, 255, 100);}
75% {background-color:green;}
100% {background-color:white;}
}
@keyframes unknown {
+112 -165
View File
@@ -1,183 +1,130 @@
//Inner state
var lastBrew = new Date(0);
var brewing = false;
var backoff = 2000;
var len = 0;
var lastBrew = "∞";
var brewtext = "";
//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);
}
$(document).ready(function(){
$('#text').bind("DOMSubtreeModified", resize);
updateTime();
setInterval(updateTime,1000);
formatBrewTime();
setInterval(formatBrewTime,10000);
});
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);
$(window).resize(resize);
function fetchdata(data, status){
if (typeof status !== 'undefined'){
if (status == "success"){
parseData(data);
}
else if (status == "error"){
handleError();
}
}
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);
$.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 updateTime(ev){
var now = ev.detail;
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 = "&infin;";
$("#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 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)
}
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(3);
$(".text").css({ top: s*0.16-font/2 + 'px',
fontSize: font + 'px',
marginLeft: -font*3*3/10 + 'px'});
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'});
}
// 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);
+2 -6
View File
@@ -10,16 +10,13 @@
<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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.2/mqttws31.js"
type="text/javascript"></script>
<link rel="stylesheet" href="/static/css/coffee.css" />
<script src="/static/js/coffee.js"></script>
</head>
<body>
<div id="container">
<span id="brewtime" class="brewtime layerone"></span>
<span class="brewtime layertwo">:)</span>
<span id="brewtime"></span>
<span id="address">
ka.dy.fi
<noscript><br>This page uses JavaScript!</noscript>
@@ -29,8 +26,7 @@
</div>
<!--Kahvinkeitin on rikki. Varakeittimellä keitettyä kahvia saattaa olla.-->
<div id="lower" class="normal">
<div id="text" class="text layerone">???</div>
<div class="text layertwo">&nbsp;+</div>
<div id="text"></div>
<div id="scale"><div id="scale2"></div></div>
</div>
</div>
+28
View File
@@ -1,2 +1,30 @@
from django.test import TestCase, Client
from django.conf import settings
from coffee_scale.mqtt import on_message
HOST = settings.MQTT_SETTINGS['HOST']
PORT = settings.MQTT_SETTINGS['PORT']
TOPICS = settings.MQTT_SETTINGS['TOPICS']
class MQTTTestCase(TestCase):
"""Tests MQTT functionality"""
class MockMessage:
def __init__(self, payload, topic):
self.payload = payload
self.topic = topic
def setUp(self):
payload = '10'.encode('utf-8')
topic = TOPICS['CUPS']
msg = MQTTTestCase.MockMessage(payload, topic)
on_message(None, None, msg)
self.c = Client()
def test_receive_cups(self):
response = self.c.get('/coffee/cups')
payload = response.json()
self.assertEquals(payload['cups'], 10)
+3 -1
View File
@@ -1,7 +1,7 @@
from django.conf.urls import url
from django.views.generic.base import RedirectView
from .views import coffee_view
from .views import coffee_view, cups_view
favicon_view = RedirectView.as_view(url='static/img/favicon.ico', permanent=True)
@@ -9,4 +9,6 @@ urlpatterns = [
# landing page
url(r'^$', coffee_view),
url(r'^cups', cups_view),
]
+16
View File
@@ -3,9 +3,25 @@ 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)
+1 -1
View File
@@ -5,7 +5,7 @@ services:
image: postgres
web:
build: .
image: git.sahkoinsinoorikilta.fi:4567/vtmk/web2.0
image: 86.50.143.82:5000/web20
command: ["bash", "-c", "cd /code && ./wait-for-it.sh db:5432 -- bash setup.sh --no-input --no-npm && python manage.py runserver 0.0.0.0:8080"]
ports:
- "8080:8080"
-45
View File
@@ -1,45 +0,0 @@
# 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.
- Signup may be attached to an event
- multiple signups to a single event?
- 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?
- NOTE: Quota related info is exposed if any info is printed
View File
-3
View File
@@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.
-5
View File
@@ -1,5 +0,0 @@
from django.apps import AppConfig
class IlmotunkkiConfig(AppConfig):
name = 'ilmotunkki'
View File
-16
View File
@@ -1,16 +0,0 @@
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)
-3
View File
@@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.
-3
View File
@@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.
-2
View File
@@ -5,7 +5,6 @@ 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 CoffeeStatsInfoItem
# Register your models here.
admin.site.register(Rotation)
@@ -16,4 +15,3 @@ admin.site.register(ABBInfoItem)
admin.site.register(InfoInstance)
admin.site.register(ExternalWebsiteInfoItem)
admin.site.register(VideoInfoItem)
admin.site.register(CoffeeStatsInfoItem)
+1 -2
View File
@@ -47,7 +47,7 @@ def fetch():
route = place['pattern']['route']['shortName']
stop_times = place['_stoptimes']
for stop_time in stop_times:
timestamp = time_utc = stop_time['serviceDay'] + stop_time['realtimeArrival']
timestamp = stop_time['serviceDay'] + stop_time['realtimeArrival']
headsign = stop_time['stopHeadsign']
stop_name = stop_time['stop']['name']
time_diff = (timestamp - timezone.now().timestamp()) / 60 # minutes
@@ -64,7 +64,6 @@ def fetch():
'headsign': headsign,
'timestamp': time,
'stop': stop_name,
'utc': time_utc,
})
return schedule
@@ -1,23 +0,0 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-11-19 14:41
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('infoscreen', '0006_delete_hsldatamodel'),
]
operations = [
migrations.CreateModel(
name='CoffeeStatsInfoItem',
fields=[
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
],
bases=('infoscreen.infoitem',),
),
]
-15
View File
@@ -269,21 +269,6 @@ class HslInfoItem(InfoItem):
return "/static/html/hsl_create.html"
class CoffeeStatsInfoItem(InfoItem):
"""Class for Coffee statistics Infoscreen item."""
display_name = _("Coffee statistics")
def get_template_url(self):
"""Return HSL infoitem template url."""
return "/static/html/coffee_stats.html"
@staticmethod
def get_create_template_url():
"""Call create HSL infoitem template url command."""
return "/static/html/coffee_stats_create.html"
class ExternalImageInfoItem(InfoItem):
"""Class for External Image Infoscreen item."""
-9
View File
@@ -1,9 +0,0 @@
<link rel="stylesheet" href="/static/css/hsl.css">
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/locale/fi.js"></script>
<script type="text/javascript" src="dygraph.js"></script>
<link rel="stylesheet" src="dygraph.css" /></head>
<div class="container" ng-controller="CoffeeStatsController">
<div id="div_g"></div>
</div>
@@ -1,11 +0,0 @@
<div ng-controller="infoadmin_coffeestatsitem_create" style="margin-top:20px;">
<div>
Create new item to show coffee statistics. Name is used only as identifier
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
<!--maybe later add option to choose data range, daily, monthly...etc.
+1 -1
View File
@@ -25,7 +25,7 @@
</tr>
</thead>
<tbody>
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['utc'] | limitTo: 11">
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['timestamp'] | limitTo: 11">
<td style="min-width: 300px">
{{x.timestamp}}
</td>
@@ -186,6 +186,5 @@ var simple_controllers = [
"hslitem",
"websiteitem",
"apyitem",
"coffeestatsitem",
];
_.each(simple_controllers, controllerGenerator);
+1 -15
View File
@@ -142,19 +142,5 @@ app.controller('timetableCtrl',
update_clock();
load();
});
app.controller('CoffeeStatsController', function($scope, $http) {
function load() {
$http.get('/infoscreen/coffee_data')
.then(function(response) {
const g = new Dygraph(document.getElementById('div_g'), response.data, {
drawPoints: true,
valueRange: [0, 10],
labels: ['Time', 'Cups']
});
});
}
load();
});
);
-4
View File
@@ -26,8 +26,6 @@ from infoscreen.views import CurrentHSLView
from infoscreen.views import createApyItem
from infoscreen.views import hsl_timetable_settings
from infoscreen.views import get_apy_json
from infoscreen.views import createCoffeeStatsItem
from infoscreen.views import CoffeeStatsView
urlpatterns = [
url(r'^$', default),
@@ -47,7 +45,6 @@ urlpatterns = [
url(r'^create_sossoitem$', createSossoItem),
url(r'^create_eventitem$', createEventItem),
url(r'^create_hslitem$', createHslItem),
url(r'^create_coffeestatsitem$', createCoffeeStatsItem),
url(r'^create_apyitem$', createApyItem),
url(r'^create_websiteitem$', createExternalWebsiteItem),
url(r'^create_rotation$', create_rotation),
@@ -55,6 +52,5 @@ urlpatterns = [
url(r'^hsl_data$', CurrentHSLView),
url(r'^hsl_data/settings$', hsl_timetable_settings),
url(r'^apyjson', get_apy_json),
url(r'^coffee_data$', CoffeeStatsView),
]
-2
View File
@@ -22,7 +22,6 @@ from infoscreen.models import ExternalWebsiteInfoItem
from infoscreen.models import ImageUploadForm
from infoscreen.models import ApyInfoItem
from infoscreen.models import VideoInfoItem
from infoscreen.models import CoffeeStatsInfoItem
@login_required(login_url='/login')
@@ -189,4 +188,3 @@ createExternalWebsiteItem = create_item_generator(ExternalWebsiteInfoItem)
createEventItem = create_item_generator(EventInfoItem)
createApyItem = create_item_generator(ApyInfoItem)
createVideoItem = create_item_generator(VideoInfoItem)
createCoffeeStatsItem = create_item_generator(CoffeeStatsInfoItem)
-15
View File
@@ -2,11 +2,8 @@ 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.utils import timezone
from django.db.models.functions import ExtractWeek
from infoscreen.models import Rotation, InfoItem, InfoInstance
from coffee_scale.models import Brewing
from infoscreen.hsl_fetcher import fetch as hsl_fetch
import json
@@ -102,15 +99,3 @@ def CurrentHSLView(request, *args, **kwargs):
return JsonResponse(error, status=200)
return JsonResponse(api_resp, status=200, safe=False)
@require_http_methods(["GET"])
def CoffeeStatsView(request, *args, **kwargs):
# stats_data = Brewing.objects.annotate(
# week=ExtractWeek('time')).values('week').get(time__week=timezone.now().isocalendar()[2])
# do filtering here based on the data we want to show
stats_data = {}
for entry in Brewing.objects.all():
stats_data[entry.id] = {'cups': entry.cups, 'time': entry.time}
return JsonResponse(stats_data, status=200, safe=False)
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
+1 -5
View File
@@ -46,7 +46,7 @@ class MemberForm(forms.ModelForm):
@staticmethod
def csv_to_models(data, payment_source='AYY', delimiter=','):
clean_data = data.strip().split('\n')
clean_data = [row.rstrip(',').rstrip('\r').strip() for row in clean_data]
clean_data = [row.rstrip(',') for row in clean_data]
csv_reader = csv.DictReader(clean_data, fieldnames=MemberForm.Meta.fields, delimiter=delimiter, quoting=csv.QUOTE_NONE)
members = []
@@ -122,7 +122,3 @@ class ApplicationForm(forms.ModelForm):
self.fields['AYY'].label = _("I'm a member of AYY")
self.fields['jas'].label = _("I want to receive a weekly newsletter")
class UploadFileForm(forms.Form):
file = forms.FileField()
-26
View File
@@ -1,26 +0,0 @@
from import_export import resources
from .models import Member, Payment, Request
class MemberResource(resources.ModelResource):
class Meta:
model = Member
exclude = ['id', 'created']
class PaymentResource(resources.ModelResource):
member = resources.Field()
class Meta:
model = Payment
exclude = ['id']
def dehydrate_member(self, payment):
return '{} {}'.format(payment.member.first_name, payment.member.last_name)
class ApplicationResource(resources.ModelResource):
class Meta:
model = Request
exclude = ['id']
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

-4
View File
@@ -15,10 +15,6 @@
{% endif %}
{{ table|safe }}
<div>
<a href="/members/export_applications" class="btn btn-info">{% trans "Download Excel" %}</a>
</div>
</div>
{% endblock content %}
+17 -24
View File
@@ -1,7 +1,7 @@
{% extends "members_base.html" %}
{% load i18n %}
{% load static %}
{% block content %}
<div>
<div>
@@ -11,28 +11,24 @@
<div>
<p>
{% blocktrans %}
Enter member information in CSV format, separate members on separate lines.
If a new member already exists in the database, a new payment event will be created for that member instead.
Enter member information in CSV format, separate members on separate lines.
{% endblocktrans %}
</p>
</div>
<p>
{% blocktrans %}
first_name, last_name, email_address and place_of_origin should be given string values.
ayy_member and jas_recipient should be given the value 0 (off) or 1 (on).
{% endblocktrans %}
</p>
<h4>{% trans "Syntax" %}</h4>
<pre>first_name, last_name, email_address, place_of_origin, ayy_member, jas_recipient</pre>
<div>
<label>{% trans "Format the member table like this:" %}</label>
<div>
<img src="{% static "img/excel_csv_save_example.png" %}">
</div>
<form name="memberTextForm" action="/members/import_csv" method="POST">{% csrf_token %}
<div class="form-group">
<label>{% trans "Data" %}</label>
<textarea name="textfield" class="form-control large-textarea" placeholder="Teemu, Teekkari, teemu.teekkari@notmail.dom, Otaniemi, 0, 0"></textarea>
</div>
<p>{% blocktrans %}Columns: First name, last name, email address, place of origin, AYY member, JAS recipient{% endblocktrans %}</p>
</div>
<div>
<label>{% trans "Save the file as CSV" %}</label>
<div><img src="{% static "img/excel_csv_save_tutorial.png" %}"></div>
</div>
<form name="memberTextForm" action="/members/import_csv" enctype="multipart/form-data" method="POST">{% csrf_token %}
<h3>{% trans "Upload file" %}</h3>
<input class="form-control-file" type="file" accept=".csv" name="csvFile" />
<div class="form-group">
<label>{% trans "Payment source" %}</label>
<select name="payment_source" class="form-control">
@@ -40,16 +36,13 @@
<option value="bank_transfer">{% trans "Bank transfer" %}</option>
<option value="cash">{% trans "Cash payment" %}</option>
</select>
<small class="form-text text-muted">
{% trans "This payment source will be used to create any payments for new members that already exist in the database." %}
</small>
</div>
<div class="form-group">
<label>{% trans "CSV delimiter" %}</label>
<input type="text" name="delimiter" class="form-control" value=";" />
<small class="form-text text-muted">
<p class="form-text text-muted">
{% blocktrans %}The symbol that is used to separate items in one line. Defaults to ';' (semicolon).{% endblocktrans %}
</small>
</p>
</div>
<div>
<button type="submit" class="btn btn-primary">{% trans "Send" %}</button>
+1 -1
View File
@@ -41,7 +41,7 @@
{{ table|safe }}
<div>
<a href="/members/export_members" class="btn btn-info">{% trans "Download Excel" %}</a>
<a href="/members/export_csv" class="btn btn-info">{% trans "Download CSV" %}</a>
</div>
</div>
{% endblock content %}
-4
View File
@@ -36,9 +36,5 @@
{% endif %}
{{ table|safe }}
<div>
<a href="/members/export_payments" class="btn btn-info">{% trans "Download Excel" %}</a>
</div>
</div>
{% endblock content %}
-10
View File
@@ -1,10 +0,0 @@
{% extends "members_base.html" %}
{% block content %}
<h1>{{ title }}</h1>
<h3>{{ header }}</h3>
<form method="POST" action="/members/import_excel" enctype="multipart/form-data">{% csrf_token %}
{{ form }}
<input type="submit" class="btn btn-primary">
</form>
{% endblock %}
@@ -1,3 +0,0 @@
Testi;Ukkeli;testi@ukkeli.fi;Espoo;1;0
Jäbä;Kakkeli;jaba@kakkeli.fi;Hamina;0;1
Kolmas;Kaveri;kolmas@kaveri.com;Mesta;1;1
1 Testi Ukkeli testi@ukkeli.fi Espoo 1 0
2 Jäbä Kakkeli jaba@kakkeli.fi Hamina 0 1
3 Kolmas Kaveri kolmas@kaveri.com Mesta 1 1
@@ -1 +0,0 @@
Testi;Ukkeli;testi@ukkeli.fi;Espoo;1;0
1 Testi Ukkeli testi@ukkeli.fi Espoo 1 0
+7 -73
View File
@@ -3,12 +3,10 @@
from django.test import TestCase, Client
from django.contrib.auth.models import User
from members.management.commands.createsahkopiikkiuser import Command as SahkopiikkiCommand
from members.models import Member, Payment, Request
from members.models import Member
from rest_framework.authtoken.models import Token
import logging
import os
import pyexcel
class MemberRegisterTestCase(TestCase):
@@ -16,13 +14,7 @@ class MemberRegisterTestCase(TestCase):
def setUp(self):
"""Setup testing environment by creating member and admin."""
memb = Member.objects.create(first_name="Tidus", last_name="Tester", email="tidus@tester.fi")
payment = Payment.objects.create(member=memb, source='AYY')
appl = Request.objects.create(
first_name="Liisa", last_name="Mattila",
email="liisa.mattila@pylly.com", POR="Kouvola",
AYY=True, jas=False)
memb = Member.objects.create(first_name="Tidus", last_name="Tester")
username, password = 'test_admin', 'password123'
test_admin = User.objects.create_superuser(
username, 'myemail@test.com', password)
@@ -39,27 +31,16 @@ class MemberRegisterTestCase(TestCase):
def test_import_csv_single_line(self):
"""Test csv import only with single line in csv file."""
current_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(current_dir, 'test_resources', 'single_line_import.csv')) as csvFile:
response = self.c.post('/members/import_csv', {
'csvFile': csvFile,
'delimiter': ';',
'payment_source': 'AYY'
}, follow=True)
data = 'Teppo, Tulppu, teppo@tulppu.fi, Ankkalinna, 0, 0'
response = self.c.post('/members/import_csv', {'textarea': data}, follow=True)
self.assertEqual(response.status_code, 200)
def test_import_csv_multi_line(self):
"""Test csv import with multilined csv."""
data = ('Teppo, Tulppu, teppo@tulppu.fi, Ankkalinna, 0, 0\n'
'Reiska, Remontti, remontti@reiska.fi, Värisilmä, 1, 1')
current_dir = os.path.dirname(os.path.abspath(__file__))
with open(os.path.join(current_dir, 'test_resources', 'multi_line_import.csv')) as csvFile:
response = self.c.post('/members/import_csv', {
'csvFile': csvFile,
'delimiter': ';',
'payment_source': 'AYY'
}, follow=True)
response = self.c.post('/members/import_csv', {'textarea': data}, follow=True)
self.assertEqual(response.status_code, 200)
def test_autocomplete_search_found(self):
@@ -106,50 +87,3 @@ class MemberRegisterTestCase(TestCase):
response = self.c.get('/members/check?email={}'.format(email), follow=True)
self.assertEqual(response.status_code, 401)
def test_export_members_excel(self):
"""Test if the user can download an excel file of the member register"""
resp = self.c.get('/members/export_members')
content_type = 'application/vnd.ms-excel'
self.assertIn(content_type, resp['Content-Type'])
content = resp.content
arrays = pyexcel.get_array(file_content=content, file_type='xlsx')
tidus_array = ['Tidus', 'Tester', 'tidus@tester.fi', '', '0', '0']
self.assertIn(tidus_array, arrays)
def test_export_payments_excel(self):
"""Test if the user can download an excel file of the payment register"""
resp = self.c.get('/members/export_payments')
content_type = 'application/vnd.ms-excel'
self.assertIn(content_type, resp['Content-Type'])
content = resp.content
arrays = pyexcel.get_array(file_content=content, file_type='xlsx')
created = Payment.objects.get(member__email='tidus@tester.fi').date.strftime('%Y-%m-%d %H:%M:%S')
tidus_array = ['Tidus Tester', created, 'AYY']
self.assertIn(tidus_array, arrays)
def test_export_applications_excel(self):
"""Test if the user can download an excel file of the member application register"""
resp = self.c.get('/members/export_applications')
content_type = 'application/vnd.ms-excel'
self.assertIn(content_type, resp['Content-Type'])
content = resp.content
arrays = pyexcel.get_array(file_content=content, file_type='xlsx')
submitted = Request.objects.get(email='liisa.mattila@pylly.com').submitted.strftime('%Y-%m-%d %H:%M:%S')
liisa_array = ['Liisa', 'Mattila', 'liisa.mattila@pylly.com', 'Kouvola', '1', '0', submitted]
self.assertIn(liisa_array, arrays)
def test_submit_member_application(self):
"""Test if submitting a member application works"""
data = {
'first_name': 'Seppo', 'last_name': 'Saastamoinen',
'email': 'seppo@saastamoin.en', 'jas': 'on',
'POR': 'Dipolin viinibaari'
}
resp = self.c.post('/members/submit_application', data=data)
self.assertEqual(resp.status_code, 200)
self.assertTrue(Request.objects.filter(email='seppo@saastamoin.en').exists())
+3 -8
View File
@@ -7,7 +7,7 @@ from django.views.generic.base import RedirectView
# members
from members.views import member_list, payment_add, payment_submit
from members.views import application_delete_confirm, application_delete
from members.views import application_accept, import_csv
from members.views import application_accept, import_csv, export_csv
from members.views import settings_page, payment_edit
from members.views import payment_delete_confirm
from members.views import payment_delete, payment_update
@@ -20,9 +20,6 @@ from members.views import member_delete_confirm
from members.views import member_delete
from members.views import payment_list
from members.views import add_many_confirm
from members.views import export_members_excel
from members.views import export_payments_excel
from members.views import export_applications_excel
# autocomplete view
from members.views import MemberAutoComplete
@@ -111,10 +108,8 @@ urlpatterns = [
# send CSV member data by POST
url(r'^import_csv', import_csv),
# export members as excel file
url(r'export_members', export_members_excel),
url(r'export_payments', export_payments_excel),
url(r'export_applications', export_applications_excel),
# download CSV member data
url(r'^export_csv', export_csv),
# favourite icon
url(r'^favicon\.ico$', favicon_view),
+25 -12
View File
@@ -14,7 +14,6 @@ import html
from members.views.utils import *
from members.tables import RequestTable
from members.forms import ApplicationForm
from members.views import error_view
@ensure_csrf_cookie
@@ -48,7 +47,8 @@ def application_edit(request, *args, **kwargs):
"""Edit member request information."""
i = kwargs.pop('index', None)
if i is None:
return error_view(request, _('No application id specified'))
return render(
request, 'error.html', {'error': _('No application id specified')})
else:
application = Request.objects.get(id=i)
form = ApplicationForm(instance=application)
@@ -68,7 +68,9 @@ def application_accept(request, *args, **kwargs):
if id is not None:
application = Request.objects.get(id=id)
else:
return error_view(request, _("Application missing 'id' field."))
return render(request,
'error.html',
{'error': _("Application missing 'id' field.")})
form = ApplicationForm(request.POST, instance=application)
if form.is_valid():
@@ -76,9 +78,9 @@ def application_accept(request, *args, **kwargs):
application = form.save()
if Member.objects.filter(email=application.email).exists():
return error_view(request, _(
'Email {} is already in use by a member. Application cannot be accepted.'
).format(application.email))
return render(request,
'error.html',
{'error': _('Email {} is already in use by a member. Application cannot be accepted.').format(application.email)})
member = application.to_member()
member.save()
@@ -94,10 +96,14 @@ def application_accept(request, *args, **kwargs):
'/members/list?notification={}'.format(html.escape(notification)))
except Exception as ex:
logging.exception('Exception while accepting application')
return error_view(request, str(ex))
return render(request,
'error.html',
{'error': str(ex)})
else:
logging.info(form)
return error_view(request, form.errors)
return render(request,
'error.html',
{'error': form.errors})
@ensure_csrf_cookie
@@ -109,7 +115,8 @@ def application_delete(request, *args, **kwargs):
try:
id = request.POST['id']
except KeyError:
return error_view(request, _('No application id specified'))
return render(
request, 'error.html', {'error': _('No application id specified')})
try:
application = Request.objects.get(id=id)
@@ -123,7 +130,9 @@ def application_delete(request, *args, **kwargs):
'/members/applications?notification={}'
.format(html.escape(notification)))
except:
return error_view(request, _('Could not delete application object'))
return render(request,
'error.html',
{'error': _('Could not delete application object')})
@ensure_csrf_cookie
@@ -134,7 +143,9 @@ def application_delete_confirm(request, *args, **kwargs):
"""Confirm application deletion."""
i = kwargs.pop('index', None)
if i is None:
return error_view(request, _('No application id specified'))
return render(request,
'error.html',
{'error': _('No application id specified')})
else:
application = Request.objects.get(id=i)
form = ApplicationForm(instance=application)
@@ -161,4 +172,6 @@ def application_submit(request, *args, **kwargs):
form.save()
return render(request, 'application_success.html', {})
else:
return error_view(request, form.errors)
return render(request,
'error.html',
{'error': form.errors})
+18 -11
View File
@@ -70,7 +70,8 @@ def member_delete_confirm(request, *args, **kwargs):
"""Render member deletion confirmation page."""
i = kwargs.pop('index', None)
if i is None:
return error_view(request, _('No member id specified'))
return render(request, 'error.html',
{'error': _('No member id specified')})
else:
member = Member.objects.get(id=i)
form = MemberForm(instance=member)
@@ -93,15 +94,11 @@ def member_add_many(request, *args, **kwargs):
@permission_required('members.add_member', raise_exception=True)
def add_many_confirm(request, *args, **kwargs):
models = request.session['models']
payment_source = request.session['payment_source']
try:
members, payments = models.members, models.payments
for member in members:
member.save()
if not member.payments.exists(): # create payment for new members
payment = Payment.objects.create(member=member, source=payment_source)
for payment in payments:
payment.save()
@@ -132,7 +129,7 @@ def member_submit(request, *args, **kwargs):
return HttpResponseRedirect(
'/members/list?notification={}'.format(html.escape(notification)))
else:
return error_view(request, form.errors)
return render(request, 'error.html', {'error': form.errors})
@ensure_csrf_cookie
@@ -146,7 +143,10 @@ def member_update(request, *args, **kwargs):
if id is not None:
member = Member.objects.get(id=id)
else:
return error_view(request, _("Member missing 'id' field."))
return render(request,
'error.html',
{'error': _("Member missing 'id' field.")})
logging.debug(member)
form = MemberForm(request.POST, instance=member)
if form.is_valid():
@@ -160,7 +160,10 @@ def member_update(request, *args, **kwargs):
return HttpResponseRedirect(
'/members/list?notification={}'.format(html.escape(notification)))
else:
return error_view(request, form.errors)
return render(
request,
'error.html',
{'error': form.errors})
@ensure_csrf_cookie
@@ -172,7 +175,8 @@ def member_delete(request, *args, **kwargs):
try:
id = request.POST['id']
except KeyError:
return error_view(request, _('No member id specified'))
return render(request,
'error.html', {'error': _('No member id specified')})
try:
member = Member.objects.get(id=id)
@@ -185,7 +189,9 @@ def member_delete(request, *args, **kwargs):
return HttpResponseRedirect(
'/members/list?notification={}'.format(html.escape(notification)))
except:
return error_view(request, _('Could not delete member object'))
return render(request,
'error.html',
{'error': _('Could not delete member object')})
@ensure_csrf_cookie
@@ -196,7 +202,8 @@ def member_edit(request, *args, **kwargs):
"""Edit member information."""
i = kwargs.pop('index', None)
if i is None:
return error_view(request, _('No member id specified'))
return render(
request, 'error.html', {'error': _('No member id specified')})
else:
member = Member.objects.get(id=i)
form = MemberForm(instance=member)
+16 -7
View File
@@ -14,7 +14,6 @@ import html
from members.views.utils import *
from members.tables import PaymentTable
from members.forms import PaymentForm
from members.views import error_view
@ensure_csrf_cookie
@@ -74,7 +73,7 @@ def payment_submit(request, *args, **kwargs):
'/members/payments?notification={}'
.format(html.escape(notification)))
else:
return error_view(request, form.errors)
return render(request, 'error.html', {'error': form.errors})
@ensure_csrf_cookie
@@ -85,7 +84,9 @@ def payment_edit(request, *args, **kwargs):
"""Edit payment."""
i = kwargs.pop('index', None)
if i is None:
return error_view(request, _('No payment id specified'))
return render(request,
'error.html',
{'error': _('No payment id specified')})
else:
payment = Payment.objects.get(id=i)
form = PaymentForm(instance=payment)
@@ -102,7 +103,9 @@ def payment_delete_confirm(request, *args, **kwargs):
"""Render payment delete confirmation page."""
i = kwargs.pop('index', None)
if i is None:
return error_view(request, _('No payment id specified'))
return render(request,
'error.html',
{'error': _('No payment id specified')})
else:
payment = Payment.objects.get(id=i)
form = PaymentForm(instance=payment)
@@ -120,7 +123,9 @@ def payment_delete(request, *args, **kwargs):
try:
id = request.POST['id']
except KeyError:
return error_view(request, _('No payment id specified'))
return render(request,
'error.html',
{'error': _('No payment id specified')})
try:
payment = Payment.objects.get(id=id)
@@ -133,7 +138,9 @@ def payment_delete(request, *args, **kwargs):
'/members/payments?notification={}'
.format(html.escape(notification)))
except:
return error_view(request, _('Could not delete payment object'))
return render(request,
'error.html',
{'error': _('Could not delete payment object')})
@ensure_csrf_cookie
@@ -158,4 +165,6 @@ def payment_update(request, *args, **kwargs):
'/members/payments?notification={}'
.format(html.escape(notification)))
else:
return error_view(request, _('Could not update payment object'))
return render(request,
'error.html',
{'error': _('Could not update payment object')})
+30 -35
View File
@@ -2,7 +2,7 @@ from django.shortcuts import render
from django.contrib.auth.decorators import permission_required, login_required
from django.views.decorators.http import require_http_methods
from django.views.decorators.csrf import ensure_csrf_cookie
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
from django.http import HttpResponse, HttpResponseRedirect
from django.core.mail import send_mail
from django.conf import settings
from django.utils.translation import ugettext as _
@@ -20,9 +20,8 @@ from rest_framework import generics
from rest_framework import permissions
from members.models import Member, Request, Payment
from members.forms import MemberForm, PaymentForm, ApplicationForm, CSVValidationError, UploadFileForm
from members.forms import MemberForm, PaymentForm, ApplicationForm, CSVValidationError
from members.tables import MemberTable, PaymentTable, RequestTable
from members.resources import MemberResource, PaymentResource, ApplicationResource
# Can be used to retrieve single member information via REST API
@@ -35,8 +34,8 @@ class MemberDetail(generics.RetrieveAPIView):
throttle_classes = (BurstRateThrottle, SustainedRateThrottle, )
def error_view(request, message, status=400):
return render(request, 'error.html', {'error': message}, status=400)
def error_view(request, message):
return render(request, 'error.html', {'error': str(message)})
def validate_recaptcha(response):
@@ -109,14 +108,13 @@ def settings_page(request, *args, **kwargs):
def import_csv(request, *args, **kwargs):
"""Get csv data imported to page and create members based on that."""
try:
csv_in_memory_file = request.FILES.get('csvFile')
csv_file = csv_in_memory_file.file
data = csv_file.read().decode('utf-8')
data = request.POST['textfield']
delimiter = request.POST.get('delimiter', ',')
payment_source = request.POST['payment_source']
except:
return error_view(request, _('Missing CSV file'))
return render(request,
'error.html',
{'error': _('Missing "textfield" POST request field')})
try:
result = MemberForm.csv_to_models(data, payment_source=payment_source, delimiter=delimiter)
@@ -125,7 +123,7 @@ def import_csv(request, *args, **kwargs):
return error_view(request, ex.form_errors)
except Exception as ex:
logging.exception('Other error in CSV import')
return error_view(request, str(ex))
return error_view(request, ex)
member_table = MemberTable(result.members,
request=request,
@@ -144,7 +142,6 @@ def import_csv(request, *args, **kwargs):
payment_table_html = convert_table_to_html(payment_table, request)
request.session['models'] = result
request.session['payment_source'] = payment_source
context = {
'members': member_table_html,
'payments': payment_table_html
@@ -152,6 +149,27 @@ def import_csv(request, *args, **kwargs):
return render(request, 'member_add_many_confirm.html', context)
@ensure_csrf_cookie
@require_http_methods(["GET"])
@permission_required('members.read_member', login_url='/login', raise_exception=True)
def export_csv(request, *args, **kwargs):
"""Export members as csv."""
response = HttpResponse()
response['Content-type'] = 'text/csv'
response['Accept'] = 'text/csv'
response['Content-Disposition'] = 'filename; filename=members.csv'
writer = csv.writer(response, csv.excel)
# BOM (optional...Excel needs it to open UTF-8 file properly)
response.write(u'\ufeff'.encode('utf8'))
for obj in Member.objects.all():
data = obj.as_array()
field_list = list(map(lambda d: str(d), data))
writer.writerow(field_list)
return response
def send_mail_wrapper(subject, message, email_to):
"""Send mail to default email."""
send_mail(subject,
@@ -159,26 +177,3 @@ def send_mail_wrapper(subject, message, email_to):
settings.DEFAULT_EMAIL_FROM,
[email_to],
fail_silently=False)
def make_excel_response(Resource):
res = Resource()
dataset = res.export()
response = HttpResponse(dataset.xlsx, content_type='application/vnd.ms-excel; charset=utf-8')
response['Content-Disposition'] = 'attachment; filename="export.xlsx"'
return response
@require_http_methods(['GET'])
def export_members_excel(request, *args, **kwargs):
return make_excel_response(MemberResource)
@require_http_methods(['GET'])
def export_payments_excel(request, *args, **kwargs):
return make_excel_response(PaymentResource)
@require_http_methods(['GET'])
def export_applications_excel(request, *args, **kwargs):
return make_excel_response(ApplicationResource)
+1 -4
View File
@@ -14,14 +14,11 @@
"author": "SIK ry",
"license": "ISC",
"dependencies": {
"dygraphs": "^2.0.0",
"eslint": "3.19.0",
"remark-cli": "^4.0.0",
"remark-preset-lint-recommended": "^3.0.1"
},
"remarkConfig": {
"plugins": [
"remark-preset-lint-recommended"
]
"plugins": ["remark-preset-lint-recommended"]
}
}
+2 -4
View File
@@ -19,18 +19,16 @@ django-nose==1.4.4
nose-exclude==0.5.0
psycopg2==2.7.1
django-bootstrap3==8.2.3
django-bootstrap4==0.0.4
django-tables2==1.6.1
pycodestyle==2.3.1
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-import-export==0.5.1
django-password-reset==1.0
pyexcel==0.5.6
pyexcel-xlsx==0.5.2
paho-mqtt==1.3.1
+2 -4
View File
@@ -78,19 +78,17 @@ INSTALLED_APPS = [
'webapp',
'members',
'infoscreen',
'coffee_scale',
'coffee_scale.apps.CoffeeScaleConfig',
'rest_framework',
'django_nose',
'bootstrap3',
'bootstrap4',
'django_tables2',
'auditlog',
'phonenumber_field',
'import_export',
'password_reset',
]
IMPORT_EXPORT_USE_TRANSACTIONS = True
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = [
+3 -19
View File
@@ -1,24 +1,8 @@
footer {
background-color: #f5f5f5;
margin-top: 1vh;
}
/*footer .container .col .nav .nav-item {
display: inline-block;
margin-right: 3vh;
.ml-auto .nav-item {
padding: 1vh;
}
.lang-button {
height: 4vh;
width: 6vh;
margin-left: 1vh;
margin-right: 1vh;
}
.lang-select {
width: 10rem;
display: inline-block;
}
footer .lang-form {
margin: 1rem auto 0;
}*/
+1 -1
View File
@@ -7,7 +7,7 @@
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="Aalto-yliopiston Sähköinsinöörikilta ry">
<meta name="author" content="Aalto-yliopiston Sähköinsinöörikilta ry">
+4 -32
View File
@@ -7,7 +7,7 @@
<div class="container">
<div class="d-flex align-items-center justify-content-end">
<div class="p-2">
<span>{% trans "Copyright Aalto-yliopiston Sähköinsinöörikilta ry" %} {% now 'Y' %}</span>
<span><i class="fa fa-copyright"></i>{% trans "Aalto-yliopiston Sähköinsinöörikilta ry" %} {% now 'Y' %}</span>
</div>
<div class="p-2">
<form class="lang-form form" action="{% url 'set_language' %}" method="post">{% csrf_token %}
@@ -28,44 +28,16 @@
</div>
<div class="ml-auto p-2">
<span class="nav-item">
<a href="/members"><i class="fa fa-group"></i></a>
<a href="/members"><i class="fa fa-group fa-2x"></i></a>
</span>
<span class="nav-item">
<a href="/infoscreen"><i class="fa fa-info"></i></a>
<a href="/infoscreen"><i class="fa fa-info fa-2x"></i></a>
</span>
<span class="nav-item">
<a href="/admin"><i class="fa fa-gears"></i></a>
<a href="/admin"><i class="fa fa-gears fa-2x"></i></a>
</span>
</div>
</div>
</div>
</footer>
<!-- <form class="lang-form form" action="{% url 'set_language' %}" method="post">{% csrf_token %}
<span>
<input name="next" type="hidden" value="{{ redirect_to }}" />
<select onchange="this.form.submit()" class="lang-select form-control" name="language">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
</span>
</form>
<span>{% trans "Copyright Aalto-yliopiston Sähköinsinöörikilta ry" %} {% now 'Y' %}</span>
-->
<!-- <span class="nav-item">
<a href="/members"><i class="fa fa-group"></i></a>
</span>
<span class="nav-item">
<a href="/infoscreen"><i class="fa fa-info"></i></a>
</span>
<span class="nav-item">
<a href="/admin"><i class="fa fa-gears"></i></a>
</span> -->
+9 -4
View File
@@ -1,8 +1,13 @@
{% extends "password_reset/base.html" %}{% load i18n %}
{% block title %}{% trans "New password set" %}{% endblock %}
{% block content %}
<p>{% trans "Your password has successfully been reset. You can use it right now on the login page." %}</p>
<p><a href="/login">Log in</a></p>
<div class="container">
<div class="d-flex justify-content-center">
<p>{% trans "Your password has successfully been reset." %}</p>
</div>
<div class="d-flex justify-content-center">
<p><a href="/login">{% trans "Log in" %}</a></p>
</div>
</div>
{% endblock %}
@@ -0,0 +1,14 @@
{% autoescape off %}
{% load i18n %}
{% blocktrans %}You're receiving this e-mail because you requested a password reset for your user account at{% endblocktrans %} {{ site.domain }}.
{% blocktrans %}Please go to the following page and choose a new password:{% endblocktrans %}
{% block reset_link %}
{% if secure %}https {% else %}http{% endif %}://{{ site.domain }}{% url "password_reset_reset" token %}
{% endblock %}
{% blocktrans %}Your username, in case you've forgotten: {% endblocktrans %}{{ user.username }}
{% blocktrans %}If you don't want to reset your password, simply ignore this email and it will stay unchanged.{% endblocktrans %}
{% endautoescape %}
@@ -0,0 +1 @@
{% load i18n %}{% blocktrans %}Password recovery on{% endblocktrans %} {{ site.domain }}
+13 -7
View File
@@ -1,12 +1,18 @@
{% extends "password_reset/base.html" %}
{% load i18n %}
{% block title %}{% trans "Password recovery" %}{% endblock %}
{% load bootstrap4 %}
{% block content %}
<form method="post" action="{{ url }}">
{% csrf_token %}
{{ form.as_p }}
<p><input type="submit" value="{% trans "Send" %}"></p>
</form>
<div class="container">
<div class="d-flex justify-content-center">
<form method="post" action="{{ url }}" class="form">{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">
{% trans "Send" %}
</button>
{% endbuttons %}
</form>
</div>
</div>
{% endblock %}
+24 -10
View File
@@ -1,14 +1,28 @@
{% extends "password_reset/base.html" %}{% load i18n %}
{% load i18n %}
{% load bootstrap4 %}
{% block content %}
{% if invalid %}{% url "password_reset_recover" as recovery_url %}
<p>{% blocktrans %}Sorry, this password reset link is invalid. You can still <a href="{{ recovery_url }}">request a new one</a>.{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans %}Hi, <strong>{{ username }}</strong>. Please choose your new password.{% endblocktrans %}</p>
<form method="post" action="{% url "password_reset_reset" token %}">
{% csrf_token %}
{{ form.as_p }}
<p><input type="submit" value="{% trans "Set new password" %}"></p>
</form>
{% endif %}
<div class="container">
{% if invalid %}{% url "password_reset_recover" as recovery_url %}
<div class="d-flex justify-content-center">
<p>{% blocktrans %}Sorry, this password reset link is invalid. You can still <a href="{{ recovery_url }}">request a new one</a>.{% endblocktrans %}</p>
</div>
{% else %}
<div class="d-flex justify-content-center">
<p>{% blocktrans %}Hi, <strong>{{ username }}</strong>. Please choose your new password.{% endblocktrans %}</p>
</div>
<div class="d-flex justify-content-center">
<form method="post" action="{% url "password_reset_reset" token %}" class="form">{% csrf_token %}
{% bootstrap_form form %}
{% buttons %}
<button type="submit" class="btn btn-primary">
{% trans "Set new password" %}
</button>
{% endbuttons %}
</form>
</div>
{% endif %}
</div>
{% endblock %}
-15
View File
@@ -1,15 +0,0 @@
{% autoescape off %}
You're receiving this e-mail because you requested a password reset for your user account at {{ site_name }}.
Please go to the following page and choose a new password:
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url django.contrib.auth.views.password_reset_confirm uidb36=uid, token=token %}
{% endblock %}
Your username, in case you've forgotten: {{ user.username }}
Thanks for using our site!
The {{ site_name }} team.
{% endautoescape %}
+5 -4
View File
@@ -1,8 +1,9 @@
{% extends "password_reset/base.html" %}
{% load i18n %}
{% block title %}{% trans "Password recovery sent" %}{% endblock %}
{% block content %}
<p>{% blocktrans with ago=timestamp|timesince %}An email was sent to <strong>{{ email }}</strong> {{ ago }} ago. Use the link in it to set a new password.{% endblocktrans %}</p>
{% endblock %}
<div class="container">
<div class="d-flex justify-content-center">
<p>{% blocktrans with ago=timestamp|timesince %}An email was sent to <strong>{{ email }}</strong> {{ ago }} ago. Use the link in it to set a new password.{% endblocktrans %}</p>
</div>
</div>{% endblock %}
+3 -3
View File
@@ -1,3 +1,3 @@
/*.main_navigation .navbar-border .navbar-nav .nav-item {
display: block;
}*/
nav {
margin-bottom: 2vh;
}
+5
View File
@@ -14,6 +14,11 @@
<h5>Lisää vain vesi</h5>
</div>
</div>
<div class="row">
<div class="col">
{% lorem %}
</div>
</div>
</div>
</div>
{% endblock %}
+36 -30
View File
@@ -1,34 +1,40 @@
{% load i18n %}
<link rel="stylesheet" href="/static/css/main_nav.css">
<nav class="navbar navbar-toggleable-md navbar-light bg-faded">
<ul class="navbar-nav mx-auto">
<li class="nav-item text-center">
<a class="nav-link" href="/guild">{% trans "Guild" %}</a>
</li>
<li class="nav-item text-center">
<a class="nav-link" href="/freshmen">{% trans "Freshmen" %}</a>
</li>
<li class="nav-item text-center">
<a class="nav-link" href="/event_calendar">{% trans "Event calendar" %}</a>
</li>
<li class="nav-item text-center">
<a class="nav-link" href="/international">{% trans "International" %}</a>
</li>
<li class="nav-item text-center">
<a class="nav-link" href="/sosso">{% trans "Sössö" %}</a>
</li>
<li class="nav-item text-center">
<a class="nav-item nav-link" href="/contact">{% trans "Contact" %}</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" id="corpDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% trans "Corporate" %}
</a>
<div class="dropdown-menu" aria-labelledby="corpDropdown">
<a class="dropdown-item" href="/jobs">{% trans "Jobs" %}</a>
<a class="dropdown-item" href="/">Yritysdadaa</a>
<a class="dropdown-item" href="/">Something else here</a>
</div>
</li>
</ul>
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<!-- <p class="navbar-brand">Kökkö</p> -->
<div class="collapse navbar-collapse" id="navbarNavDropdown">
<ul class="navbar-nav mx-auto">
<li class="nav-item text-center">
<a class="nav-link" href="/guild">{% trans "Guild" %}</a>
</li>
<li class="nav-item text-center">
<a class="nav-link" href="/freshmen">{% trans "Freshmen" %}</a>
</li>
<li class="nav-item text-center">
<a class="nav-link" href="/event_calendar">{% trans "Event calendar" %}</a>
</li>
<li class="nav-item text-center">
<a class="nav-link" href="/international">{% trans "International" %}</a>
</li>
<li class="nav-item text-center">
<a class="nav-link" href="/sosso">{% trans "Sössö" %}</a>
</li>
<li class="nav-item text-center">
<a class="nav-item nav-link" href="/contact">{% trans "Contact" %}</a>
</li>
<li class="nav-item dropdown text-center">
<a class="nav-link dropdown-toggle" id="corpDropdown" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{% trans "Corporate" %}
</a>
<div class="dropdown-menu text-center" aria-labelledby="corpDropdown">
<a class="dropdown-item" href="/jobs">{% trans "Jobs" %}</a>
<a class="dropdown-item" href="/">Yritysdadaa</a>
<a class="dropdown-item" href="/">Something else here</a>
</div>
</li>
</ul>
</div>
</nav>