26 Commits

Author SHA1 Message Date
Lavikainen Joel 5e6b4153e6 Add data endpoint and create infoscreen view for coffee stats 2017-11-19 17:40:04 +02:00
Lavikainen Joel 66e86ab621 Save brewing data as db model 2017-11-19 14:19:37 +02:00
Ilkka Oksanen e4f701711c Fix pep stuff 2017-11-16 13:23:09 +02:00
Ilkka Oksanen 2363362202 Add structure for new ilmotunkki 2017-11-16 12:50:04 +02:00
Jan Tuomi ab2682a0d3 Fix eslint offenses 2017-11-08 18:09:45 +02:00
Jan Tuomi 6678c691dd Add error to coffee display when there is a connection problem
Resolves #87
2017-11-08 17:55:56 +02:00
Jan Tuomi aaf773c600 Gitlab runner is being funky so I'm grasping at straws 2017-11-05 17:21:29 +02:00
Jan Tuomi 417083b050 Do not prune docker volumes on dev deploy 2017-11-05 11:48:09 +02:00
Jan Tuomi 08e675f698 Add missing script parameter 2017-11-05 10:47:30 +02:00
Jan Tuomi efb1ee6182 Improve dev deployment 2017-11-05 10:38:39 +02:00
Jan Tuomi 99788e8d47 Use correct registry URL in docker-compose 2017-11-05 10:06:39 +02:00
Jan Tuomi 356038a622 Merge branch 'feature-excel-import-export' into 'develop'
Feature excel import export

See merge request vtmk/web2.0!97
2017-11-02 23:41:36 +02:00
Jan Tuomi 31e324e478 Remove obsolete setting in base.py 2017-11-02 23:20:57 +02:00
Jan Tuomi e5c00a47e8 Add missing pyexcel dependency 2017-11-02 23:11:59 +02:00
Jan Tuomi 1fdc9e9ff8 Write translations 2017-11-02 23:10:24 +02:00
Jan Tuomi cf33d81d69 Write some unit tests for excel stuff 2017-11-02 22:56:34 +02:00
Jan Tuomi 29070165eb Merge develop to feature-excel-import-export 2017-11-02 15:15:41 +02:00
Jan Tuomi b9e9cdb2b0 Add new payment event for new members 2017-11-02 15:12:32 +02:00
Ilkka Oksanen e0e73976db Add question marks to coffee in case of few errors 2017-11-01 13:39:46 +02:00
Jan Tuomi 755abe5647 Add missing pip dependency 2017-11-01 13:17:31 +02:00
Jan Tuomi 2f80159144 Merge branch 'feature-coffee-upgrade' into 'develop'
Feature coffee upgrade

See merge request vtmk/web2.0!92
2017-11-01 13:14:44 +02:00
Jan Tuomi e709570f22 Change stuff to use excel files 2017-10-31 22:36:31 +02:00
Juhana Luomanen e63f8d5418 Fix #91 bug with HSL timetable sorting 2017-10-31 22:34:39 +02:00
Ilkka Oksanen b8fd237918 Mute a few eslint bloopers 2017-10-31 18:10:08 +02:00
Ilkka Oksanen 6c153e7ab0 Add fancier animations 2017-10-31 17:56:18 +02:00
okalintu 8f74c87df5 Reimplement serverside coffee scale 2017-10-29 23:46:20 +02:00
58 changed files with 1123 additions and 645 deletions
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
echo "Deploying to development."
set -e
set -x
pushd deployment
docker-compose down
docker pull "$1"
docker-compose up -d
popd
set +x
set +e
+2 -1
View File
@@ -76,7 +76,8 @@ deploy_dev:
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
script:
- scp docker-compose.yml $DEV_SSH_USER@$DEV_SSH_HOST:~/deployment/docker-compose.yml
- ssh $DEV_SSH_USER@$DEV_SSH_HOST "cd deployment && docker-compose down && docker pull \"$IMAGE_NAME\" && docker-compose up -d && docker image prune -f"
- scp .deploy_dev.sh $DEV_SSH_USER@$DEV_SSH_HOST:~/deployment/deploy_dev.sh
- ssh $DEV_SSH_USER@$DEV_SSH_HOST "bash ~/deployment/deploy_dev.sh \"$IMAGE_NAME\""
deploy_production:
stage: deploy
-23
View File
@@ -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))
@@ -0,0 +1,74 @@
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
@@ -0,0 +1,24 @@
# -*- 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()),
],
),
]
+4 -1
View File
@@ -1,3 +1,6 @@
from django.db import models
# Create your models here.
class Brewing(models.Model):
cups = models.PositiveSmallIntegerField()
time = models.DateTimeField()
-58
View File
@@ -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
+7 -4
View File
@@ -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 {
+166 -113
View File
@@ -1,130 +1,183 @@
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();
}
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) + '%'});
}
$.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 tick(){
var ev = new CustomEvent("tick", {'detail': new Date()});
window.dispatchEvent(ev);
}
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();
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("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);
+6 -2
View File
@@ -10,13 +10,16 @@
<meta http-equiv="pragma" content="no-cache" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<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"></span>
<span id="brewtime" class="brewtime layerone"></span>
<span class="brewtime layertwo">:)</span>
<span id="address">
ka.dy.fi
<noscript><br>This page uses JavaScript!</noscript>
@@ -26,7 +29,8 @@
</div>
<!--Kahvinkeitin on rikki. Varakeittimellä keitettyä kahvia saattaa olla.-->
<div id="lower" class="normal">
<div id="text"></div>
<div id="text" class="text layerone">???</div>
<div class="text layertwo">&nbsp;+</div>
<div id="scale"><div id="scale2"></div></div>
</div>
</div>
-28
View File
@@ -1,30 +1,2 @@
from django.test import TestCase, Client
from django.conf import settings
from coffee_scale.mqtt import on_message
HOST = settings.MQTT_SETTINGS['HOST']
PORT = settings.MQTT_SETTINGS['PORT']
TOPICS = settings.MQTT_SETTINGS['TOPICS']
class MQTTTestCase(TestCase):
"""Tests MQTT functionality"""
class MockMessage:
def __init__(self, payload, topic):
self.payload = payload
self.topic = topic
def setUp(self):
payload = '10'.encode('utf-8')
topic = TOPICS['CUPS']
msg = MQTTTestCase.MockMessage(payload, topic)
on_message(None, None, msg)
self.c = Client()
def test_receive_cups(self):
response = self.c.get('/coffee/cups')
payload = response.json()
self.assertEquals(payload['cups'], 10)
+1 -3
View File
@@ -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),
]
-16
View File
@@ -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)
+1 -1
View File
@@ -5,7 +5,7 @@ services:
image: postgres
web:
build: .
image: 86.50.143.82:5000/web20
image: git.sahkoinsinoorikilta.fi:4567/vtmk/web2.0
command: ["bash", "-c", "cd /code && ./wait-for-it.sh db:5432 -- bash setup.sh --no-input --no-npm && python manage.py runserver 0.0.0.0:8080"]
ports:
- "8080:8080"
+45
View File
@@ -0,0 +1,45 @@
# 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
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class IlmotunkkiConfig(AppConfig):
name = 'ilmotunkki'
View File
+16
View File
@@ -0,0 +1,16 @@
from django.db import models
from django.utils import timezone
class Signup(models.Model):
start = models.DateTimeField()
end = models.DateTimeField()
class Question(models.Model):
pass
class Answer(models.Model):
signup = models.ForeignKey(Signup, on_delete=models.CASCADE)
question = models.ForeignKey(Question, on_delete=models.PROTECT)
+3
View File
@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
+2
View File
@@ -5,6 +5,7 @@ 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)
@@ -15,3 +16,4 @@ admin.site.register(ABBInfoItem)
admin.site.register(InfoInstance)
admin.site.register(ExternalWebsiteInfoItem)
admin.site.register(VideoInfoItem)
admin.site.register(CoffeeStatsInfoItem)
+2 -1
View File
@@ -47,7 +47,7 @@ def fetch():
route = place['pattern']['route']['shortName']
stop_times = place['_stoptimes']
for stop_time in stop_times:
timestamp = stop_time['serviceDay'] + stop_time['realtimeArrival']
timestamp = time_utc = stop_time['serviceDay'] + stop_time['realtimeArrival']
headsign = stop_time['stopHeadsign']
stop_name = stop_time['stop']['name']
time_diff = (timestamp - timezone.now().timestamp()) / 60 # minutes
@@ -64,6 +64,7 @@ def fetch():
'headsign': headsign,
'timestamp': time,
'stop': stop_name,
'utc': time_utc,
})
return schedule
@@ -0,0 +1,23 @@
# -*- 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,6 +269,21 @@ 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
@@ -0,0 +1,9 @@
<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>
@@ -0,0 +1,11 @@
<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: ['timestamp'] | limitTo: 11">
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['utc'] | limitTo: 11">
<td style="min-width: 300px">
{{x.timestamp}}
</td>
@@ -186,5 +186,6 @@ var simple_controllers = [
"hslitem",
"websiteitem",
"apyitem",
"coffeestatsitem",
];
_.each(simple_controllers, controllerGenerator);
+15 -1
View File
@@ -142,5 +142,19 @@ 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,6 +26,8 @@ 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),
@@ -45,6 +47,7 @@ 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),
@@ -52,5 +55,6 @@ 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,6 +22,7 @@ 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')
@@ -188,3 +189,4 @@ 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,8 +2,11 @@ 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
@@ -99,3 +102,15 @@ 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.
+231 -174
View File
@@ -3,19 +3,19 @@
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-30 13:41+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"POT-Creation-Date: 2017-11-02 21:59+0100\n"
"PO-Revision-Date: 2017-11-02 23:09+0200\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Last-Translator: \n"
"Language-Team: \n"
"X-Generator: Poedit 2.0.4\n"
#: .\infoscreen\models.py:97
msgid "ABB jobs"
@@ -177,11 +177,11 @@ msgstr "Member"
#: .\members\forms.py:123
msgid "I'm a member of AYY"
msgstr ""
msgstr "I'm a member of AYY"
#: .\members\forms.py:124
msgid "I want to receive a weekly newsletter"
msgstr ""
msgstr "I want to receive a weekly newsletter"
#: .\members\models.py:14
msgid "First name"
@@ -201,7 +201,7 @@ msgid "Place of residence"
msgstr "Place of residence"
#: .\members\models.py:19 .\members\models.py:70
#: .\members\templates\member_add_many.html:35
#: .\members\templates\member_add_many.html:39
msgid "AYY"
msgstr "AYY"
@@ -225,7 +225,7 @@ msgstr "Source"
msgid "Cash"
msgstr "Cash"
#: .\members\models.py:72 .\members\templates\member_add_many.html:36
#: .\members\models.py:72 .\members\templates\member_add_many.html:40
msgid "Bank transfer"
msgstr "Bank transfer"
@@ -235,7 +235,6 @@ msgstr "Created"
#: .\members\models.py:104 .\members\templates\member_add_many_confirm.html:12
#: .\members\templates\members_base.html:52
#: .\webapp\templates\main_index.html:7
msgid "Members"
msgstr "Members"
@@ -248,10 +247,8 @@ msgid "Edit"
msgstr "Edit"
#: .\members\templates\application_delete_confirm.html:9
#, fuzzy
#| msgid "Are you sure you want to delete this payment?"
msgid "Are you sure you want to delete this application?"
msgstr "Are you sure you want to delete this payment?"
msgstr "Are you sure you want to delete this application?"
#: .\members\templates\application_delete_confirm.html:19
#: .\members\templates\member_delete_confirm.html:19
@@ -284,8 +281,9 @@ msgid "Muista myös maksaa jäsenmaksusi!"
msgstr "Don't forget to pay your membership fee!"
#: .\members\templates\application_index.html:16
#: .\members\templates\member_add_many.html:48
#: .\members\templates\member_add_many.html:55
#: .\members\templates\member_add_many_confirm.html:22
#: .\templates\password_reset\recovery_form.html:10
#: .\webapp\templates\kaehmy_list.html:48
msgid "Send"
msgstr "Send"
@@ -294,6 +292,12 @@ msgstr "Send"
msgid "Member applications"
msgstr "Member applications"
#: .\members\templates\application_list.html:20
#: .\members\templates\member_list.html:44
#: .\members\templates\payment_list.html:41
msgid "Download Excel"
msgstr "Download Excel"
#: .\members\templates\application_success.html:8
msgid "Hienoa! Jäsenhakemuksesi on nyt lähetetty."
msgstr "Amazing! Your membership application has been sent."
@@ -316,61 +320,75 @@ msgstr "Save"
#: .\members\templates\member_add_many.html:8
msgid "Add many members"
msgstr ""
msgstr "Add many members"
#: .\members\templates\member_add_many.html:13
msgid ""
"\n"
" Enter member information in CSV format, separate members on "
"separate lines.\n"
"separate lines. \n"
" If a new member already exists in the database, a new payment "
"event will be created for that member instead.\n"
" "
msgstr ""
"\n"
" Enter member information in CSV format, separate members on "
"separate lines. \n"
" If a new member already exists in the database, a new payment "
"event will be created for that member instead.\n"
" "
#: .\members\templates\member_add_many.html:18
#: .\members\templates\member_add_many.html:21
msgid "Format the member table like this:"
msgstr "Format the member table like this:"
#: .\members\templates\member_add_many.html:25
msgid ""
"\n"
" first_name, last_name, email_address and place_of_origin should "
"be given string values.\n"
" ayy_member and jas_recipient should be given the value 0 (off) "
"or 1 (on).\n"
" "
"Columns: First name, last name, email address, place of origin, AYY member, "
"JAS recipient"
msgstr ""
"\n"
" first_name, last_name, email_address and place_of_origin should "
"be given string values.\n"
" ayy_member and jas_recipient should be given the value 0 (off) "
"or 1 (on).\n"
" "
"Columns: First name, last name, email address, place of origin, AYY member, "
"JAS recipient"
#: .\members\templates\member_add_many.html:23
msgid "Syntax"
msgstr "Syntax"
#: .\members\templates\member_add_many.html:29
msgid "Data"
msgstr ""
#: .\members\templates\member_add_many.html:28
msgid "Save the file as CSV"
msgstr "Save the file as CSV"
#: .\members\templates\member_add_many.html:33
msgid "Payment source"
msgstr ""
msgid "Upload file"
msgstr "Upload file"
#: .\members\templates\member_add_many.html:37
msgid "Cash payment"
msgstr ""
msgid "Payment source"
msgstr "Payment source"
#: .\members\templates\member_add_many.html:41
msgid "CSV delimiter"
msgstr ""
msgid "Cash payment"
msgstr "Cash payment"
#: .\members\templates\member_add_many.html:44
msgid ""
"This payment source will be used to create any payments for new members that "
"already exist in the database."
msgstr ""
"This payment source will be used to create any payments for new members that "
"already exist in the database."
#: .\members\templates\member_add_many.html:48
msgid "CSV delimiter"
msgstr "CSV delimiter"
#: .\members\templates\member_add_many.html:51
msgid ""
"The symbol that is used to separate items in one line. Defaults to "
"';' (semicolon)."
msgstr ""
"The symbol that is used to separate items in one line. Defaults to "
"';' (semicolon)."
#: .\members\templates\member_add_many_confirm.html:8
msgid "Confirm adding these entries?"
msgstr ""
msgstr "Confirm adding these entries?"
#: .\members\templates\member_add_many_confirm.html:16
#: .\members\templates\members_base.html:60
@@ -392,21 +410,17 @@ msgstr "Member register"
#: .\members\templates\member_list.html:21
msgid "Members in register:"
msgstr ""
msgstr "Members in register:"
#: .\members\templates\member_list.html:28
#: .\members\templates\payment_list.html:25
msgid "Search"
msgstr ""
msgstr "Search"
#: .\members\templates\member_list.html:36
#: .\members\templates\payment_list.html:33
msgid "Showing results for"
msgstr ""
#: .\members\templates\member_list.html:44
msgid "Download CSV"
msgstr "Download CSV"
msgstr "Showing results for"
#: .\members\templates\members_base.html:33
#: .\members\templates\members_base.html:42
@@ -460,20 +474,18 @@ msgid "Payment events"
msgstr "Payment events"
#: .\members\templates\payment_list.html:18
#, fuzzy
#| msgid "Member register"
msgid "Payments in register:"
msgstr "Member register"
msgstr "Payments in register:"
#: .\members\templates\settings.html:17
msgid "Language"
msgstr "Language"
#: .\members\templates\settings.html:20 .\sikweb\base.py:222
#: .\members\templates\settings.html:20 .\sikweb\base.py:226
msgid "Finnish"
msgstr "Finnish"
#: .\members\templates\settings.html:21 .\sikweb\base.py:223
#: .\members\templates\settings.html:21 .\sikweb\base.py:227
msgid "English"
msgstr "English"
@@ -481,92 +493,98 @@ msgstr "English"
msgid "Submit"
msgstr "Submit"
#: .\members\views\applications.py:51 .\members\views\applications.py:119
#: .\members\views\applications.py:148
#: .\members\views\applications.py:51 .\members\views\applications.py:112
#: .\members\views\applications.py:137
msgid "No application id specified"
msgstr "No application id specified"
#: .\members\views\applications.py:73
#: .\members\views\applications.py:71
msgid "Application missing 'id' field."
msgstr ""
msgstr "Application missing 'id' field."
#: .\members\views\applications.py:83
#: .\members\views\applications.py:80
msgid "Email {} is already in use by a member. Application cannot be accepted."
msgstr ""
"Email {} is already in use by a member. Application cannot be accepted."
#: .\members\views\applications.py:93
#: .\members\views\applications.py:91
msgid "Successfully accepted application"
msgstr "Successfully accepted application"
#: .\members\views\applications.py:123
#: .\members\views\applications.py:116
msgid "Successfully deleted application"
msgstr "Successfully deleted application"
#: .\members\views\applications.py:135
#: .\members\views\applications.py:126
msgid "Could not delete application object"
msgstr "Could not delete application object"
#: .\members\views\members.py:74 .\members\views\members.py:179
#: .\members\views\members.py:206
#: .\members\views\members.py:73 .\members\views\members.py:175
#: .\members\views\members.py:199
msgid "No member id specified"
msgstr "No member id specified"
#: .\members\views\members.py:111
#: .\members\views\members.py:114
msgid "Failed to import members"
msgstr "Failed to import members"
#: .\members\views\members.py:125
#: .\members\views\members.py:128
msgid "Successfully added member"
msgstr "Successfully added member"
#: .\members\views\members.py:148
#: .\members\views\members.py:149
msgid "Member missing 'id' field."
msgstr ""
msgstr "Member missing 'id' field."
#: .\members\views\members.py:158
msgid "Successfully updated member"
msgstr "Successfully updated member"
#: .\members\views\members.py:183
#: .\members\views\members.py:179
msgid "Successfully deleted member"
msgstr "Successfully deleted member"
#: .\members\views\members.py:194
#: .\members\views\members.py:188
msgid "Could not delete member object"
msgstr "Could not delete member object"
#: .\members\views\payments.py:70
#: .\members\views\payments.py:71
msgid "Successfully added payment for member"
msgstr "Successfully added payment for member"
#: .\members\views\payments.py:89 .\members\views\payments.py:108
#: .\members\views\payments.py:128
#: .\members\views\payments.py:88 .\members\views\payments.py:105
#: .\members\views\payments.py:123
msgid "No payment id specified"
msgstr "No payment id specified"
#: .\members\views\payments.py:133
#: .\members\views\payments.py:128
msgid "Successfully deleted payment"
msgstr "Successfully deleted payment"
#: .\members\views\payments.py:143
#: .\members\views\payments.py:136
msgid "Could not delete payment object"
msgstr "Could not delete payment object"
#: .\members\views\payments.py:163
#: .\members\views\payments.py:156
msgid "Successfully updated payment"
msgstr "Successfully updated payment"
#: .\members\views\payments.py:170
#: .\members\views\payments.py:161
msgid "Could not update payment object"
msgstr "Could not update payment object"
#: .\members\views\utils.py:117
msgid "Missing \"textfield\" POST request field"
msgstr "Missing \"textfield\" POST request field"
#: .\members\views\utils.py:119
msgid "Missing CSV file"
msgstr "Missing CSV file"
#: .\templates\admin\base_site.html:43
msgid "Go"
msgstr ""
msgstr "Go"
#: .\templates\base.html:14 .\webapp\templates\kaehmy_base.html:14
#: .\webapp\templates\main_index.html:9
msgid "Aalto-yliopiston Sähköinsinöörikilta ry"
msgstr "Aalto-yliopiston Sähköinsinöörikilta ry"
#: .\templates\error.html:8 .\webapp\templates\kaehmy_error.html:8
msgid "Error"
@@ -576,53 +594,117 @@ msgstr "Error"
msgid "Back"
msgstr "Back"
#: .\templates\footer.html:23
#: .\templates\footer.html:10 .\templates\footer.html:60
#: .\webapp\templates\kaehmy_footer.html:23
msgid "Copyright Aalto-yliopiston Sähköinsinöörikilta ry"
msgstr "Copyright Aalto-yliopiston Sähköinsinöörikilta ry"
#: .\templates\login.html:12 .\templates\login.html:13
msgid "Username"
msgstr "Username"
#: .\templates\login.html:16 .\templates\login.html:17
msgid "Password"
msgstr "Password"
#: .\templates\login.html:20
msgid "Forgot password?"
msgstr "Forgot password?"
#: .\templates\login.html:26
msgid "Log in"
msgstr "Log in"
#: .\templates\password_reset\recovery_done.html:3
msgid "New password set"
msgstr "New password set"
#: .\templates\password_reset\recovery_done.html:6
msgid ""
"Your password has successfully been reset. You can use it right now on the "
"login page."
msgstr ""
"Your password has successfully been reset. You can use it right now on the "
"login page."
#: .\templates\password_reset\recovery_form.html:4
msgid "Password recovery"
msgstr "Password recovery"
#: .\templates\password_reset\reset.html:5
#, python-format
msgid ""
"Sorry, this password reset link is invalid. You can still <a href="
"\"%(recovery_url)s\">request a new one</a>."
msgstr ""
"Sorry, this password reset link is invalid. You can still <a href="
"\"%(recovery_url)s\">request a new one</a>."
#: .\templates\password_reset\reset.html:7
#, python-format
msgid "Hi, <strong>%(username)s</strong>. Please choose your new password."
msgstr "Hi, <strong>%(username)s</strong>. Please choose your new password."
#: .\templates\password_reset\reset.html:11
msgid "Set new password"
msgstr "Set new password"
#: .\templates\password_reset\reset_sent.html:4
msgid "Password recovery sent"
msgstr "Password recovery sent"
#: .\templates\password_reset\reset_sent.html:7
#, python-format
msgid ""
"An email was sent to <strong>%(email)s</strong> %(ago)s ago. Use the link in "
"it to set a new password."
msgstr ""
"An email was sent to <strong>%(email)s</strong> %(ago)s ago. Use the link in "
"it to set a new password."
#: .\webapp\forms.py:38
msgid "Email (not public)"
msgstr ""
msgstr "Email (not public)"
#: .\webapp\forms.py:39
msgid "Phone number (not public)"
msgstr ""
msgstr "Phone number (not public)"
#: .\webapp\forms.py:44
msgid "Custom roles"
msgstr ""
msgstr "Custom roles"
#: .\webapp\forms.py:52 .\webapp\templates\kaehmy.html:41
msgid "Preset roles"
msgstr ""
msgstr "Preset roles"
#: .\webapp\forms.py:76
msgid "Invalid phone number"
msgstr ""
msgstr "Invalid phone number"
#: .\webapp\forms.py:84
msgid "Custom role with the same name already exists."
msgstr ""
msgstr "Custom role with the same name already exists."
#: .\webapp\models.py:17
msgid "Webapp"
msgstr ""
msgstr "Webapp"
#: .\webapp\models.py:28
msgid "Tag"
msgstr ""
msgstr "Tag"
#: .\webapp\models.py:29
msgid "Tags"
msgstr ""
msgstr "Tags"
#: .\webapp\models.py:32
msgid "Tag: {}"
msgstr ""
msgstr "Tag: {}"
#: .\webapp\models.py:52
msgid "Feed: {}"
msgstr ""
msgstr "Feed: {}"
#: .\webapp\models.py:55
msgid "Feed"
@@ -656,13 +738,13 @@ msgstr ""
msgid "Corporate affairs"
msgstr ""
#: .\webapp\models.py:95 .\webapp\templates\freshmen.html:7
#: .\webapp\templates\navigation.html:10
#: .\webapp\models.py:95 .\webapp\templates\freshmen.html:10
#: .\webapp\templates\navigation.html:9
msgid "Freshmen"
msgstr "Freshmen"
#: .\webapp\models.py:96 .\webapp\templates\international.html:7
#: .\webapp\templates\navigation.html:16
#: .\webapp\models.py:96 .\webapp\templates\international.html:10
#: .\webapp\templates\navigation.html:15
msgid "International"
msgstr "International"
@@ -699,66 +781,48 @@ msgid "Studies"
msgstr ""
#: .\webapp\models.py:105
#, fuzzy
#| msgid "Sössö articles"
msgid "Sössö magazine"
msgstr "Sössö articles"
msgstr "Sössö magazine"
#: .\webapp\models.py:106
#, fuzzy
#| msgid "Applications"
msgid "Alumni relations"
msgstr "Applications"
msgstr "Alumni relations"
#: .\webapp\models.py:107
msgid "Others"
msgstr ""
#: .\webapp\models.py:111 .\webapp\models.py:191
#, fuzzy
#| msgid "Add member"
msgid "Board member"
msgstr "Add member"
msgstr "Board member"
#: .\webapp\models.py:112
msgid "Category"
msgstr ""
#: .\webapp\models.py:116
#, fuzzy
#| msgid "Add member"
msgid "board member"
msgstr "Add member"
msgstr "board member"
#: .\webapp\models.py:122
#, fuzzy
#| msgid "Duration"
msgid "Description"
msgstr "Duration"
msgstr "Description"
#: .\webapp\models.py:129
#, fuzzy
#| msgid "Total challenges:"
msgid "Preset kaehmy role"
msgstr "Total challenges:"
msgstr "Preset kaehmy role"
#: .\webapp\models.py:130
#, fuzzy
#| msgid "Total challenges:"
msgid "Preset kaehmy roles"
msgstr "Total challenges:"
msgstr "Preset kaehmy roles"
#: .\webapp\models.py:137
#, fuzzy
#| msgid "Total challenges:"
msgid "Custom kaehmy role"
msgstr "Total challenges:"
msgstr "Custom kaehmy role"
#: .\webapp\models.py:138
#, fuzzy
#| msgid "Total challenges:"
msgid "Custom kaehmy roles"
msgstr "Total challenges:"
msgstr "Custom kaehmy roles"
#: .\webapp\models.py:145
msgid "Timestamp"
@@ -801,10 +865,8 @@ msgid "Custom role name"
msgstr ""
#: .\webapp\models.py:199
#, fuzzy
#| msgid "Member applications"
msgid "Kaehmy application: {}"
msgstr "Member applications"
msgstr "Kaehmy application: {}"
#: .\webapp\models.py:221
msgid "Board: {}"
@@ -850,24 +912,20 @@ msgstr ""
msgid "SIK Admin"
msgstr "SIK Admin"
#: .\webapp\templates\base.html:15
msgid "Aalto-yliopiston Sähköinsinöörikilta ry"
msgstr "Aalto-yliopiston Sähköinsinöörikilta ry"
#: .\webapp\templates\contact.html:7 .\webapp\templates\navigation.html:22
#: .\webapp\templates\contact.html:10 .\webapp\templates\navigation.html:21
msgid "Contact"
msgstr "Contact"
#: .\webapp\templates\event_calendar.html:7
#: .\webapp\templates\navigation.html:13
#: .\webapp\templates\event_calendar.html:10
#: .\webapp\templates\navigation.html:12
msgid "Event calendar"
msgstr "Event calendar"
#: .\webapp\templates\guild.html:7
#: .\webapp\templates\guild.html:10
msgid "Kilta"
msgstr "Guild"
#: .\webapp\templates\jobs.html:7 .\webapp\templates\navigation.html:29
#: .\webapp\templates\jobs.html:10 .\webapp\templates\navigation.html:28
msgid "Jobs"
msgstr "Jobs"
@@ -929,22 +987,16 @@ msgid "Vaalikokous, osa 3 (toimarien valinta)"
msgstr "Election meeting, part 3 (non-board election)"
#: .\webapp\templates\kaehmy_export.html:9
#, fuzzy
#| msgid "Applications"
msgid "All applications"
msgstr "Applications"
msgstr "All applications"
#: .\webapp\templates\kaehmy_export.html:13
#, fuzzy
#| msgid "Applications"
msgid "Board applications"
msgstr "Applications"
msgstr "Board applications"
#: .\webapp\templates\kaehmy_export.html:18
#, fuzzy
#| msgid "Member applications"
msgid "Non-board applications"
msgstr "Member applications"
msgstr "Non-board applications"
#: .\webapp\templates\kaehmy_export.html:23
msgid "Front page"
@@ -964,10 +1016,8 @@ msgid "Comment"
msgstr ""
#: .\webapp\templates\kaehmy_list.html:57
#, fuzzy
#| msgid "List kaehmys"
msgid "Filter kaehmys"
msgstr "List applications"
msgstr "Filter kaehmys"
#: .\webapp\templates\kaehmy_list.html:70
#: .\webapp\templates\kaehmy_statistics.html:18
@@ -999,35 +1049,15 @@ msgstr "New application"
msgid "Statistics"
msgstr ""
#: .\webapp\templates\login.html:25 .\webapp\templates\login.html:27
msgid "Username"
msgstr "Username"
#: .\webapp\templates\login.html:31 .\webapp\templates\login.html:33
msgid "Password"
msgstr "Password"
#: .\webapp\templates\login.html:43
msgid "Log in"
msgstr "Log in"
#: .\webapp\templates\main_index.html:8
msgid "Infoscreen"
msgstr "Infoscreen"
#: .\webapp\templates\main_index.html:9
msgid "Admin tools"
msgstr "Admin tools"
#: .\webapp\templates\navigation.html:7
#: .\webapp\templates\navigation.html:6
msgid "Guild"
msgstr "Guild"
#: .\webapp\templates\navigation.html:19 .\webapp\templates\sosso.html:7
#: .\webapp\templates\navigation.html:18 .\webapp\templates\sosso.html:10
msgid "Sössö"
msgstr "Sössö"
#: .\webapp\templates\navigation.html:26
#: .\webapp\templates\navigation.html:25
msgid "Corporate"
msgstr "Corporate"
@@ -1047,5 +1077,32 @@ msgstr "All challenges"
msgid "Total challenges:"
msgstr "Total challenges:"
#~ msgid "Infoscreen"
#~ msgstr "Infoscreen"
#~ msgid "Admin tools"
#~ msgstr "Admin tools"
#~ msgid ""
#~ "\n"
#~ " first_name, last_name, email_address and place_of_origin "
#~ "should be given string values.\n"
#~ " ayy_member and jas_recipient should be given the value 0 "
#~ "(off) or 1 (on).\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ " first_name, last_name, email_address and place_of_origin "
#~ "should be given string values.\n"
#~ " ayy_member and jas_recipient should be given the value 0 "
#~ "(off) or 1 (on).\n"
#~ " "
#~ msgid "Syntax"
#~ msgstr "Syntax"
#~ msgid "Missing \"textfield\" POST request field"
#~ msgstr "Missing \"textfield\" POST request field"
#~ msgid "Options"
#~ msgstr "Options"
Binary file not shown.
+180 -97
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-30 13:41+0100\n"
"PO-Revision-Date: 2017-10-30 14:42+0200\n"
"POT-Creation-Date: 2017-11-02 21:59+0100\n"
"PO-Revision-Date: 2017-11-02 23:04+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fi\n"
@@ -202,7 +202,7 @@ msgid "Place of residence"
msgstr "Asuinpaikka"
#: .\members\models.py:19 .\members\models.py:70
#: .\members\templates\member_add_many.html:35
#: .\members\templates\member_add_many.html:39
msgid "AYY"
msgstr "AYY"
@@ -226,7 +226,7 @@ msgstr "Lähde"
msgid "Cash"
msgstr "Käteinen"
#: .\members\models.py:72 .\members\templates\member_add_many.html:36
#: .\members\models.py:72 .\members\templates\member_add_many.html:40
msgid "Bank transfer"
msgstr "Tilisiirto"
@@ -236,7 +236,6 @@ msgstr "Lisätty"
#: .\members\models.py:104 .\members\templates\member_add_many_confirm.html:12
#: .\members\templates\members_base.html:52
#: .\webapp\templates\main_index.html:7
msgid "Members"
msgstr "Jäsenet"
@@ -283,8 +282,9 @@ msgid "Muista myös maksaa jäsenmaksusi!"
msgstr "Muista myös maksaa jäsenmaksusi!"
#: .\members\templates\application_index.html:16
#: .\members\templates\member_add_many.html:48
#: .\members\templates\member_add_many.html:55
#: .\members\templates\member_add_many_confirm.html:22
#: .\templates\password_reset\recovery_form.html:10
#: .\webapp\templates\kaehmy_list.html:48
msgid "Send"
msgstr "Lähetä"
@@ -293,6 +293,12 @@ msgstr "Lähetä"
msgid "Member applications"
msgstr "Jäsenhakemukset"
#: .\members\templates\application_list.html:20
#: .\members\templates\member_list.html:44
#: .\members\templates\payment_list.html:41
msgid "Download Excel"
msgstr "Lataa Excel"
#: .\members\templates\application_success.html:8
msgid "Hienoa! Jäsenhakemuksesi on nyt lähetetty."
msgstr "Hienoa! Jäsenhakemuksesi on nyt lähetetty."
@@ -321,50 +327,58 @@ msgstr "Lisää useita"
msgid ""
"\n"
" Enter member information in CSV format, separate members on "
"separate lines.\n"
"separate lines. \n"
" If a new member already exists in the database, a new payment "
"event will be created for that member instead.\n"
" "
msgstr ""
"\n"
" Syötä jäsentiedot CSV-formaatissa, erilliset jäsenet omilla "
"riveillään.\n"
" "
" Jos jäsenen tiedot ovat jo tietokannassa, tehdään tälle "
"jäsenelle uusi maksutapahtuma."
#: .\members\templates\member_add_many.html:18
#: .\members\templates\member_add_many.html:21
msgid "Format the member table like this:"
msgstr "Jäsentele taulukko seuraavasti:"
#: .\members\templates\member_add_many.html:25
msgid ""
"\n"
" first_name, last_name, email_address and place_of_origin should "
"be given string values.\n"
" ayy_member and jas_recipient should be given the value 0 (off) "
"or 1 (on).\n"
" "
"Columns: First name, last name, email address, place of origin, AYY member, "
"JAS recipient"
msgstr ""
"\n"
" first_name, last_name, email_address ja place_of_origin ovat "
"merkkijonoja.\n"
" ayy_member ja jas_recipient ovat joko 0 (off) tai 1 (on).\n"
" "
"Kolumnit: Etunimi, sukunimi, sähköpostiosoite, asuinpaikka, AYY:n jäsen, "
"jäsenmailin vastaanottaja"
#: .\members\templates\member_add_many.html:23
msgid "Syntax"
msgstr "Syntaksi"
#: .\members\templates\member_add_many.html:29
msgid "Data"
msgstr "Data"
#: .\members\templates\member_add_many.html:28
msgid "Save the file as CSV"
msgstr "Tallenna tiedosto CSV-formaatissa"
#: .\members\templates\member_add_many.html:33
msgid "Upload file"
msgstr "Lataa tiedosto"
#: .\members\templates\member_add_many.html:37
msgid "Payment source"
msgstr "Maksutapa"
#: .\members\templates\member_add_many.html:37
#: .\members\templates\member_add_many.html:41
msgid "Cash payment"
msgstr "Käteismaksu"
#: .\members\templates\member_add_many.html:41
#: .\members\templates\member_add_many.html:44
msgid ""
"This payment source will be used to create any payments for new members that "
"already exist in the database."
msgstr ""
"Tätä maksutapaa käytetään, kun jo tietokannassa oleville jäsenille luodaan "
"maksutapahtuma."
#: .\members\templates\member_add_many.html:48
msgid "CSV delimiter"
msgstr "CSV-erotin"
#: .\members\templates\member_add_many.html:44
#: .\members\templates\member_add_many.html:51
msgid ""
"The symbol that is used to separate items in one line. Defaults to "
"';' (semicolon)."
@@ -407,10 +421,6 @@ msgstr "Hae"
msgid "Showing results for"
msgstr "Näytetään tulokset haulle"
#: .\members\templates\member_list.html:44
msgid "Download CSV"
msgstr "Lataa CSV"
#: .\members\templates\members_base.html:33
#: .\members\templates\members_base.html:42
msgid "Member register of SIK ry"
@@ -470,11 +480,11 @@ msgstr "Maksutapahtumia:"
msgid "Language"
msgstr "Kieli"
#: .\members\templates\settings.html:20 .\sikweb\base.py:222
#: .\members\templates\settings.html:20 .\sikweb\base.py:226
msgid "Finnish"
msgstr "suomi"
#: .\members\templates\settings.html:21 .\sikweb\base.py:223
#: .\members\templates\settings.html:21 .\sikweb\base.py:227
msgid "English"
msgstr "englanti"
@@ -482,45 +492,45 @@ msgstr "englanti"
msgid "Submit"
msgstr "Lisää"
#: .\members\views\applications.py:51 .\members\views\applications.py:119
#: .\members\views\applications.py:148
#: .\members\views\applications.py:51 .\members\views\applications.py:112
#: .\members\views\applications.py:137
msgid "No application id specified"
msgstr "Hakemuksen ID ei määritelty"
#: .\members\views\applications.py:73
#: .\members\views\applications.py:71
msgid "Application missing 'id' field."
msgstr "Hakemuksen ID ei määritelty."
#: .\members\views\applications.py:83
#: .\members\views\applications.py:80
msgid "Email {} is already in use by a member. Application cannot be accepted."
msgstr "Sähköpostiosoite {} on jo käytössä. Hakemusta ei hyväksytty."
#: .\members\views\applications.py:93
#: .\members\views\applications.py:91
msgid "Successfully accepted application"
msgstr "Onnistuneesti hyväksyttiin hakemus"
#: .\members\views\applications.py:123
#: .\members\views\applications.py:116
msgid "Successfully deleted application"
msgstr "Onnistuneesti poistettiin hakemus"
#: .\members\views\applications.py:135
#: .\members\views\applications.py:126
msgid "Could not delete application object"
msgstr "Hakemusobjektia ei voitu poistaa"
#: .\members\views\members.py:74 .\members\views\members.py:179
#: .\members\views\members.py:206
#: .\members\views\members.py:73 .\members\views\members.py:175
#: .\members\views\members.py:199
msgid "No member id specified"
msgstr "Jäsenen ID ei määritelty"
#: .\members\views\members.py:111
#: .\members\views\members.py:114
msgid "Failed to import members"
msgstr "Jäsenten tuonti epäonnistui"
#: .\members\views\members.py:125
#: .\members\views\members.py:128
msgid "Successfully added member"
msgstr "Onnistuneesti lisättiin jäsen"
#: .\members\views\members.py:148
#: .\members\views\members.py:149
msgid "Member missing 'id' field."
msgstr "Jäsenen ID ei määritelty."
@@ -528,47 +538,52 @@ msgstr "Jäsenen ID ei määritelty."
msgid "Successfully updated member"
msgstr "Onnistuneesti päivitettiin jäsen"
#: .\members\views\members.py:183
#: .\members\views\members.py:179
msgid "Successfully deleted member"
msgstr "Onnistuneesti poistettiin jäsen"
#: .\members\views\members.py:194
#: .\members\views\members.py:188
msgid "Could not delete member object"
msgstr "Jäsenobjektia ei voitu poistaa"
#: .\members\views\payments.py:70
#: .\members\views\payments.py:71
msgid "Successfully added payment for member"
msgstr "Onnistuneesti lisättiin maksutapahtuma jäsenelle"
#: .\members\views\payments.py:89 .\members\views\payments.py:108
#: .\members\views\payments.py:128
#: .\members\views\payments.py:88 .\members\views\payments.py:105
#: .\members\views\payments.py:123
msgid "No payment id specified"
msgstr "Maksutapahtuman ID ei määritelty"
#: .\members\views\payments.py:133
#: .\members\views\payments.py:128
msgid "Successfully deleted payment"
msgstr "Onnistuneesti poistettiin maksutapahtuma"
#: .\members\views\payments.py:143
#: .\members\views\payments.py:136
msgid "Could not delete payment object"
msgstr "Maksutapahtumaobjektia ei voitu poistaa"
#: .\members\views\payments.py:163
#: .\members\views\payments.py:156
msgid "Successfully updated payment"
msgstr "Onnistuneesti päivitettiin maksutapahtuma"
#: .\members\views\payments.py:170
#: .\members\views\payments.py:161
msgid "Could not update payment object"
msgstr "Maksutapahtumaobjektia ei voitu päivittää"
#: .\members\views\utils.py:117
msgid "Missing \"textfield\" POST request field"
msgstr "Puuttuva \"textfield\" POST-kenttä"
#: .\members\views\utils.py:119
msgid "Missing CSV file"
msgstr "Puuttuva CSV-tiedosto"
#: .\templates\admin\base_site.html:43
msgid "Go"
msgstr "Vaihda"
#: .\templates\base.html:14 .\webapp\templates\kaehmy_base.html:14
#: .\webapp\templates\main_index.html:9
msgid "Aalto-yliopiston Sähköinsinöörikilta ry"
msgstr "Aalto-yliopiston Sähköinsinöörikilta ry"
#: .\templates\error.html:8 .\webapp\templates\kaehmy_error.html:8
msgid "Error"
msgstr "Virhe"
@@ -577,10 +592,73 @@ msgstr "Virhe"
msgid "Back"
msgstr "Takaisin"
#: .\templates\footer.html:23
#: .\templates\footer.html:10 .\templates\footer.html:60
#: .\webapp\templates\kaehmy_footer.html:23
msgid "Copyright Aalto-yliopiston Sähköinsinöörikilta ry"
msgstr "Copyright Aalto-yliopiston Sähköinsinöörikilta ry"
#: .\templates\login.html:12 .\templates\login.html:13
msgid "Username"
msgstr "Käyttäjänimi"
#: .\templates\login.html:16 .\templates\login.html:17
msgid "Password"
msgstr "Salasana"
#: .\templates\login.html:20
msgid "Forgot password?"
msgstr "Unohditko salasanasi?"
#: .\templates\login.html:26
msgid "Log in"
msgstr "Kirjaudu sisään"
#: .\templates\password_reset\recovery_done.html:3
msgid "New password set"
msgstr "Uusi salasana asetettu"
#: .\templates\password_reset\recovery_done.html:6
msgid ""
"Your password has successfully been reset. You can use it right now on the "
"login page."
msgstr ""
"Salasanasi on asetettu onnistuneesti. Voit käyttää sitä nyt kirjautuessasi."
#: .\templates\password_reset\recovery_form.html:4
msgid "Password recovery"
msgstr "Salasanan palautus"
#: .\templates\password_reset\reset.html:5
#, python-format
msgid ""
"Sorry, this password reset link is invalid. You can still <a href="
"\"%(recovery_url)s\">request a new one</a>."
msgstr ""
"Pahoittelut, tämä salasanan palautuslinkki on epäkelpo. Voit kuitenkin <a "
"href=\"%(recovery_url)s\">hankkia uuden</a>."
#: .\templates\password_reset\reset.html:7
#, python-format
msgid "Hi, <strong>%(username)s</strong>. Please choose your new password."
msgstr "Hei, <strong>%(username)s</strong>. Valitse uusi salasanasi."
#: .\templates\password_reset\reset.html:11
msgid "Set new password"
msgstr "Aseta uusi salasana"
#: .\templates\password_reset\reset_sent.html:4
msgid "Password recovery sent"
msgstr "Salasanan palautusviesti lähetetty"
#: .\templates\password_reset\reset_sent.html:7
#, python-format
msgid ""
"An email was sent to <strong>%(email)s</strong> %(ago)s ago. Use the link in "
"it to set a new password."
msgstr ""
"Sähköposti on lähetetty osoitteeseen <strong>%(email)s</strong> %(ago)s:a "
"sitten. Käytä linkkiä asettaaksesi uuden salasanan."
#: .\webapp\forms.py:38
msgid "Email (not public)"
msgstr "Sähköposti (ei julkinen)"
@@ -657,13 +735,13 @@ msgstr "Ilmoittautumiset"
msgid "Corporate affairs"
msgstr "Yrityssuhteet"
#: .\webapp\models.py:95 .\webapp\templates\freshmen.html:7
#: .\webapp\templates\navigation.html:10
#: .\webapp\models.py:95 .\webapp\templates\freshmen.html:10
#: .\webapp\templates\navigation.html:9
msgid "Freshmen"
msgstr "Fuksit"
#: .\webapp\models.py:96 .\webapp\templates\international.html:7
#: .\webapp\templates\navigation.html:16
#: .\webapp\models.py:96 .\webapp\templates\international.html:10
#: .\webapp\templates\navigation.html:15
msgid "International"
msgstr "International"
@@ -831,24 +909,20 @@ msgstr "Telegram-kanavat"
msgid "SIK Admin"
msgstr "SIK Hallintapaneeli"
#: .\webapp\templates\base.html:15
msgid "Aalto-yliopiston Sähköinsinöörikilta ry"
msgstr "Aalto-yliopiston Sähköinsinöörikilta ry"
#: .\webapp\templates\contact.html:7 .\webapp\templates\navigation.html:22
#: .\webapp\templates\contact.html:10 .\webapp\templates\navigation.html:21
msgid "Contact"
msgstr "Yhteystiedot"
#: .\webapp\templates\event_calendar.html:7
#: .\webapp\templates\navigation.html:13
#: .\webapp\templates\event_calendar.html:10
#: .\webapp\templates\navigation.html:12
msgid "Event calendar"
msgstr "Tapahtumakalenteri"
#: .\webapp\templates\guild.html:7
#: .\webapp\templates\guild.html:10
msgid "Kilta"
msgstr "Kilta"
#: .\webapp\templates\jobs.html:7 .\webapp\templates\navigation.html:29
#: .\webapp\templates\jobs.html:10 .\webapp\templates\navigation.html:28
msgid "Jobs"
msgstr "Työpaikat"
@@ -975,35 +1049,15 @@ msgstr "Uusi kaehmy"
msgid "Statistics"
msgstr "Kaehmytilastot"
#: .\webapp\templates\login.html:25 .\webapp\templates\login.html:27
msgid "Username"
msgstr "Käyttäjänimi"
#: .\webapp\templates\login.html:31 .\webapp\templates\login.html:33
msgid "Password"
msgstr "Salasana"
#: .\webapp\templates\login.html:43
msgid "Log in"
msgstr "Kirjaudu sisään"
#: .\webapp\templates\main_index.html:8
msgid "Infoscreen"
msgstr "Infonäyttö"
#: .\webapp\templates\main_index.html:9
msgid "Admin tools"
msgstr "Hallintatyökalut"
#: .\webapp\templates\navigation.html:7
#: .\webapp\templates\navigation.html:6
msgid "Guild"
msgstr "Kilta"
#: .\webapp\templates\navigation.html:19 .\webapp\templates\sosso.html:7
#: .\webapp\templates\navigation.html:18 .\webapp\templates\sosso.html:10
msgid "Sössö"
msgstr "Sössö"
#: .\webapp\templates\navigation.html:26
#: .\webapp\templates\navigation.html:25
msgid "Corporate"
msgstr "Yritys"
@@ -1023,5 +1077,34 @@ msgstr "Kaikki haasteet"
msgid "Total challenges:"
msgstr "Haasteita yhteensä:"
#~ msgid "Infoscreen"
#~ msgstr "Infonäyttö"
#~ msgid "Admin tools"
#~ msgstr "Hallintatyökalut"
#~ msgid ""
#~ "\n"
#~ " first_name, last_name, email_address and place_of_origin "
#~ "should be given string values.\n"
#~ " ayy_member and jas_recipient should be given the value 0 "
#~ "(off) or 1 (on).\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ " first_name, last_name, email_address ja place_of_origin ovat "
#~ "merkkijonoja.\n"
#~ " ayy_member ja jas_recipient ovat joko 0 (off) tai 1 (on).\n"
#~ " "
#~ msgid "Syntax"
#~ msgstr "Syntaksi"
#~ msgid "Data"
#~ msgstr "Data"
#~ msgid "Missing \"textfield\" POST request field"
#~ msgstr "Puuttuva \"textfield\" POST-kenttä"
#~ msgid "Applied for board"
#~ msgstr "Hakenut hallitukseen"
+5 -1
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(',') for row in clean_data]
clean_data = [row.rstrip(',').rstrip('\r').strip() for row in clean_data]
csv_reader = csv.DictReader(clean_data, fieldnames=MemberForm.Meta.fields, delimiter=delimiter, quoting=csv.QUOTE_NONE)
members = []
@@ -122,3 +122,7 @@ 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
@@ -0,0 +1,26 @@
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.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

+4
View File
@@ -15,6 +15,10 @@
{% endif %}
{{ table|safe }}
<div>
<a href="/members/export_applications" class="btn btn-info">{% trans "Download Excel" %}</a>
</div>
</div>
{% endblock content %}
+24 -17
View File
@@ -1,7 +1,7 @@
{% extends "members_base.html" %}
{% load i18n %}
{% load static %}
{% block content %}
<div>
<div>
@@ -11,24 +11,28 @@
<div>
<p>
{% blocktrans %}
Enter member information in CSV format, separate members on separate lines.
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.
{% endblocktrans %}
</p>
<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>
<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>
<label>{% trans "Format the member table like this:" %}</label>
<div>
<img src="{% static "img/excel_csv_save_example.png" %}">
</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">
@@ -36,13 +40,16 @@
<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=";" />
<p class="form-text text-muted">
<small class="form-text text-muted">
{% blocktrans %}The symbol that is used to separate items in one line. Defaults to ';' (semicolon).{% endblocktrans %}
</p>
</small>
</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_csv" class="btn btn-info">{% trans "Download CSV" %}</a>
<a href="/members/export_members" class="btn btn-info">{% trans "Download Excel" %}</a>
</div>
</div>
{% endblock content %}
+4
View File
@@ -36,5 +36,9 @@
{% endif %}
{{ table|safe }}
<div>
<a href="/members/export_payments" class="btn btn-info">{% trans "Download Excel" %}</a>
</div>
</div>
{% endblock content %}
+10
View File
@@ -0,0 +1,10 @@
{% 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 %}
@@ -0,0 +1,3 @@
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
@@ -0,0 +1 @@
Testi;Ukkeli;testi@ukkeli.fi;Espoo;1;0
1 Testi Ukkeli testi@ukkeli.fi Espoo 1 0
+73 -7
View File
@@ -3,10 +3,12 @@
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
from members.models import Member, Payment, Request
from rest_framework.authtoken.models import Token
import logging
import os
import pyexcel
class MemberRegisterTestCase(TestCase):
@@ -14,7 +16,13 @@ class MemberRegisterTestCase(TestCase):
def setUp(self):
"""Setup testing environment by creating member and admin."""
memb = Member.objects.create(first_name="Tidus", last_name="Tester")
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)
username, password = 'test_admin', 'password123'
test_admin = User.objects.create_superuser(
username, 'myemail@test.com', password)
@@ -31,16 +39,27 @@ class MemberRegisterTestCase(TestCase):
def test_import_csv_single_line(self):
"""Test csv import only with single line in csv file."""
data = 'Teppo, Tulppu, teppo@tulppu.fi, Ankkalinna, 0, 0'
response = self.c.post('/members/import_csv', {'textarea': data}, follow=True)
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)
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')
response = self.c.post('/members/import_csv', {'textarea': data}, follow=True)
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)
self.assertEqual(response.status_code, 200)
def test_autocomplete_search_found(self):
@@ -87,3 +106,50 @@ 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())
+8 -3
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, export_csv
from members.views import application_accept, import_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,6 +20,9 @@ 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
@@ -108,8 +111,10 @@ urlpatterns = [
# send CSV member data by POST
url(r'^import_csv', import_csv),
# download CSV member data
url(r'^export_csv', export_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),
# favourite icon
url(r'^favicon\.ico$', favicon_view),
+12 -25
View File
@@ -14,6 +14,7 @@ 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
@@ -47,8 +48,7 @@ def application_edit(request, *args, **kwargs):
"""Edit member request information."""
i = kwargs.pop('index', None)
if i is None:
return render(
request, 'error.html', {'error': _('No application id specified')})
return error_view(request, _('No application id specified'))
else:
application = Request.objects.get(id=i)
form = ApplicationForm(instance=application)
@@ -68,9 +68,7 @@ def application_accept(request, *args, **kwargs):
if id is not None:
application = Request.objects.get(id=id)
else:
return render(request,
'error.html',
{'error': _("Application missing 'id' field.")})
return error_view(request, _("Application missing 'id' field."))
form = ApplicationForm(request.POST, instance=application)
if form.is_valid():
@@ -78,9 +76,9 @@ def application_accept(request, *args, **kwargs):
application = form.save()
if Member.objects.filter(email=application.email).exists():
return render(request,
'error.html',
{'error': _('Email {} is already in use by a member. Application cannot be accepted.').format(application.email)})
return error_view(request, _(
'Email {} is already in use by a member. Application cannot be accepted.'
).format(application.email))
member = application.to_member()
member.save()
@@ -96,14 +94,10 @@ def application_accept(request, *args, **kwargs):
'/members/list?notification={}'.format(html.escape(notification)))
except Exception as ex:
logging.exception('Exception while accepting application')
return render(request,
'error.html',
{'error': str(ex)})
return error_view(request, str(ex))
else:
logging.info(form)
return render(request,
'error.html',
{'error': form.errors})
return error_view(request, form.errors)
@ensure_csrf_cookie
@@ -115,8 +109,7 @@ def application_delete(request, *args, **kwargs):
try:
id = request.POST['id']
except KeyError:
return render(
request, 'error.html', {'error': _('No application id specified')})
return error_view(request, _('No application id specified'))
try:
application = Request.objects.get(id=id)
@@ -130,9 +123,7 @@ def application_delete(request, *args, **kwargs):
'/members/applications?notification={}'
.format(html.escape(notification)))
except:
return render(request,
'error.html',
{'error': _('Could not delete application object')})
return error_view(request, _('Could not delete application object'))
@ensure_csrf_cookie
@@ -143,9 +134,7 @@ def application_delete_confirm(request, *args, **kwargs):
"""Confirm application deletion."""
i = kwargs.pop('index', None)
if i is None:
return render(request,
'error.html',
{'error': _('No application id specified')})
return error_view(request, _('No application id specified'))
else:
application = Request.objects.get(id=i)
form = ApplicationForm(instance=application)
@@ -172,6 +161,4 @@ def application_submit(request, *args, **kwargs):
form.save()
return render(request, 'application_success.html', {})
else:
return render(request,
'error.html',
{'error': form.errors})
return error_view(request, form.errors)
+11 -18
View File
@@ -70,8 +70,7 @@ def member_delete_confirm(request, *args, **kwargs):
"""Render member deletion confirmation page."""
i = kwargs.pop('index', None)
if i is None:
return render(request, 'error.html',
{'error': _('No member id specified')})
return error_view(request, _('No member id specified'))
else:
member = Member.objects.get(id=i)
form = MemberForm(instance=member)
@@ -94,11 +93,15 @@ 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()
@@ -129,7 +132,7 @@ def member_submit(request, *args, **kwargs):
return HttpResponseRedirect(
'/members/list?notification={}'.format(html.escape(notification)))
else:
return render(request, 'error.html', {'error': form.errors})
return error_view(request, form.errors)
@ensure_csrf_cookie
@@ -143,10 +146,7 @@ def member_update(request, *args, **kwargs):
if id is not None:
member = Member.objects.get(id=id)
else:
return render(request,
'error.html',
{'error': _("Member missing 'id' field.")})
logging.debug(member)
return error_view(request, _("Member missing 'id' field."))
form = MemberForm(request.POST, instance=member)
if form.is_valid():
@@ -160,10 +160,7 @@ def member_update(request, *args, **kwargs):
return HttpResponseRedirect(
'/members/list?notification={}'.format(html.escape(notification)))
else:
return render(
request,
'error.html',
{'error': form.errors})
return error_view(request, form.errors)
@ensure_csrf_cookie
@@ -175,8 +172,7 @@ def member_delete(request, *args, **kwargs):
try:
id = request.POST['id']
except KeyError:
return render(request,
'error.html', {'error': _('No member id specified')})
return error_view(request, _('No member id specified'))
try:
member = Member.objects.get(id=id)
@@ -189,9 +185,7 @@ def member_delete(request, *args, **kwargs):
return HttpResponseRedirect(
'/members/list?notification={}'.format(html.escape(notification)))
except:
return render(request,
'error.html',
{'error': _('Could not delete member object')})
return error_view(request, _('Could not delete member object'))
@ensure_csrf_cookie
@@ -202,8 +196,7 @@ def member_edit(request, *args, **kwargs):
"""Edit member information."""
i = kwargs.pop('index', None)
if i is None:
return render(
request, 'error.html', {'error': _('No member id specified')})
return error_view(request, _('No member id specified'))
else:
member = Member.objects.get(id=i)
form = MemberForm(instance=member)
+7 -16
View File
@@ -14,6 +14,7 @@ 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
@@ -73,7 +74,7 @@ def payment_submit(request, *args, **kwargs):
'/members/payments?notification={}'
.format(html.escape(notification)))
else:
return render(request, 'error.html', {'error': form.errors})
return error_view(request, form.errors)
@ensure_csrf_cookie
@@ -84,9 +85,7 @@ def payment_edit(request, *args, **kwargs):
"""Edit payment."""
i = kwargs.pop('index', None)
if i is None:
return render(request,
'error.html',
{'error': _('No payment id specified')})
return error_view(request, _('No payment id specified'))
else:
payment = Payment.objects.get(id=i)
form = PaymentForm(instance=payment)
@@ -103,9 +102,7 @@ def payment_delete_confirm(request, *args, **kwargs):
"""Render payment delete confirmation page."""
i = kwargs.pop('index', None)
if i is None:
return render(request,
'error.html',
{'error': _('No payment id specified')})
return error_view(request, _('No payment id specified'))
else:
payment = Payment.objects.get(id=i)
form = PaymentForm(instance=payment)
@@ -123,9 +120,7 @@ def payment_delete(request, *args, **kwargs):
try:
id = request.POST['id']
except KeyError:
return render(request,
'error.html',
{'error': _('No payment id specified')})
return error_view(request, _('No payment id specified'))
try:
payment = Payment.objects.get(id=id)
@@ -138,9 +133,7 @@ def payment_delete(request, *args, **kwargs):
'/members/payments?notification={}'
.format(html.escape(notification)))
except:
return render(request,
'error.html',
{'error': _('Could not delete payment object')})
return error_view(request, _('Could not delete payment object'))
@ensure_csrf_cookie
@@ -165,6 +158,4 @@ def payment_update(request, *args, **kwargs):
'/members/payments?notification={}'
.format(html.escape(notification)))
else:
return render(request,
'error.html',
{'error': _('Could not update payment object')})
return error_view(request, _('Could not update payment object'))
+35 -30
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
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest
from django.core.mail import send_mail
from django.conf import settings
from django.utils.translation import ugettext as _
@@ -20,8 +20,9 @@ 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
from members.forms import MemberForm, PaymentForm, ApplicationForm, CSVValidationError, UploadFileForm
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
@@ -34,8 +35,8 @@ class MemberDetail(generics.RetrieveAPIView):
throttle_classes = (BurstRateThrottle, SustainedRateThrottle, )
def error_view(request, message):
return render(request, 'error.html', {'error': str(message)})
def error_view(request, message, status=400):
return render(request, 'error.html', {'error': message}, status=400)
def validate_recaptcha(response):
@@ -108,13 +109,14 @@ 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:
data = request.POST['textfield']
csv_in_memory_file = request.FILES.get('csvFile')
csv_file = csv_in_memory_file.file
data = csv_file.read().decode('utf-8')
delimiter = request.POST.get('delimiter', ',')
payment_source = request.POST['payment_source']
except:
return render(request,
'error.html',
{'error': _('Missing "textfield" POST request field')})
return error_view(request, _('Missing CSV file'))
try:
result = MemberForm.csv_to_models(data, payment_source=payment_source, delimiter=delimiter)
@@ -123,7 +125,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, ex)
return error_view(request, str(ex))
member_table = MemberTable(result.members,
request=request,
@@ -142,6 +144,7 @@ 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
@@ -149,27 +152,6 @@ 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,
@@ -177,3 +159,26 @@ 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)
+4 -1
View File
@@ -14,11 +14,14 @@
"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"
]
}
}
+4 -1
View File
@@ -25,9 +25,12 @@ 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
+4 -1
View File
@@ -78,16 +78,19 @@ INSTALLED_APPS = [
'webapp',
'members',
'infoscreen',
'coffee_scale.apps.CoffeeScaleConfig',
'coffee_scale',
'rest_framework',
'django_nose',
'bootstrap3',
'django_tables2',
'auditlog',
'phonenumber_field',
'import_export',
'password_reset',
]
IMPORT_EXPORT_USE_TRANSACTIONS = True
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
NOSE_ARGS = [