2 Commits

Author SHA1 Message Date
Lavikainen Joel 5e6b4153e6 Add data endpoint and create infoscreen view for coffee stats 2017-11-19 17:40:04 +02:00
Lavikainen Joel 66e86ab621 Save brewing data as db model 2017-11-19 14:19:37 +02:00
443 changed files with 7263 additions and 20466 deletions
-4
View File
@@ -1,7 +1,3 @@
[report]
show_missing = True
[run]
omit =
*/migrations/*
*/admin.py
*/translation.py
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
echo "Deploying to development."
set -e
set -x
pushd deployment
docker-compose down
docker pull "$1"
docker-compose up -d
popd
set +x
set +e
-32
View File
@@ -1,32 +0,0 @@
.DS_Store
.dockerignore
.git/
.husky/
.venv/
.vscode/
collected_static/
logs/
!logs/README
media/
!media/REMOVE_ME
misc/
node_modules/
scripts/
.coverage
.coveragerc
.env*
.eslintignore
.eslintrc.json
.gitignore
.gitlab-ci.yml
.python-version
docker-compose.yml
!manage.py
package*.json
!poetry.lock
!production_entrypoint.sh
pycodestyle.cfg
!pyproject.toml
pyright.json
README.md
stack-compose*.yml
-13
View File
@@ -1,13 +0,0 @@
DEPLOY_ENV=local
SENTRY_DSN=
HOST=localhost
DEBUG=True
SECRET_KEY=7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(
DB_NAME=postgres
DB_USER=postgres
DB_PASSWD=postgres
DB_HOST=localhost
DB_PORT=5432
EMAIL_API_KEY=
GROUP_KEY=
GOOGLE_CREDS='{}'
-13
View File
@@ -1,13 +0,0 @@
DEPLOY_ENV=local
#SENTRY_DSN=
HOST=localhost
DEBUG=True
SECRET_KEY=7p$85^4ibb^p4-=vs44b7!y0e-zemugze18@a#30&71=a8)dp(
DB_NAME=postgres
DB_USER=postgres
DB_PASSWD=postgres
DB_HOST=db
DB_PORT=5432
EMAIL_API_KEY=
GROUP_KEY=
GOOGLE_CREDS='{}'
+5
View File
@@ -0,0 +1,5 @@
members/static/js/lib
infoscreen/static/js/lib
webapp/static/js/lib
static/js/lib
collected_static
+252
View File
@@ -0,0 +1,252 @@
{
"env": {
"browser": true,
"jquery": true
},
"globals": {
"angular": 1,
"noty": 1,
"app": 1,
"_": 1,
"moment": 1
},
"extends": "eslint:recommended",
"rules": {
"no-unused-vars": "warn",
"accessor-pairs": "error",
"array-bracket-spacing": "off",
"array-callback-return": "error",
"arrow-body-style": "error",
"arrow-parens": "error",
"arrow-spacing": "error",
"block-scoped-var": "off",
"block-spacing": "off",
"brace-style": "off",
"callback-return": "off",
"camelcase": "off",
"capitalized-comments": "off",
"class-methods-use-this": "error",
"comma-dangle": "off",
"comma-spacing": "off",
"comma-style": "off",
"complexity": "off",
"computed-property-spacing": "off",
"consistent-return": "off",
"consistent-this": "off",
"curly": "off",
"default-case": "off",
"dot-location": [
"error",
"property"
],
"dot-notation": "off",
"eol-last": "off",
"eqeqeq": "off",
"func-call-spacing": "error",
"func-name-matching": "error",
"func-names": "off",
"func-style": "off",
"generator-star-spacing": "error",
"global-require": "off",
"guard-for-in": "off",
"handle-callback-err": "off",
"id-blacklist": "error",
"id-length": "off",
"id-match": "error",
"indent": "off",
"init-declarations": "off",
"jsx-quotes": "error",
"key-spacing": "off",
"keyword-spacing": "off",
"line-comment-position": "off",
"linebreak-style": "off",
"lines-around-comment": "off",
"lines-around-directive": "off",
"max-depth": "off",
"max-len": "off",
"max-lines": "off",
"max-nested-callbacks": "error",
"max-params": "off",
"max-statements": "off",
"max-statements-per-line": "off",
"multiline-ternary": "off",
"new-parens": "off",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"no-alert": "error",
"no-array-constructor": "off",
"no-await-in-loop": "error",
"no-bitwise": "off",
"no-caller": "error",
"no-catch-shadow": "off",
"no-confusing-arrow": "error",
"no-constant-condition": [
"error",
{
"checkLoops": false
}
],
"no-continue": "off",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-else-return": "off",
"no-empty-function": "off",
"no-eq-null": "off",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-label": "error",
"no-extra-parens": "off",
"no-floating-decimal": "off",
"no-implicit-coercion": [
"error",
{
"boolean": false,
"number": false,
"string": false
}
],
"no-implicit-globals": "off",
"no-implied-eval": "error",
"no-inline-comments": "off",
"no-inner-declarations": [
"error",
"functions"
],
"no-invalid-this": "off",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-lonely-if": "off",
"no-loop-func": "error",
"no-magic-numbers": "off",
"no-mixed-operators": "off",
"no-mixed-requires": "error",
"no-multi-assign": "off",
"no-multi-spaces": "off",
"no-multi-str": "error",
"no-multiple-empty-lines": "off",
"no-native-reassign": "off",
"no-negated-condition": "off",
"no-negated-in-lhs": "error",
"no-nested-ternary": "off",
"no-new": "error",
"no-new-func": "off",
"no-new-object": "error",
"no-new-require": "error",
"no-new-wrappers": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-path-concat": "error",
"no-plusplus": "off",
"no-process-env": "error",
"no-process-exit": "error",
"no-proto": "error",
"no-prototype-builtins": "off",
"no-restricted-globals": "error",
"no-restricted-imports": "error",
"no-restricted-modules": "error",
"no-restricted-properties": "error",
"no-restricted-syntax": "error",
"no-return-assign": "off",
"no-return-await": "error",
"no-script-url": "error",
"no-self-compare": "error",
"no-sequences": "off",
"no-shadow": "off",
"no-shadow-restricted-names": "error",
"no-spaced-func": "error",
"no-sync": "error",
"no-tabs": "off",
"no-template-curly-in-string": "error",
"no-ternary": "off",
"no-throw-literal": "off",
"no-trailing-spaces": "off",
"no-undef-init": "error",
"no-undefined": "off",
"no-underscore-dangle": "off",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": [
"error",
{
"defaultAssignment": true
}
],
"no-unused-expressions": "off",
"no-use-before-define": "off",
"no-useless-call": "off",
"no-useless-computed-key": "error",
"no-useless-concat": "error",
"no-useless-constructor": "error",
"no-useless-escape": "off",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-var": "off",
"no-void": "off",
"no-warning-comments": "off",
"no-whitespace-before-property": "error",
"no-with": "error",
"object-curly-newline": "off",
"object-curly-spacing": "off",
"object-property-newline": "off",
"object-shorthand": "off",
"one-var": "off",
"one-var-declaration-per-line": "off",
"operator-assignment": "off",
"operator-linebreak": "off",
"padded-blocks": "off",
"prefer-arrow-callback": "off",
"prefer-const": "error",
"prefer-destructuring": [
"error",
{
"array": false,
"object": false
}
],
"prefer-numeric-literals": "error",
"prefer-promise-reject-errors": "error",
"prefer-reflect": "off",
"prefer-rest-params": "off",
"prefer-spread": "off",
"prefer-template": "off",
"quote-props": "off",
"quotes": "off",
"radix": "off",
"require-await": "error",
"require-jsdoc": "off",
"rest-spread-spacing": "error",
"semi": "off",
"semi-spacing": "off",
"sort-imports": "error",
"sort-keys": "off",
"sort-vars": "off",
"space-before-blocks": "off",
"space-before-function-paren": "off",
"space-in-parens": "off",
"space-infix-ops": "off",
"space-unary-ops": [
"error",
{
"nonwords": false,
"words": false
}
],
"spaced-comment": "off",
"strict": "off",
"symbol-description": "error",
"template-curly-spacing": "error",
"unicode-bom": [
"error",
"never"
],
"valid-jsdoc": "off",
"vars-on-top": "off",
"wrap-iife": "off",
"wrap-regex": "off",
"yield-star-spacing": "error",
"yoda": "off"
}
}
+17 -11
View File
@@ -1,14 +1,20 @@
.DS_Store
.env
*.swp
sikweb/settings.py
*~
*.pyc
/collected_static/
/media/
logs/
*.sqlite3
uwsgi.ini
uwsgi.log
infoscreen/static/js/hsl.json
members/logs/*
node_modules/
.coverage
.vscode/
.idea/
*.code-workspace
venv/
.venv/
logs/
/media/
node_modules/
/.coverage
db.sqlite3
requirements_henu.txt
/collected_static/
mydatabase
settings.json
.vscode/
+91 -179
View File
@@ -1,189 +1,101 @@
stages:
- setup
- audit
- lint
- test
- publish
- deploy
- cleanup
install:
image: node:22
stage: setup
only:
- pushes
script:
- npm ci
artifacts:
paths:
- node_modules
expire_in: 1 week
audit:
image: python:3.12.9
stage: audit
allow_failure: true
only:
- pushes
needs: []
before_script:
- pip install poetry==2.0.1
- poetry config virtualenvs.create false
- poetry install --no-interaction --no-ansi
script:
- safety check
- test
- lint
- publish
- deploy
test:
image: python:3.12.9
stage: test
only:
- pushes
needs: []
services:
- postgres:12
variables:
POSTGRES_DB: ci
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
DB_HOST: postgres
before_script:
- pip install poetry==2.0.1
- poetry config virtualenvs.create false
- poetry install --no-interaction --no-ansi
script:
- python manage.py migrate --noinput
- python manage.py createdefaultadmin
- python manage.py test
image: python:3.5
stage: test
services:
- postgres:latest
variables:
POSTGRES_DB: ci
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/$POSTGRES_DB"
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
lint:py:
image: python:3.12.9
stage: lint
only:
- pushes
needs: []
script:
- pip install black==22.3.0
- black --check .
pycodestyle:
image: python:3.5
stage: lint
script:
- pip install pycodestyle
- pycodestyle --config=setup.cfg --count .
lint:js:
image: node:22
stage: lint
only:
- pushes
needs: ["install"]
script:
- npm run lint:js
eslint:
image: node:7.10.0
stage: lint
before_script:
- npm install
script:
- npm run eslint
lint:md:
image: node:22
stage: lint
only:
- pushes
needs: ["install"]
script:
- npm run lint:md
remark:
image: node:7.10.0
stage: lint
before_script:
- npm install
script:
- npm run remark
publish:
image: docker:25-cli
stage: publish
needs: ["test", "lint:py", "lint:js", "lint:md"]
services:
- docker:25-dind
only:
- develop
- 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"
stage: publish
image: docker:latest
only:
- develop
before_script:
- docker info
- docker login -u gitlab-ci-token -p $CI_BUILD_TOKEN $REGISTRY_URL
script:
- docker build . -t "$IMAGE_NAME"
- docker push "$IMAGE_NAME"
deploy:dev:
image: docker:25-cli
stage: deploy
only:
- develop
environment:
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_JOB_TOKEN $CI_REGISTRY
script:
- docker stack deploy --with-registry-auth -c stack-compose-dev.yml "$SERVICE_NAME"
after_script:
- docker logout "$CI_REGISTRY"
deploy_dev:
stage: deploy
image: alpine:latest
environment:
name: dev
url: http://web.sik.party:8080
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
- scp .deploy_dev.sh $DEV_SSH_USER@$DEV_SSH_HOST:~/deployment/deploy_dev.sh
- ssh $DEV_SSH_USER@$DEV_SSH_HOST "bash ~/deployment/deploy_dev.sh \"$IMAGE_NAME\""
deploy:production:
stage: deploy
image: docker:25-cli
only:
- master
environment:
name: production
url: https://api.sahkoinsinoorikilta.fi
when: manual
variables:
DOCKER_HOST: $CI_DOCKER_HOST
DOCKER_TLS_VERIFY: 1
before_script:
- 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_JOB_TOKEN $CI_REGISTRY
script:
- docker stack deploy --with-registry-auth -c stack-compose.yml "$SERVICE_NAME"
after_script:
- docker logout "$CI_REGISTRY"
docker_prune:dev:
image: docker:stable
stage: cleanup
only:
- schedules
environment:
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_JOB_TOKEN $CI_REGISTRY
script:
- docker system prune
after_script:
- docker logout "$CI_REGISTRY"
docker_prune:prod:
image: docker:stable
stage: cleanup
only:
- schedules
environment:
name: production
url: https://api.sahkoinsinoorikilta.fi
variables:
DOCKER_HOST: $CI_DOCKER_HOST
DOCKER_TLS_VERIFY: 1
before_script:
- 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_JOB_TOKEN $CI_REGISTRY
script:
- docker system prune
after_script:
- docker logout "$CI_REGISTRY"
deploy_production:
stage: deploy
image: alpine:latest
environment:
name: production
url: https://sika.sahkoinsinoorikilta.fi
when: manual
only:
- master
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
script:
- ssh $PROD_SSH_USER@$PROD_SSH_HOST "zsh ~/deploy.sh"
-12
View File
@@ -1,12 +0,0 @@
PURPLE='\033[0;35m'
NC='\033[0m' # No Color
. "${VIRTUAL_ENV}/bin/activate"
if [ $? -ne 0 ]
then
printf "${PURPLE}Failed to find virtualenv. Skipping pre-commit hook.\n${NC}"
exit 0
fi
npm run lint
-21
View File
@@ -1,21 +0,0 @@
PURPLE='\033[0;35m'
NC='\033[0m' # No Color
. "${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"
-1
View File
@@ -1 +0,0 @@
22.13.1
-1
View File
@@ -1 +0,0 @@
3.12.9
+1
View File
@@ -0,0 +1 @@
global_static
+7 -28
View File
@@ -1,29 +1,8 @@
FROM python:3.12.9-slim-bullseye AS builder
FROM python:3
ENV PYTHONUNBUFFERED 1
COPY . ./
ENV POETRY_VERSION=2.0.1
RUN pip install "poetry==$POETRY_VERSION"
RUN poetry self add poetry-plugin-export
RUN poetry export --without-hashes > requirements.txt
FROM python:3.12.9-slim-bullseye AS server
WORKDIR /app
COPY . ./
COPY --from=builder requirements.txt ./
ENV PYTHONUNBUFFERED=1 \
# prevents python creating .pyc files
PYTHONDONTWRITEBYTECODE=1 \
# pip
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100
RUN apt-get update && apt-get install --no-install-recommends -y build-essential
RUN pip install --no-deps -r requirements.txt
RUN python manage.py collectstatic --noinput
CMD ["sh", "-c", "./production_entrypoint.sh"]
ENV IS_DOCKER 1
RUN mkdir /code
WORKDIR /code
ADD requirements.txt /code/
RUN env
ADD . /code/
-156
View File
@@ -1,156 +0,0 @@
# Web 2.0 Backend
[Django](https://www.djangoproject.com/) backend containing multiple small applications and api for Next.js frontend.
* **Web app:** Backend for the main website.
* **Member register:** Data table app for viewing and modifying the member register, member applications and membership payments.
* **Kaehmy:** Form for creating and listing kaehmys
* **Ohlhafv:** Form for creating and listing ohlhafv challenges.
* **Infoscreen:** Angular-based slideshow app for the guild room's screens.
## Installation
Set up your SSH key authentication in GitLab Profile Settings. Then clone the repository and checkout the development branch:
```bash
git clone git@gitlab.com:sahkoinsinoorikilta/vtmk/web2.0-backend.git
cd web2.0-backend
git checkout develop
```
Copy env file for local use:
```bash
cp .env.dev .env
```
### Poetry
For depedencies and virtual environment, we use [poetry](https://python-poetry.org/).
First install [python](https://wiki.python.org/moin/BeginnersGuide/Download). Then install poetry:
```bash
python -m pip install poetry==2.0.1
```
The easiest integration with VSCode is to have poetry install virtual environment in project folder, configured with CMD
```bash
python -m poetry config virtualenvs.in-project true
```
### Node
We use Node.js for few development tasks, like linting. Easiest way to install Node is [nvm](https://github.com/nvm-sh/nvm). After installing install dependencies:
```bash
npm install
```
TODO: List scripts
### Database
To run a local development database **[docker](https://docs.docker.com/engine/install/)** is recommended. If you want to additianally use a db management tool **[pgAdmin](https://www.pgadmin.org/download/)** is nice.
After installing docker use the following to create a database:
```bash
docker run --name sik.web.db -e POSTGRES_PASSWORD=postgres -p 5432:5432 -d postgres:12
```
## Development
Activate virtual environment in shell
```bash
eval $(python -m poetry env activate)
```
Install dependencies
```bash
poetry install
```
### Initializing data
Run the following `manage.py` commands to initialize a new database. Do not run these in production without thinking!
```bash
python manage.py migrate # run migrations
python manage.py createdefaultadmin # creates an admin user
python manage.py initialize # creates user groups
python manage.py createdummydata # creates dummy members to the member register
```
### Running
```bash
python manage.py runserver
```
Visit [https://localhost:8000](https://localhost:8000) in your browser!
Using address `0.0.0.0` will bind to all IP addresses. Using `localhost` will only bind to your machine.
```bash
python manage.py runserver 0.0.0.0:8000
```
### Development workflow
When you start working on a feature, create a feature branch for your changes. These feature branches should be prefixed with `feature`.
Example of creating a feature branch:
```bash
git checkout -b feature-branch-name
```
When your changes are ready and the code works without errors, submit a merge request to `develop` in GitLab. Another developer reviews your changes and runs the merge. Feature branches should be closed on merge.
Bugfixes do not need their own feature branches and can be pushed straight to `develop`, but if the fix needs a notable amount of work, it should be done in a `bugfix` branch instead.
Merge requests to `master` should be reviewed by multiple developers. Only a moderator can accept merge requests to `master`.
### Linting
Lint python files using `black` with
```bash
npm run lint:py # check changes
npm run lint:py:fix # fix errors
```
Lint javascript and markdown using `eslint` and `remark` with
```bash
npm run lint:md # markdown
npm run lint:js # javascript
```
Use an editor with linting capabilities to write pretty code that passes linting. Examples include _VSCode_, _Atom_ and _Pycharm_.
### Unit tests
Run unit tests with
```bash
python manage.py test
```
Due to the mostly static nature of the project, most elements are difficult to properly unit test. If you write code with actual logic, make sure to write at least one unit or integration test that tests your code's core functionality.
Tests are located in `tests.py` under every subproject.
## Production
Project is run in production with Docker. See `Dockerfile` for details.
For more information about deployment check **[infra](https://gitlab.com/sahkoinsinoorikilta/vtmk/infra)** repository.
## GitLab CI
All pushed changes go through the GitLab Continuous Integration, which consists of automated unit testing and linting. Make sure your changes pass both before merging to `develop` or `master`.
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
@@ -0,0 +1,74 @@
from django.core.management.base import BaseCommand, CommandError
from django.utils import timezone
from django.conf import settings
from coffee_scale.models import Brewing
import paho.mqtt.client as mqtt
import random
brewing = False
lastbrew = timezone.now()
lastcups = 0
def on_connect(client, userdata, flags, rc):
if rc != 0:
print("Failed to connect with result code: {}".format(rc))
print("Connected with result code: {}".format(rc))
client.subscribe(settings.MQTT_SETTINGS.HOST.TOPICS.CUPS)
client.subscribe(settings.MQTT_SETTINGS.HOST.TOPICS.BREWING)
client.subscribe(settings.MQTT_SETTINGS.HOST.TOPICS.BREW_TIME)
def on_message(client, userdata, message):
print("%s %s".format(message.topic, message.payload.decode()))
def on_message_cups(client, userdata, message):
cups = int(message.payload.decode())
print("cups: {}".format(cups))
print("{}".format(timezone.now()))
# checks if new coffee was brewed so we don't add the same brewing again to db
global lastcups # ;/ have to use global to store state instead of class
if cups > lastcups:
new_brew = Brewing(cups=cups, time=timezone.now())
print(new_brew.time)
new_brew.save()
lastcups = cups
def on_message_brewtime(client, userdata, message):
brewtime = datetime.fromtimestamp(int(message.payload.decode()))
print("brewtime: {}".format(brewtime))
def on_message_brewing(client, userdata, message):
brewing = bool(int(message.payload.decode()))
print("brewing: {}".format(brewing))
class Command(BaseCommand):
help = "Fetches coffee mqtt messages"
def add_arquments(self, parser):
pass
def handle(self, *args, **options):
self.username = "coffee-user-%d".format(random.randint(0, 100))
self.client = mqtt.Client("coffee")
self.client.username_pw_set(self.username, password=None)
self.client.tls_set()
# callbacks for different topics
self.client.message_callback_add(settings.MQTT_SETTINGS.HOST.TOPICS.BREW_TIME, on_message_brewtime)
self.client.message_callback_add(settings.MQTT_SETTINGS.HOST.TOPICS.CUPS, on_message_cups)
self.client.message_callback_add(settings.MQTT_SETTINGS.HOST.TOPICS.BREWING, on_message_brewing)
# self.client.connect("localhost", port=1883) # used for local testing
self.client.connect(settings.MQTT_SETTINGS.HOST, port=settings.MQTT_SETTINGS.PORT)
self.client.on_message = on_message
self.client.on_connect = on_connect
while True:
self.client.loop()
+24
View File
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-11-19 10:55
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Brewing',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('cups', models.PositiveSmallIntegerField()),
('time', models.DateTimeField()),
],
),
]
+6
View File
@@ -0,0 +1,6 @@
from django.db import models
class Brewing(models.Model):
cups = models.PositiveSmallIntegerField()
time = models.DateTimeField()
+124
View File
@@ -0,0 +1,124 @@
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;
}
.layertwo{
display: None;
}
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:rgb(100, 255, 100);}
50% {background-color:white;}
75% {background-color:rgb(100, 255, 100);}
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);}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

+183
View File
@@ -0,0 +1,183 @@
//Inner state
var lastBrew = new Date(0);
var brewing = false;
var backoff = 2000;
//MQTT client config
var username = "coffee-user-"+ Math.random();
// eslint-disable-next-line no-undef
var client = new Paho.MQTT.Client("sika.sahkoinsinoorikilta.fi", 9001, username);
client.onMessageArrived = function (message) {
// eslint-disable-next-line no-console
console.log("Topic: "+message.destinationName+" msg: "+message.payloadString);
var ev = new CustomEvent(message.destinationName, {'detail': message.payloadString});
window.dispatchEvent(ev);
}
function reconnect(responseObject){
if (responseObject.errorCode !== 0) {
console.log("connection lost! Reason: "+responseObject.errorMessage); // eslint-disable-line no-console
setTimeout(function(){
client.connect({onSuccess:onConnect, useSSL:true, onFailure: reconnect});
}, backoff);
}
}
function onConnect() {
console.log("MQTT connected"); // eslint-disable-line no-console
//set and reset reconnector
client.onConnectionLost = reconnect
// subscribe to topics
client.subscribe("sik/kiltahuone/kahvivaaka/cups");
client.subscribe("sik/kiltahuone/kahvivaaka/brewing");
client.subscribe("sik/kiltahuone/kahvivaaka/brewtime");
}
// data update and parse functions
function parseCups(ev){
var cups = parseFloat(ev.detail).toFixed(1)
function makeEvent(cups) {
return (String(cups) !== '-1.0')
? new CustomEvent("cupsChanged", {'detail': cups})
: new CustomEvent("cupsError", {'detail': 'Error: unable to fetch cups :('});
}
window.dispatchEvent(makeEvent(cups));
}
function updateCups(ev){
$("#text").text(ev.detail);
}
function showCupsError(ev) {
$('#text').text(ev.detail);
$('#text').css({
'font-size': '7vh',
'left': '0',
'top': '40%',
'width': '100%',
'text-align': 'center',
'color': 'red',
});
$('#lower').css({'background-image': 'none'});
}
function updateScale(ev){
$("#scale2").css({width: Math.min(ev.detail/9*100,100) + '%'});
}
function tick(){
var ev = new CustomEvent("tick", {'detail': new Date()});
window.dispatchEvent(ev);
}
function updateTime(ev){
var now = ev.detail;
$("#time").html(formatTime(now.getHours(),now.getMinutes(),now.getSeconds()));
}
function coffeeLowEffect(ev){
ev.detail <= 2 ? $("#text").addClass("hurry") : $("#text").removeClass("hurry");
}
function coffeeReadyEffect(){
$("body").addClass("coffeeready");
// autoclear animation class in 10s
setTimeout(function(){$("body").removeClass("coffeeready");}, 10000);
}
function hotEffect(ev){
var opa = Math.max(100 - ev.detail / 90000,0);
$("#upper").css({opacity: opa/100});
}
function brewAnimStart(){
$(".text").addClass("brewing");
$(".layerone").hide();
$(".layertwo").show();
}
function brewAnimEnd(){
$(".text").removeClass("brewing");
$(".layertwo").hide();
$(".layerone").show();
}
function brewNotifier(ev){
var new_brewing = parseInt(ev.detail);
if (new_brewing == 1 && brewing == 0){
window.dispatchEvent(new Event("brewStart"));
} else if (new_brewing == 0 && brewing == 1){
window.dispatchEvent(new Event("brewEnd"));
}
brewing = new_brewing;
}
function brewTimeParser(ev){
lastBrew = new Date(parseInt(ev.detail)*1000.0);
}
function updateBrewDiff(){
var now = new Date();
var timeDiff = Math.max(now.getTime() - lastBrew.getTime(), 0);
var eve = new CustomEvent("dtUpdate", {'detail': timeDiff});
window.dispatchEvent(eve);
}
function updateBrewTime(ev){
var timeDiff = ev.detail;
var timeStr;
if (timeDiff < 3600000){
timeStr = Math.round(timeDiff / 60000) + ' min'
} else if (timeDiff < 10000* 3600 * 1000){ // 1000h
timeStr = '~' + Math.round(timeDiff / 3600000 * 2) / 2 + ' h';
} else {
timeStr = "???"
}
$("#brewtime").html(timeStr);
}
// Helpers
function nToS(num){
return num < 10 ? "0" + num : "" + num;
}
function formatTime(hours, minutes, seconds){
return nToS(hours)+":"+nToS(minutes)+":"+nToS(seconds)
}
function resize(){
var w = $("#container").width();
var h = $("#container").height();
var s = w > h ? h : w;
var font = s * 0.8 * 0.38/Math.sqrt(3);
$(".text").css({ top: s*0.16-font/2 + 'px',
fontSize: font + 'px',
marginLeft: -font*3*3/10 + 'px'});
}
// Init everything
$(document).ready(function(){
client.connect({onSuccess:onConnect, useSSL:true, onFailure:reconnect});
//connect MQTT event listeners
window.addEventListener("sik/kiltahuone/kahvivaaka/cups", parseCups);
window.addEventListener("sik/kiltahuone/kahvivaaka/brewing", brewNotifier);
window.addEventListener("sik/kiltahuone/kahvivaaka/brewtime", brewTimeParser);
//connect other event listeners
window.addEventListener("cupsChanged", updateCups);
window.addEventListener("cupsChanged", coffeeLowEffect);
window.addEventListener("cupsChanged", updateScale);
window.addEventListener("cupsChanged", resize);
window.addEventListener("cupsError", showCupsError);
window.addEventListener("cupsError", coffeeLowEffect);
window.addEventListener("cupsError", updateScale);
window.addEventListener("brewStart", brewAnimStart);
window.addEventListener("brewEnd", brewAnimEnd);
window.addEventListener("brewEnd", coffeeReadyEffect);
window.addEventListener("tick", updateTime);
window.addEventListener("tick", updateBrewDiff);
window.addEventListener("dtUpdate", updateBrewTime);
window.addEventListener("dtUpdate", hotEffect);
//start time based events
setInterval(tick, 100);
tick();
});
$(window).resize(resize);
+38
View File
@@ -0,0 +1,38 @@
<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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.2/mqttws31.js"
type="text/javascript"></script>
<link rel="stylesheet" href="/static/css/coffee.css" />
<script src="/static/js/coffee.js"></script>
</head>
<body>
<div id="container">
<span id="brewtime" class="brewtime layerone"></span>
<span class="brewtime layertwo">:)</span>
<span id="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" class="text layerone">???</div>
<div class="text layertwo">&nbsp;+</div>
<div id="scale"><div id="scale2"></div></div>
</div>
</div>
</body>
</html>
+2
View File
@@ -0,0 +1,2 @@
from django.test import TestCase, Client
from django.conf import settings
+12
View File
@@ -0,0 +1,12 @@
from django.conf.urls import url
from django.views.generic.base import RedirectView
from .views import coffee_view
favicon_view = RedirectView.as_view(url='static/img/favicon.ico', permanent=True)
urlpatterns = [
# landing page
url(r'^$', coffee_view),
]
+11
View File
@@ -0,0 +1,11 @@
from django.shortcuts import render
from django.http import JsonResponse
from django.utils import timezone
import logging
from django.conf import settings
def coffee_view(request):
return render(request, 'coffee.html')
+6 -15
View File
@@ -1,22 +1,13 @@
version: '3'
services:
db:
image: postgres:12
volumes:
- dbdata:/var/lib/postgresql/data
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=postgres
image: postgres
web:
build: .
image: registry.gitlab.com/sahkoinsinoorikilta/vtmk/web2.0-backend
env_file:
- .env
image: git.sahkoinsinoorikilta.fi:4567/vtmk/web2.0
command: ["bash", "-c", "cd /code && ./wait-for-it.sh db:5432 -- bash setup.sh --no-input --no-npm && python manage.py runserver 0.0.0.0:8080"]
ports:
- "8000:8000"
- "8080:8080"
depends_on:
- db
volumes:
dbdata:
-23
View File
@@ -1,23 +0,0 @@
import globals from "globals";
import js from "@eslint/js";
export default [
{
ignores: ["**/.venv/", "**/collected_static/", "**/static/js/lib/**"],
},
{
languageOptions: {
globals: {
...globals.browser,
...globals.jquery,
angular: true,
moment: true,
_: true
},
},
},
{
...js.configs.recommended
}
];
+45
View File
@@ -0,0 +1,45 @@
# Ilmotunkki
## Terms
- Signup, Form with collection of questions
- Response, One answer to some signup
- Quota, Amount of people allowed to respond with some option selected.
- In generic case there is no option and quota is just max number of people.
## Requirements
- Officials may generate signups forms
- Officials may see results from signups
- Officials may see some stats from their signups
- for example distributions of multiple choice answers
- Officials should be able to edit signups wherever possible
- Propably not possible to edit after first response
- Officials should be able to delete responses
- Officials should be able to embed payment information to the signup?
- TODO: is there need for unique reference numbers for every response?
- Officials should be able to save a signup to a reusable template.
- Signup may be attached to an event
- multiple signups to a single event?
- Signup should support custom quotas
- Atleast quotas from multiple choices and checkboxes
- Text quotas are risky (typos everywhere!!)
- Signup should have start and end times
- signup should support atleast following questiontypes
- Text
- multiple choice (select one)
- checkbox (boolean yes/no)
- Signup should support reserve slots.
TODO: quota based reserves or generic? or both?
- Responding should send confirm email
- Response should be editable by responder and only by the responder until the closing of the signup
- TODO: is there need to custom edit period or disable?
- Responders should see amount of quotas left.
- Responders should see some information about other responses
- TODO: names? should this be editable by officials?
- NOTE: Quota related info is exposed if any info is printed
+3
View File
@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.
+5
View File
@@ -0,0 +1,5 @@
from django.apps import AppConfig
class IlmotunkkiConfig(AppConfig):
name = 'ilmotunkki'
+16
View File
@@ -0,0 +1,16 @@
from django.db import models
from django.utils import timezone
class Signup(models.Model):
start = models.DateTimeField()
end = models.DateTimeField()
class Question(models.Model):
pass
class Answer(models.Model):
signup = models.ForeignKey(Signup, on_delete=models.CASCADE)
question = models.ForeignKey(Question, on_delete=models.PROTECT)
+3
View File
@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.
+6 -10
View File
@@ -1,16 +1,11 @@
"""Admin site registers."""
from django.contrib import admin
from infoscreen.models import (
Rotation,
InfoItem,
InfoInstance,
ImageInfoItem,
ExternalImageInfoItem,
ABBInfoItem,
ExternalWebsiteInfoItem,
VideoInfoItem,
)
from infoscreen.models import Rotation, InfoItem, InfoInstance
from infoscreen.models import ImageInfoItem, ExternalImageInfoItem, ABBInfoItem
from infoscreen.models import ExternalWebsiteInfoItem
from infoscreen.models import VideoInfoItem
from infoscreen.models import CoffeeStatsInfoItem
# Register your models here.
admin.site.register(Rotation)
@@ -21,3 +16,4 @@ admin.site.register(ABBInfoItem)
admin.site.register(InfoInstance)
admin.site.register(ExternalWebsiteInfoItem)
admin.site.register(VideoInfoItem)
admin.site.register(CoffeeStatsInfoItem)
+1 -1
View File
@@ -6,4 +6,4 @@ from django.apps import AppConfig
class InfoscreenConfig(AppConfig):
"""Infoscreen app configuration."""
name = "infoscreen"
name = 'infoscreen'
+70
View File
@@ -0,0 +1,70 @@
"""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 = time_utc = stop_time['serviceDay'] + stop_time['realtimeArrival']
headsign = stop_time['stopHeadsign']
stop_name = stop_time['stop']['name']
time_diff = (timestamp - timezone.now().timestamp()) / 60 # minutes
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,
'utc': time_utc,
})
return schedule
+71
View File
@@ -0,0 +1,71 @@
query NearestRoutesContainer($lat_0: Float!, $lon_1: Float!, $maxDistance_2: Int!, $maxResults_3: Int!, $timeRange_7: Int!, $numberOfDepartures_8: Int!, $filterByModes_4: [Mode]!, $filterByPlaceTypes_5: [FilterPlaceType]!, $startTime_6: Long!) {
viewer {
...F5
}
}
fragment F0 on DepartureRow {
_stoptimes4caEfh: stoptimes(startTime: $startTime_6, timeRange: $timeRange_7, numberOfDepartures: $numberOfDepartures_8) {
pickupType
serviceDay
realtimeDeparture
}
id
}
fragment F1 on DepartureRow {
pattern {
route {
shortName
}
}
_stoptimes: stoptimes(startTime: $startTime_6, timeRange: $timeRange_7, numberOfDepartures: $numberOfDepartures_8) {
realtimeArrival
serviceDay
stopHeadsign
stop {
name
}
}
}
fragment F2 on BikeRentalStation {
id
}
fragment F3 on placeAtDistance {
distance
place {
id
__typename
...F1
...F2
}
id
}
fragment F4 on placeAtDistanceConnection {
edges {
node {
distance
place {
id
__typename
...F0
}
id
...F3
}
cursor
}
pageInfo {
hasNextPage
hasPreviousPage
}
}
fragment F5 on QueryType {
_nearest: nearest(lat: $lat_0, lon: $lon_1, maxDistance: $maxDistance_2, maxResults: $maxResults_3, first: $maxResults_3, filterByModes: $filterByModes_4, filterByPlaceTypes: $filterByPlaceTypes_5) {
...F4
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"lat_0": 60.190480099999995,
"lon_1": 24.8275665,
"maxDistance_2": 1000,
"maxResults_3": 50,
"numberOfDepartures_8": 2,
"timeRange_7": 7200,
"filterByModes_4": ["BUS"],
"filterByPlaceTypes_5": ["DEPARTURE_ROW"]
}
+10
View File
@@ -0,0 +1,10 @@
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()
+36 -128
View File
@@ -11,173 +11,81 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
("contenttypes", "0002_remove_content_type_name"),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name="HSLDataModel",
name='HSLDataModel',
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("data", models.TextField(default="", editable=False)),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('data', models.TextField(default='', editable=False)),
],
),
migrations.CreateModel(
name="InfoInstance",
name='InfoInstance',
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("duration", models.FloatField(default=15.0)),
("item_id", models.PositiveIntegerField()),
(
"item_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.ContentType",
),
),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('duration', models.FloatField(default=15.0)),
('item_id', models.PositiveIntegerField()),
('item_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
],
),
migrations.CreateModel(
name="InfoItem",
name='InfoItem',
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
("expire_date", models.DateTimeField(blank=True, null=True)),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('expire_date', models.DateTimeField(blank=True, null=True)),
],
),
migrations.CreateModel(
name="Rotation",
name='Rotation',
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255)),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
],
),
migrations.CreateModel(
name="ABBInfoItem",
name='ABBInfoItem',
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",
),
),
('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",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="ExternalImageInfoItem",
name='ExternalImageInfoItem',
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",
),
),
("url", models.TextField()),
('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')),
('url', models.TextField()),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="HslInfoItem",
name='HslInfoItem',
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",
),
),
('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",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="ImageInfoItem",
name='ImageInfoItem',
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",
),
),
("img", models.ImageField(upload_to="infoimages/")),
('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')),
('img', models.ImageField(upload_to='infoimages/')),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="SossoInfoItem",
name='SossoInfoItem',
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",
),
),
('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",),
bases=('infoscreen.infoitem',),
),
migrations.AddField(
model_name="infoinstance",
name="rotation",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="instances",
to="infoscreen.Rotation",
),
model_name='infoinstance',
name='rotation',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='infoscreen.Rotation'),
),
]
+4 -14
View File
@@ -9,25 +9,15 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0001_initial"),
('infoscreen', '0001_initial'),
]
operations = [
migrations.CreateModel(
name="CoffeeInfoItem",
name='CoffeeInfoItem',
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",
),
),
('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",),
bases=('infoscreen.infoitem',),
),
]
@@ -9,63 +9,33 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0002_coffeeinfoitem"),
('infoscreen', '0002_coffeeinfoitem'),
]
operations = [
migrations.CreateModel(
name="ApyInfoItem",
name='ApyInfoItem',
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",
),
),
('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",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="EventInfoItem",
name='EventInfoItem',
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",
),
),
('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",),
bases=('infoscreen.infoitem',),
),
migrations.CreateModel(
name="ExternalWebsiteInfoItem",
name='ExternalWebsiteInfoItem',
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",
),
),
("url", models.TextField()),
('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')),
('url', models.TextField()),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
migrations.DeleteModel(
name="CoffeeInfoItem",
name='CoffeeInfoItem',
),
]
+5 -15
View File
@@ -9,26 +9,16 @@ import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0003_auto_20170329_1857"),
('infoscreen', '0003_auto_20170329_1857'),
]
operations = [
migrations.CreateModel(
name="VideoInfoItem",
name='VideoInfoItem',
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",
),
),
("video", models.FileField(upload_to="infovideos/")),
('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')),
('video', models.FileField(upload_to='infovideos/')),
],
bases=("infoscreen.infoitem",),
bases=('infoscreen.infoitem',),
),
]
@@ -8,18 +8,18 @@ from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0004_videoinfoitem"),
('infoscreen', '0004_videoinfoitem'),
]
operations = [
migrations.AlterField(
model_name="externalimageinfoitem",
name="url",
model_name='externalimageinfoitem',
name='url',
field=models.URLField(),
),
migrations.AlterField(
model_name="externalwebsiteinfoitem",
name="url",
model_name='externalwebsiteinfoitem',
name='url',
field=models.URLField(),
),
]
@@ -8,14 +8,11 @@ from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0005_auto_20170913_1841"),
('infoscreen', '0005_auto_20170913_1841'),
]
operations = [
migrations.DeleteModel(
name="HSLDataModel",
),
migrations.DeleteModel(
name="HslInfoItem",
name='HSLDataModel',
),
]
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11 on 2017-11-19 14:41
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('infoscreen', '0006_delete_hsldatamodel'),
]
operations = [
migrations.CreateModel(
name='CoffeeStatsInfoItem',
fields=[
('infoitem_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='infoscreen.InfoItem')),
],
bases=('infoscreen.infoitem',),
),
]
-31
View File
@@ -1,31 +0,0 @@
# 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",),
),
]
@@ -1,28 +0,0 @@
# Generated by Django 3.2.14 on 2022-08-01 19:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("infoscreen", "0007_lunchitem"),
]
operations = [
migrations.AlterField(
model_name="infoinstance",
name="id",
field=models.AutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name="infoitem",
name="id",
field=models.AutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name="rotation",
name="id",
field=models.AutoField(primary_key=True, serialize=False),
),
]
+100 -81
View File
@@ -7,7 +7,7 @@ from django import forms
from django.utils import timezone
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from django.utils.translation import ugettext as _
class InfoItem(models.Model):
@@ -16,7 +16,6 @@ class InfoItem(models.Model):
class __meta__:
abstract = True
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
# expire_date = None means never expiring item
expire_date = models.DateTimeField(blank=True, null=True)
@@ -24,14 +23,14 @@ class InfoItem(models.Model):
def get_template_url(self):
"""Get infoscreen template url."""
raise NotImplementedError("inheriting classes must implement get_template_url")
raise NotImplementedError(
"inheriting classes must implement get_template_url")
@staticmethod
def get_create_template_url():
"""Get create infoscreen template url command."""
raise NotImplementedError(
"inheriting classes must implement get_create_template_url"
)
"inheriting classes must implement get_create_template_url")
@classmethod
def create_from_dict(cls, d):
@@ -43,13 +42,14 @@ class InfoItem(models.Model):
def update_from_dict(self, d):
"""Update model based on given dict."""
try:
expire_date = d.pop("expire_date", None)
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S")
expire_date = d.pop('expire_date', None)
self.expire_date = datetime.strptime(
expire_date, "%Y-%m-%d %H:%M:%S")
except:
pass
dmap = {
"name": "name",
'name': 'name',
}
for k, v in d.items():
try:
@@ -61,13 +61,13 @@ class InfoItem(models.Model):
def get_dict(self):
"""Convert django model to dict and return it."""
return {
"id": self.id,
"name": self.name,
"item_type": ContentType.objects.get_for_model(self).id,
"template_url": self.get_template_url(),
"display_name": self.display_name,
"create_template_url": self.get_create_template_url(),
"options": {},
'id': self.id,
'name': self.name,
'item_type': ContentType.objects.get_for_model(self).id,
'template_url': self.get_template_url(),
'display_name': self.display_name,
'create_template_url': self.get_create_template_url(),
'options': {}
}
def delete(self):
@@ -75,8 +75,8 @@ class InfoItem(models.Model):
# since generic foreign keys suck, delete info
# items pointing here manually
InfoInstance.objects.filter(
item_id=self.id, item_type=ContentType.objects.get_for_model(self)
).delete()
item_id=self.id,
item_type=ContentType.objects.get_for_model(self)).delete()
super().delete()
@classmethod
@@ -98,12 +98,12 @@ class ABBInfoItem(InfoItem):
def get_template_url(self):
"""Return ABB infoitem template url."""
return "/static/infoscreen/html/abb.html"
return "/static/html/abb.html"
@staticmethod
def get_create_template_url():
"""Call create ABB infoitem template url command."""
return "/static/infoscreen/html/abb_create.html"
return "/static/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/infoscreen/html/apy.html"
return "/static/html/apy.html"
@staticmethod
def get_create_template_url():
"""Call create APY infoitem template url command."""
return "/static/infoscreen/html/apy_create.html"
return "/static/html/apy_create.html"
class ExternalWebsiteInfoItem(InfoItem):
@@ -129,17 +129,17 @@ class ExternalWebsiteInfoItem(InfoItem):
def get_template_url(self):
"""Return external website infoitem template url."""
return "/static/infoscreen/html/external_website.html?url={}".format(self.name)
return "/static/html/external_website.html?url={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create external website infoitem template url command."""
return "/static/infoscreen/html/external_website_create.html"
return "/static/html/external_website_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
d = super().get_dict()
d["options"] = {"url": self.url}
d["options"] = {'url': self.url}
return d
@classmethod
@@ -152,22 +152,23 @@ class ExternalWebsiteInfoItem(InfoItem):
def get_list(self):
"""Return list containing infoitem data."""
return {
"id": self.id,
"name": self.name,
"url": self.url,
'id': self.id,
'name': self.name,
'url': self.url,
}
def update_from_dict(self, d):
"""Update model based on given dict."""
try:
expire_date = d.pop("expire_date", None)
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S")
expire_date = d.pop('expire_date', None)
self.expire_date = datetime.strptime(
expire_date, "%Y-%m-%d %H:%M:%S")
except:
pass
dmap = {
"name": "name",
"url": "url",
'name': 'name',
'url': 'url',
}
for k, v in d.items():
try:
@@ -184,25 +185,12 @@ class SossoInfoItem(InfoItem):
def get_template_url(self):
"""Return Sosso infoitem template url."""
return "/static/infoscreen/html/sosso.html"
return "/static/html/sosso.html"
@staticmethod
def get_create_template_url():
"""Call create Sosso infoitem template url command."""
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"
return "/static/html/sosso_create.html"
class EventInfoItem(InfoItem):
@@ -212,12 +200,12 @@ class EventInfoItem(InfoItem):
def get_template_url(self):
"""Return Event infoitem template url."""
return "/static/infoscreen/html/events.html"
return "/static/html/events.html"
@staticmethod
def get_create_template_url():
"""Call create Event infoitem template url command."""
return "/static/infoscreen/html/events_create.html"
return "/static/html/events_create.html"
class ImageInfoItem(InfoItem):
@@ -230,42 +218,72 @@ class ImageInfoItem(InfoItem):
"""Return Image infoitem template url."""
# get param to avoid angular from optimizing same template
# with different options
return "/static/infoscreen/html/generic_image.html?img={}".format(self.name)
return "/static/html/generic_image.html?img={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create Image infoitem template url command."""
return "/static/infoscreen/html/generic_image_create.html"
return "/static/html/generic_image_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
d = super().get_dict()
d["options"] = {"img": self.img.url}
d["options"] = {'img': self.img.url}
return d
class VideoInfoItem(InfoItem):
"""Class for Video Infoscreen item."""
display_name = "Video"
display_name = ("Video")
video = models.FileField(upload_to="infovideos/")
def get_template_url(self):
"""Return Video infoitem template url."""
return "/static/infoscreen/html/generic_video.html?video={}".format(self.name)
return "/static/html/generic_video.html?video={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create Video infoitem template url command."""
return "/static/infoscreen/html/generic_video_create.html"
return "/static/html/generic_video_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
d = super().get_dict()
d["options"] = {"video": self.video.url}
d["options"] = {'video': self.video.url}
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 CoffeeStatsInfoItem(InfoItem):
"""Class for Coffee statistics Infoscreen item."""
display_name = _("Coffee statistics")
def get_template_url(self):
"""Return HSL infoitem template url."""
return "/static/html/coffee_stats.html"
@staticmethod
def get_create_template_url():
"""Call create HSL infoitem template url command."""
return "/static/html/coffee_stats_create.html"
class ExternalImageInfoItem(InfoItem):
"""Class for External Image Infoscreen item."""
@@ -274,17 +292,17 @@ class ExternalImageInfoItem(InfoItem):
def get_template_url(self):
"""Return External Image infoitem template url."""
return "/static/infoscreen/html/generic_image.html?img={}".format(self.name)
return "/static/html/generic_image.html?img={}".format(self.name)
@staticmethod
def get_create_template_url():
"""Call create External Image infoitem template url command."""
return "/static/infoscreen/html/generic_external_image_create.html"
return "/static/html/generic_external_image_create.html"
def get_dict(self):
"""Convert django model to dict and return it."""
d = super().get_dict()
d["options"] = {"img": self.url}
d["options"] = {'img': self.url}
return d
@classmethod
@@ -297,14 +315,15 @@ class ExternalImageInfoItem(InfoItem):
def update_from_dict(self, d):
"""Update model based on given dict."""
try:
expire_date = d.pop("expire_date", None)
self.expire_date = datetime.strptime(expire_date, "%Y-%m-%d %H:%M:%S")
expire_date = d.pop('expire_date', None)
self.expire_date = datetime.strptime(
expire_date, "%Y-%m-%d %H:%M:%S")
except:
pass
dmap = {
"name": "name",
"url": "url",
'name': 'name',
'url': 'url',
}
for k, v in d.items():
try:
@@ -317,15 +336,12 @@ class ExternalImageInfoItem(InfoItem):
class InfoInstance(models.Model):
"""Class for Info instance in Infoscreen."""
id = models.AutoField(primary_key=True)
rotation = models.ForeignKey(
"Rotation", related_name="instances", on_delete=models.CASCADE
)
rotation = models.ForeignKey('Rotation', related_name='instances')
duration = models.FloatField(default=15.0) # seconds
# generic relation to some kind of InfoItem
item_id = models.PositiveIntegerField()
item_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
item = GenericForeignKey("item_type", "item_id")
item = GenericForeignKey('item_type', 'item_id')
@classmethod
def create_from_dict(cls, d):
@@ -338,27 +354,31 @@ class InfoInstance(models.Model):
except:
raise RuntimeError("invalid parameters supplied supplied")
try:
return cls.objects.create(rotation=rotation, item=item, duration=duration)
return cls.objects.create(
rotation=rotation,
item=item,
duration=duration
)
except:
raise RuntimeError("error while adding instance to db")
def get_dict(self):
"""Convert django model to dict and return it."""
return {
"id": self.id,
"item": self.item.get_dict(),
"duration": self.duration,
'id': self.id,
'item': self.item.get_dict(),
'duration': self.duration,
}
def __str__(self):
"""Return model name."""
return "{}: {} ({}s)".format(self.rotation.name, self.item.name, self.duration)
return "{}: {} ({}s)".format(
self.rotation.name, self.item.name, self.duration)
class Rotation(models.Model):
"""Class for rotation model."""
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255)
def get_dict(self):
@@ -367,20 +387,21 @@ class Rotation(models.Model):
# to avoid excluding items with no expire_date)
now = timezone.now()
instances = self.instances.all()
filtered = filter(lambda i: (i.item.expire_date or now) >= now, list(instances))
filtered = filter(lambda i: (i.item.expire_date or now) >= now,
list(instances))
instance_list = list(map(lambda i: i.get_dict(), filtered))
return {
"id": self.id,
"name": self.name,
"instances": instance_list,
'id': self.id,
'name': self.name,
'instances': instance_list,
}
def get_list(self):
"""Return list containing infoitem data."""
return {
"id": self.id,
"name": self.name,
'id': self.id,
'name': self.name,
}
def __str__(self):
@@ -391,7 +412,6 @@ class Rotation(models.Model):
class ImageUploadForm(forms.Form):
"""Form used to handle imageuploads to infoscreen app."""
id = models.AutoField(primary_key=True)
name = forms.CharField()
image = forms.ImageField()
@@ -399,6 +419,5 @@ class ImageUploadForm(forms.Form):
class UploadFileForm(forms.Form):
"""Form used for uploading file."""
id = models.AutoField(primary_key=True)
name = forms.CharField()
video = forms.FileField()
@@ -2,10 +2,7 @@ body {
font-family: 'Open Sans', sans-serif;
}
#header:after {
content: " ";
display: block;
clear: both;
#header {
}
#header-logo {
@@ -12,7 +12,7 @@ body {
.event {
font-size: 100px;
font-weight: bold;
margin-left: 20px;
margin-left: 20px;
}
.event-col{
padding-top:1vh;
@@ -21,7 +21,7 @@ body {
.header-row{
margin: 30px;
margin-left: 20px;
margin-left: 20px;
font-size: 130px;
padding-bottom:20px;
color:#24a05f;
+74
View File
@@ -0,0 +1,74 @@
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,8 +1,8 @@
#infocontent {
width: 100%;
height: 100%;
position: fixed;
left: 0px;
width: 100%;
height: 100%;
position: fixed;
left: 0px;
top: 0px;
z-index: -1; /* Ensure div tag stays behind content; -999 might work, too. */
}
@@ -5,7 +5,6 @@ html {
body {
padding: 1.5rem;
margin: 0.5rem;
height: 100%;
}
tbody {
@@ -50,18 +49,7 @@ td {
}
}
#header {
max-width: 100%;
display: flex;
justify-content: flex-end;
}
.tab-content {
margin-top: 1rem;
}
.rotation-title-row {
display: flex;
justify-content: space-between;
align-items: center;
}
@@ -19,7 +19,7 @@
.article-thumb-col {
max-height: 200px;
text-align: left;
text-align: right;
}
.article-title-col {
@@ -31,10 +31,10 @@
max-height 200px;
}
#sossoimage {
#sossoimage {
height:300px;
position: relative;
left: 0px;
position: relative;
left: 0px;
top: 0px;
}
@@ -1,10 +1,10 @@
<link rel="stylesheet" href="/static/infoscreen/css/abb.css">
<link rel="stylesheet" href="/static/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/infoscreen/img/ABB_logo.png">
<img src="/static/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/infoscreen/img/ABB_uralle.png" style="position:absolute;left:0;top:0;width:100vw;">
<img src="/static/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 class="form-control" type="text" ng-model="item.name"></input>
<input 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/infoscreen/css/apy.css">
<link rel="stylesheet" href="/static/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 new ÄPY statistics item
create apyitem
</div>
<div class="form-group">
<label>Name:</label>
<input class="form-control" type="text" ng-model="item.name"></input>
<input type="text" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
+4
View File
@@ -0,0 +1,4 @@
<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>
+9
View File
@@ -0,0 +1,9 @@
<link rel="stylesheet" href="/static/css/hsl.css">
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.19.1/locale/fi.js"></script>
<script type="text/javascript" src="dygraph.js"></script>
<link rel="stylesheet" src="dygraph.css" /></head>
<div class="container" ng-controller="CoffeeStatsController">
<div id="div_g"></div>
</div>
@@ -0,0 +1,11 @@
<div ng-controller="infoadmin_coffeestatsitem_create" style="margin-top:20px;">
<div>
Create new item to show coffee statistics. Name is used only as identifier
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
<!--maybe later add option to choose data range, daily, monthly...etc.
@@ -1,5 +1,5 @@
<link rel="stylesheet" href="/static/infoscreen/css/events.css">
<link href="https://fonts.googleapis.com/css?family=Droid+Sans+Mono" rel="stylesheet">
<link rel="stylesheet" href="/static/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">
<div class="col-sm-6">Tapahtuma</div>
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input>
<input 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/infoscreen/css/external_website.css">
<link rel="stylesheet" href="/static/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 "https://ka.dy.fi".
Create new item to show external website. For example "ka.dy.fi".
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input>
<input type="text" ng-model="item.name"></input>
</div>
<div class="form-group">
<label>Url:</label>
<input type="text" class="form-control" ng-model="item.url"></input>
<input type="text" 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" class="form-control" ng-model="item.name"></input>
<input type="text" ng-model="item.name"></input>
</div>
<div class="form-group">
<label>Url:</label>
<input type="text" class="form-control" ng-model="item.url"></input>
<input type="text" 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" class="form-control" ng-model="imagename"></input>
<input type="text" 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/infoscreen/css/video.css">
<link rel="stylesheet" href="/static/css/video.css">
<div class="fullscreen-bg">
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="name"></input>
<input type="text" ng-model="name"></input>
</div>
<div class="form-group">
<input type="file" ngf-select ng-model="video" name="file" required>
+41
View File
@@ -0,0 +1,41 @@
<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&#228;kki
</th>
</tr>
</thead>
<tbody>
<tr class="repeat-item" ng-repeat="x in stoptimes | orderBy: ['utc'] | 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>
+10
View File
@@ -0,0 +1,10 @@
<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>
+20
View File
@@ -0,0 +1,20 @@
<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>
@@ -4,7 +4,7 @@
</div>
<div class="form-group">
<label>Name:</label>
<input type="text" class="form-control" ng-model="item.name"></input>
<input type="text" ng-model="item.name"></input>
</div>
<input type="button" class="btn btn-success" ng-click="send()" value="create"></input>
</div>
+1
View File
@@ -0,0 +1 @@
<h1>testi2</h1>
+1
View File
@@ -0,0 +1 @@
<h1>testi3</h1>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 736 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

@@ -1,53 +0,0 @@
#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%;
}
@@ -1,11 +0,0 @@
<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>
@@ -1,10 +0,0 @@
<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>

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