Compare commits
599 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bea6a34a6 | |||
| abbf23456b | |||
| 6a156dd9db | |||
| 9fb9bd447a | |||
| ba69f54465 | |||
| 9a450ea44a | |||
| 242a45a92e | |||
| ad472ca8ff | |||
| 928a98904b | |||
| 2882c1a4c8 | |||
| 1e0c050f99 | |||
| 744874a54c | |||
| 4dfa033f0b | |||
| d56ab02c27 | |||
| 01c5a0ed53 | |||
| 06fcfcf3a1 | |||
| 6567cd87d5 | |||
| 9efe0d7489 | |||
| d7b6ef3c72 | |||
| 0a31492733 | |||
| c96ff2f112 | |||
| 2b0d1c6ca7 | |||
| 90342011f5 | |||
| 27f9c79cf3 | |||
| de1ba7c9f2 | |||
| c2fb77816a | |||
| 4be0b75a1c | |||
| 09aa42cfb8 | |||
| c5008e3656 | |||
| e5224fe1e2 | |||
| b834567309 | |||
| b1921d4926 | |||
| 00c6920c8a | |||
| 1fe323cbdd | |||
| 3a58ff0ef1 | |||
| d06c24bde0 | |||
| 2359743b20 | |||
| 437936ad64 | |||
| 27dd56b6fb | |||
| ff5a92897c | |||
| e9c38d56d8 | |||
| 57bc12b621 | |||
| 1ff188eddd | |||
| b443b39457 | |||
| 22615da3b4 | |||
| d09cbf3b2c | |||
| 66fcfab4da | |||
| a7d0f49190 | |||
| 519571f84c | |||
| 30847e8bc2 | |||
| df69a64ffc | |||
| 157f1c487c | |||
| 1e5a691d73 | |||
| 43fe25ba64 | |||
| 851269b13f | |||
| 0e5e18b035 | |||
| f543e83e80 | |||
| 0b72c20e32 | |||
| 04e6eb3ec2 | |||
| 4e1efff7f2 | |||
| d45f7195f5 | |||
| dc6347a6eb | |||
| 1304cffa2a | |||
| 656b2d1d95 | |||
| 9b9406ab4b | |||
| 3630b360d7 | |||
| c6a3445774 | |||
| 9b7e9a6d92 | |||
| 3de975203e | |||
| 38669947cd | |||
| e03b6e6606 | |||
| 8491842a04 | |||
| eb80250335 | |||
| dec348b032 | |||
| 5071bd6172 | |||
| f98971270a | |||
| e2d55ffce2 | |||
| 37cd2eda4c | |||
| aa0ae01227 | |||
| 54ded7d3f9 | |||
| ef38d8be46 | |||
| 673bbc09eb | |||
| 9fa62e9a2f | |||
| 32e0704f53 | |||
| 39ab86fe3e | |||
| f9e7c4a904 | |||
| 0adca6bd2e | |||
| 5c4e4c7f5a | |||
| 76e1c71cd1 | |||
| 67b31060b2 | |||
| 110d426efd | |||
| 62bb317c16 | |||
| a379390ca1 | |||
| 75ece86fb0 | |||
| 26af46fa12 | |||
| 027b9c370c | |||
| 59c9fec51c | |||
| 98ea2d8a47 | |||
| 0b9aed128a | |||
| 4ebba7f74a | |||
| 11c36f2111 | |||
| ef78ef9dcd | |||
| 34984d4a5e | |||
| 319324503f | |||
| 683f50b199 | |||
| 2d6c474baa | |||
| b302681bbf | |||
| 90d8ee2ea9 | |||
| f505fae3e6 | |||
| 2433c7828d | |||
| 0edde936cc | |||
| 2e11621e5b | |||
| 5f706d9236 | |||
| d3350c06e0 | |||
| 648feaa762 | |||
| 6d4662a1cb | |||
| 523adcd256 | |||
| 5f0ab2ddbf | |||
| 6e3b3bd52f | |||
| 6dcbc1c652 | |||
| 80e00e2b05 | |||
| 80298d174f | |||
| b790906b23 | |||
| e7164bb613 | |||
| a2a2b9830e | |||
| eb0bfdbd21 | |||
| d9116e51ca | |||
| 3188a8e520 | |||
| a9c1d06ece | |||
| c32292c7d9 | |||
| f2ffcaa5a9 | |||
| d0216bb85c | |||
| 62263848f1 | |||
| 242199399b | |||
| 66d9ee0a75 | |||
| c2222d4426 | |||
| 368b28d241 | |||
| c5df450a96 | |||
| c1e0a32a08 | |||
| 373db50995 | |||
| f422e31a42 | |||
| f7cf54cab2 | |||
| d17e6710e6 | |||
| f64cec7d2e | |||
| ff87767aab | |||
| 08ec9bb9cc | |||
| eb63c9fc1c | |||
| eec157d7f3 | |||
| 1ce3c28e94 | |||
| 52a646fe04 | |||
| 8c4f6e886c | |||
| a9d6bf50d6 | |||
| 8f0c9feecf | |||
| 5fba7d6249 | |||
| 3d755bca8c | |||
| ff04e645e4 | |||
| 2c72e01353 | |||
| 3836e00174 | |||
| b33f29d620 | |||
| 8afeec21cc | |||
| 1778a0509c | |||
| 86ae2197f3 | |||
| 4859daf0b3 | |||
| ff9cad1f47 | |||
| 01161db33b | |||
| a7728e50dd | |||
| edb6d73c06 | |||
| 0f50daf289 | |||
| f4b55b7863 | |||
| 00de36ca44 | |||
| 59f47dec26 | |||
| 8c4b997f0f | |||
| b2e2ff6699 | |||
| ff98f700f9 | |||
| fe48a2a336 | |||
| 80c8d9bc90 | |||
| 31ebace4cd | |||
| b3a3a9815f | |||
| 81a16680e4 | |||
| 04e91a0e8d | |||
| 72b2a18c63 | |||
| 029089b32d | |||
| 3ea513785a | |||
| 8dc576983c | |||
| a7ada33ff9 | |||
| 59a59685d1 | |||
| 2f0024630d | |||
| e9706d0e4f | |||
| baa329ad5d | |||
| 389408a59e | |||
| 2f4009d002 | |||
| dab6f98ec2 | |||
| b23e439eb5 | |||
| fdea5858ba | |||
| 96fc8a5569 | |||
| c408a6532e | |||
| 09e973fab1 | |||
| 971247e67c | |||
| 59dcb382ce | |||
| 86af724ccc | |||
| e5cc62bbbf | |||
| 4b63de6fc4 | |||
| d0d63a6e29 | |||
| d7f57c5b3a | |||
| 777cca4917 | |||
| e05fcb423c | |||
| 7457b8a24d | |||
| 8a3fdc40eb | |||
| 192be6e28b | |||
| 6ba91f1fab | |||
| 4725432c32 | |||
| 4e5b6c565a | |||
| 4e216891e6 | |||
| 562738f945 | |||
| 7ffddf41f8 | |||
| 88f37928ac | |||
| fcd7ae7c04 | |||
| 995d42640f | |||
| 13583bd6ab | |||
| 522c804369 | |||
| 046ae89d29 | |||
| 9ffec51537 | |||
| c07ec5855a | |||
| c445a4a66c | |||
| 96dd77e455 | |||
| 84f1f9239b | |||
| a24a7f02b9 | |||
| d3c50dc608 | |||
| 34a4f027f5 | |||
| 66ed4c0713 | |||
| 1038e9c67e | |||
| 6a3398e8de | |||
| 8a87288d25 | |||
| 1d4a509c7e | |||
| 29d2f6b90d | |||
| 5d6cf1fb80 | |||
| 95e72b7ab2 | |||
| d17d1fb1b7 | |||
| d0d822b8f4 | |||
| f6abe6f999 | |||
| 8a5bbaa229 | |||
| 5750d62fe0 | |||
| 71e683fbe3 | |||
| e214144ec3 | |||
| 0faf19e319 | |||
| 10552c5f2a | |||
| 4181b246a1 | |||
| c39979e253 | |||
| 807c599b85 | |||
| 9e186c32ba | |||
| e3d0709b31 | |||
| bd8fc6b404 | |||
| 8500ed8fbd | |||
| cb86c9a2ae | |||
| bb0f4244c7 | |||
| cd2fe4e8f2 | |||
| bf3f1f783c | |||
| 37f509ff76 | |||
| b58dbde83c | |||
| 0da0a02bac | |||
| fef74dd995 | |||
| 2356365197 | |||
| 5a29c8c49d | |||
| e365c394d8 | |||
| 22371b8f15 | |||
| 32feb46f88 | |||
| 6c77fe4da6 | |||
| 5dc7442d03 | |||
| 9f664737b6 | |||
| 1c3b4367a2 | |||
| 3b48012e4f | |||
| 9a9757c914 | |||
| 2bf19d357e | |||
| 7e31f72301 | |||
| 68bb003411 | |||
| 7133f36e5d | |||
| c9962c4d8e | |||
| 5832bd1765 | |||
| 4046d2c753 | |||
| 93298618ae | |||
| 709a637ae2 | |||
| a88bb30b62 | |||
| 38527207ac | |||
| 6f90fcc016 | |||
| 1e633ded31 | |||
| 4b3b8be4e4 | |||
| effd58a424 | |||
| 850587006c | |||
| 423b574cca | |||
| 7bb4a6cfc1 | |||
| 94f392ab87 | |||
| ccef40e63f | |||
| 1c51b666ab | |||
| b5462d5757 | |||
| 587632d7a3 | |||
| 11c92cdd5f | |||
| e4577e13e0 | |||
| 6365e0c63e | |||
| 4735541344 | |||
| 8e40434867 | |||
| 8194ab43cd | |||
| a708a6377d | |||
| 3fc810697a | |||
| 3305704926 | |||
| 206ec80a9a | |||
| dce19b8d38 | |||
| 8a8751ce66 | |||
| 0f0389b8ff | |||
| 061c9c8483 | |||
| 005d34daf5 | |||
| 95e028d546 | |||
| 95c29d8528 | |||
| bda342b05b | |||
| 889ad4f980 | |||
| a5bca8eab1 | |||
| 2dfb53bd5e | |||
| 474df33a99 | |||
| a2906966ba | |||
| a81096fa13 | |||
| b56ff6c5cd | |||
| 168de8088b | |||
| e7862df57c | |||
| ec829a5683 | |||
| bb3476a8ad | |||
| bab5c88061 | |||
| 7431aadfba | |||
| 1ecf3c368b | |||
| d3e6013840 | |||
| e1220d17bb | |||
| 55fdf8f60d | |||
| 65aac3daf1 | |||
| 1f8e9e582a | |||
| 97e6ce3d8d | |||
| 316ab679f8 | |||
| bf5620c3b6 | |||
| 642aa9847f | |||
| 90f430c95c | |||
| bf4c5bf3d9 | |||
| b7beb72409 | |||
| 0617dd6a93 | |||
| d99f9c53f7 | |||
| 3cfa0df43b | |||
| dc2982965e | |||
| 3c58d764bd | |||
| 4d2584dee4 | |||
| 3294d6ffa0 | |||
| 8398928a97 | |||
| 09a2e09c1b | |||
| d0f5bc223b | |||
| 7a58fb0af5 | |||
| 395c597a7f | |||
| e43734e32e | |||
| eb5659d0da | |||
| 50268b98a9 | |||
| 4a2a5f9d76 | |||
| ef6b8ed5a5 | |||
| b1eda70e7a | |||
| 2ae9ee7352 | |||
| f7b00ab3cc | |||
| 9cad12d879 | |||
| 5967d0f7b3 | |||
| d4800c449d | |||
| 8f9c504bec | |||
| d3d4ce5e5d | |||
| 9dbc2424b1 | |||
| 32771493f3 | |||
| 5b38065f13 | |||
| 1879c265cd | |||
| c654c41380 | |||
| 4273b95b70 | |||
| e4cffd5667 | |||
| e96698ef2e | |||
| e04b8ddb9b | |||
| 6fe3e69cd9 | |||
| 7d1a8bd284 | |||
| 5b44d2e4c7 | |||
| fa0954e8ca | |||
| 8552bde245 | |||
| 9a4d7fc498 | |||
| 6dea241408 | |||
| b3b1bec12c | |||
| 02cc305abf | |||
| ffe0c45eea | |||
| 499ddc0979 | |||
| b154ffb79e | |||
| 87dfab0e57 | |||
| 92ec8b1b4e | |||
| ac8fb0bfe3 | |||
| 0ce6af8f7c | |||
| 053b705cc1 | |||
| 3676f23f65 | |||
| 7e821f277f | |||
| 91cbdef71f | |||
| 0cfb78bc69 | |||
| 01c20b1a6e | |||
| 1711aca5ec | |||
| c536899cc9 | |||
| 60da9d8256 | |||
| a923e225e8 | |||
| 2d7c9d779a | |||
| aea9898563 | |||
| 4e3f71ea43 | |||
| 6acbdbc760 | |||
| 11b6e68fe1 | |||
| cc3aa66e49 | |||
| 563344e8b4 | |||
| 129b8e4601 | |||
| efde69984d | |||
| c66c8e7367 | |||
| c31f454c78 | |||
| 18926d16d1 | |||
| 4e8adebb2d | |||
| 0207bdf22b | |||
| 5caacd8f44 | |||
| d7a3433d2c | |||
| 6e68e106aa | |||
| aeda06374d | |||
| 9225ff5967 | |||
| ac64a1766f | |||
| 6996bb8015 | |||
| 7aff7c46ee | |||
| 882732d054 | |||
| 9e0d911f7b | |||
| ab6b7d19fb | |||
| b95be67051 | |||
| 48b6ed5b69 | |||
| e25041d38d | |||
| 7bc77ef232 | |||
| 0f8a7d76da | |||
| ecf34d9039 | |||
| a9cf253c83 | |||
| 884c2552a8 | |||
| 45e98e0220 | |||
| d8ea26c777 | |||
| c398e53750 | |||
| 9ce0eccfce | |||
| 17e6bb86ed | |||
| a1b85b3c6c | |||
| fafc988a60 | |||
| 55bcc78670 | |||
| 7bc277a978 | |||
| 780e2d6acb | |||
| bbc5743c39 | |||
| f9082237e6 | |||
| 3298e55fb0 | |||
| 2b2d20f796 | |||
| a72b313b83 | |||
| 117802bf10 | |||
| 94f8a92e65 | |||
| 8bd53ba897 | |||
| 5114b2ef85 | |||
| f80cbd7457 | |||
| d7f53b9700 | |||
| 1ab3180c0d | |||
| 009495b11b | |||
| b8a8cb2c6d | |||
| f9bb4bba25 | |||
| a56e8ef241 | |||
| d5d11edbe7 | |||
| f7c2516cd7 | |||
| 45b27e3ac9 | |||
| fe71f24ced | |||
| 52613ba7d4 | |||
| af2719082e | |||
| f136b34b6c | |||
| 4d90454cf8 | |||
| 82e0c5c995 | |||
| dec3bf5ccf | |||
| 23c00ce167 | |||
| bdc21f79e8 | |||
| 2fc71eabe3 | |||
| 3debd4b1ec | |||
| b765ae37e8 | |||
| 9eae16110c | |||
| ce1f4eb7c9 | |||
| 661dc84973 | |||
| 6b21ea4af8 | |||
| 37ab278086 | |||
| 509b157d65 | |||
| 253f720043 | |||
| bdf6b469ad | |||
| 7dc9fac597 | |||
| 77330dffe9 | |||
| f0ea3505e4 | |||
| 06c2a2b9a6 | |||
| c671206e8a | |||
| cf50050eba | |||
| f8e68acd4d | |||
| 9b6fc5e687 | |||
| 7d17d8a84f | |||
| b2aa6d1a3e | |||
| 5a2c6d9aaa | |||
| 1ecbda0731 | |||
| 9e9049709b | |||
| ac9a5db356 | |||
| 67ed7edefb | |||
| 181be6b80c | |||
| 1e2bf10494 | |||
| a219b930b3 | |||
| 78ba2d7afa | |||
| 6fe01dadf1 | |||
| 9df62a1247 | |||
| a9164f8c6e | |||
| 6e5074f8fe | |||
| efe8808e79 | |||
| 77cdce714a | |||
| 69c1b2dcb2 | |||
| 2a50f7ef43 | |||
| 0c93446b81 | |||
| 0e52efb449 | |||
| 283d5b566e | |||
| a4367bbc9d | |||
| 935f7a38f1 | |||
| 9973057051 | |||
| 9527e6de5f | |||
| 86e2827f3b | |||
| a519d51309 | |||
| 9ee4d600a7 | |||
| 2b2d635cb0 | |||
| ec6051d3d6 | |||
| e4f701711c | |||
| 2363362202 | |||
| 5479f0e1a7 | |||
| ab2682a0d3 | |||
| 6678c691dd | |||
| aaf773c600 | |||
| 417083b050 | |||
| 08e675f698 | |||
| efb1ee6182 | |||
| 99788e8d47 | |||
| c219f32266 | |||
| 356038a622 | |||
| 31e324e478 | |||
| e5c00a47e8 | |||
| 1fdc9e9ff8 | |||
| cf33d81d69 | |||
| 29070165eb | |||
| b9e9cdb2b0 | |||
| e111d1884c | |||
| e0e73976db | |||
| 755abe5647 | |||
| 2f80159144 | |||
| e709570f22 | |||
| e63f8d5418 | |||
| b8fd237918 | |||
| 6c153e7ab0 | |||
| 7554750883 | |||
| e75f0cfc67 | |||
| 8f74c87df5 | |||
| 7d3208651a | |||
| e949e93799 | |||
| f544cf6183 | |||
| 197c7b0a8b | |||
| abb3f6659d | |||
| 8141ccfa13 | |||
| f395e8de06 | |||
| 7614c127ba | |||
| b2cc29f6af | |||
| 7efb8d3e7b | |||
| 48fa13e37b | |||
| d5ead73869 | |||
| a81f305e54 | |||
| dcef7aa49a | |||
| e0f571a201 | |||
| e5e9774681 | |||
| 6f18fa0aea | |||
| 9a918da2f0 | |||
| 17d1d4fea2 | |||
| 85fcf6feb9 | |||
| 89665d2b48 | |||
| c3478ff47c | |||
| b77dd3f50e | |||
| 7009f38652 | |||
| 4ff426327b | |||
| d7d0b377f2 | |||
| 4f499af4cf | |||
| d9ed6e7b17 | |||
| efbfe64d1e | |||
| cc9794d6a8 | |||
| 19ff70d0bd | |||
| d82e3ec4d9 | |||
| d0825e96bc | |||
| a981f6a31f | |||
| 524c6bbb9e | |||
| ac4902f867 | |||
| 416be8e2cd | |||
| 4cee5582da | |||
| 160f5adc59 | |||
| b8267e411c | |||
| ab2fcf04eb | |||
| 4a79ed341e | |||
| 7b8c5f3d8f | |||
| 170635a8db | |||
| a86f71b422 | |||
| 2585c4885b | |||
| 1cba13f4ff | |||
| 63e669ff0c | |||
| 2d95dbb0f1 | |||
| 8f50a6e0bf |
@@ -1,3 +1,7 @@
|
||||
[report]
|
||||
show_missing = True
|
||||
[run]
|
||||
omit =
|
||||
*/migrations/*
|
||||
*/admin.py
|
||||
*/translation.py
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
HOST=api.dev.sahkoinsinoorikilta.fi
|
||||
DEBUG=True
|
||||
SECRET_KEY=7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(
|
||||
TG_BOT_TOKEN=
|
||||
EMAIL_HOST=
|
||||
EMAIL_PASSWD=
|
||||
DB_USER=postgres
|
||||
DB_PASSWD=postgres
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
@@ -0,0 +1,10 @@
|
||||
HOST=localhost
|
||||
DEBUG=True
|
||||
SECRET_KEY=7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(
|
||||
TG_BOT_TOKEN=
|
||||
EMAIL_HOST=
|
||||
EMAIL_PASSWD=
|
||||
DB_USER=postgres
|
||||
DB_PASSWD=postgres
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
@@ -3,3 +3,4 @@ infoscreen/static/js/lib
|
||||
webapp/static/js/lib
|
||||
static/js/lib
|
||||
collected_static
|
||||
venv
|
||||
|
||||
@@ -4,11 +4,10 @@
|
||||
"jquery": true
|
||||
},
|
||||
"globals": {
|
||||
"angular": 1,
|
||||
"noty": 1,
|
||||
"app": 1,
|
||||
"_": 1,
|
||||
"moment": 1
|
||||
"angular": true,
|
||||
"noty": true,
|
||||
"_": true,
|
||||
"moment": true
|
||||
},
|
||||
"extends": "eslint:recommended",
|
||||
"rules": {
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
*.swp
|
||||
sikweb/settings.py
|
||||
.env
|
||||
*~
|
||||
*.pyc
|
||||
*.sqlite3
|
||||
uwsgi.ini
|
||||
uwsgi.log
|
||||
infoscreen/static/js/hsl.json
|
||||
members/logs/*
|
||||
.idea/
|
||||
logs/
|
||||
/media/
|
||||
node_modules/
|
||||
/.coverage
|
||||
.coverage
|
||||
db.sqlite3
|
||||
requirements_henu.txt
|
||||
/collected_static/
|
||||
mydatabase
|
||||
settings.json
|
||||
.vscode/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
*.code-workspace
|
||||
sik_test
|
||||
venv/
|
||||
@@ -1,100 +1,117 @@
|
||||
stages:
|
||||
- test
|
||||
- setup
|
||||
- lint
|
||||
- test
|
||||
- publish
|
||||
- deploy
|
||||
|
||||
install:
|
||||
image: node:14
|
||||
stage: setup
|
||||
script:
|
||||
- npm ci
|
||||
artifacts:
|
||||
paths:
|
||||
- node_modules
|
||||
expire_in: 1 week
|
||||
|
||||
test:
|
||||
image: python:3.5
|
||||
image: python:3.7
|
||||
stage: test
|
||||
needs: []
|
||||
services:
|
||||
- postgres:latest
|
||||
- postgres:12
|
||||
variables:
|
||||
POSTGRES_DB: ci
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
|
||||
DB_HOST: postgres
|
||||
script:
|
||||
- python -V
|
||||
- pip install -r requirements.txt
|
||||
- cp sikweb/settings-sample.py sikweb/default_settings.py
|
||||
- cp sikweb/.ci-settings.py sikweb/settings.py
|
||||
- python manage.py migrate --noinput
|
||||
- python manage.py createdefaultadmin
|
||||
- python manage.py test
|
||||
|
||||
pycodestyle:
|
||||
image: python:3.5
|
||||
lint:py:
|
||||
image: python:3.7
|
||||
stage: lint
|
||||
needs: []
|
||||
script:
|
||||
- pip install pycodestyle
|
||||
- pycodestyle --config=setup.cfg --count .
|
||||
- pycodestyle --config=pycodestyle.cfg --count .
|
||||
|
||||
eslint:
|
||||
image: node:7.10.0
|
||||
lint:js:
|
||||
image: node:14
|
||||
stage: lint
|
||||
before_script:
|
||||
- npm install
|
||||
needs: ["install"]
|
||||
script:
|
||||
- npm run eslint
|
||||
- npm run lint:js
|
||||
|
||||
remark:
|
||||
image: node:7.10.0
|
||||
lint:md:
|
||||
image: node:14
|
||||
stage: lint
|
||||
before_script:
|
||||
- npm install
|
||||
needs: ["install"]
|
||||
script:
|
||||
- npm run remark
|
||||
- npm run lint:md
|
||||
|
||||
publish:
|
||||
stage: publish
|
||||
image: docker:latest
|
||||
image: docker:stable
|
||||
needs: ["test", "lint:py", "lint:js", "lint:md"]
|
||||
services:
|
||||
- docker:stable-dind
|
||||
only:
|
||||
- develop
|
||||
before_script:
|
||||
- docker info
|
||||
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $REGISTRY_URL
|
||||
- master
|
||||
script:
|
||||
- docker info
|
||||
- docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY
|
||||
- docker build . -t "$IMAGE_NAME"
|
||||
- docker push "$IMAGE_NAME"
|
||||
|
||||
deploy_dev:
|
||||
deploy:dev:
|
||||
stage: deploy
|
||||
image: alpine:latest
|
||||
environment:
|
||||
name: dev
|
||||
url: http://web.sik.party:8080
|
||||
image: docker:stable
|
||||
only:
|
||||
- develop
|
||||
before_script:
|
||||
- pwd
|
||||
- apk add --update openssh
|
||||
- ssh -V
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- '[[ -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"
|
||||
|
||||
deploy_production:
|
||||
stage: deploy
|
||||
image: alpine:latest
|
||||
environment:
|
||||
name: production
|
||||
url: https://sika.sahkoinsinoorikilta.fi
|
||||
when: manual
|
||||
name: dev
|
||||
url: http://api.dev.sahkoinsinoorikilta.fi
|
||||
variables:
|
||||
DOCKER_HOST: $DEV_CI_DOCKER_HOST
|
||||
DOCKER_TLS_VERIFY: 1
|
||||
before_script:
|
||||
- mkdir -p ~/.docker
|
||||
- echo "$DEV_TLSCACERT" > ~/.docker/ca.pem
|
||||
- echo "$DEV_TLSCERT" > ~/.docker/cert.pem
|
||||
- echo "$DEV_TLSKEY" > ~/.docker/key.pem
|
||||
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
|
||||
script:
|
||||
- docker stack deploy --with-registry-auth -c stack-compose-dev.yml "$SERVICE_NAME"
|
||||
after_script:
|
||||
- docker logout "$CI_REGISTRY"
|
||||
|
||||
deploy:production:
|
||||
stage: deploy
|
||||
image: docker:stable
|
||||
only:
|
||||
- master
|
||||
environment:
|
||||
name: production
|
||||
url: api.sahkoinsinoorikilta.fi
|
||||
when: manual
|
||||
variables:
|
||||
DOCKER_HOST: $CI_DOCKER_HOST
|
||||
DOCKER_TLS_VERIFY: 1
|
||||
before_script:
|
||||
- pwd
|
||||
- apk add --update openssh
|
||||
- ssh -V
|
||||
- mkdir -p ~/.ssh
|
||||
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
- chmod 600 ~/.ssh/id_rsa
|
||||
- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
|
||||
- mkdir -p ~/.docker
|
||||
- echo "$TLSCACERT" > ~/.docker/ca.pem
|
||||
- echo "$TLSCERT" > ~/.docker/cert.pem
|
||||
- echo "$TLSKEY" > ~/.docker/key.pem
|
||||
- docker login -u gitlab-ci-token -p "$CI_BUILD_TOKEN" "$CI_REGISTRY"
|
||||
script:
|
||||
- ssh $PROD_SSH_USER@$PROD_SSH_HOST "zsh ~/deploy.sh"
|
||||
|
||||
- docker stack deploy --with-registry-auth -c stack-compose.yml "$SERVICE_NAME"
|
||||
after_script:
|
||||
- docker logout "$CI_REGISTRY"
|
||||
@@ -0,0 +1 @@
|
||||
_
|
||||
@@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
PURPLE='\033[0;35m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
source "${VIRTUAL_ENV}/bin/activate"
|
||||
|
||||
if [ $? -ne 0 ]
|
||||
then
|
||||
printf "${PURPLE}Failed to find virtualenv. Skipping pre-push hook.\n${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
set -e
|
||||
|
||||
printf "${PURPLE}Running pre-push tests.${NC}\n"
|
||||
printf "${PURPLE}linters...${NC}\n"
|
||||
npm run lint
|
||||
printf "${PURPLE}unit tests...${NC}\n"
|
||||
python -Wall manage.py test --noinput
|
||||
set +e
|
||||
|
||||
printf "${PURPLE}Tests passed.${NC}\n"
|
||||
@@ -0,0 +1 @@
|
||||
3.7.4
|
||||
@@ -1,8 +1,20 @@
|
||||
FROM python:3
|
||||
FROM python:3.7-alpine
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV IS_DOCKER 1
|
||||
RUN mkdir /code
|
||||
WORKDIR /code
|
||||
ADD requirements.txt /code/
|
||||
RUN env
|
||||
ADD . /code/
|
||||
WORKDIR /app
|
||||
COPY requirements.txt ./
|
||||
COPY requirements.production.txt ./
|
||||
COPY . ./
|
||||
|
||||
# uWSGI, gunicorn etc.
|
||||
RUN apk add --no-cache python3-dev build-base linux-headers pcre-dev openssl bash \
|
||||
# PSQL
|
||||
&& apk add --no-cache postgresql-dev \
|
||||
# Pillow
|
||||
&& apk add --no-cache jpeg-dev zlib-dev \
|
||||
&& pip install --upgrade pip \
|
||||
&& pip install -r requirements.txt \
|
||||
&& pip install -r requirements.production.txt
|
||||
|
||||
RUN python manage.py collectstatic --noinput
|
||||
|
||||
CMD ["sh", "-c", "./production_entrypoint.sh"]
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@@ -1,23 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from coffee_scale import mqtt
|
||||
|
||||
|
||||
class CoffeeScaleConfig(AppConfig):
|
||||
name = 'coffee_scale'
|
||||
|
||||
def ready(self):
|
||||
if ('makemigrations' in sys.argv or 'migrate' in sys.argv):
|
||||
return
|
||||
|
||||
try:
|
||||
logging.info('Connecting to MQTT (coffee scale) at {}...'.format(mqtt.HOST))
|
||||
logging.info('If there is no confirmation, the MQTT connection has probably failed.')
|
||||
mqtt.client.connect_async(mqtt.HOST, mqtt.PORT, 60)
|
||||
mqtt.client.loop_start()
|
||||
except Exception as ex:
|
||||
logging.error(ex)
|
||||
logging.error('Failed to connect to MQTT at {}'.format(mqtt.HOST))
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
||||
@@ -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
|
||||
@@ -1,121 +0,0 @@
|
||||
body {
|
||||
background-color: white;
|
||||
font-family: monospace;
|
||||
color: black;
|
||||
}
|
||||
#container{
|
||||
position:relative;
|
||||
width:95%;
|
||||
margin-left:auto;
|
||||
margin-right:auto;
|
||||
height:100%;
|
||||
overflow:hidden;
|
||||
|
||||
}
|
||||
#upper{
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: bottom center;
|
||||
background-image: url("/static/img/smokes.png");
|
||||
transform-origin: bottom;
|
||||
animation: smokes 8s ease-in-out 0s infinite;
|
||||
opacity:0;
|
||||
height:40%;
|
||||
}
|
||||
#lower{
|
||||
position:relative;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: top center;
|
||||
background-image: url("/static/img/coffeecup3.png");
|
||||
height:60%;
|
||||
}
|
||||
#scale{
|
||||
position:absolute;
|
||||
top:80%;
|
||||
width:90%;
|
||||
height:10%;
|
||||
margin: 0% 5% 0% 5%;
|
||||
background: lightgrey;
|
||||
border-radius: 10px;
|
||||
overflow:hidden;
|
||||
}
|
||||
#scale2{
|
||||
width: 0%;
|
||||
transition: width 2s;
|
||||
height:100%;
|
||||
background: green;
|
||||
border-radius: 10px;
|
||||
}
|
||||
#brewtime{
|
||||
text-align:right;
|
||||
position:absolute;
|
||||
right:0px;
|
||||
z-index:5;
|
||||
font-size:10vw;
|
||||
}
|
||||
#address{
|
||||
text-align:left;
|
||||
position:absolute;
|
||||
left:0px;
|
||||
z-index:5;
|
||||
font-size:4vw;
|
||||
color: #333;
|
||||
}
|
||||
noscript{
|
||||
color:red;
|
||||
}
|
||||
#text{
|
||||
color:green;
|
||||
position:absolute;
|
||||
top:50%;
|
||||
left:50%;
|
||||
}
|
||||
.brewing{
|
||||
animation: brewing 5s ease-in-out 0s infinite;
|
||||
}
|
||||
.hurry{
|
||||
color:red !important;
|
||||
}
|
||||
.unknown{
|
||||
color:orange !important;
|
||||
animation: unknown 5s ease-in-out 0s infinite;
|
||||
}
|
||||
.friday{
|
||||
animation: friday 20s ease-in-out 0s infinite;
|
||||
}
|
||||
.normal{
|
||||
animation: normal 1000s ease-in-out 0s infinite;
|
||||
}
|
||||
.coffeeready{
|
||||
animation: coffeeready 10s ease-in-out 0s;
|
||||
}
|
||||
@keyframes smokes {
|
||||
0% {transform: skewX(-10deg);}
|
||||
50% {transform: skewX(10deg);}
|
||||
100% {transform: skewX(-10deg);}
|
||||
}
|
||||
@keyframes brewing {
|
||||
0% {color:green;}
|
||||
50% {color: transparent;}
|
||||
100% {color:green;}
|
||||
}
|
||||
@keyframes coffeeready {
|
||||
0% {background-color:white;}
|
||||
25% {background-color:green;}
|
||||
50% {background-color:white;}
|
||||
75% {background-color:green;}
|
||||
100% {background-color:white;}
|
||||
}
|
||||
@keyframes unknown {
|
||||
0%,40% {transform: rotate(0deg);}
|
||||
60%,100% {transform: rotate(360deg);}
|
||||
}
|
||||
@keyframes friday {
|
||||
0% {transform: rotate(0deg);}
|
||||
100% {transform: rotate(360deg);}
|
||||
}
|
||||
@keyframes normal {
|
||||
0%,49% {transform: rotate(0deg);}
|
||||
50%,100% {transform: rotate(360deg);}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 9.0 KiB |
@@ -1,130 +0,0 @@
|
||||
var len = 0;
|
||||
var lastBrew = "∞";
|
||||
var brewtext = "";
|
||||
|
||||
$(document).ready(function(){
|
||||
$('#text').bind("DOMSubtreeModified", resize);
|
||||
updateTime();
|
||||
setInterval(updateTime,1000);
|
||||
formatBrewTime();
|
||||
setInterval(formatBrewTime,10000);
|
||||
});
|
||||
|
||||
$(window).resize(resize);
|
||||
|
||||
function fetchdata(data, status){
|
||||
if (typeof status !== 'undefined'){
|
||||
if (status == "success"){
|
||||
parseData(data);
|
||||
}
|
||||
else if (status == "error"){
|
||||
handleError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$.getJSON("/coffee/cups", fetchdata);
|
||||
setInterval(function() {
|
||||
$.getJSON("/coffee/cups", fetchdata);
|
||||
}, 2000);
|
||||
|
||||
function formatBrewTime(){
|
||||
if (!brewtext && lastBrew instanceof Date){
|
||||
var now = new Date();
|
||||
var timeDiff = Math.max(now.getTime() - lastBrew.getTime(), 0);
|
||||
var tmp = (timeDiff < 3600000)
|
||||
? Math.round(timeDiff / 60000) + ' min'
|
||||
: '~' + Math.round(timeDiff / 3600000 * 2) / 2 + ' h';
|
||||
|
||||
$("#brewtime").html(tmp);
|
||||
} else {
|
||||
$("#brewtime").html(brewtext);
|
||||
}
|
||||
}
|
||||
|
||||
function handleError() {
|
||||
setData("?", 0, 0, Number.MAX_VALUE, Number.MAX_VALUE);
|
||||
}
|
||||
|
||||
function parseData(data) {
|
||||
if (data) {
|
||||
var date = new Date(data.date);
|
||||
lastBrew = new Date(data.last_brew);
|
||||
var now = new Date();
|
||||
var cups = data.cups;
|
||||
var brewing = data.brewing;
|
||||
var timeDiff = Math.max(now.getTime() - lastBrew.getTime(),0);
|
||||
var opa = Math.max(100 - timeDiff / 90000,0);
|
||||
setData(cups, data.temp, opa,now.getTime()-date.getTime(), timeDiff, brewing);
|
||||
}
|
||||
else{
|
||||
handleError();
|
||||
}
|
||||
}
|
||||
|
||||
function setData(cups, temp, opa, timeFromUpdate, timeFromBrew, brewing){
|
||||
if (cups == 0) {
|
||||
opa = 0;
|
||||
}
|
||||
brewtext = "";
|
||||
$("#upper").css({opacity: opa/100});
|
||||
$("#scale2").css({width: Math.min(cups/9*100,100) + '%'});
|
||||
$("#text,body").removeClass();
|
||||
|
||||
cups = Number(cups).toFixed(1);
|
||||
|
||||
if(timeFromUpdate > 600000){
|
||||
cups = "?";
|
||||
brewtext = "∞";
|
||||
$("#text").addClass("unknown");
|
||||
}
|
||||
else if(brewing){
|
||||
cups = "+";
|
||||
brewtext = ":)";
|
||||
$("#text").addClass("brewing");
|
||||
}
|
||||
else if(cups <= 2){
|
||||
$("#text").addClass("hurry");
|
||||
}
|
||||
formatBrewTime();
|
||||
if($("#text").html() == "+" && !brewing)
|
||||
$("body").addClass("coffeeready");
|
||||
|
||||
var cupsString = cups.toString();
|
||||
len = cupsString.length;
|
||||
$("#text").html(cups);
|
||||
}
|
||||
|
||||
function updateTime(){
|
||||
var now = new Date();
|
||||
$("#time").html(formatTime(now.getHours(),now.getMinutes(),now.getSeconds()));
|
||||
}
|
||||
|
||||
function formatTime(hours, minutes, seconds){
|
||||
var str = "";
|
||||
|
||||
if(hours < 10)
|
||||
str += "0";
|
||||
str += hours;
|
||||
|
||||
str += ":";
|
||||
|
||||
if(minutes < 10)
|
||||
str += "0";
|
||||
str += minutes;
|
||||
|
||||
str += ":";
|
||||
|
||||
if(seconds < 10)
|
||||
str += "0";
|
||||
str += seconds;
|
||||
|
||||
return str;
|
||||
}
|
||||
function resize(){
|
||||
var w = $("#container").width();
|
||||
var h = $("#container").height();
|
||||
var s = w > h ? h : w;
|
||||
var font = s*0.8*0.38/Math.sqrt(len);
|
||||
$("#text").css({ top: s*0.16-font/2 + 'px', fontSize: font + 'px', marginLeft: -font*len*3/10 + 'px'});
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Coffee Cups @Guild Room - AYY SIK ry</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="refresh" content="3600">
|
||||
<meta http-equiv="cache-control" content="max-age=0" />
|
||||
<meta http-equiv="cache-control" content="no-cache" />
|
||||
<meta http-equiv="expires" content="0" />
|
||||
<meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
|
||||
<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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container">
|
||||
<span id="brewtime"></span>
|
||||
<span id="address">
|
||||
ka.dy.fi
|
||||
<noscript><br>This page uses JavaScript!</noscript>
|
||||
<br><span id="time"></span></span>
|
||||
</span>
|
||||
<div id="upper">
|
||||
</div>
|
||||
<!--Kahvinkeitin on rikki. Varakeittimellä keitettyä kahvia saattaa olla.-->
|
||||
<div id="lower" class="normal">
|
||||
<div id="text"></div>
|
||||
<div id="scale"><div id="scale2"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,30 +0,0 @@
|
||||
from django.test import TestCase, Client
|
||||
from django.conf import settings
|
||||
|
||||
from coffee_scale.mqtt import on_message
|
||||
|
||||
HOST = settings.MQTT_SETTINGS['HOST']
|
||||
PORT = settings.MQTT_SETTINGS['PORT']
|
||||
TOPICS = settings.MQTT_SETTINGS['TOPICS']
|
||||
|
||||
|
||||
class MQTTTestCase(TestCase):
|
||||
"""Tests MQTT functionality"""
|
||||
|
||||
class MockMessage:
|
||||
def __init__(self, payload, topic):
|
||||
self.payload = payload
|
||||
self.topic = topic
|
||||
|
||||
def setUp(self):
|
||||
payload = '10'.encode('utf-8')
|
||||
topic = TOPICS['CUPS']
|
||||
msg = MQTTTestCase.MockMessage(payload, topic)
|
||||
|
||||
on_message(None, None, msg)
|
||||
self.c = Client()
|
||||
|
||||
def test_receive_cups(self):
|
||||
response = self.c.get('/coffee/cups')
|
||||
payload = response.json()
|
||||
self.assertEquals(payload['cups'], 10)
|
||||
@@ -1,14 +0,0 @@
|
||||
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)
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# landing page
|
||||
url(r'^$', coffee_view),
|
||||
url(r'^cups', cups_view),
|
||||
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
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)
|
||||
@@ -2,12 +2,24 @@ version: '3'
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres
|
||||
image: postgres:12
|
||||
volumes:
|
||||
- dbdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
- POSTGRES_PASSWORD=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: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-backend
|
||||
command: ["bash", "-c", "cd /app & bash setup.sh --no-input --no-npm && gunicorn -w 4 -b 0.0.0.0:8000 sikweb.wsgi"]
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
volumes:
|
||||
dbdata:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
"""File containing Infoscreen HSL data fetcher classes."""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pytz
|
||||
|
||||
from datetime import timedelta, datetime
|
||||
from django.utils import timezone, dateparse
|
||||
from django.utils.dateformat import format
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
with open(os.path.join(settings.BASE_DIR, 'infoscreen', 'hsl_stops.graphql')) as stops_file:
|
||||
STOPS_QUERY = stops_file.read()
|
||||
|
||||
with open(os.path.join(settings.BASE_DIR, 'infoscreen', 'hsl_stops_variables.json')) as vars_file:
|
||||
STOPS_VARS = json.loads(vars_file.read())
|
||||
|
||||
API_URL = 'https://api.digitransit.fi/routing/v1/routers/hsl/index/graphql'
|
||||
API_HEADERS = {'Content-Type': 'application/json'}
|
||||
|
||||
|
||||
def fetch():
|
||||
"""Fetch data from HSL API."""
|
||||
|
||||
query_vars = STOPS_VARS.copy()
|
||||
query_vars['startTime_6'] = format(timezone.now(), 'U')
|
||||
|
||||
post_data = json.dumps({
|
||||
'operationName': 'NearestRoutesContainer',
|
||||
'query': STOPS_QUERY,
|
||||
'variables': query_vars,
|
||||
})
|
||||
|
||||
resp = requests.post(API_URL, data=post_data, headers=API_HEADERS)
|
||||
|
||||
data = resp.json()
|
||||
|
||||
items = data['data']['viewer']['_nearest']['edges']
|
||||
places = map(lambda item: item['node']['place'], items)
|
||||
|
||||
schedule = []
|
||||
for place in places:
|
||||
route = place['pattern']['route']['shortName']
|
||||
stop_times = place['_stoptimes']
|
||||
for stop_time in stop_times:
|
||||
timestamp = stop_time['serviceDay'] + stop_time['realtimeArrival']
|
||||
headsign = stop_time['stopHeadsign']
|
||||
stop_name = stop_time['stop']['name']
|
||||
time_diff = (timestamp - timezone.now().timestamp()) / 60 # minutes
|
||||
|
||||
if time_diff < settings.HSL_DEPARTURE_THRESHOLD:
|
||||
continue
|
||||
elif time_diff < settings.HSL_HURRY_THRESHOLD:
|
||||
time = '{} min'.format(int(time_diff))
|
||||
else:
|
||||
time = pytz.utc.localize(datetime.fromtimestamp(timestamp)).strftime('%H:%M')
|
||||
|
||||
schedule.append({
|
||||
'route': route,
|
||||
'headsign': headsign,
|
||||
'timestamp': time,
|
||||
'stop': stop_name,
|
||||
})
|
||||
|
||||
return schedule
|
||||
@@ -1,71 +0,0 @@
|
||||
query NearestRoutesContainer($lat_0: Float!, $lon_1: Float!, $maxDistance_2: Int!, $maxResults_3: Int!, $timeRange_7: Int!, $numberOfDepartures_8: Int!, $filterByModes_4: [Mode]!, $filterByPlaceTypes_5: [FilterPlaceType]!, $startTime_6: Long!) {
|
||||
viewer {
|
||||
...F5
|
||||
}
|
||||
}
|
||||
|
||||
fragment F0 on DepartureRow {
|
||||
_stoptimes4caEfh: stoptimes(startTime: $startTime_6, timeRange: $timeRange_7, numberOfDepartures: $numberOfDepartures_8) {
|
||||
pickupType
|
||||
serviceDay
|
||||
realtimeDeparture
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
fragment F1 on DepartureRow {
|
||||
pattern {
|
||||
route {
|
||||
shortName
|
||||
}
|
||||
}
|
||||
_stoptimes: stoptimes(startTime: $startTime_6, timeRange: $timeRange_7, numberOfDepartures: $numberOfDepartures_8) {
|
||||
realtimeArrival
|
||||
serviceDay
|
||||
stopHeadsign
|
||||
stop {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fragment F2 on BikeRentalStation {
|
||||
id
|
||||
}
|
||||
|
||||
fragment F3 on placeAtDistance {
|
||||
distance
|
||||
place {
|
||||
id
|
||||
__typename
|
||||
...F1
|
||||
...F2
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
fragment F4 on placeAtDistanceConnection {
|
||||
edges {
|
||||
node {
|
||||
distance
|
||||
place {
|
||||
id
|
||||
__typename
|
||||
...F0
|
||||
}
|
||||
id
|
||||
...F3
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
}
|
||||
}
|
||||
|
||||
fragment F5 on QueryType {
|
||||
_nearest: nearest(lat: $lat_0, lon: $lon_1, maxDistance: $maxDistance_2, maxResults: $maxResults_3, first: $maxResults_3, filterByModes: $filterByModes_4, filterByPlaceTypes: $filterByPlaceTypes_5) {
|
||||
...F4
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"lat_0": 60.190480099999995,
|
||||
"lon_1": 24.8275665,
|
||||
"maxDistance_2": 1000,
|
||||
"maxResults_3": 50,
|
||||
"numberOfDepartures_8": 2,
|
||||
"timeRange_7": 7200,
|
||||
"filterByModes_4": ["BUS"],
|
||||
"filterByPlaceTypes_5": ["DEPARTURE_ROW"]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from infoscreen.hsl_fetcher import HSLFetcher
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Loads HSL timetables and save to json file.'
|
||||
|
||||
def handle(self, *args, **options):
|
||||
fetcher = HSLFetcher()
|
||||
fetcher.fetch()
|
||||
@@ -15,4 +15,7 @@ class Migration(migrations.Migration):
|
||||
migrations.DeleteModel(
|
||||
name='HSLDataModel',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='HslInfoItem',
|
||||
),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 2.1.5 on 2019-03-26 12:49
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('infoscreen', '0006_delete_hsldatamodel'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LunchItem',
|
||||
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',),
|
||||
),
|
||||
]
|
||||
@@ -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,25 @@ 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 LunchItem(InfoItem):
|
||||
"""Class for Lunch Infoscreen item."""
|
||||
|
||||
display_name = _("Today's lunch")
|
||||
|
||||
def get_template_url(self):
|
||||
return "/static/infoscreen/html/lunch.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
return "/static/infoscreen/html/lunch_create.html"
|
||||
|
||||
|
||||
class EventInfoItem(InfoItem):
|
||||
@@ -200,12 +213,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 +231,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 +253,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."""
|
||||
@@ -254,21 +267,6 @@ class VideoInfoItem(InfoItem):
|
||||
return d
|
||||
|
||||
|
||||
class HslInfoItem(InfoItem):
|
||||
"""Class for HSL Infoscreen item."""
|
||||
|
||||
display_name = _("HSL timetables")
|
||||
|
||||
def get_template_url(self):
|
||||
"""Return HSL infoitem template url."""
|
||||
return "/static/html/hsl.html"
|
||||
|
||||
@staticmethod
|
||||
def get_create_template_url():
|
||||
"""Call create HSL infoitem template url command."""
|
||||
return "/static/html/hsl_create.html"
|
||||
|
||||
|
||||
class ExternalImageInfoItem(InfoItem):
|
||||
"""Class for External Image Infoscreen item."""
|
||||
|
||||
@@ -277,12 +275,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 +319,7 @@ class ExternalImageInfoItem(InfoItem):
|
||||
class InfoInstance(models.Model):
|
||||
"""Class for Info instance in Infoscreen."""
|
||||
|
||||
rotation = models.ForeignKey('Rotation', related_name='instances')
|
||||
rotation = models.ForeignKey('Rotation', related_name='instances', on_delete=models.CASCADE)
|
||||
duration = models.FloatField(default=15.0) # seconds
|
||||
# generic relation to some kind of InfoItem
|
||||
item_id = models.PositiveIntegerField()
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
table {
|
||||
font-size: 4vh;
|
||||
font-family: 'Droid Sans Mono', monospace;
|
||||
}
|
||||
.red {
|
||||
color: red;
|
||||
-webkit-animation-name: blinker;
|
||||
-webkit-animation-duration: 2s;
|
||||
-webkit-animation-timing-function: linear;
|
||||
-webkit-animation-iteration-count: infinite;
|
||||
|
||||
-moz-animation-name: blinker;
|
||||
-moz-animation-duration: 2s;
|
||||
-moz-animation-timing-function: linear;
|
||||
-moz-animation-iteration-count: infinite;
|
||||
|
||||
animation-name: blinker;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
.black {
|
||||
color: black;
|
||||
}
|
||||
@-moz-keyframes blinker {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0.1; }
|
||||
100% { opacity: 1.0; }
|
||||
}
|
||||
|
||||
@-webkit-keyframes blinker {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0.1; }
|
||||
100% { opacity: 1.0; }
|
||||
}
|
||||
|
||||
@keyframes blinker {
|
||||
0% { opacity: 1.0; }
|
||||
50% { opacity: 0.1; }
|
||||
100% { opacity: 1.0; }
|
||||
}
|
||||
thead{
|
||||
background: #f0f0f0;
|
||||
}
|
||||
.header-row{
|
||||
background: #f0f0f0;
|
||||
font-size: 7vh;
|
||||
font-family: 'Droid Sans Mono', monospace;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100vw;
|
||||
padding: 0 0 0 0;
|
||||
}
|
||||
|
||||
.container .table {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.repeat-item.ng-leave {
|
||||
|
||||
}
|
||||
|
||||
.repeat-item.ng-leave.ng-leave-active {
|
||||
opacity: 0;
|
||||
font-size: 0vh;
|
||||
}
|
||||
|
||||
.repeat-item.ng-leave{
|
||||
opacity: 1;
|
||||
font-size: 5vh;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<link rel="stylesheet" href="/static/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,41 +0,0 @@
|
||||
<link rel="stylesheet" href="/static/css/hsl.css">
|
||||
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/locale/fi.js"></script>
|
||||
<div class="container" ng-app="myApp" ng-controller="timetableCtrl">
|
||||
<div class="header-row row">
|
||||
<div class="col-sm-2"><p>{{clock | date:'HH:mm'}}</p></div>
|
||||
<div class="col-sm-8">HSL-Aikataulut</div>
|
||||
<div class="col-sm-2 time"></div>
|
||||
</div>
|
||||
<h1 style="font-size: 10vh; text-align: center" ng-if="error">
|
||||
{{error}}
|
||||
</h1>
|
||||
<table ng-if="!error" class="table table-striped row">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Aika
|
||||
</th>
|
||||
<th>
|
||||
Linja
|
||||
</th>
|
||||
<th>
|
||||
Pysäkki
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['timestamp'] | limitTo: 11">
|
||||
<td style="min-width: 300px">
|
||||
{{x.timestamp}}
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{x.route}}</strong>, {{x.headsign}}
|
||||
</td>
|
||||
<td>
|
||||
{{x.stop}}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,10 +0,0 @@
|
||||
<div ng-controller="infoadmin_hslitem_create" style="margin-top:20px;">
|
||||
<div>
|
||||
Create new item to show hsl ttimetables. 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>
|
||||
@@ -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>
|
||||
@@ -1 +0,0 @@
|
||||
<h1>testi2</h1>
|
||||
@@ -1 +0,0 @@
|
||||
<h1>testi3</h1>
|
||||
|
Before Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 163 KiB |
|
Before Width: | Height: | Size: 736 KiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 96 KiB |
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#header {
|
||||
height: 30%;
|
||||
width: 100%;
|
||||
background-color:#7c1330;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#header-image {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.article-row {
|
||||
min-height: 20vh;
|
||||
margin: 10px 10px 10px 10px;
|
||||
}
|
||||
|
||||
.article-thumb-col {
|
||||
max-height: 200px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.article-title-col {
|
||||
font-size: 3vw;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
max-width: 355px;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
#sossoimage {
|
||||
height: 300px;
|
||||
position: relative;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.stretch {
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
#post {
|
||||
height: 540px;
|
||||
border:2px solid black;
|
||||
}
|
||||
|
||||
#container {
|
||||
max-height: 70%;
|
||||
}
|
||||
@@ -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/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>
|
||||
@@ -0,0 +1,11 @@
|
||||
<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/lunch.css">
|
||||
<div ng-controller="LunchController">
|
||||
<div id="container">
|
||||
<div class="restaurant row" ng-repeat="restaurant in data">
|
||||
<div class="lunch-option" ng-repeat="l in lunch">
|
||||
<h3 ng-bind-html="l.title | unsafe"></h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,10 @@
|
||||
<div ng-controller="infoadmin_lunchitem_create" style="margin-top:20px;">
|
||||
<div>
|
||||
Create new item to show restaurants. Name is used only as identifier
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Name:</label>
|
||||
<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: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
@@ -182,8 +182,8 @@ var simple_controllers = [
|
||||
"external_image",
|
||||
"abbitem",
|
||||
"sossoitem",
|
||||
"lunchitem",
|
||||
"eventitem",
|
||||
"hslitem",
|
||||
"websiteitem",
|
||||
"apyitem",
|
||||
];
|
||||
@@ -46,10 +46,18 @@ 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");
|
||||
var url = "https://sahkoinsinoorikilta.fi/api/news.php";
|
||||
// TODO: FIX, we try to get rid of php, not depend on it!
|
||||
var url = "https://old.sahkoinsinoorikilta.fi/api/news.php";
|
||||
var params = "?type=11&lang=fi&title_search=ABB&min_date="+min_date
|
||||
$http.get(url+params).then(function(response){
|
||||
$scope.jobs = _.filter(response.data, function(job){
|
||||
@@ -75,6 +83,21 @@ app.controller('SossoController', function($scope, $http) {
|
||||
})
|
||||
});
|
||||
|
||||
app.controller('LunchController', function ($scope, $http) {
|
||||
$scope.data = [];
|
||||
var restaurants = [42];
|
||||
var restaurant_names = ["TUAS"]
|
||||
var cur_date = new Date().toISOString().split("T")[0]
|
||||
$http.get("https://kitchen.kanttiinit.fi/menus?restaurants=" + restaurants.join(",") + "&days=" + cur_date).then(function (response) {
|
||||
$scope.data = restaurant_names.map(function(n, idx) {
|
||||
return {
|
||||
name: n,
|
||||
lunch: response[idx][cur_date],
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
app.controller('ApyController', function($scope, $http) {
|
||||
$scope.items = [];
|
||||
$http.get("/infoscreen/apyjson").then(function(response)
|
||||
@@ -112,35 +135,3 @@ app.filter('unixTimeToDifference', function() {
|
||||
return res;
|
||||
}
|
||||
})
|
||||
|
||||
app.controller('timetableCtrl',
|
||||
function($scope, $http, $interval) {
|
||||
function load() {
|
||||
$http.get('/infoscreen/hsl_data')
|
||||
.then(function(data, status, headers, config) { //eslint-disable-line no-unused-vars
|
||||
$scope.stoptimes = data.data;
|
||||
$scope.error = data.data.error || null;
|
||||
});
|
||||
$http.get('/infoscreen/hsl_data/settings')
|
||||
.then(function(data, status, headers, config) { //eslint-disable-line no-unused-vars
|
||||
$scope.departureThreshold = data.data['departure_threshold'];
|
||||
$scope.hurryThreshold = data.data['hurry_threshold'];
|
||||
});
|
||||
}
|
||||
|
||||
function update_clock() {
|
||||
$scope.clock = Date.now();
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
$interval.cancel(load_interval);
|
||||
$interval.cancel(clock_interval);
|
||||
});
|
||||
|
||||
var load_interval = $interval(load, 5000);
|
||||
var clock_interval = $interval(update_clock, 1000);
|
||||
|
||||
update_clock();
|
||||
load();
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,44 @@
|
||||
{% 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/underscore-min.js" %}"></script>
|
||||
<script src="{% static "js/lib/moment.js" %}"></script>
|
||||
<script src="{% static "js/lib/angular-route.js" %}"></script>
|
||||
<script src="{% static "js/lib/angular-animate.js" %}"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/tether/1.4.0/js/tether.min.js" integrity="sha384-DztdAPBWPRXSA/3eYEEUWrWCy7G5KFbe8fFjk5JAIxUYHKkDx6Qin1DkWx51bBrb" crossorigin="anonymous"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
|
||||
{% endblock libraries %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{% static "webapp/css/footer.css" %}">
|
||||
{% endblock styles %}
|
||||
|
||||
{% block controllers %}
|
||||
<script src="{% static "infoscreen/js/infoadmin_controllers.js" %}"></script>
|
||||
{% endblock controllers %}
|
||||
</head>
|
||||
<body>
|
||||
{% block body %}
|
||||
{% endblock body %}
|
||||
</body>
|
||||
</html>
|
||||
{% endblock html %}
|
||||
@@ -0,0 +1,7 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<div class="header-content">
|
||||
<div class="logo">
|
||||
<a href="/"><img src="{% static "webapp/img/logo_header.png" %}" alt="Aalto-yliopiston Sähköinsinöörikilta ry"></a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,154 +1,45 @@
|
||||
{% extends "infoscreen:base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load staticfiles %}
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="infoAdmin">
|
||||
<head>
|
||||
<meta charset="utf-8" name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1" />
|
||||
<title>Infoscreen admin</title>
|
||||
<script src="{% static "js/lib/angular.js" %}"></script>
|
||||
<script src="{% static "js/lib/ng-file-upload-bower-12.2.11/ng-file-upload-all.js" %}"></script>
|
||||
<script src="{% static "js/lib/jquery-3.1.0.min.js" %}"></script>
|
||||
<script src="{% static "js/lib/bootstrap.min.js" %}"></script>
|
||||
<script src="{% static "js/lib/underscore-min.js" %}"></script>
|
||||
<script src="{% static "js/infoadmin_controllers.js" %}"></script>
|
||||
|
||||
<link rel="stylesheet" href="{% static "css/lib/bootstrap.min.css" %}"></link>
|
||||
<link rel="stylesheet" href="{% static "css/base.css" %}"></link>
|
||||
<link rel="stylesheet" href="{% static "css/infoscreen_admin.css" %}"></link>
|
||||
</head>
|
||||
<body>
|
||||
<div id="header" class="row">
|
||||
<div class="logout-button">
|
||||
<form action="/logout" method="post"> {% csrf_token %}
|
||||
<input type="Submit" value="{% trans "Log out" %}" name="Logout" class="btn btn-danger"/>
|
||||
</form>
|
||||
</div>
|
||||
{% block appname %}infoAdmin{% endblock appname %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Infoscreen admin" %}
|
||||
{% endblock title %}
|
||||
|
||||
{% block styles %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static "infoscreen/css/admin.css" %}"></link>
|
||||
{% endblock styles %}
|
||||
|
||||
{% block controllers %}
|
||||
<script src="{% static "infoscreen/js/infoadmin_controllers.js" %}"></script>
|
||||
{% endblock controllers %}
|
||||
|
||||
{% block body %}
|
||||
<div id="header" class="row">
|
||||
<div class="logout-button">
|
||||
<form action="/admin/logout/" method="post"> {% csrf_token %}
|
||||
<input type="Submit" value="{% trans "Log out" %}" name="Logout" class="btn btn-danger"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" ng-controller="infoadmin_ctrl">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h1>{% trans "Infoscreen Admin Pane" %}</h1>
|
||||
</div>
|
||||
<div class="container" ng-controller="infoadmin_ctrl">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>{% trans "Infoscreen Admin Pane" %}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li class="active"><a data-toggle="tab" href="#slides" role="tab">{% trans "Manage Slides" %}</a></li>
|
||||
<li class="dropdown">
|
||||
<a data-toggle="dropdown" href="#">{% trans "Manage Rotations" %}<span class="caret"></span></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="r in rotations"><a href="#rotations" ng-click="selectRotation(r.id)" data-toggle="tab">{$ r.name $}</a></li>
|
||||
<li class="divider">
|
||||
<li class="nav-item"><a data-toggle="tab" href="#deleterot" role="tab">{% trans "Create/Delete" %}</a></li>
|
||||
</ul>
|
||||
</ul>
|
||||
<div class="tab-content row">
|
||||
<div id="slides" class="tab-pane active">
|
||||
<div ng-controller="infoadmin_ctrl">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<h2>{% trans "Create new item" %}</h2>
|
||||
<div>{% trans "Create a new item by type" %}</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<td>{% trans "Item type" %}</td>
|
||||
<td>
|
||||
<select class="form-control form-control-sm" ng-model="createtype", ng-options="t.name for t in infotypes | orderBy: 'name'">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div ng-include="createtype.create_template_url"></div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<h2>{% trans "Info items" %}</h2>
|
||||
<div>{% trans "Infoitems available for rotations" %}</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Item" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="i in infoitems | orderBy:['display_name','name']">
|
||||
<td>{$ i.name $}</td>
|
||||
<td>{$ i.display_name $}</td>
|
||||
<td><input type="button" class="btn btn-danger" ng-click="deleteItem(i.item_type, i.id);" value="{% trans "Delete" %}"></input></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include "infoscreen:nav.html" %}
|
||||
<div class="tab-content" id="tabContent">
|
||||
{% include "infoscreen:tabs/slides.html" %}
|
||||
{% include "infoscreen:tabs/rotations.html" %}
|
||||
{% include "infoscreen:tabs/add_remove.html" %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rotations" class="tab-pane" ng-controller="infoadmin_ctrl">
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<h2>{% trans "Info items" %}</h2>
|
||||
<div>{% trans "Infoitems available for rotations" %}</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Item" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Set duration" %}</th>
|
||||
<th>{% trans "Add to rotation" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="i in infoitems | orderBy:['display_name','name']">
|
||||
<td>{$ i.name $}</td>
|
||||
<td>{$ i.display_name $}</td>
|
||||
<td><input type="number" min="1" max="60" class="form-control" ng-model="i.duration"></input></td>
|
||||
<td><input type="button" class="btn btn-success" ng-click="createInstance(selected_rot.id, i.id, i.item_type, i.duration);" value="{% trans "Add" %}"></input></td>
|
||||
<td><input type="button" class="btn btn-danger" ng-click="deleteItem(i.item_type, i.id);" value="{% trans "Delete" %}"></input></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-md-6">
|
||||
<h2>{% trans "Rotation" %}: {$ selected_rot.name $}<a href="/infoscreen/{$ selected_rot.id $}"><input type="button" class="btn btn-primary pull-right" value="{% trans "Preview" %}"></a></h2>
|
||||
<div>{% trans "Instances in currently selected rotation" %}:</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Instance" %}</th>
|
||||
<th>{% trans "Duration" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="i in selected_rot.instances">
|
||||
<td>{$ i.item.name $}</td><td>{$ i.duration $} s</td>
|
||||
<td><input type="button" ng-click="deleteInstance(i.id);" value="{% trans "Delete" %}" class="btn btn-danger"></input></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div id="deleterot" class="tab-pane">
|
||||
<div class="col-xs-12 " ng-controller="infoadmin_ctrl">
|
||||
<h2>{% trans "Rotations" %}</h2>
|
||||
<div>
|
||||
{% trans "Select rotation to edit" %}:
|
||||
</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Rotation" %}</th>
|
||||
<th>{% trans "id" %}</th>
|
||||
<th>{% trans "Preview" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="r in rotations">
|
||||
<td>{$ r.name $}</td>
|
||||
<td>{$ r.id $}</td>
|
||||
<td><a href="/infoscreen/{$ r.id $}"><input type="button" class="btn btn-primary" value="{% trans "Preview" %}"></a></td>
|
||||
<td><input type="button" class="btn btn-danger" ng-click="deleteRotation(r.id)" value="{% trans "Delete" %}"></input></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input type="text" class="form-control" ng-model="r.name" placeholder="{% trans "Name" %}"></input></td>
|
||||
<td><input type="button" class="btn btn-success" ng-click="createRotation(r.name)" value="{% trans "Create new" %}"></input></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 100px;">
|
||||
{% include "footer.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% include "webapp:footer.html" %}
|
||||
{% endblock body %}
|
||||
@@ -1,29 +1,25 @@
|
||||
{% extends "infoscreen:base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html ng-app="infoApp">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{% block appname %}infoApp{% endblock appname %}
|
||||
|
||||
<title>Infoscreen</title>
|
||||
<script src="{% static "js/lib/moment.js" %}"></script>
|
||||
<script src="{% static "js/lib/angular.js" %}"></script>
|
||||
<script src="{% static "js/lib/angular-route.js" %}"></script>
|
||||
<script src="{% static "js/lib/angular-animate.js" %}"></script>
|
||||
<script src="{% static "js/lib/jquery-3.1.0.min.js" %}"></script>
|
||||
<script src="{% static "js/lib/bootstrap.min.js" %}"></script>
|
||||
<script src="{% static "js/lib/underscore-min.js" %}"></script>
|
||||
<script src="{% static "js/infoscreen_controllers.js" %}"></script>
|
||||
{% block title %}
|
||||
{% trans "Infoscreen" %}
|
||||
{% endblock title %}
|
||||
|
||||
<link rel="stylesheet" href="{% static "css/lib/bootstrap.min.css" %}"></link>
|
||||
<link rel="stylesheet" href="{% static "css/infoscreen.css" %}"></link>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container ng-scope" ng-controller="infoscreen_main" ng-init="init({{ rotation }})">
|
||||
<div ng-animate-swap="index" class="cell swap-animation">
|
||||
<div id="infocontent" ng-include="active.template" onload="active.onload()"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{% block controllers %}
|
||||
<script src="{% static "infoscreen/js/infoscreen_controllers.js" %}"></script>
|
||||
{% endblock controllers %}
|
||||
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" href="{% static "infoscreen/css/infoscreen.css" %}"></link>
|
||||
{% endblock styles %}
|
||||
|
||||
{% block body %}
|
||||
<div class="container ng-scope" ng-controller="infoscreen_main" ng-init="init({{ rotation }})">
|
||||
<div ng-animate-swap="index" class="cell swap-animation">
|
||||
<div id="infocontent" ng-include="active.template" onload="active.onload()"></div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock body %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% load i18n %}
|
||||
|
||||
<ul class="nav nav-tabs" id="tabs" role="tablist">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" data-toggle="tab" role="tab" href="#slides">{% trans "Manage Slides" %}</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" role="button" data-toggle="dropdown" href="#" aria-haspopup="true" aria-expanded="false">
|
||||
{% trans "Manage Rotations" %}<span class="caret"></span>
|
||||
</a>
|
||||
<div class="dropdown-menu">
|
||||
<a class="dropdown-item" role="tab" href="#rotations" ng-repeat="r in rotations" ng-click="selectRotation(r.id)" data-toggle="tab">{$ r.name $}</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<a class="dropdown-item" data-toggle="tab" href="#deleterot" role="tab">{% trans "Create/Delete" %}</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -0,0 +1,30 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div id="deleterot" class="tab-pane fade" role="tabpanel">
|
||||
<div class="col" ng-controller="infoadmin_ctrl">
|
||||
<h2>{% trans "Rotations" %}</h2>
|
||||
<div>
|
||||
{% trans "Select rotation to edit" %}:
|
||||
</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Rotation" %}</th>
|
||||
<th>{% trans "id" %}</th>
|
||||
<th>{% trans "Preview" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="r in rotations">
|
||||
<td>{$ r.name $}</td>
|
||||
<td>{$ r.id $}</td>
|
||||
<td><a class="btn btn-primary" href="/infoscreen/{$ r.id $}">{% trans "Preview" %}</a></td>
|
||||
<td><a class="btn btn-danger" href="#" ng-click="deleteRotation(r.id)">{% trans "Delete" %}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><input type="text" class="form-control" ng-model="r.name" placeholder="{% trans "Name" %}"></input></td>
|
||||
<td><input type="button" class="btn btn-success" ng-click="createRotation(r.name)" value="{% trans "Create new" %}"></input></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,44 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div id="rotations" class="tab-pane fade" role="tabpanel" ng-controller="infoadmin_ctrl">
|
||||
<div class="col">
|
||||
<h2>{% trans "Info items" %}</h2>
|
||||
<div>{% trans "Infoitems available for rotations" %}</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Item" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Set duration" %}</th>
|
||||
<th>{% trans "Add to rotation" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="i in infoitems | orderBy:['display_name','name']">
|
||||
<td>{$ i.name $}</td>
|
||||
<td>{$ i.display_name $}</td>
|
||||
<td><input type="number" min="1" max="60" class="form-control" ng-model="i.duration"></input></td>
|
||||
<td><input type="button" class="btn btn-success" ng-click="createInstance(selected_rot.id, i.id, i.item_type, i.duration);" value="{% trans "Add" %}"></input></td>
|
||||
<td><input type="button" class="btn btn-danger" ng-click="deleteItem(i.item_type, i.id);" value="{% trans "Delete" %}"></input></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="rotation-title-row">
|
||||
<h2>{% trans "Rotation" %}: {$ selected_rot.name $}</h2>
|
||||
<a class="btn btn-primary" href="/infoscreen/{$ selected_rot.id $}">{% trans "Preview" %}</a>
|
||||
</div>
|
||||
<div>{% trans "Instances in currently selected rotation" %}:</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Instance" %}</th>
|
||||
<th>{% trans "Duration" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="i in selected_rot.instances">
|
||||
<td>{$ i.item.name $}</td><td>{$ i.duration $} s</td>
|
||||
<td><input type="button" ng-click="deleteInstance(i.id);" value="{% trans "Delete" %}" class="btn btn-danger"></input></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
{% load i18n %}
|
||||
|
||||
<div id="slides" class="tab-pane fade show active" role="tabpanel">
|
||||
<div ng-controller="infoadmin_ctrl">
|
||||
<div class="col">
|
||||
<h2>{% trans "Create new item" %}</h2>
|
||||
<div>{% trans "Create a new item by type" %}</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<td>{% trans "Item type" %}</td>
|
||||
<td>
|
||||
<select class="form-control form-control-sm" ng-model="createtype", ng-options="t.name for t in infotypes | orderBy: 'name'">
|
||||
<option value=""></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div ng-include="createtype.create_template_url"></div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<h2>{% trans "Info items" %}</h2>
|
||||
<div>{% trans "Infoitems available for rotations" %}</div>
|
||||
<table class="table table-striped">
|
||||
<tr>
|
||||
<th>{% trans "Item" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th>{% trans "Delete" %}</th>
|
||||
</tr>
|
||||
<tr ng-repeat="i in infoitems | orderBy:['display_name','name']">
|
||||
<td>{$ i.name $}</td>
|
||||
<td>{$ i.display_name $}</td>
|
||||
<td><input type="button" class="btn btn-danger" ng-click="deleteItem(i.item_type, i.id);" value="{% trans "Delete" %}"></input></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,6 +1,7 @@
|
||||
"""File containing infoscreen urls."""
|
||||
|
||||
from django.conf.urls import url
|
||||
from django.conf import settings
|
||||
|
||||
from infoscreen.views import index
|
||||
from infoscreen.views import admin
|
||||
@@ -17,14 +18,12 @@ from infoscreen.views import create_image_item
|
||||
from infoscreen.views import create_video_item
|
||||
from infoscreen.views import createABBItem
|
||||
from infoscreen.views import createSossoItem
|
||||
from infoscreen.views import createHslItem
|
||||
from infoscreen.views import createLunchItem
|
||||
from infoscreen.views import createEventItem
|
||||
from infoscreen.views import createExternalWebsiteItem
|
||||
from infoscreen.views import create_rotation
|
||||
from infoscreen.views import delete_rotation
|
||||
from infoscreen.views import CurrentHSLView
|
||||
from infoscreen.views import createApyItem
|
||||
from infoscreen.views import hsl_timetable_settings
|
||||
from infoscreen.views import get_apy_json
|
||||
|
||||
urlpatterns = [
|
||||
@@ -43,14 +42,15 @@ urlpatterns = [
|
||||
url(r'^create_video$', create_video_item),
|
||||
url(r'^create_abbitem$', createABBItem),
|
||||
url(r'^create_sossoitem$', createSossoItem),
|
||||
url(r'^create_lunchitem$', createLunchItem),
|
||||
url(r'^create_eventitem$', createEventItem),
|
||||
url(r'^create_hslitem$', createHslItem),
|
||||
url(r'^create_apyitem$', createApyItem),
|
||||
url(r'^create_websiteitem$', createExternalWebsiteItem),
|
||||
url(r'^create_rotation$', create_rotation),
|
||||
url(r'^delete_rotation/(?P<id>\d+)$', delete_rotation),
|
||||
url(r'^hsl_data$', CurrentHSLView),
|
||||
url(r'^hsl_data/settings$', hsl_timetable_settings),
|
||||
url(r'^apyjson', get_apy_json),
|
||||
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
||||
@@ -6,6 +6,7 @@ from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.auth.decorators import permission_required, login_required
|
||||
from django.db import DatabaseError
|
||||
from infoscreen.models import UploadFileForm
|
||||
|
||||
import sikweb.settings as settings
|
||||
@@ -14,21 +15,17 @@ import logging
|
||||
import threading
|
||||
import requests
|
||||
|
||||
from infoscreen.models import Rotation, InfoItem, InfoInstance
|
||||
from infoscreen.models import (ABBInfoItem, ExternalImageInfoItem,
|
||||
ImageInfoItem, SossoInfoItem, HslInfoItem)
|
||||
from infoscreen.models import EventInfoItem
|
||||
from infoscreen.models import ExternalWebsiteInfoItem
|
||||
from infoscreen.models import ImageUploadForm
|
||||
from infoscreen.models import ApyInfoItem
|
||||
from infoscreen.models import VideoInfoItem
|
||||
from infoscreen.models import (
|
||||
Rotation, InfoItem, InfoInstance, ABBInfoItem, ExternalImageInfoItem,
|
||||
ImageInfoItem, SossoInfoItem, LunchItem, 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)
|
||||
|
||||
@@ -182,7 +179,7 @@ createInstance = create_item_generator(InfoInstance)
|
||||
deleteInstance = delete_item_generator(InfoInstance)
|
||||
createABBItem = create_item_generator(ABBInfoItem)
|
||||
createSossoItem = create_item_generator(SossoInfoItem)
|
||||
createHslItem = create_item_generator(HslInfoItem)
|
||||
createLunchItem = create_item_generator(LunchItem)
|
||||
createExternalImageInfoItem = create_item_generator(ExternalImageInfoItem)
|
||||
createExternalWebsiteItem = create_item_generator(ExternalWebsiteInfoItem)
|
||||
createEventItem = create_item_generator(EventInfoItem)
|
||||
|
||||
@@ -2,13 +2,14 @@ from django.shortcuts import render
|
||||
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.conf import settings
|
||||
from django.db import DatabaseError
|
||||
|
||||
from infoscreen.models import Rotation, InfoItem, InfoInstance
|
||||
from infoscreen.hsl_fetcher import fetch as hsl_fetch
|
||||
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import requests
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@@ -22,7 +23,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)
|
||||
|
||||
@@ -77,25 +78,3 @@ def info_items(request, *args, **kwargs):
|
||||
items.append(i.get_dict())
|
||||
|
||||
return JsonResponse(items, safe=False)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def hsl_timetable_settings(request, *args, **kwargs):
|
||||
"""Set HSL timetable settings."""
|
||||
d = {"departure_threshold": settings.HSL_DEPARTURE_THRESHOLD,
|
||||
"hurry_threshold": settings.HSL_HURRY_THRESHOLD}
|
||||
|
||||
return JsonResponse(d, status=200)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
def CurrentHSLView(request, *args, **kwargs):
|
||||
"""Get HSL data and return it."""
|
||||
try:
|
||||
api_resp = hsl_fetch()
|
||||
except Exception as ex:
|
||||
logging.exception('Failed to fetch HSL timetables.')
|
||||
error = {'error': 'Aikataulujen haku epäonnistui.'}
|
||||
return JsonResponse(error, status=200)
|
||||
|
||||
return JsonResponse(api_resp, status=200, safe=False)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
from django.contrib import admin
|
||||
from modeltranslation.admin import TranslationAdmin
|
||||
|
||||
from kaehmy.models import Application, Comment, CustomRole, PresetRole, TelegramChannel
|
||||
|
||||
admin.site.register(Application)
|
||||
admin.site.register(Comment)
|
||||
admin.site.register(CustomRole)
|
||||
admin.site.register(PresetRole, TranslationAdmin)
|
||||
admin.site.register(TelegramChannel)
|
||||
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class KaehmyConfig(AppConfig):
|
||||
name = 'kaehmy'
|
||||
@@ -1,54 +1,51 @@
|
||||
"""File containing webapp forms."""
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from webapp.models import CustomKaehmyRole, PresetKaehmyRole, BaseRole
|
||||
from webapp.models import OhlhafvChallenge, KaehmyForm, KaehmyMessage
|
||||
from kaehmy.models import PresetRole, CustomRole, Application, Comment, KaehmyBaseRole
|
||||
|
||||
|
||||
class KaehmyCheckboxSelectMultiple(forms.widgets.CheckboxSelectMultiple):
|
||||
option_template_name = 'kaehmy_checkbox_option.html'
|
||||
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(KaehmyCheckboxSelectMultiple, self).create_option(name, value, label, selected, index, subindex, attrs)
|
||||
description = PresetKaehmyRole.objects.get(id=value).description
|
||||
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(KaehmyCheckboxSelectMultiple, self).__init__(*args, **kwargs)
|
||||
super(CheckboxSelectMultiple, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class KaehmyForm_Form(forms.ModelForm):
|
||||
class ApplicationForm(forms.ModelForm):
|
||||
"""Class representing Kaehmy form."""
|
||||
|
||||
class Meta:
|
||||
"""Meta for class KaehmyForm."""
|
||||
"""Meta for class Application."""
|
||||
|
||||
model = KaehmyForm
|
||||
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(KaehmyForm_Form, self).__init__(*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 = CustomKaehmyRole.objects.all().exists()
|
||||
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 = CustomKaehmyRole.objects.all()
|
||||
self.fields["custom_roles"].queryset = CustomRole.objects.all()
|
||||
|
||||
for cat_id, category in BaseRole.CATEGORIES:
|
||||
for cat_id, category in KaehmyBaseRole.CATEGORIES:
|
||||
key = 'preset_roles_{}'.format(cat_id)
|
||||
qset = PresetKaehmyRole.objects.filter(category=cat_id).order_by('category', '-is_board')
|
||||
qset = PresetRole.objects.filter(category=cat_id).order_by('category', '-is_board')
|
||||
self.fields[key] = forms.ModelMultipleChoiceField(qset)
|
||||
self.fields[key].widget = KaehmyCheckboxSelectMultiple(attrs={
|
||||
self.fields[key].widget = CheckboxSelectMultiple(attrs={
|
||||
'title': _('Preset roles'),
|
||||
'name': 'preset_roles',
|
||||
})
|
||||
@@ -58,13 +55,11 @@ class KaehmyForm_Form(forms.ModelForm):
|
||||
self.fields[key].required = False
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(KaehmyForm_Form, self).clean()
|
||||
print(cleaned_data)
|
||||
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]
|
||||
|
||||
print(cleaned_data)
|
||||
return cleaned_data
|
||||
|
||||
def clean_phone_number(self):
|
||||
@@ -78,7 +73,7 @@ class KaehmyForm_Form(forms.ModelForm):
|
||||
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 CustomKaehmyRole.objects.filter(name=custom_name).exists():
|
||||
if not CustomRole.objects.filter(name=custom_name).exists():
|
||||
return custom_name
|
||||
else:
|
||||
raise ValidationError(_('Custom role with the same name already exists.'))
|
||||
@@ -87,19 +82,8 @@ class KaehmyForm_Form(forms.ModelForm):
|
||||
return [self.fields[k] for k in self.fields.keys() if k not in ["preset_roles", "custom_roles"]]
|
||||
|
||||
|
||||
class KaehmyCommentForm(forms.ModelForm):
|
||||
class CommentForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = KaehmyMessage
|
||||
model = Comment
|
||||
fields = ['name', 'email', 'message', 'parent']
|
||||
|
||||
|
||||
class OhlhafvForm(forms.ModelForm):
|
||||
"""Class representing Ohlhafv form."""
|
||||
|
||||
class Meta:
|
||||
"""Meta class for Ohlhafv form."""
|
||||
|
||||
model = OhlhafvChallenge
|
||||
fields = ['challenger', 'challenger_email',
|
||||
'victim', 'victim_email', 'series', 'message']
|
||||
@@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2018-01-25 22:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('webapp', '0037_auto_20180125_2131'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CommentParent',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(default='', max_length=255, verbose_name='Name')),
|
||||
('email', models.EmailField(default='', max_length=254, verbose_name='Email')),
|
||||
('timestamp', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Timestamp')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomRole',
|
||||
fields=[
|
||||
('baserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='webapp.BaseRole')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Custom kaehmy roles',
|
||||
'verbose_name': 'Custom kaehmy role',
|
||||
},
|
||||
bases=('webapp.baserole',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PresetRole',
|
||||
fields=[
|
||||
('presetrole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='webapp.PresetRole')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Preset kaehmy roles',
|
||||
'verbose_name': 'Preset kaehmy role',
|
||||
},
|
||||
bases=('webapp.presetrole',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TelegramChannel',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255)),
|
||||
('channel_id', models.CharField(max_length=255, unique=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Telegram channels',
|
||||
'verbose_name': 'Telegram channel',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
fields=[
|
||||
('commentparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.CommentParent')),
|
||||
('phone_number', models.CharField(default='', max_length=10, verbose_name='Phone number')),
|
||||
('year', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, 'N')], verbose_name='Year')),
|
||||
('text', models.TextField(default='', max_length=300, verbose_name='Text')),
|
||||
('custom_role_name', models.CharField(blank=True, max_length=255, verbose_name='Custom role name')),
|
||||
('custom_role_is_board', models.BooleanField(verbose_name='Board member')),
|
||||
('custom_roles', models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.CustomRole')),
|
||||
('preset_roles', models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.PresetRole')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Kaehmylomakkeet',
|
||||
'verbose_name': 'Kaehmylomake',
|
||||
},
|
||||
bases=('kaehmy.commentparent',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Comment',
|
||||
fields=[
|
||||
('commentparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.CommentParent')),
|
||||
('message', models.TextField(verbose_name='Message')),
|
||||
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='kaehmy.CommentParent')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Kaehmykommentit',
|
||||
'verbose_name': 'Kaehmykommentti',
|
||||
},
|
||||
bases=('kaehmy.commentparent',),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 2.0.7 on 2018-09-02 16:29
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('webapp', '0047_auto_20180710_2110'),
|
||||
('kaehmy', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='KaehmyBaseRole',
|
||||
fields=[
|
||||
('baserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='webapp.BaseRole')),
|
||||
('category', models.CharField(choices=[('corporate', 'Corporate affairs'), ('freshman', 'Freshmen'), ('international', 'International'), ('external', 'External affairs'), ('media', 'Media'), ('tech', 'Technology'), ('wellbeing', 'Wellbeing'), ('elepaja', 'Elepaja'), ('ceremonies', 'Ceremonies'), ('culture', 'Culture'), ('studies', 'Studies'), ('sosso', 'Sössö magazine'), ('alumni', 'Alumni relations'), ('others', 'Others')], default='others', max_length=255, verbose_name='Category')),
|
||||
],
|
||||
bases=('webapp.baserole',),
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Application',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='customrole',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='presetrole',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,63 @@
|
||||
# Generated by Django 2.0.7 on 2018-09-02 16:43
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('kaehmy', '0002_auto_20180902_1929'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Application',
|
||||
fields=[
|
||||
('commentparent_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.CommentParent')),
|
||||
('phone_number', models.CharField(default='', max_length=10, verbose_name='Phone number')),
|
||||
('year', models.IntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, 'N')], verbose_name='Year')),
|
||||
('text', models.TextField(default='', max_length=300, verbose_name='Text')),
|
||||
('custom_role_name', models.CharField(blank=True, max_length=255, verbose_name='Custom role name')),
|
||||
('custom_role_is_board', models.BooleanField(verbose_name='Board member')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Kaehmylomake',
|
||||
'verbose_name_plural': 'Kaehmylomakkeet',
|
||||
},
|
||||
bases=('kaehmy.commentparent',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CustomRole',
|
||||
fields=[
|
||||
('kaehmybaserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.KaehmyBaseRole')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Custom kaehmy role',
|
||||
'verbose_name_plural': 'Custom kaehmy roles',
|
||||
},
|
||||
bases=('kaehmy.kaehmybaserole',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PresetRole',
|
||||
fields=[
|
||||
('kaehmybaserole_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='kaehmy.KaehmyBaseRole')),
|
||||
('description', models.TextField(verbose_name='Description')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Preset kaehmy role',
|
||||
'verbose_name_plural': 'Preset kaehmy roles',
|
||||
},
|
||||
bases=('kaehmy.kaehmybaserole',),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='custom_roles',
|
||||
field=models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.CustomRole'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='application',
|
||||
name='preset_roles',
|
||||
field=models.ManyToManyField(blank=True, related_name='forms', to='kaehmy.PresetRole'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.0.7 on 2018-10-18 18:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('kaehmy', '0003_auto_20180902_1943'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='kaehmybaserole',
|
||||
name='category',
|
||||
field=models.CharField(choices=[('corporate', 'Corporate affairs'), ('freshman', 'Freshmen'), ('international', 'International'), ('external', 'External affairs'), ('media', 'Media'), ('tech', 'Technology'), ('wellbeing', 'Wellbeing'), ('elepaja', 'Elepaja'), ('ceremonies', 'Ceremonies'), ('studies', 'Studies'), ('sosso', 'Sössö magazine'), ('alumni', 'Alumni relations'), ('others', 'Others')], default='others', max_length=255, verbose_name='Category'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.1.5 on 2019-03-12 12:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('kaehmy', '0004_auto_20181018_2121'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='application',
|
||||
name='custom_role_is_board',
|
||||
field=models.BooleanField(blank=True, verbose_name='Board member'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,170 @@
|
||||
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
|
||||
from webapp.models import BaseRole
|
||||
import logging
|
||||
|
||||
|
||||
# TODO: Move BaseRole to Kaehmt App; will fuck up the DB since table is removed, if no data migration is done before-hand.
|
||||
# Either reconstruct all kaehmy roles from scratch then, or do these migrations:
|
||||
# 1. Create table here
|
||||
# 2. Data migrate from webapp BaseRole to new kaehmy BaseRole
|
||||
# 3. Delete webapp BaseRole table
|
||||
|
||||
VERBOSE_NAME = _('Kaehmy')
|
||||
|
||||
|
||||
class KaehmyBaseRole(BaseRole):
|
||||
"""ABC"""
|
||||
|
||||
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)
|
||||