175 Commits

Author SHA1 Message Date
Aarni Halinen 4e9f91efce Write new email messages for member applicants 2018-10-29 15:09:23 +02:00
Aarni Halinen 3c098a36bc Update application success texts 2018-10-29 14:57:12 +02:00
Aarni Halinen bc328995c5 Comment out recaptcha 2018-10-29 14:56:30 +02:00
Aarni Halinen eb5659d0da Minor cleanup of infoscreen 2018-10-24 20:54:31 +03:00
Aarni Halinen 50268b98a9 Remove underscore.js 2018-10-24 20:54:08 +03:00
Aarni Halinen 4a2a5f9d76 Bugfix #117: API Event tags
Add API for tags so events can get the related field.
2018-10-23 14:45:08 +03:00
Aarni Halinen b1eda70e7a Kaehmy translations 2018-10-20 23:14:48 +03:00
Aarni Halinen f7b00ab3cc Add GDPR checkbox 2018-10-20 22:51:42 +03:00
Aarni Halinen 9cad12d879 Login required for kaehmy export page 2018-10-20 22:39:22 +03:00
Aarni Halinen d4800c449d Replace kahmyopas.pdf 2018-10-19 13:41:27 +03:00
Aarni Halinen d3d4ce5e5d Real update to kaehmy banner 2018-10-18 23:22:04 +03:00
Aarni Halinen 9dbc2424b1 Update kaehmy banner 2018-10-18 23:02:16 +03:00
Aarni Halinen 5b38065f13 Remove culture category 2018-10-18 21:21:29 +03:00
Aarni Halinen 1879c265cd Update kaehmy dates 2018-10-18 21:20:37 +03:00
Aarni Halinen 4273b95b70 Update final kaehmy date 2018-10-16 18:53:29 +03:00
Aarni Halinen e96698ef2e Update kaehmy translations 2018-10-15 18:33:14 +03:00
Aarni Halinen e04b8ddb9b Update kaehmy dates 2018-10-15 18:30:08 +03:00
Aarni Halinen 6fe3e69cd9 Update package-lock 2018-10-03 14:38:50 +03:00
Aarni Halinen 7d1a8bd284 Ignore sik_test 2018-10-03 14:38:30 +03:00
Aarni Halinen 5b44d2e4c7 Fix favicon 2018-09-10 21:08:37 +03:00
Aarni Halinen 8552bde245 Fix members static file paths 2018-09-10 13:16:51 +03:00
Aarni Halinen 9a4d7fc498 Fix Sosso containers 2018-09-10 12:04:50 +03:00
Aarni Halinen 6dea241408 Add missing import 2018-09-10 11:43:59 +03:00
Aarni Halinen b3b1bec12c Add special characters to Sosso infoscreen 2018-09-10 11:40:57 +03:00
Aarni Halinen 02cc305abf Fix indentation 2018-09-02 22:27:14 +03:00
Aarni Halinen ffe0c45eea Fix few kaehmy problems
- Fix variable naming in statistics
- Add scope for html files to fix rendering of errors
- Fix footer.css path and remove duplicate
2018-09-02 22:23:52 +03:00
Aarni Halinen 499ddc0979 New kaehmy BaseRole class 2018-09-02 22:03:54 +03:00
Aarni Halinen b154ffb79e Add normal permissions to DRF root 2018-08-21 19:27:18 +03:00
Aarni Halinen 87dfab0e57 Change dev server port on gitlab-ci.yml 2018-08-15 23:37:40 +03:00
Aarni Halinen 92ec8b1b4e Dev server docker and host ports to 8000 2018-08-15 23:32:41 +03:00
Aarni Halinen ac8fb0bfe3 Set authentication to webapp DRF 2018-07-24 19:09:42 +03:00
Aarni Halinen 0ce6af8f7c Fix import and redefinition issues 2018-07-24 18:30:21 +03:00
Aarni Halinen 053b705cc1 Merge branch 'Django2.0' into 'develop'
:Django2.0

See merge request vtmk/web2.0!115
2018-07-24 18:29:00 +03:00
Aarni Halinen 3676f23f65 Fix payment autocomplete permissions and remove test skips
Fixed decorators in urls.py. Moved to method_decorators on view class
2018-07-24 18:19:57 +03:00
Aarni Halinen 7e821f277f Fix pycodestyle 2018-07-23 12:38:14 +03:00
Aarni Halinen 91cbdef71f Merge branch 'develop' into 'Django2.0'
# Conflicts:
#   webapp/urls.py
2018-07-18 20:44:23 +03:00
Aarni Halinen 0cfb78bc69 Remove not used api_view 2018-07-18 20:40:44 +03:00
Aarni Halinen 01c20b1a6e Add contacts API 2018-07-18 20:40:19 +03:00
Aarni Halinen 1711aca5ec Add templatequestions API 2018-07-18 20:39:08 +03:00
Aarni Halinen c536899cc9 Bring about page back 2018-07-18 20:12:06 +03:00
Aarni Halinen 60da9d8256 Add event update serializer 2018-07-18 20:11:24 +03:00
Aarni Halinen a923e225e8 API root to /api 2018-07-18 18:41:20 +03:00
Aarni Halinen 2d7c9d779a Feed API added 2018-07-18 18:37:01 +03:00
Aarni Halinen aea9898563 Skip failing autocomplete test 2018-07-17 22:00:39 +03:00
Aarni Halinen 4e3f71ea43 Remove duplicate imports 2018-07-17 22:00:08 +03:00
Aarni Halinen 6acbdbc760 Update to Django 2.0.7 for sqlite bugfix 2018-07-17 21:37:23 +03:00
Aarni Halinen 11b6e68fe1 Remove password_reset and own login, redirect to admin/login
Django admin login to be used. Thus we should be able to upgrade to
Django2.0
2018-07-17 20:58:13 +03:00
Aarni Halinen cc3aa66e49 Merge branch 'develop' into Django2.0 2018-07-17 20:57:58 +03:00
Aarni Halinen 563344e8b4 Merge branch 'feature-events' into 'develop'
Basic API for events

See merge request vtmk/web2.0!120
2018-07-17 18:43:01 +03:00
Aarni Halinen 129b8e4601 Use id instead of url for SignUpSerializer 2018-07-10 22:42:37 +03:00
Aarni Halinen efde69984d Revert changes to base.py 2018-07-10 22:42:05 +03:00
Aarni Halinen c66c8e7367 Rest framework for Event and SignupForm 2018-07-10 22:33:26 +03:00
Aarni Halinen c31f454c78 Move signup models back to webapp 2018-07-05 18:58:33 +03:00
Aarni Halinen 18926d16d1 Create new signup related models 2018-06-19 20:46:06 +03:00
Aarni Halinen 4e8adebb2d Move Event and Registration model to own app 2018-06-05 19:33:35 +03:00
Aarni Halinen 0207bdf22b Upgrade psycopg2 2018-05-31 17:26:49 +03:00
Aarni Halinen 5caacd8f44 Add and fix translations 2018-05-22 23:54:58 +03:00
Aarni Halinen d7a3433d2c Merge branch 'feature-contacts' into develop
Conflicts:
	webapp/admin.py
	webapp/models.py
	webapp/views.py
2018-05-16 22:49:38 +03:00
Aarni Halinen 6e68e106aa Update contact models and template 2018-05-16 22:39:43 +03:00
Aarni Halinen 9225ff5967 Filter paid member fees by date 2018-05-14 01:36:11 +03:00
Aarni Halinen 6996bb8015 Merge branch 'feature-number-of-members' into 'develop'
Show number of paid members in member list

See merge request vtmk/web2.0!116
2018-05-13 13:15:30 +03:00
Aarni Halinen 7aff7c46ee Fix pycodestyle 2018-05-13 13:06:19 +03:00
Aarni Halinen 882732d054 Show number of paid members in member list
Member filtering also applies to this feature. Moved 'last_paid' Subquery to Member method
2018-05-13 13:02:27 +03:00
Aarni Halinen 9e0d911f7b Fix date rendering 2018-05-09 22:16:40 +03:00
Aarni Halinen ab6b7d19fb Update pyexcel 2018-05-09 21:24:36 +03:00
Aarni Halinen b95be67051 Fix pycodestyle 2018-05-09 20:24:16 +03:00
Aarni Halinen 48b6ed5b69 Update Django 2.0 2018-05-09 20:05:20 +03:00
Aarni Halinen e25041d38d Update django-nose 2018-05-09 18:57:29 +03:00
Aarni Halinen 7bc77ef232 Update django-suit 2018-05-09 18:49:01 +03:00
Aarni Halinen 0f8a7d76da Update auditlog 2018-05-09 18:46:35 +03:00
Aarni Halinen ecf34d9039 Fix deprecations in project 2018-05-09 18:33:59 +03:00
Aarni Halinen a9cf253c83 Silence pyexcel DEBUG logs
Close issue #103
2018-05-07 01:12:54 +03:00
Aarni Halinen 45e98e0220 Fix uncommited code 2018-03-02 02:57:26 +02:00
Aarni Halinen d8ea26c777 Pycodestyle fixes 2018-03-02 02:48:18 +02:00
Aarni Halinen c398e53750 Add translations 2018-03-02 02:45:59 +02:00
Aarni Halinen 9ce0eccfce Move member specific error to member app 2018-03-02 02:43:02 +02:00
Aarni Halinen 17e6bb86ed Fix last paid sorting
Bug #105. Rendering needs better formatting.
2018-03-02 02:40:42 +02:00
Aarni Halinen a1b85b3c6c Merge branch 'bugfix-members-sort' into 'develop'
Bugfix: #105

See merge request vtmk/web2.0!113
2018-03-02 00:40:28 +02:00
Aarni Halinen fafc988a60 Fix sorting bug #105
Also removed unneeded code
2018-03-02 00:32:11 +02:00
Elias 55bcc78670 Sorted roles by committees and some basic layout 2018-03-02 00:31:10 +02:00
Elias 7bc277a978 korjauksia 2018-03-01 20:53:31 +02:00
Elias 780e2d6acb Committee model 2018-03-01 19:28:27 +02:00
Aarni Halinen bbc5743c39 Add VSCode workspace files to gitignore 2018-03-01 19:27:51 +02:00
Jan Tuomi 3298e55fb0 Fix password reset form error 2018-02-25 20:34:39 +02:00
Aarni Halinen 2b2d20f796 Remove references to deleted css files 2018-02-22 16:31:12 +02:00
Aarni Halinen 117802bf10 Revert deletion on footer-padder element 2018-02-22 01:43:15 +02:00
Aarni Halinen 8bd53ba897 Fix footer bug #104 2018-02-21 22:18:43 +02:00
Aarni Halinen 5114b2ef85 Revert "Remove bugging footer from infoscreen"
This reverts commit d7f53b9700.
2018-02-06 20:47:43 +02:00
Aarni Halinen d7f53b9700 Remove bugging footer from infoscreen 2018-02-06 19:32:28 +02:00
Aarni Halinen 1ab3180c0d Add 'Wall' flag to pre-push hook
This should help noticing deprecations
2018-02-05 10:25:01 +02:00
Aarni Halinen b8a8cb2c6d Add message to Ohlhafv email 2018-02-01 22:23:02 +02:00
Aarni Halinen a56e8ef241 Fix typo 2018-02-01 22:11:50 +02:00
Aarni Halinen d5d11edbe7 Fix remark issues in ilmotunkki readme 2018-02-01 16:32:14 +02:00
Aarni Halinen f7c2516cd7 Fix ABB.css header height issue 2018-02-01 16:26:57 +02:00
Aarni Halinen 45b27e3ac9 Fix ABB.html empty jobs path 2018-02-01 15:53:04 +02:00
Aarni Halinen 52613ba7d4 Fix ABB css in infoscreen 2018-01-30 17:28:14 +02:00
Aarni Halinen f136b34b6c Merge branch 'hotfix-ohlhafv-form' into 'develop'
Remove unneeded challenger email field

See merge request vtmk/web2.0!104
2018-01-30 17:00:56 +02:00
Aarni Halinen 4d90454cf8 Remove unneeded challenger email field 2018-01-30 16:48:41 +02:00
Aarni Halinen 82e0c5c995 Merge branch 'feature-ohlhafv-mail' into 'develop'
Feature ohlhafv mail

See merge request vtmk/web2.0!103
2018-01-30 16:44:03 +02:00
Aarni Halinen dec3bf5ccf Remove old code 2018-01-30 16:34:53 +02:00
Aarni Halinen 23c00ce167 Add Email sending to Ohlhafv 2018-01-30 16:23:19 +02:00
Aarni Halinen bdc21f79e8 Fix missing settings import 2018-01-30 14:50:32 +02:00
Aarni Halinen 2fc71eabe3 Merge branch 'feature-refactor-webapp' into 'develop'
Feature refactor webapp

See merge request vtmk/web2.0!102
2018-01-30 11:38:58 +02:00
Jan Tuomi 3debd4b1ec Fix comment data migration 2018-01-30 10:51:20 +02:00
Jan Tuomi b765ae37e8 Remove unused print 2018-01-28 23:45:40 +02:00
Jan Tuomi 9eae16110c Write data migration for ohlhafv challenges 2018-01-28 23:39:27 +02:00
Jan Tuomi ce1f4eb7c9 Write data migrations for kaehmys 2018-01-28 23:30:22 +02:00
Jan Tuomi 661dc84973 Fix pycodestyle 2018-01-28 21:19:19 +02:00
Jan Tuomi 6b21ea4af8 Move postgres init to own script 2018-01-28 21:14:00 +02:00
Jan Tuomi 37ab278086 Refactor member register 2018-01-28 20:59:15 +02:00
Jan Tuomi 509b157d65 Refactor coffee scale app 2018-01-28 20:32:07 +02:00
Jan Tuomi 253f720043 Restructure webapp and infoscreen 2018-01-28 20:23:56 +02:00
Jan Tuomi bdf6b469ad Move ohlhafv to its own app 2018-01-28 18:54:45 +02:00
Jan Tuomi 7dc9fac597 Fix kaehmy styles and templates 2018-01-28 16:56:51 +02:00
Jan Tuomi 77330dffe9 Refactor how url loading works for static files 2018-01-26 08:22:48 +02:00
Jan Tuomi f0ea3505e4 Move and rename a lot of stuff into kaehmy app 2018-01-26 01:15:35 +02:00
Jan Tuomi 06c2a2b9a6 Rename kaehmy templates 2018-01-25 23:28:49 +02:00
Jan Tuomi c671206e8a Rename pre-commit to pre-push 2018-01-25 22:36:38 +02:00
Jan Tuomi cf50050eba Update pre-commit script 2018-01-25 22:23:51 +02:00
Aarni Halinen f8e68acd4d Fix pydcodestyle warnings 2018-01-25 21:57:23 +02:00
Jan Tuomi 9b6fc5e687 Fix openpyxl version conflict by downgrading to 2.4.11 2018-01-25 21:52:34 +02:00
Aarni Halinen 7d17d8a84f Add Ohlhafv to admin and auditlog 2018-01-25 21:36:10 +02:00
Jan Tuomi b2aa6d1a3e Bump django-import-export version 2018-01-25 21:33:40 +02:00
Jan Tuomi 5a2c6d9aaa Bump pyexcel and pyexcel-xlsx versions 2018-01-25 21:25:15 +02:00
Aarni Halinen 1ecbda0731 Update ilmotunkki readme 2018-01-25 21:21:12 +02:00
Jan Tuomi 9e9049709b Fix display issue in charfield 2018-01-25 20:55:03 +02:00
Jan Tuomi ac9a5db356 Add missing translations 2018-01-25 20:54:37 +02:00
Aarni Halinen 67ed7edefb Merge branch 'feature-ohlhafv' into develop 2018-01-25 20:19:47 +02:00
Aarni Halinen 181be6b80c Fix bug in Ohlhafv model regarding translation 2018-01-25 20:10:54 +02:00
Aarni Halinen 1e2bf10494 Allow blank Ohlhafv messages 2018-01-25 20:06:17 +02:00
Jan Tuomi a219b930b3 Make a title black 2018-01-25 18:33:04 +02:00
Jan Tuomi 78ba2d7afa Add hot new designs to Ohlhafv 🔥 2018-01-25 18:31:06 +02:00
Jan Tuomi 6fe01dadf1 Add a couple missing translations 2018-01-25 16:30:05 +02:00
Aarni Halinen 9df62a1247 Fix translations 2018-01-24 19:09:51 +02:00
Aarni Halinen a9164f8c6e Construct Ohlhafv page based on kaehmy 2018-01-24 19:02:44 +02:00
Aarni Halinen 6e5074f8fe Rearrange html files to folders 2018-01-24 18:12:21 +02:00
Elias efe8808e79 Yhteystiedot alustava 2018-01-24 17:48:14 +02:00
Aarni Halinen 77cdce714a Add translations to Ohlhafv page 2018-01-24 17:37:15 +02:00
Aarni Halinen 69c1b2dcb2 Add team challenge option 2018-01-24 17:20:55 +02:00
Aarni Halinen 2a50f7ef43 Pycodestyle whitepsace fixes 2018-01-24 16:46:34 +02:00
Jan Tuomi 0c93446b81 Add some translations 2018-01-24 16:32:29 +02:00
Jan Tuomi 0e52efb449 Fix footer pos and add placeholder content 2018-01-24 16:24:54 +02:00
Elias 283d5b566e Except handling for 'Git not found' in about page 2018-01-24 16:14:29 +02:00
Jan Tuomi a4367bbc9d Merge branch feature-refactor-login 2018-01-10 19:54:08 +02:00
Jan Tuomi 935f7a38f1 Fix login styles 2018-01-10 19:49:59 +02:00
henu 9973057051 Add new styles to main web page 2018-01-10 19:42:20 +02:00
Jan Tuomi 9527e6de5f Skip pre-commit if virtualenv is not found 2017-11-26 00:10:46 +02:00
Jan Tuomi a519d51309 Fix style issues in kaehmy list 2017-11-25 23:29:20 +02:00
Jan Tuomi 9ee4d600a7 Update setup.sh 2017-11-25 22:18:41 +02:00
Jan Tuomi 2b2d635cb0 Update readme and pre-commit 2017-11-25 21:56:07 +02:00
Jan Tuomi ec6051d3d6 Add pre-commit hook 2017-11-25 21:49:30 +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
262 changed files with 9647 additions and 3353 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
+4 -1
View File
@@ -17,4 +17,7 @@ requirements_henu.txt
/collected_static/
mydatabase
settings.json
.vscode/
.vscode/
.DS_Store
*.code-workspace
sik_test
+3 -2
View File
@@ -63,7 +63,7 @@ deploy_dev:
image: alpine:latest
environment:
name: dev
url: http://web.sik.party:8080
url: http://web.sik.party:8000
only:
- develop
before_script:
@@ -76,7 +76,8 @@ deploy_dev:
- '[[ -f /.dockerenv ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'
script:
- scp docker-compose.yml $DEV_SSH_USER@$DEV_SSH_HOST:~/deployment/docker-compose.yml
- ssh $DEV_SSH_USER@$DEV_SSH_HOST "cd deployment && docker-compose down && docker pull \"$IMAGE_NAME\" && docker-compose up -d && docker image prune -f"
- scp .deploy_dev.sh $DEV_SSH_USER@$DEV_SSH_HOST:~/deployment/deploy_dev.sh
- ssh $DEV_SSH_USER@$DEV_SSH_HOST "bash ~/deployment/deploy_dev.sh \"$IMAGE_NAME\""
deploy_production:
stage: deploy
-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))
-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
@@ -16,7 +16,7 @@ body {
background-size: contain;
background-repeat: no-repeat;
background-position: bottom center;
background-image: url("/static/img/smokes.png");
background-image: url("/static/coffee_scale/img/smokes.png");
transform-origin: bottom;
animation: smokes 8s ease-in-out 0s infinite;
opacity:0;
@@ -27,7 +27,7 @@ body {
background-size: contain;
background-repeat: no-repeat;
background-position: top center;
background-image: url("/static/img/coffeecup3.png");
background-image: url("/static/coffee_scale/img/coffeecup3.png");
height:60%;
}
#scale{
@@ -47,7 +47,7 @@ body {
background: green;
border-radius: 10px;
}
#brewtime{
.brewtime{
text-align:right;
position:absolute;
right:0px;
@@ -62,10 +62,13 @@ body {
font-size:4vw;
color: #333;
}
.layertwo{
display: None;
}
noscript{
color:red;
}
#text{
.text{
color:green;
position:absolute;
top:50%;
@@ -102,9 +105,9 @@ noscript{
}
@keyframes coffeeready {
0% {background-color:white;}
25% {background-color:green;}
25% {background-color:rgb(100, 255, 100);}
50% {background-color:white;}
75% {background-color:green;}
75% {background-color:rgb(100, 255, 100);}
100% {background-color:white;}
}
@keyframes unknown {

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

@@ -0,0 +1,183 @@
//Inner state
var lastBrew = new Date(0);
var brewing = false;
var backoff = 2000;
//MQTT client config
var username = "coffee-user-"+ Math.random();
// eslint-disable-next-line no-undef
var client = new Paho.MQTT.Client("sika.sahkoinsinoorikilta.fi", 9001, username);
client.onMessageArrived = function (message) {
// eslint-disable-next-line no-console
console.log("Topic: "+message.destinationName+" msg: "+message.payloadString);
var ev = new CustomEvent(message.destinationName, {'detail': message.payloadString});
window.dispatchEvent(ev);
}
function reconnect(responseObject){
if (responseObject.errorCode !== 0) {
console.log("connection lost! Reason: "+responseObject.errorMessage); // eslint-disable-line no-console
setTimeout(function(){
client.connect({onSuccess:onConnect, useSSL:true, onFailure: reconnect});
}, backoff);
}
}
function onConnect() {
console.log("MQTT connected"); // eslint-disable-line no-console
//set and reset reconnector
client.onConnectionLost = reconnect
// subscribe to topics
client.subscribe("sik/kiltahuone/kahvivaaka/cups");
client.subscribe("sik/kiltahuone/kahvivaaka/brewing");
client.subscribe("sik/kiltahuone/kahvivaaka/brewtime");
}
// data update and parse functions
function parseCups(ev){
var cups = parseFloat(ev.detail).toFixed(1)
function makeEvent(cups) {
return (String(cups) !== '-1.0')
? new CustomEvent("cupsChanged", {'detail': cups})
: new CustomEvent("cupsError", {'detail': 'Error: unable to fetch cups :('});
}
window.dispatchEvent(makeEvent(cups));
}
function updateCups(ev){
$("#text").text(ev.detail);
}
function showCupsError(ev) {
$('#text').text(ev.detail);
$('#text').css({
'font-size': '7vh',
'left': '0',
'top': '40%',
'width': '100%',
'text-align': 'center',
'color': 'red',
});
$('#lower').css({'background-image': 'none'});
}
function updateScale(ev){
$("#scale2").css({width: Math.min(ev.detail/9*100,100) + '%'});
}
function tick(){
var ev = new CustomEvent("tick", {'detail': new Date()});
window.dispatchEvent(ev);
}
function updateTime(ev){
var now = ev.detail;
$("#time").html(formatTime(now.getHours(),now.getMinutes(),now.getSeconds()));
}
function coffeeLowEffect(ev){
ev.detail <= 2 ? $("#text").addClass("hurry") : $("#text").removeClass("hurry");
}
function coffeeReadyEffect(){
$("body").addClass("coffeeready");
// autoclear animation class in 10s
setTimeout(function(){$("body").removeClass("coffeeready");}, 10000);
}
function hotEffect(ev){
var opa = Math.max(100 - ev.detail / 90000,0);
$("#upper").css({opacity: opa/100});
}
function brewAnimStart(){
$(".text").addClass("brewing");
$(".layerone").hide();
$(".layertwo").show();
}
function brewAnimEnd(){
$(".text").removeClass("brewing");
$(".layertwo").hide();
$(".layerone").show();
}
function brewNotifier(ev){
var new_brewing = parseInt(ev.detail);
if (new_brewing == 1 && brewing == 0){
window.dispatchEvent(new Event("brewStart"));
} else if (new_brewing == 0 && brewing == 1){
window.dispatchEvent(new Event("brewEnd"));
}
brewing = new_brewing;
}
function brewTimeParser(ev){
lastBrew = new Date(parseInt(ev.detail)*1000.0);
}
function updateBrewDiff(){
var now = new Date();
var timeDiff = Math.max(now.getTime() - lastBrew.getTime(), 0);
var eve = new CustomEvent("dtUpdate", {'detail': timeDiff});
window.dispatchEvent(eve);
}
function updateBrewTime(ev){
var timeDiff = ev.detail;
var timeStr;
if (timeDiff < 3600000){
timeStr = Math.round(timeDiff / 60000) + ' min'
} else if (timeDiff < 10000* 3600 * 1000){ // 1000h
timeStr = '~' + Math.round(timeDiff / 3600000 * 2) / 2 + ' h';
} else {
timeStr = "???"
}
$("#brewtime").html(timeStr);
}
// Helpers
function nToS(num){
return num < 10 ? "0" + num : "" + num;
}
function formatTime(hours, minutes, seconds){
return nToS(hours)+":"+nToS(minutes)+":"+nToS(seconds)
}
function resize(){
var w = $("#container").width();
var h = $("#container").height();
var s = w > h ? h : w;
var font = s * 0.8 * 0.38/Math.sqrt(3);
$(".text").css({ top: s*0.16-font/2 + 'px',
fontSize: font + 'px',
marginLeft: -font*3*3/10 + 'px'});
}
// Init everything
$(document).ready(function(){
client.connect({onSuccess:onConnect, useSSL:true, onFailure:reconnect});
//connect MQTT event listeners
window.addEventListener("sik/kiltahuone/kahvivaaka/cups", parseCups);
window.addEventListener("sik/kiltahuone/kahvivaaka/brewing", brewNotifier);
window.addEventListener("sik/kiltahuone/kahvivaaka/brewtime", brewTimeParser);
//connect other event listeners
window.addEventListener("cupsChanged", updateCups);
window.addEventListener("cupsChanged", coffeeLowEffect);
window.addEventListener("cupsChanged", updateScale);
window.addEventListener("cupsChanged", resize);
window.addEventListener("cupsError", showCupsError);
window.addEventListener("cupsError", coffeeLowEffect);
window.addEventListener("cupsError", updateScale);
window.addEventListener("brewStart", brewAnimStart);
window.addEventListener("brewEnd", brewAnimEnd);
window.addEventListener("brewEnd", coffeeReadyEffect);
window.addEventListener("tick", updateTime);
window.addEventListener("tick", updateBrewDiff);
window.addEventListener("dtUpdate", updateBrewTime);
window.addEventListener("dtUpdate", hotEffect);
//start time based events
setInterval(tick, 100);
tick();
});
$(window).resize(resize);
-130
View File
@@ -1,130 +0,0 @@
var len = 0;
var lastBrew = "&infin;";
var brewtext = "";
$(document).ready(function(){
$('#text').bind("DOMSubtreeModified", resize);
updateTime();
setInterval(updateTime,1000);
formatBrewTime();
setInterval(formatBrewTime,10000);
});
$(window).resize(resize);
function fetchdata(data, status){
if (typeof status !== 'undefined'){
if (status == "success"){
parseData(data);
}
else if (status == "error"){
handleError();
}
}
}
$.getJSON("/coffee/cups", fetchdata);
setInterval(function() {
$.getJSON("/coffee/cups", fetchdata);
}, 2000);
function formatBrewTime(){
if (!brewtext && lastBrew instanceof Date){
var now = new Date();
var timeDiff = Math.max(now.getTime() - lastBrew.getTime(), 0);
var tmp = (timeDiff < 3600000)
? Math.round(timeDiff / 60000) + ' min'
: '~' + Math.round(timeDiff / 3600000 * 2) / 2 + ' h';
$("#brewtime").html(tmp);
} else {
$("#brewtime").html(brewtext);
}
}
function handleError() {
setData("?", 0, 0, Number.MAX_VALUE, Number.MAX_VALUE);
}
function parseData(data) {
if (data) {
var date = new Date(data.date);
lastBrew = new Date(data.last_brew);
var now = new Date();
var cups = data.cups;
var brewing = data.brewing;
var timeDiff = Math.max(now.getTime() - lastBrew.getTime(),0);
var opa = Math.max(100 - timeDiff / 90000,0);
setData(cups, data.temp, opa,now.getTime()-date.getTime(), timeDiff, brewing);
}
else{
handleError();
}
}
function setData(cups, temp, opa, timeFromUpdate, timeFromBrew, brewing){
if (cups == 0) {
opa = 0;
}
brewtext = "";
$("#upper").css({opacity: opa/100});
$("#scale2").css({width: Math.min(cups/9*100,100) + '%'});
$("#text,body").removeClass();
cups = Number(cups).toFixed(1);
if(timeFromUpdate > 600000){
cups = "?";
brewtext = "&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 formatTime(hours, minutes, seconds){
var str = "";
if(hours < 10)
str += "0";
str += hours;
str += ":";
if(minutes < 10)
str += "0";
str += minutes;
str += ":";
if(seconds < 10)
str += "0";
str += seconds;
return str;
}
function resize(){
var w = $("#container").width();
var h = $("#container").height();
var s = w > h ? h : w;
var font = s*0.8*0.38/Math.sqrt(len);
$("#text").css({ top: s*0.16-font/2 + 'px', fontSize: font + 'px', marginLeft: -font*len*3/10 + 'px'});
}
+11 -4
View File
@@ -1,3 +1,6 @@
{% load i18n %}
{% load static %}
<html>
<head>
<title>Coffee Cups @Guild Room - AYY SIK ry</title>
@@ -10,13 +13,16 @@
<meta http-equiv="pragma" content="no-cache" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<link rel="stylesheet" href="/static/css/coffee.css" />
<script src="/static/js/coffee.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.2/mqttws31.js"
type="text/javascript"></script>
<link rel="stylesheet" href="{% static "coffee_scale/css/coffee.css" %}">
<script src="{% static "coffee_scale/js/coffee.js" %}"></script>
</head>
<body>
<div id="container">
<span id="brewtime"></span>
<span id="brewtime" class="brewtime layerone"></span>
<span class="brewtime layertwo">:)</span>
<span id="address">
ka.dy.fi
<noscript><br>This page uses JavaScript!</noscript>
@@ -26,7 +32,8 @@
</div>
<!--Kahvinkeitin on rikki. Varakeittimellä keitettyä kahvia saattaa olla.-->
<div id="lower" class="normal">
<div id="text"></div>
<div id="text" class="text layerone">???</div>
<div class="text layertwo">&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)
+6 -8
View File
@@ -1,14 +1,12 @@
from django.conf.urls import url
from django.views.generic.base import RedirectView
from .views import coffee_view, cups_view
favicon_view = RedirectView.as_view(url='static/img/favicon.ico', permanent=True)
from django.conf import settings
from .views import coffee_view
urlpatterns = [
# landing page
url(r'^$', coffee_view),
url(r'^cups', cups_view),
]
if settings.DEBUG:
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns()
+1 -17
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)
return render(request, 'coffee_scale:coffee.html')
+3 -3
View File
@@ -5,9 +5,9 @@ services:
image: postgres
web:
build: .
image: 86.50.143.82:5000/web20
command: ["bash", "-c", "cd /code && ./wait-for-it.sh db:5432 -- bash setup.sh --no-input --no-npm && python manage.py runserver 0.0.0.0:8080"]
image: git.sahkoinsinoorikilta.fi:4567/vtmk/web2.0
command: ["bash", "-c", "cd /code && ./wait-for-it.sh db:5432 -- bash setup.sh --no-input --no-npm && python manage.py runserver 0.0.0.0:8000"]
ports:
- "8080:8080"
- "8000:8000"
depends_on:
- db
+49
View File
@@ -0,0 +1,49 @@
# Ilmotunkki
## Terms
- Signup, Form with collection of questions
- Response, One answer to some signup
- Quota, Amount of people allowed to respond with some option selected.
- In generic case there is no option and quota is just max number of people.
## Requirements
- Officials may generate signups forms
- Officials may see results from signups
- Officials may see some stats from their signups
- for example distributions of multiple choice answers
- Officials should be able to edit signups wherever possible
- Propably not possible to edit after first response
- Officials should be able to delete responses
- Officials should be able to embed payment information to the signup?
- TODO: is there need for unique reference numbers for every response?
- Officials should be able to save a signup to a reusable template.
- Possibility to save templates?
- Signup may be attached to an event
- Multiple signups to a single event should be possible (FTMK uses for museum visits? Erna asked if it was possible in old web)
- Possibility for external service (Google Form, URL will suffice)
- Signup should support custom quotas
- Atleast quotas from multiple choices and checkboxes
- Text quotas are risky (typos everywhere!!)
- Signup should have start and end times
- Signup should support atleast following questiontypes
- Text
- multiple choice (select one)
- checkbox (boolean yes/no)
- Signup should support reserve slots.
- TODO: quota based reserves or generic? or both?
- Responding should send confirm email
- Response should be editable by responder and only by the responder until the closing of the signup
- TODO: is there need to custom edit period or disable?
- Responders should see amount of quotas left.
- Responders should see some information about other responses
- TODO: names? should this be editable by officials?
- Or superadmin can edit and the one signing up within edit period
- NOTE: Quota related info is exposed if any info is printed
- When quotas need to be hidden? PoTa?
+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'
+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.
+3 -4
View File
@@ -1,10 +1,9 @@
"""Admin site registers."""
from django.contrib import admin
from infoscreen.models import Rotation, InfoItem, InfoInstance
from infoscreen.models import ImageInfoItem, ExternalImageInfoItem, ABBInfoItem
from infoscreen.models import ExternalWebsiteInfoItem
from infoscreen.models import VideoInfoItem
from infoscreen.models import (
Rotation, InfoItem, InfoInstance, ImageInfoItem,
ExternalImageInfoItem, ABBInfoItem, ExternalWebsiteInfoItem, VideoInfoItem)
# Register your models here.
admin.site.register(Rotation)
+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
+19 -19
View File
@@ -98,12 +98,12 @@ class ABBInfoItem(InfoItem):
def get_template_url(self):
"""Return ABB infoitem template url."""
return "/static/html/abb.html"
return "/static/infoscreen/html/abb.html"
@staticmethod
def get_create_template_url():
"""Call create ABB infoitem template url command."""
return "/static/html/abb_create.html"
return "/static/infoscreen/html/abb_create.html"
class ApyInfoItem(InfoItem):
@@ -113,12 +113,12 @@ class ApyInfoItem(InfoItem):
def get_template_url(self):
"""Return APY infoitem template url."""
return "/static/html/apy.html"
return "/static/infoscreen/html/apy.html"
@staticmethod
def get_create_template_url():
"""Call create APY infoitem template url command."""
return "/static/html/apy_create.html"
return "/static/infoscreen/html/apy_create.html"
class ExternalWebsiteInfoItem(InfoItem):
@@ -129,12 +129,12 @@ class ExternalWebsiteInfoItem(InfoItem):
def get_template_url(self):
"""Return external website infoitem template url."""
return "/static/html/external_website.html?url={}".format(self.name)
return "/static/infoscreen/html/external_website.html?url={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create external website infoitem template url command."""
return "/static/html/external_website_create.html"
return "/static/infoscreen/html/external_website_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
@@ -185,12 +185,12 @@ class SossoInfoItem(InfoItem):
def get_template_url(self):
"""Return Sosso infoitem template url."""
return "/static/html/sosso.html"
return "/static/infoscreen/html/sosso.html"
@staticmethod
def get_create_template_url():
"""Call create Sosso infoitem template url command."""
return "/static/html/sosso_create.html"
return "/static/infoscreen/html/sosso_create.html"
class EventInfoItem(InfoItem):
@@ -200,12 +200,12 @@ class EventInfoItem(InfoItem):
def get_template_url(self):
"""Return Event infoitem template url."""
return "/static/html/events.html"
return "/static/infoscreen/html/events.html"
@staticmethod
def get_create_template_url():
"""Call create Event infoitem template url command."""
return "/static/html/events_create.html"
return "/static/infoscreen/html/events_create.html"
class ImageInfoItem(InfoItem):
@@ -218,12 +218,12 @@ class ImageInfoItem(InfoItem):
"""Return Image infoitem template url."""
# get param to avoid angular from optimizing same template
# with different options
return "/static/html/generic_image.html?img={}".format(self.name)
return "/static/infoscreen/html/generic_image.html?img={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create Image infoitem template url command."""
return "/static/html/generic_image_create.html"
return "/static/infoscreen/html/generic_image_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
@@ -240,12 +240,12 @@ class VideoInfoItem(InfoItem):
def get_template_url(self):
"""Return Video infoitem template url."""
return "/static/html/generic_video.html?video={}".format(self.name)
return "/static/infoscreen/html/generic_video.html?video={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create Video infoitem template url command."""
return "/static/html/generic_video_create.html"
return "/static/infoscreen/html/generic_video_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
@@ -261,12 +261,12 @@ class HslInfoItem(InfoItem):
def get_template_url(self):
"""Return HSL infoitem template url."""
return "/static/html/hsl.html"
return "/static/infoscreen/html/hsl.html"
@staticmethod
def get_create_template_url():
"""Call create HSL infoitem template url command."""
return "/static/html/hsl_create.html"
return "/static/infoscreen/html/hsl_create.html"
class ExternalImageInfoItem(InfoItem):
@@ -277,12 +277,12 @@ class ExternalImageInfoItem(InfoItem):
def get_template_url(self):
"""Return External Image infoitem template url."""
return "/static/html/generic_image.html?img={}".format(self.name)
return "/static/infoscreen/html/generic_image.html?img={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create External Image infoitem template url command."""
return "/static/html/generic_external_image_create.html"
return "/static/infoscreen/html/generic_external_image_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
@@ -321,7 +321,7 @@ class ExternalImageInfoItem(InfoItem):
class InfoInstance(models.Model):
"""Class for Info instance in Infoscreen."""
rotation = models.ForeignKey('Rotation', related_name='instances')
rotation = models.ForeignKey('Rotation', related_name='instances', on_delete=models.CASCADE)
duration = models.FloatField(default=15.0) # seconds
# generic relation to some kind of InfoItem
item_id = models.PositiveIntegerField()
-20
View File
@@ -1,20 +0,0 @@
<link rel="stylesheet" href="/static/css/sosso.css">
<div ng-controller="SossoController">
<div id="header">
<img id="header-image" src="/static/img/sossoheader.png" >
</div>
<div id="container">
<div class="article-row row" ng-repeat="post in data.posts">
<div class="article-thumb-col col-md-6">
<img class="thumbnail" ng-src="{{ post.thumbnail_images['mh-edition-lite-medium'].url }}">
</div>
<div>
<h1 class="col-md-6">
<div class="article-title-col">
{{ post.title }}
</div>
</h1>
</div>
</div>
</div>
</div>
@@ -2,7 +2,10 @@ body {
font-family: 'Open Sans', sans-serif;
}
#header {
#header:after {
content: " ";
display: block;
clear: both;
}
#header-logo {
@@ -5,6 +5,7 @@ html {
body {
padding: 1.5rem;
margin: 0.5rem;
height: 100%;
}
tbody {
@@ -49,7 +50,18 @@ td {
}
}
#header {
max-width: 100%;
display: flex;
justify-content: flex-end;
}
.tab-content {
margin-top: 1rem;
}
.rotation-title-row {
display: flex;
justify-content: space-between;
align-items: center;
}
@@ -19,7 +19,7 @@
.article-thumb-col {
max-height: 200px;
text-align: right;
text-align: left;
}
.article-title-col {
@@ -1,10 +1,10 @@
<link rel="stylesheet" href="/static/css/abb.css">
<link rel="stylesheet" href="/static/infoscreen/css/abb.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
<div ng-controller="ABBController">
<!-- Only show the job listing if there are any jobs, i.e, the jobs list is non-empty -->
<div id="header" class="row" ng-if="jobs.length > 0">
<div id="header-logo">
<img src="/static/img/ABB_logo.png">
<img src="/static/infoscreen/img/ABB_logo.png">
</div>
<div id="header-title">
TYÖPAIKAT
@@ -28,6 +28,6 @@
<!-- If there are no jobs, show a static image -->
<div class="row" ng-if="jobs.length == 0">
<img src="/static/img/ABB_uralle.png" style="position:absolute;left:0;top:0;width:100vw;">
<img src="/static/infoscreen/img/ABB_uralle.png" style="position:absolute;left:0;top:0;width:100vw;">
</div>
</div>
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="item.name"></input>
<input class="form-control" type="text" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -1,4 +1,4 @@
<link rel="stylesheet" href="/static/css/apy.css">
<link rel="stylesheet" href="/static/infoscreen/css/apy.css">
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro" rel="stylesheet">
<div id="bg">
<div class="container " ng-controller="ApyController">
@@ -1,10 +1,10 @@
<div ng-controller="infoadmin_apyitem_create" style="margin-top:20px;">
<div>
create apyitem
Create new ÄPY statistics item
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="item.name"></input>
<input class="form-control" type="text" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -1,4 +1,4 @@
<link rel="stylesheet" href="/static/css/coffee.css">
<link rel="stylesheet" href="/static/infoscreen/css/coffee.css">
<iframe src="https://host2.kilta.aalto.fi/kahvi/cups" allowfullscreen=true sandbox="allow-scripts allow-pointer-lock allow-same-origin">
<p>Your browser does not support iframes.</p>
</iframe>
@@ -1,4 +1,4 @@
<link rel="stylesheet" href="/static/css/events.css">
<link rel="stylesheet" href="/static/infoscreen/css/events.css">
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
<div class="container" ng-app="myApp" ng-controller="EventController">
<div class="header-row row">
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="item.name"></input>
<input type="text" class="form-control" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -1,4 +1,4 @@
<link rel="stylesheet" href="/static/css/external_website.css">
<link rel="stylesheet" href="/static/infoscreen/css/external_website.css">
<iframe ng-src="{{ url | trusted_url }}" allowfullscreen=true sandbox="allow-scripts allow-pointer-lock allow-same-origin">
<p>Your browser does not support iframes.</p>
</iframe>
@@ -1,14 +1,14 @@
<div ng-controller="infoadmin_websiteitem_create" style="margin-top:20px;">
<div>
Create new item to show external website. For example "ka.dy.fi".
Create new item to show external website. For example "https://ka.dy.fi".
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="item.name"></input>
<input type="text" class="form-control" ng-model="item.name"></input>
</div>
<div class="form-group">
<label>Url:</label>
<input type="text" ng-model="item.url"></input>
<input type="text" class="form-control" ng-model="item.url"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -4,11 +4,11 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="item.name"></input>
<input type="text" class="form-control" ng-model="item.name"></input>
</div>
<div class="form-group">
<label>Url:</label>
<input type="text" ng-model="item.url"></input>
<input type="text" class="form-control" ng-model="item.url"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="imagename"></input>
<input type="text" class="form-control" ng-model="imagename"></input>
</div>
<div class="form-group">
<input type="file" ngf-select ng-model="img" name="file" required>
@@ -1,4 +1,4 @@
<link rel="stylesheet" href="/static/css/video.css">
<link rel="stylesheet" href="/static/infoscreen/css/video.css">
<div class="fullscreen-bg">
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="name"></input>
<input type="text" class="form-control" ng-model="name"></input>
</div>
<div class="form-group">
<input type="file" ngf-select ng-model="video" name="file" required>
@@ -1,4 +1,4 @@
<link rel="stylesheet" href="/static/css/hsl.css">
<link rel="stylesheet" href="/static/infoscreen/css/hsl.css">
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/locale/fi.js"></script>
<div class="container" ng-app="myApp" ng-controller="timetableCtrl">
@@ -25,7 +25,7 @@
</tr>
</thead>
<tbody>
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['timestamp'] | limitTo: 11">
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['utc'] | limitTo: 11">
<td style="min-width: 300px">
{{x.timestamp}}
</td>
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="item.name"></input>
<input type="text" class="form-control" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
@@ -0,0 +1,17 @@
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
<link rel="stylesheet" href="/static/infoscreen/css/sosso.css">
<div ng-controller="SossoController">
<div id="header">
<img id="header-image" src="/static/infoscreen/img/sossoheader.png" >
</div>
<div id="container">
<div class="article-row row" ng-repeat="post in data.posts">
<div class="article-thumb-col col-md-4">
<img class="thumbnail" ng-src="{{ post.thumbnail_images['mh-edition-lite-medium'].url }}">
</div>
<div class="col-md-8 article-title-col">
<h1 ng-bind-html="post.title | unsafe"></h1>
</div>
</div>
</div>
</div>
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="item.name"></input>
<input type="text" class="form-control" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Before

Width:  |  Height:  |  Size: 736 KiB

After

Width:  |  Height:  |  Size: 736 KiB

Before

Width:  |  Height:  |  Size: 1.8 MiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

@@ -46,6 +46,13 @@ app.filter('trusted_url', ['$sce', function ($sce) {
};
}]);
//Used for special characters in Sosso. This may open up XSS, so we need to trust that sosso.fi doesn't get compromised...
app.filter('unsafe', function($sce) {
return function(val) {
return $sce.trustAsHtml(val);
};
});
app.controller('ABBController', function($scope, $http){
$scope.jobs = [];
var min_date = moment().subtract(30,'days').format("YYYY-MM-DD%20HH:mm:ss");
+43
View File
@@ -0,0 +1,43 @@
{% load i18n %}
{% load static %}
{% load staticfiles %}
<!DOCTYPE html>
{% block html %}
<html ng-app="{% block appname %}{% endblock appname %}">
<head>
<meta charset="utf-8" name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1" />
<title>
{% block title %}
{% endblock title %}
</title>
{% block libraries %}
<script src="{% static "js/lib/angular.js" %}"></script>
<script src="{% static "js/lib/ng-file-upload-bower-12.2.11/ng-file-upload-all.js" %}"></script>
<script src="{% static "js/lib/jquery-3.1.0.min.js" %}"></script>
<script src="{% static "js/lib/moment.js" %}"></script>
<script src="{% static "js/lib/angular-route.js" %}"></script>
<script src="{% static "js/lib/angular-animate.js" %}"></script>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
{% endblock libraries %}
{% block styles %}
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<link rel="stylesheet" href="{% static "webapp/css/footer.css" %}">
{% endblock styles %}
{% block controllers %}
<script src="{% static "infoscreen/js/infoadmin_controllers.js" %}"></script>
{% endblock controllers %}
</head>
<body>
{% block body %}
{% endblock body %}
</body>
</html>
{% endblock html %}
+7
View File
@@ -0,0 +1,7 @@
{% load i18n %}
{% load static %}
<div class="header-content">
<div class="logo">
<a href="/"><img src="{% static "webapp/img/logo_header.png" %}" alt="Aalto-yliopiston Sähköinsinöörikilta ry"></a>
</div>
</div>
+39 -148
View File
@@ -1,154 +1,45 @@
{% extends "infoscreen:base.html" %}
{% load i18n %}
{% load static %}
{% load staticfiles %}
<!DOCTYPE html>
<html ng-app="infoAdmin">
<head>
<meta charset="utf-8" name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1" />
<title>Infoscreen admin</title>
<script src="{% static "js/lib/angular.js" %}"></script>
<script src="{% static "js/lib/ng-file-upload-bower-12.2.11/ng-file-upload-all.js" %}"></script>
<script src="{% static "js/lib/jquery-3.1.0.min.js" %}"></script>
<script src="{% static "js/lib/bootstrap.min.js" %}"></script>
<script src="{% static "js/lib/underscore-min.js" %}"></script>
<script src="{% static "js/infoadmin_controllers.js" %}"></script>
<link rel="stylesheet" href="{% static "css/lib/bootstrap.min.css" %}"></link>
<link rel="stylesheet" href="{% static "css/base.css" %}"></link>
<link rel="stylesheet" href="{% static "css/infoscreen_admin.css" %}"></link>
</head>
<body>
<div id="header" class="row">
<div class="logout-button">
<form action="/logout" method="post"> {% csrf_token %}
<input type="Submit" value="{% trans "Log out" %}" name="Logout" class="btn btn-danger"/>
</form>
</div>
{% block appname %}infoAdmin{% endblock appname %}
{% block title %}
{% trans "Infoscreen admin" %}
{% endblock title %}
{% block styles %}
{{ block.super }}
<link rel="stylesheet" href="{% static "infoscreen/css/admin.css" %}"></link>
{% endblock styles %}
{% block controllers %}
<script src="{% static "infoscreen/js/infoadmin_controllers.js" %}"></script>
{% endblock controllers %}
{% block body %}
<div id="header" class="row">
<div class="logout-button">
<form action="/admin/logout/" method="post"> {% csrf_token %}
<input type="Submit" value="{% trans "Log out" %}" name="Logout" class="btn btn-danger"/>
</form>
</div>
</div>
<div class="container" ng-controller="infoadmin_ctrl">
<div class="row">
<div class="col">
<h1>{% trans "Infoscreen Admin Pane" %}</h1>
</div>
<div class="container" ng-controller="infoadmin_ctrl">
<div class="row">
<div class="col-md-12">
<h1>{% trans "Infoscreen Admin Pane" %}</h1>
</div>
</div>
<ul class="nav nav-tabs" role="tablist">
<li class="active"><a data-toggle="tab" href="#slides" role="tab">{% trans "Manage Slides" %}</a></li>
<li class="dropdown">
<a data-toggle="dropdown" href="#">{% trans "Manage Rotations" %}<span class="caret"></span></a>
<ul class="dropdown-menu">
<li ng-repeat="r in rotations"><a href="#rotations" ng-click="selectRotation(r.id)" data-toggle="tab">{$ r.name $}</a></li>
<li class="divider">
<li class="nav-item"><a data-toggle="tab" href="#deleterot" role="tab">{% trans "Create/Delete" %}</a></li>
</ul>
</ul>
<div class="tab-content row">
<div id="slides" class="tab-pane active">
<div ng-controller="infoadmin_ctrl">
<div class="col-xs-12 col-md-6">
<h2>{% trans "Create new item" %}</h2>
<div>{% trans "Create a new item by type" %}</div>
<table class="table table-striped">
<tr>
<td>{% trans "Item type" %}</td>
<td>
<select class="form-control form-control-sm" ng-model="createtype", ng-options="t.name for t in infotypes | orderBy: 'name'">
<option value=""></option>
</select>
</td>
</tr>
</table>
<div ng-include="createtype.create_template_url"></div>
</div>
<div class="col-xs-12 col-md-6">
<h2>{% trans "Info items" %}</h2>
<div>{% trans "Infoitems available for rotations" %}</div>
<table class="table table-striped">
<tr>
<th>{% trans "Item" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
<tr ng-repeat="i in infoitems | orderBy:['display_name','name']">
<td>{$ i.name $}</td>
<td>{$ i.display_name $}</td>
<td><input type="button" class="btn btn-danger" ng-click="deleteItem(i.item_type, i.id);" value="{% trans "Delete" %}"></input></td>
</tr>
</table>
</div>
{% include "infoscreen:nav.html" %}
<div class="tab-content" id="tabContent">
{% include "infoscreen:tabs/slides.html" %}
{% include "infoscreen:tabs/rotations.html" %}
{% include "infoscreen:tabs/add_remove.html" %}
</div>
</div>
</div>
</div>
</div>
<div id="rotations" class="tab-pane" ng-controller="infoadmin_ctrl">
<div class="col-xs-12 col-md-6">
<h2>{% trans "Info items" %}</h2>
<div>{% trans "Infoitems available for rotations" %}</div>
<table class="table table-striped">
<tr>
<th>{% trans "Item" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Set duration" %}</th>
<th>{% trans "Add to rotation" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
<tr ng-repeat="i in infoitems | orderBy:['display_name','name']">
<td>{$ i.name $}</td>
<td>{$ i.display_name $}</td>
<td><input type="number" min="1" max="60" class="form-control" ng-model="i.duration"></input></td>
<td><input type="button" class="btn btn-success" ng-click="createInstance(selected_rot.id, i.id, i.item_type, i.duration);" value="{% trans "Add" %}"></input></td>
<td><input type="button" class="btn btn-danger" ng-click="deleteItem(i.item_type, i.id);" value="{% trans "Delete" %}"></input></td>
</tr>
</table>
</div>
<div class="col-xs-12 col-md-6">
<h2>{% trans "Rotation" %}: {$ selected_rot.name $}<a href="/infoscreen/{$ selected_rot.id $}"><input type="button" class="btn btn-primary pull-right" value="{% trans "Preview" %}"></a></h2>
<div>{% trans "Instances in currently selected rotation" %}:</div>
<table class="table table-striped">
<tr>
<th>{% trans "Instance" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
<tr ng-repeat="i in selected_rot.instances">
<td>{$ i.item.name $}</td><td>{$ i.duration $} s</td>
<td><input type="button" ng-click="deleteInstance(i.id);" value="{% trans "Delete" %}" class="btn btn-danger"></input></td>
</tr>
</table>
</div>
</div>
<div id="deleterot" class="tab-pane">
<div class="col-xs-12 " ng-controller="infoadmin_ctrl">
<h2>{% trans "Rotations" %}</h2>
<div>
{% trans "Select rotation to edit" %}:
</div>
<table class="table table-striped">
<tr>
<th>{% trans "Rotation" %}</th>
<th>{% trans "id" %}</th>
<th>{% trans "Preview" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
<tr ng-repeat="r in rotations">
<td>{$ r.name $}</td>
<td>{$ r.id $}</td>
<td><a href="/infoscreen/{$ r.id $}"><input type="button" class="btn btn-primary" value="{% trans "Preview" %}"></a></td>
<td><input type="button" class="btn btn-danger" ng-click="deleteRotation(r.id)" value="{% trans "Delete" %}"></input></td>
</tr>
<tr>
<td><input type="text" class="form-control" ng-model="r.name" placeholder="{% trans "Name" %}"></input></td>
<td><input type="button" class="btn btn-success" ng-click="createRotation(r.name)" value="{% trans "Create new" %}"></input></td>
<td></td>
<td></td>
</tr>
</table>
</div>
</div>
</div>
<div style="margin-top: 100px;">
{% include "footer.html" %}
</div>
</div>
</body>
</html>
{% include "webapp:footer.html" %}
{% endblock body %}
+21 -25
View File
@@ -1,29 +1,25 @@
{% extends "infoscreen:base.html" %}
{% load static %}
{% load i18n %}
<!DOCTYPE html>
<html ng-app="infoApp">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{% block appname %}infoApp{% endblock appname %}
<title>Infoscreen</title>
<script src="{% static "js/lib/moment.js" %}"></script>
<script src="{% static "js/lib/angular.js" %}"></script>
<script src="{% static "js/lib/angular-route.js" %}"></script>
<script src="{% static "js/lib/angular-animate.js" %}"></script>
<script src="{% static "js/lib/jquery-3.1.0.min.js" %}"></script>
<script src="{% static "js/lib/bootstrap.min.js" %}"></script>
<script src="{% static "js/lib/underscore-min.js" %}"></script>
<script src="{% static "js/infoscreen_controllers.js" %}"></script>
{% block title %}
{% trans "Infoscreen" %}
{% endblock title %}
<link rel="stylesheet" href="{% static "css/lib/bootstrap.min.css" %}"></link>
<link rel="stylesheet" href="{% static "css/infoscreen.css" %}"></link>
</head>
<body>
<div class="container ng-scope" ng-controller="infoscreen_main" ng-init="init({{ rotation }})">
<div ng-animate-swap="index" class="cell swap-animation">
<div id="infocontent" ng-include="active.template" onload="active.onload()"></div>
</div>
</div>
</body>
</html>
{% block controllers %}
<script src="{% static "infoscreen/js/infoscreen_controllers.js" %}"></script>
{% endblock controllers %}
{% block styles %}
<link rel="stylesheet" href="{% static "infoscreen/css/infoscreen.css" %}"></link>
{% endblock styles %}
{% block body %}
<div class="container ng-scope" ng-controller="infoscreen_main" ng-init="init({{ rotation }})">
<div ng-animate-swap="index" class="cell swap-animation">
<div id="infocontent" ng-include="active.template" onload="active.onload()"></div>
</div>
</div>
{% endblock body %}
+17
View File
@@ -0,0 +1,17 @@
{% load i18n %}
<ul class="nav nav-tabs" id="tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" data-toggle="tab" role="tab" href="#slides">{% trans "Manage Slides" %}</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" role="button" data-toggle="dropdown" href="#" aria-haspopup="true" aria-expanded="false">
{% trans "Manage Rotations" %}<span class="caret"></span>
</a>
<div class="dropdown-menu">
<a class="dropdown-item" role="tab" href="#rotations" ng-repeat="r in rotations" ng-click="selectRotation(r.id)" data-toggle="tab">{$ r.name $}</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" data-toggle="tab" href="#deleterot" role="tab">{% trans "Create/Delete" %}</a>
</div>
</li>
</ul>
+30
View File
@@ -0,0 +1,30 @@
{% load i18n %}
<div id="deleterot" class="tab-pane fade" role="tabpanel">
<div class="col" ng-controller="infoadmin_ctrl">
<h2>{% trans "Rotations" %}</h2>
<div>
{% trans "Select rotation to edit" %}:
</div>
<table class="table table-striped">
<tr>
<th>{% trans "Rotation" %}</th>
<th>{% trans "id" %}</th>
<th>{% trans "Preview" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
<tr ng-repeat="r in rotations">
<td>{$ r.name $}</td>
<td>{$ r.id $}</td>
<td><a class="btn btn-primary" href="/infoscreen/{$ r.id $}">{% trans "Preview" %}</a></td>
<td><a class="btn btn-danger" href="#" ng-click="deleteRotation(r.id)">{% trans "Delete" %}</a></td>
</tr>
<tr>
<td><input type="text" class="form-control" ng-model="r.name" placeholder="{% trans "Name" %}"></input></td>
<td><input type="button" class="btn btn-success" ng-click="createRotation(r.name)" value="{% trans "Create new" %}"></input></td>
<td></td>
<td></td>
</tr>
</table>
</div>
</div>
+44
View File
@@ -0,0 +1,44 @@
{% load i18n %}
<div id="rotations" class="tab-pane fade" role="tabpanel" ng-controller="infoadmin_ctrl">
<div class="col">
<h2>{% trans "Info items" %}</h2>
<div>{% trans "Infoitems available for rotations" %}</div>
<table class="table table-striped">
<tr>
<th>{% trans "Item" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Set duration" %}</th>
<th>{% trans "Add to rotation" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
<tr ng-repeat="i in infoitems | orderBy:['display_name','name']">
<td>{$ i.name $}</td>
<td>{$ i.display_name $}</td>
<td><input type="number" min="1" max="60" class="form-control" ng-model="i.duration"></input></td>
<td><input type="button" class="btn btn-success" ng-click="createInstance(selected_rot.id, i.id, i.item_type, i.duration);" value="{% trans "Add" %}"></input></td>
<td><input type="button" class="btn btn-danger" ng-click="deleteItem(i.item_type, i.id);" value="{% trans "Delete" %}"></input></td>
</tr>
</table>
</div>
<div class="col">
<div class="rotation-title-row">
<h2>{% trans "Rotation" %}: {$ selected_rot.name $}</h2>
<a class="btn btn-primary" href="/infoscreen/{$ selected_rot.id $}">{% trans "Preview" %}</a>
</div>
<div>{% trans "Instances in currently selected rotation" %}:</div>
<table class="table table-striped">
<tr>
<th>{% trans "Instance" %}</th>
<th>{% trans "Duration" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
<tr ng-repeat="i in selected_rot.instances">
<td>{$ i.item.name $}</td><td>{$ i.duration $} s</td>
<td><input type="button" ng-click="deleteInstance(i.id);" value="{% trans "Delete" %}" class="btn btn-danger"></input></td>
</tr>
</table>
</div>
</div>
+37
View File
@@ -0,0 +1,37 @@
{% load i18n %}
<div id="slides" class="tab-pane fade show active" role="tabpanel">
<div ng-controller="infoadmin_ctrl">
<div class="col">
<h2>{% trans "Create new item" %}</h2>
<div>{% trans "Create a new item by type" %}</div>
<table class="table table-striped">
<tr>
<td>{% trans "Item type" %}</td>
<td>
<select class="form-control form-control-sm" ng-model="createtype", ng-options="t.name for t in infotypes | orderBy: 'name'">
<option value=""></option>
</select>
</td>
</tr>
</table>
<div ng-include="createtype.create_template_url"></div>
</div>
<div class="col">
<h2>{% trans "Info items" %}</h2>
<div>{% trans "Infoitems available for rotations" %}</div>
<table class="table table-striped">
<tr>
<th>{% trans "Item" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Delete" %}</th>
</tr>
<tr ng-repeat="i in infoitems | orderBy:['display_name','name']">
<td>{$ i.name $}</td>
<td>{$ i.display_name $}</td>
<td><input type="button" class="btn btn-danger" ng-click="deleteItem(i.item_type, i.id);" value="{% trans "Delete" %}"></input></td>
</tr>
</table>
</div>
</div>
</div>
+5 -1
View File
@@ -1,6 +1,7 @@
"""File containing infoscreen urls."""
from django.conf.urls import url
from django.conf import settings
from infoscreen.views import index
from infoscreen.views import admin
@@ -52,5 +53,8 @@ urlpatterns = [
url(r'^hsl_data$', CurrentHSLView),
url(r'^hsl_data/settings$', hsl_timetable_settings),
url(r'^apyjson', get_apy_json),
]
if settings.DEBUG:
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
urlpatterns += staticfiles_urlpatterns()
+20 -23
View File
@@ -6,6 +6,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods
from django.contrib.contenttypes.models import ContentType
from django.contrib.auth.decorators import permission_required, login_required
from django.db import DatabaseError
from infoscreen.models import UploadFileForm
import sikweb.settings as settings
@@ -14,21 +15,17 @@ import logging
import threading
import requests
from infoscreen.models import Rotation, InfoItem, InfoInstance
from infoscreen.models import (ABBInfoItem, ExternalImageInfoItem,
ImageInfoItem, SossoInfoItem, HslInfoItem)
from infoscreen.models import EventInfoItem
from infoscreen.models import ExternalWebsiteInfoItem
from infoscreen.models import ImageUploadForm
from infoscreen.models import ApyInfoItem
from infoscreen.models import VideoInfoItem
from infoscreen.models import (
Rotation, InfoItem, InfoInstance, ABBInfoItem, ExternalImageInfoItem,
ImageInfoItem, SossoInfoItem, HslInfoItem, EventInfoItem,
ExternalWebsiteInfoItem, ImageUploadForm, ApyInfoItem, VideoInfoItem)
@login_required(login_url='/login')
@login_required(login_url='/admin/login')
@permission_required('infoscreen.change_infoinstance', raise_exception=True)
def admin(request, *args, **kwargs):
"""Render infoscreen admin page."""
return render(request, 'infoscreen_admin.html', {})
return render(request, 'infoscreen:infoscreen_admin.html', {})
def create_item_generator(model):
@@ -36,12 +33,12 @@ def create_item_generator(model):
@ensure_csrf_cookie
@require_http_methods(["POST"])
@login_required(login_url='/login')
@login_required(login_url='/admin/login')
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
def create_item(request, *args, **kwargs):
try:
data = json.loads(request.body.decode("utf-8"))
except ValueError:
except json.JSONDecodeError:
return HttpResponseBadRequest(
'{"status":"failure","error":"invalid json supplied"}')
try:
@@ -58,7 +55,7 @@ def delete_item_generator(model):
@ensure_csrf_cookie
@require_http_methods(["DELETE"])
@login_required(login_url='/login')
@login_required(login_url='/admin/login')
@permission_required('infoscreen.delete_infoinstance', raise_exception=True)
def delete_item(request, *args, **kwargs):
idx = kwargs.pop("idx", 0)
@@ -71,7 +68,7 @@ def delete_item_generator(model):
try:
item.delete()
return HttpResponse('{"status":"success"}')
except:
except DatabaseError:
resp = HttpResponse('{"error" : "could not delete item"}')
resp.status_code = 500
return resp
@@ -80,7 +77,7 @@ def delete_item_generator(model):
# due to model structure this is little complicated
@ensure_csrf_cookie
@login_required(login_url='/login')
@login_required(login_url='/admin/login')
@permission_required('infoscreen.delete_infoinstance', raise_exception=True)
@require_http_methods(["DELETE"])
def delete_info_item(request, *args, **kwargs):
@@ -97,7 +94,7 @@ def delete_info_item(request, *args, **kwargs):
try:
item.delete()
return HttpResponse('{"status":"success"}')
except:
except DatabaseError:
resp = HttpResponse('{"error" : "could not delete item"}')
resp.status_code = 500
return resp
@@ -105,7 +102,7 @@ def delete_info_item(request, *args, **kwargs):
@require_http_methods(["POST"])
@ensure_csrf_cookie
@login_required(login_url='/login')
@login_required(login_url='/admin/login')
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
def create_image_item(request, *args, **kwargs):
"""Create image Infoscreen item."""
@@ -122,7 +119,7 @@ def create_image_item(request, *args, **kwargs):
@require_http_methods(["POST"])
@ensure_csrf_cookie
@login_required(login_url='/login')
@login_required(login_url='/admin/login')
@permission_required('infoscreen.add_infoinstance', raise_exception=True)
def create_video_item(request, *args, **kwargs):
"""Create video Infoscreen item."""
@@ -139,20 +136,20 @@ def create_video_item(request, *args, **kwargs):
@require_http_methods(["POST"])
@ensure_csrf_cookie
@login_required(login_url='/login')
@login_required(login_url='/admin/login')
@permission_required('infoscreen.add_rotation', raise_exception=True)
def create_rotation(request, *args, **kwargs):
"""Create rotation."""
try:
data = json.loads(request.body.decode("utf-8"))
except:
except json.JSONDecodeError:
return HttpResponse('{"error": "bad post body!"}', status=400)
try:
name = data["name"]
Rotation.objects.create(name=name)
resp = HttpResponse(status=200)
except:
except DatabaseError:
resp = HttpResponse(
'{"error" : "could not create rotation!"}', status=400)
@@ -161,7 +158,7 @@ def create_rotation(request, *args, **kwargs):
@require_http_methods(["DELETE"])
@ensure_csrf_cookie
@login_required(login_url='/login')
@login_required(login_url='/admin/login')
@permission_required('infoscreen.delete_rotation', raise_exception=True)
def delete_rotation(request, *args, **kwargs):
"""Delete rotation."""
@@ -171,7 +168,7 @@ def delete_rotation(request, *args, **kwargs):
try:
Rotation.objects.filter(id=id).delete()
resp = HttpResponse(status=200)
except:
except DatabaseError:
resp = HttpResponse(
'{"error" : "could not delete rotation!"}', status=400)
+3 -1
View File
@@ -2,6 +2,7 @@ from django.shortcuts import render
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
from django.views.decorators.http import require_http_methods
from django.conf import settings
from django.db import DatabaseError
from infoscreen.models import Rotation, InfoItem, InfoInstance
from infoscreen.hsl_fetcher import fetch as hsl_fetch
@@ -9,6 +10,7 @@ from infoscreen.hsl_fetcher import fetch as hsl_fetch
import json
import logging
import threading
import requests
@require_http_methods(["GET"])
@@ -22,7 +24,7 @@ def default(request, *args, **kwargs):
"""Try getting first rotation item."""
try:
first = Rotation.objects.all()[0].id
except:
except DatabaseError:
first = 0
return index(request, first, *args, **kwargs)
View File
+10
View File
@@ -0,0 +1,10 @@
from django.contrib import admin
from modeltranslation.admin import TranslationAdmin
from kaehmy.models import Application, Comment, CustomRole, PresetRole, TelegramChannel
admin.site.register(Application)
admin.site.register(Comment)
admin.site.register(CustomRole)
admin.site.register(PresetRole, TranslationAdmin)
admin.site.register(TelegramChannel)
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class KaehmyConfig(AppConfig):
name = 'kaehmy'
+89
View File
@@ -0,0 +1,89 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.core.exceptions import ValidationError
from kaehmy.models import PresetRole, CustomRole, Application, Comment, KaehmyBaseRole
class CheckboxSelectMultiple(forms.widgets.CheckboxSelectMultiple):
option_template_name = 'checkbox_option.html'
def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
dic = super(CheckboxSelectMultiple, self).create_option(name, value, label, selected, index, subindex, attrs)
description = PresetRole.objects.get(id=value).description
dic['description'] = description
return dic
def __init__(self, *args, **kwargs):
super(CheckboxSelectMultiple, self).__init__(*args, **kwargs)
class ApplicationForm(forms.ModelForm):
"""Class representing Kaehmy form."""
class Meta:
"""Meta for class Application."""
model = Application
fields = ['name', 'email', 'phone_number', 'year',
'preset_roles', 'custom_roles', 'custom_role_name',
'custom_role_is_board', 'text']
def __init__(self, *args, **kwargs):
super(ApplicationForm, self).__init__(*args, **kwargs)
self.fields["email"].label = _('Email (not public)')
self.fields["phone_number"].label = _('Phone number (not public)')
custom_roles_exist = CustomRole.objects.all().exists()
self.fields["custom_roles"].widget = forms.widgets.CheckboxSelectMultiple() if custom_roles_exist else forms.HiddenInput()
self.fields["custom_roles"].help_text = ""
self.fields["custom_roles"].label = _('Custom roles')
self.fields["custom_roles"].queryset = CustomRole.objects.all()
for cat_id, category in KaehmyBaseRole.CATEGORIES:
key = 'preset_roles_{}'.format(cat_id)
qset = PresetRole.objects.filter(category=cat_id).order_by('category', '-is_board')
self.fields[key] = forms.ModelMultipleChoiceField(qset)
self.fields[key].widget = CheckboxSelectMultiple(attrs={
'title': _('Preset roles'),
'name': 'preset_roles',
})
self.fields[key].help_text = ""
self.fields[key].queryset = qset
self.fields[key].label = _(category)
self.fields[key].required = False
def clean(self):
cleaned_data = super(ApplicationForm, self).clean()
for key in cleaned_data.keys():
if 'preset_roles_' in key:
cleaned_data['preset_roles'] = cleaned_data['preset_roles'] | cleaned_data[key]
return cleaned_data
def clean_phone_number(self):
"""Clean phone number field."""
number = self.cleaned_data.get('phone_number')
if number.isdigit():
return number
else:
raise ValidationError(_('Invalid phone number'))
def clean_custom_role_name(self):
"""Check that no other custom role with same name exists."""
custom_name = self.cleaned_data.get('custom_role_name')
if not CustomRole.objects.filter(name=custom_name).exists():
return custom_name
else:
raise ValidationError(_('Custom role with the same name already exists.'))
def non_role_fields(self):
return [self.fields[k] for k in self.fields.keys() if k not in ["preset_roles", "custom_roles"]]
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ['name', 'email', 'message', 'parent']
+93
View File
@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2018-01-25 22:31
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
('webapp', '0037_auto_20180125_2131'),
]
operations = [
migrations.CreateModel(
name='CommentParent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(default='', max_length=255, verbose_name='Name')),
('email', models.EmailField(default='', max_length=254, verbose_name='Email')),
('timestamp', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Timestamp')),
],
),
migrations.CreateModel(
name='CustomRole',
fields=[
('baserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='webapp.BaseRole')),
],
options={
'verbose_name_plural': 'Custom kaehmy roles',
'verbose_name': 'Custom kaehmy role',
},
bases=('webapp.baserole',),
),
migrations.CreateModel(
name='PresetRole',
fields=[
('presetrole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='webapp.PresetRole')),
],
options={
'verbose_name_plural': 'Preset kaehmy roles',
'verbose_name': 'Preset kaehmy role',
},
bases=('webapp.presetrole',),
),
migrations.CreateModel(
name='TelegramChannel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('channel_id', models.CharField(max_length=255, unique=True)),
],
options={
'verbose_name_plural': 'Telegram channels',
'verbose_name': 'Telegram channel',
},
),
migrations.CreateModel(
name='Application',
fields=[
('commentparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.CommentParent')),
('phone_number', models.CharField(default='', max_length=10, verbose_name='Phone number')),
('year', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, 'N')], verbose_name='Year')),
('text', models.TextField(default='', max_length=300, verbose_name='Text')),
('custom_role_name', models.CharField(blank=True, max_length=255, verbose_name='Custom role name')),
('custom_role_is_board', models.BooleanField(verbose_name='Board member')),
('custom_roles', models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.CustomRole')),
('preset_roles', models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.PresetRole')),
],
options={
'verbose_name_plural': 'Kaehmylomakkeet',
'verbose_name': 'Kaehmylomake',
},
bases=('kaehmy.commentparent',),
),
migrations.CreateModel(
name='Comment',
fields=[
('commentparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.CommentParent')),
('message', models.TextField(verbose_name='Message')),
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='kaehmy.CommentParent')),
],
options={
'verbose_name_plural': 'Kaehmykommentit',
'verbose_name': 'Kaehmykommentti',
},
bases=('kaehmy.commentparent',),
),
]
@@ -0,0 +1,32 @@
# Generated by Django 2.0.7 on 2018-09-02 16:29
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('webapp', '0047_auto_20180710_2110'),
('kaehmy', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='KaehmyBaseRole',
fields=[
('baserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='webapp.BaseRole')),
('category', models.CharField(choices=[('corporate', 'Corporate affairs'), ('freshman', 'Freshmen'), ('international', 'International'), ('external', 'External affairs'), ('media', 'Media'), ('tech', 'Technology'), ('wellbeing', 'Wellbeing'), ('elepaja', 'Elepaja'), ('ceremonies', 'Ceremonies'), ('culture', 'Culture'), ('studies', 'Studies'), ('sosso', 'Sössö magazine'), ('alumni', 'Alumni relations'), ('others', 'Others')], default='others', max_length=255, verbose_name='Category')),
],
bases=('webapp.baserole',),
),
migrations.DeleteModel(
name='Application',
),
migrations.DeleteModel(
name='customrole',
),
migrations.DeleteModel(
name='presetrole',
),
]
@@ -0,0 +1,63 @@
# Generated by Django 2.0.7 on 2018-09-02 16:43
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('kaehmy', '0002_auto_20180902_1929'),
]
operations = [
migrations.CreateModel(
name='Application',
fields=[
('commentparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.CommentParent')),
('phone_number', models.CharField(default='', max_length=10, verbose_name='Phone number')),
('year', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, 'N')], verbose_name='Year')),
('text', models.TextField(default='', max_length=300, verbose_name='Text')),
('custom_role_name', models.CharField(blank=True, max_length=255, verbose_name='Custom role name')),
('custom_role_is_board', models.BooleanField(verbose_name='Board member')),
],
options={
'verbose_name': 'Kaehmylomake',
'verbose_name_plural': 'Kaehmylomakkeet',
},
bases=('kaehmy.commentparent',),
),
migrations.CreateModel(
name='CustomRole',
fields=[
('kaehmybaserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.KaehmyBaseRole')),
],
options={
'verbose_name': 'Custom kaehmy role',
'verbose_name_plural': 'Custom kaehmy roles',
},
bases=('kaehmy.kaehmybaserole',),
),
migrations.CreateModel(
name='PresetRole',
fields=[
('kaehmybaserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.KaehmyBaseRole')),
('description', models.TextField(verbose_name='Description')),
],
options={
'verbose_name': 'Preset kaehmy role',
'verbose_name_plural': 'Preset kaehmy roles',
},
bases=('kaehmy.kaehmybaserole',),
),
migrations.AddField(
model_name='application',
name='custom_roles',
field=models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.CustomRole'),
),
migrations.AddField(
model_name='application',
name='preset_roles',
field=models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.PresetRole'),
),
]
@@ -0,0 +1,18 @@
# Generated by Django 2.0.7 on 2018-10-18 18:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kaehmy', '0003_auto_20180902_1943'),
]
operations = [
migrations.AlterField(
model_name='kaehmybaserole',
name='category',
field=models.CharField(choices=[('corporate', 'Corporate affairs'), ('freshman', 'Freshmen'), ('international', 'International'), ('external', 'External affairs'), ('media', 'Media'), ('tech', 'Technology'), ('wellbeing', 'Wellbeing'), ('elepaja', 'Elepaja'), ('ceremonies', 'Ceremonies'), ('studies', 'Studies'), ('sosso', 'Sössö magazine'), ('alumni', 'Alumni relations'), ('others', 'Others')], default='others', max_length=255, verbose_name='Category'),
),
]
View File
+164
View File
@@ -0,0 +1,164 @@
from django.db import models
from django.utils import timezone
from datetime import timedelta
from webapp.utils import month_from_now
from django.utils.translation import ugettext_lazy as _
from auditlog.registry import auditlog
from phonenumber_field.modelfields import PhoneNumberField
import logging
import webapp.models
VERBOSE_NAME = _('Kaehmy')
class KaehmyBaseRole(webapp.models.BaseRole):
CATEGORIES = (
('corporate', _('Corporate affairs')),
('freshman', _('Freshmen')),
('international', _('International')),
('external', _('External affairs')),
('media', _('Media')),
('tech', _('Technology')),
('wellbeing', _('Wellbeing')),
('elepaja', _('Elepaja')),
('ceremonies', _('Ceremonies')),
('studies', _('Studies')),
('sosso', _('Sössö magazine')),
('alumni', _('Alumni relations')),
('others', _('Others')),
)
category = models.CharField(_('Category'), choices=CATEGORIES, default='others', max_length=255)
class PresetRole(KaehmyBaseRole):
"""Model for kaehmy role."""
description = models.TextField(_('Description'))
class Meta:
verbose_name = _('Preset kaehmy role')
verbose_name_plural = _('Preset kaehmy roles')
class CustomRole(KaehmyBaseRole):
"""Model representing a user-specified custom occupation."""
class Meta:
verbose_name = _('Custom kaehmy role')
verbose_name_plural = _('Custom kaehmy roles')
class CommentParent(models.Model):
name = models.CharField(_('Name'), max_length=255, default='')
email = models.EmailField(_('Email'), default='')
timestamp = models.DateTimeField(_('Timestamp'), default=timezone.now)
def __str__(self):
return 'Message parent #{}'.format(self.id)
class Comment(CommentParent):
"""
Model representing a kaehmymessage.
Every message relates to certain kaehmyform or parent message.
"""
class Meta:
verbose_name = _('Kaehmykommentti')
verbose_name_plural = _('Kaehmykommentit')
message = models.TextField(_('Message'))
parent = models.ForeignKey('CommentParent', related_name='messages', on_delete=models.CASCADE)
class Application(CommentParent):
"""
Model representing a form for kaehmy.
Allows user to choose from existing roles or to create custom ones.
"""
YEAR_CHOICES = (
(1, '1'),
(2, '2'),
(3, '3'),
(4, '4'),
(5, 'N'),
)
class Meta:
verbose_name = _('Kaehmylomake')
verbose_name_plural = _('Kaehmylomakkeet')
phone_number = models.CharField(
_('Phone number'), max_length=10, default="")
year = models.IntegerField(_('Year'), choices=YEAR_CHOICES)
text = models.TextField(_('Text'), default="", max_length=300)
custom_role_name = models.CharField(
_('Custom role name'), max_length=255, blank=True)
custom_role_is_board = models.BooleanField(
_('Board member'), blank=True)
custom_roles = models.ManyToManyField(
'kaehmy.CustomRole', related_name='forms', blank=True)
preset_roles = models.ManyToManyField(
'kaehmy.PresetRole', related_name='forms', blank=True)
def __str__(self):
"""Return model info."""
return _('Kaehmy application: {}').format(self.name)
def comment_count(self):
"""Count comments for kaehmy."""
total = 0
def recurse(message):
count = 0
for msg in message.messages.all():
count += recurse(msg)
return count + 1
for message in self.messages.all():
total += recurse(message)
return total
def board_roles(self):
presets = [r.name.capitalize() for r in self.preset_roles.filter(is_board=True)]
customs = [r.name.capitalize() for r in self.custom_roles.filter(is_board=True)]
combined = presets + customs
return _('Board: {}').format(', '.join(combined)) if len(combined) > 0 else ''
def official_roles(self):
presets = [r.name.capitalize() for r in self.preset_roles.filter(is_board=False)]
customs = [r.name.capitalize() for r in self.custom_roles.filter(is_board=False)]
combined = presets + customs
return _('Official: {}').format(', '.join(combined)) if len(combined) > 0 else ''
def all_roles(self):
presets = [r.name.capitalize() for r in self.preset_roles.all()]
customs = [r.name.capitalize() for r in self.custom_roles.all()]
combined = presets + customs
return ', '.join(combined) if len(combined) > 0 else ''
def has_any_board_role(self):
return self.preset_roles.filter(is_board=True).exists() or self.custom_roles.filter(is_board=True)
# Telegram channel entry for Kaehmys
class TelegramChannel(models.Model):
"""Model containing the channel id of a Telegram chat"""
class Meta:
verbose_name = _('Telegram channel')
verbose_name_plural = _('Telegram channels')
name = models.CharField(max_length=255)
channel_id = models.CharField(max_length=255, unique=True)
def __str__(self):
return 'Telegram channel: "{}"'.format(self.name)
@@ -24,3 +24,24 @@ div.tooltip-inner {
width: auto;
margin-bottom: 1rem;
}
.kaehmy-content {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
p {
overflow-wrap: break-word;
}
.card {
margin-bottom: 1rem;
}
.role-filter-form select {
margin-left: 1rem;
}
.card a.comment-button {
cursor: pointer;
}
@@ -7,7 +7,7 @@ footer {
bottom: 0;
width: 100%;
height: 60px; /* Set the fixed height of the footer here */
/* line-height: 60px; /* Vertically center the text there */ */
/* line-height: 60px; /* Vertically center the text there */
margin-top: 2rem;
margin-bottom: 1rem;
}
@@ -7,5 +7,5 @@
}
.navbar-light .navbar-nav .nav-link {
color: #102a40;
color: black;
}
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

+13
View File
@@ -0,0 +1,13 @@
import django_tables2 as tables
from django.db.models import Count, Q
from django.utils.translation import ugettext as _
from kaehmy.models import Application
class ExportTable(tables.Table):
class Meta:
model = Application
exclude = ['text', 'messageparent_ptr', 'custom_role_name', 'custom_role_is_board', 'timestamp']
all_roles = tables.Column(verbose_name=_('Roles'), orderable=False)
+35
View File
@@ -0,0 +1,35 @@
{% extends "project.html" %}
{% load static %}
{% load i18n %}
{% block styles %}
<link rel="stylesheet" href="{% static "kaehmy/css/base.css" %}">
<link rel="stylesheet" href="{% static "kaehmy/css/header.css" %}">
<link rel="stylesheet" href="{% static "kaehmy/css/nav.css" %}">
<link rel="stylesheet" href="{% static "kaehmy/css/footer.css" %}">
{% endblock styles %}
{% block body %}
{% block header %}
<div class="kaehmy_header">
{% include "kaehmy:header.html" %}
</div>
{% endblock header %}
{% block navigation %}
{% include "kaehmy:navigation.html" %}
{% endblock %}
<div class="kaehmy-content">
{% block content %}
{% endblock %}
</div>
<div class="footer">
{% block footer %}
{% include "kaehmy:footer.html" %}
{% endblock footer %}
</div>
{% endblock body %}
@@ -1,4 +1,4 @@
{% extends "kaehmy_base.html" %}
{% extends "kaehmy:base.html" %}
{% load static %}
{% load i18n %}
@@ -1,4 +1,4 @@
{% extends "kaehmy_base.html" %}
{% extends "base.html" %}
{% load static %}
{% load i18n %}

Some files were not shown because too many files have changed in this diff Show More